feat(server): implement server patch check notifications

- Added a new job, ServerPatchCheckJob, to handle server patch checks and notifications.
- Introduced a new notification class, ServerPatchCheck, for sending updates via email, Discord, Slack, Pushover, and Telegram.
- Updated notification settings models to include server patch notification options for email, Discord, Slack, Pushover, and Telegram.
- Created a migration to add server patch notification fields to the respective settings tables.
- Enhanced the UI to allow users to enable/disable server patch notifications across different channels.
This commit is contained in:
Andras Bacsai
2025-05-26 14:03:59 +02:00
parent 86f6cd5fd6
commit 6ea6d2742b
23 changed files with 520 additions and 21 deletions

View File

@@ -83,8 +83,6 @@ class CheckUpdates
$out = $this->parseDnfOutput($output); $out = $this->parseDnfOutput($output);
$out['osId'] = $osId; $out['osId'] = $osId;
$out['package_manager'] = $packageManager; $out['package_manager'] = $packageManager;
$rebootRequired = instant_remote_process(['LANG=C dnf needs-restarting -r'], $server);
$out['reboot_required'] = $rebootRequired !== '0';
return $out; return $out;
case 'apt': case 'apt':
@@ -94,8 +92,6 @@ class CheckUpdates
$out = $this->parseAptOutput($output); $out = $this->parseAptOutput($output);
$out['osId'] = $osId; $out['osId'] = $osId;
$out['package_manager'] = $packageManager; $out['package_manager'] = $packageManager;
$rebootRequired = instant_remote_process(['LANG=C test -f /var/run/reboot-required && echo "YES" || echo "NO"'], $server);
$out['reboot_required'] = $rebootRequired === 'YES' ? true : false;
return $out; return $out;
default: default:

View File

@@ -12,6 +12,7 @@ use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\RegenerateSslCertJob; use App\Jobs\RegenerateSslCertJob;
use App\Jobs\ScheduledTaskJob; use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob; use App\Jobs\ServerCheckJob;
use App\Jobs\ServerPatchCheckJob;
use App\Jobs\ServerStorageCheckJob; use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob; use App\Jobs\UpdateCoolifyJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
@@ -175,6 +176,9 @@ class Kernel extends ConsoleKernel
} }
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer(); $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer();
// Server patch check - weekly
$this->scheduleInstance->job(new ServerPatchCheckJob($server))->weekly()->timezone($serverTimezone)->onOneServer();
// Cleanup multiplexed connections every hour // Cleanup multiplexed connections every hour
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Jobs;
use App\Actions\Server\CheckUpdates;
use App\Models\Server;
use App\Notifications\Server\ServerPatchCheck;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerPatchCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $timeout = 600; // 10 minutes timeout
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function __construct(public Server $server) {}
public function handle(): void
{
try {
if ($this->server->isFunctional() === false) {
return;
}
$team = data_get($this->server, 'team');
if (! $team) {
return;
}
// Check for updates
$patchData = CheckUpdates::run($this->server);
if (isset($patchData['error'])) {
return; // Skip if there's an error checking for updates
}
$totalUpdates = $patchData['total_updates'] ?? 0;
// Only send notification if there are updates available
if ($totalUpdates > 0) {
$team->notify(new ServerPatchCheck($this->server, $patchData));
}
} catch (\Throwable $e) {
// Log error but don't fail the job
\Illuminate\Support\Facades\Log::error('ServerPatchCheckJob failed: '.$e->getMessage(), [
'server_id' => $this->server->id,
'server_name' => $this->server->name,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
}
}

View File

@@ -56,6 +56,9 @@ class Discord extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $serverUnreachableDiscordNotifications = true; public bool $serverUnreachableDiscordNotifications = true;
#[Validate(['boolean'])]
public bool $serverPatchDiscordNotifications = false;
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $discordPingEnabled = true; public bool $discordPingEnabled = true;
@@ -89,6 +92,7 @@ class Discord extends Component
$this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications; $this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications;
$this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications; $this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications;
$this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications; $this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications;
$this->settings->server_patch_discord_notifications = $this->serverPatchDiscordNotifications;
$this->settings->discord_ping_enabled = $this->discordPingEnabled; $this->settings->discord_ping_enabled = $this->discordPingEnabled;
@@ -110,6 +114,7 @@ class Discord extends Component
$this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications; $this->serverDiskUsageDiscordNotifications = $this->settings->server_disk_usage_discord_notifications;
$this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications; $this->serverReachableDiscordNotifications = $this->settings->server_reachable_discord_notifications;
$this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications; $this->serverUnreachableDiscordNotifications = $this->settings->server_unreachable_discord_notifications;
$this->serverPatchDiscordNotifications = $this->settings->server_patch_discord_notifications;
$this->discordPingEnabled = $this->settings->discord_ping_enabled; $this->discordPingEnabled = $this->settings->discord_ping_enabled;
} }

View File

@@ -98,6 +98,9 @@ class Email extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $serverUnreachableEmailNotifications = true; public bool $serverUnreachableEmailNotifications = true;
#[Validate(['boolean'])]
public bool $serverPatchEmailNotifications = false;
#[Validate(['nullable', 'email'])] #[Validate(['nullable', 'email'])]
public ?string $testEmailAddress = null; public ?string $testEmailAddress = null;
@@ -146,6 +149,7 @@ class Email extends Component
$this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications; $this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications;
$this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications; $this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications;
$this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications; $this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications;
$this->settings->server_patch_email_notifications = $this->serverPatchEmailNotifications;
$this->settings->save(); $this->settings->save();
} else { } else {
@@ -177,6 +181,7 @@ class Email extends Component
$this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications; $this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications;
$this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications; $this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications;
$this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications; $this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications;
$this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications;
} }
} }

View File

@@ -64,6 +64,9 @@ class Pushover extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $serverUnreachablePushoverNotifications = true; public bool $serverUnreachablePushoverNotifications = true;
#[Validate(['boolean'])]
public bool $serverPatchPushoverNotifications = false;
public function mount() public function mount()
{ {
try { try {
@@ -95,6 +98,7 @@ class Pushover extends Component
$this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications; $this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications;
$this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications; $this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications;
$this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications; $this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications;
$this->settings->server_patch_pushover_notifications = $this->serverPatchPushoverNotifications;
$this->settings->save(); $this->settings->save();
refreshSession(); refreshSession();
@@ -115,6 +119,7 @@ class Pushover extends Component
$this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications; $this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications;
$this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications; $this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications;
$this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications; $this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications;
$this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications;
} }
} }

View File

@@ -61,6 +61,9 @@ class Slack extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $serverUnreachableSlackNotifications = true; public bool $serverUnreachableSlackNotifications = true;
#[Validate(['boolean'])]
public bool $serverPatchSlackNotifications = false;
public function mount() public function mount()
{ {
try { try {
@@ -91,6 +94,7 @@ class Slack extends Component
$this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications; $this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications;
$this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications; $this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications;
$this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications; $this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications;
$this->settings->server_patch_slack_notifications = $this->serverPatchSlackNotifications;
$this->settings->save(); $this->settings->save();
refreshSession(); refreshSession();
@@ -110,6 +114,7 @@ class Slack extends Component
$this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications; $this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications;
$this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications; $this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications;
$this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications; $this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications;
$this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications;
} }
} }

View File

@@ -64,6 +64,9 @@ class Telegram extends Component
#[Validate(['boolean'])] #[Validate(['boolean'])]
public bool $serverUnreachableTelegramNotifications = true; public bool $serverUnreachableTelegramNotifications = true;
#[Validate(['boolean'])]
public bool $serverPatchTelegramNotifications = false;
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsDeploymentSuccessThreadId = null; public ?string $telegramNotificationsDeploymentSuccessThreadId = null;
@@ -100,6 +103,9 @@ class Telegram extends Component
#[Validate(['nullable', 'string'])] #[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerUnreachableThreadId = null; public ?string $telegramNotificationsServerUnreachableThreadId = null;
#[Validate(['nullable', 'string'])]
public ?string $telegramNotificationsServerPatchThreadId = null;
public function mount() public function mount()
{ {
try { try {
@@ -131,6 +137,7 @@ class Telegram extends Component
$this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications; $this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications;
$this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications; $this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications;
$this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications; $this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications;
$this->settings->server_patch_telegram_notifications = $this->serverPatchTelegramNotifications;
$this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId; $this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId;
$this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId; $this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId;
@@ -144,6 +151,7 @@ class Telegram extends Component
$this->settings->telegram_notifications_server_disk_usage_thread_id = $this->telegramNotificationsServerDiskUsageThreadId; $this->settings->telegram_notifications_server_disk_usage_thread_id = $this->telegramNotificationsServerDiskUsageThreadId;
$this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId; $this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId;
$this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId; $this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId;
$this->settings->telegram_notifications_server_patch_thread_id = $this->telegramNotificationsServerPatchThreadId;
$this->settings->save(); $this->settings->save();
} else { } else {
@@ -163,6 +171,7 @@ class Telegram extends Component
$this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications; $this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications;
$this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications; $this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications;
$this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications; $this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications;
$this->serverPatchTelegramNotifications = $this->settings->server_patch_telegram_notifications;
$this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id; $this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id;
$this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id; $this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id;
@@ -176,6 +185,7 @@ class Telegram extends Component
$this->telegramNotificationsServerDiskUsageThreadId = $this->settings->telegram_notifications_server_disk_usage_thread_id; $this->telegramNotificationsServerDiskUsageThreadId = $this->settings->telegram_notifications_server_disk_usage_thread_id;
$this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id; $this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id;
$this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id; $this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id;
$this->telegramNotificationsServerPatchThreadId = $this->settings->telegram_notifications_server_patch_thread_id;
} }
} }

View File

@@ -28,6 +28,7 @@ class DiscordNotificationSettings extends Model
'server_disk_usage_discord_notifications', 'server_disk_usage_discord_notifications',
'server_reachable_discord_notifications', 'server_reachable_discord_notifications',
'server_unreachable_discord_notifications', 'server_unreachable_discord_notifications',
'server_patch_discord_notifications',
'discord_ping_enabled', 'discord_ping_enabled',
]; ];
@@ -46,6 +47,7 @@ class DiscordNotificationSettings extends Model
'server_disk_usage_discord_notifications' => 'boolean', 'server_disk_usage_discord_notifications' => 'boolean',
'server_reachable_discord_notifications' => 'boolean', 'server_reachable_discord_notifications' => 'boolean',
'server_unreachable_discord_notifications' => 'boolean', 'server_unreachable_discord_notifications' => 'boolean',
'server_patch_discord_notifications' => 'boolean',
'discord_ping_enabled' => 'boolean', 'discord_ping_enabled' => 'boolean',
]; ];

View File

@@ -35,6 +35,7 @@ class EmailNotificationSettings extends Model
'scheduled_task_success_email_notifications', 'scheduled_task_success_email_notifications',
'scheduled_task_failure_email_notifications', 'scheduled_task_failure_email_notifications',
'server_disk_usage_email_notifications', 'server_disk_usage_email_notifications',
'server_patch_email_notifications',
]; ];
protected $casts = [ protected $casts = [
@@ -61,6 +62,7 @@ class EmailNotificationSettings extends Model
'scheduled_task_success_email_notifications' => 'boolean', 'scheduled_task_success_email_notifications' => 'boolean',
'scheduled_task_failure_email_notifications' => 'boolean', 'scheduled_task_failure_email_notifications' => 'boolean',
'server_disk_usage_email_notifications' => 'boolean', 'server_disk_usage_email_notifications' => 'boolean',
'server_patch_email_notifications' => 'boolean',
]; ];
public function team() public function team()

View File

@@ -29,6 +29,7 @@ class PushoverNotificationSettings extends Model
'server_disk_usage_pushover_notifications', 'server_disk_usage_pushover_notifications',
'server_reachable_pushover_notifications', 'server_reachable_pushover_notifications',
'server_unreachable_pushover_notifications', 'server_unreachable_pushover_notifications',
'server_patch_pushover_notifications',
]; ];
protected $casts = [ protected $casts = [
@@ -47,6 +48,7 @@ class PushoverNotificationSettings extends Model
'server_disk_usage_pushover_notifications' => 'boolean', 'server_disk_usage_pushover_notifications' => 'boolean',
'server_reachable_pushover_notifications' => 'boolean', 'server_reachable_pushover_notifications' => 'boolean',
'server_unreachable_pushover_notifications' => 'boolean', 'server_unreachable_pushover_notifications' => 'boolean',
'server_patch_pushover_notifications' => 'boolean',
]; ];
public function team() public function team()

View File

@@ -28,6 +28,7 @@ class SlackNotificationSettings extends Model
'server_disk_usage_slack_notifications', 'server_disk_usage_slack_notifications',
'server_reachable_slack_notifications', 'server_reachable_slack_notifications',
'server_unreachable_slack_notifications', 'server_unreachable_slack_notifications',
'server_patch_slack_notifications',
]; ];
protected $casts = [ protected $casts = [
@@ -45,6 +46,7 @@ class SlackNotificationSettings extends Model
'server_disk_usage_slack_notifications' => 'boolean', 'server_disk_usage_slack_notifications' => 'boolean',
'server_reachable_slack_notifications' => 'boolean', 'server_reachable_slack_notifications' => 'boolean',
'server_unreachable_slack_notifications' => 'boolean', 'server_unreachable_slack_notifications' => 'boolean',
'server_patch_slack_notifications' => 'boolean',
]; ];
public function team() public function team()

View File

@@ -29,6 +29,7 @@ class TelegramNotificationSettings extends Model
'server_disk_usage_telegram_notifications', 'server_disk_usage_telegram_notifications',
'server_reachable_telegram_notifications', 'server_reachable_telegram_notifications',
'server_unreachable_telegram_notifications', 'server_unreachable_telegram_notifications',
'server_patch_telegram_notifications',
'telegram_notifications_deployment_success_thread_id', 'telegram_notifications_deployment_success_thread_id',
'telegram_notifications_deployment_failure_thread_id', 'telegram_notifications_deployment_failure_thread_id',
@@ -41,6 +42,7 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_server_disk_usage_thread_id', 'telegram_notifications_server_disk_usage_thread_id',
'telegram_notifications_server_reachable_thread_id', 'telegram_notifications_server_reachable_thread_id',
'telegram_notifications_server_unreachable_thread_id', 'telegram_notifications_server_unreachable_thread_id',
'telegram_notifications_server_patch_thread_id',
]; ];
protected $casts = [ protected $casts = [
@@ -59,6 +61,7 @@ class TelegramNotificationSettings extends Model
'server_disk_usage_telegram_notifications' => 'boolean', 'server_disk_usage_telegram_notifications' => 'boolean',
'server_reachable_telegram_notifications' => 'boolean', 'server_reachable_telegram_notifications' => 'boolean',
'server_unreachable_telegram_notifications' => 'boolean', 'server_unreachable_telegram_notifications' => 'boolean',
'server_patch_telegram_notifications' => 'boolean',
'telegram_notifications_deployment_success_thread_id' => 'encrypted', 'telegram_notifications_deployment_success_thread_id' => 'encrypted',
'telegram_notifications_deployment_failure_thread_id' => 'encrypted', 'telegram_notifications_deployment_failure_thread_id' => 'encrypted',
@@ -71,6 +74,7 @@ class TelegramNotificationSettings extends Model
'telegram_notifications_server_disk_usage_thread_id' => 'encrypted', 'telegram_notifications_server_disk_usage_thread_id' => 'encrypted',
'telegram_notifications_server_reachable_thread_id' => 'encrypted', 'telegram_notifications_server_reachable_thread_id' => 'encrypted',
'telegram_notifications_server_unreachable_thread_id' => 'encrypted', 'telegram_notifications_server_unreachable_thread_id' => 'encrypted',
'telegram_notifications_server_patch_thread_id' => 'encrypted',
]; ];
public function team() public function team()

View File

@@ -34,6 +34,7 @@ class TelegramChannel
\App\Notifications\Server\HighDiskUsage::class => $settings->telegram_notifications_server_disk_usage_thread_id, \App\Notifications\Server\HighDiskUsage::class => $settings->telegram_notifications_server_disk_usage_thread_id,
\App\Notifications\Server\Unreachable::class => $settings->telegram_notifications_server_unreachable_thread_id, \App\Notifications\Server\Unreachable::class => $settings->telegram_notifications_server_unreachable_thread_id,
\App\Notifications\Server\Reachable::class => $settings->telegram_notifications_server_reachable_thread_id, \App\Notifications\Server\Reachable::class => $settings->telegram_notifications_server_reachable_thread_id,
\App\Notifications\Server\ServerPatchCheck::class => $settings->telegram_notifications_server_patch_thread_id,
default => null, default => null,
}; };

View File

@@ -0,0 +1,245 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use App\Notifications\CustomEmailNotification;
use App\Notifications\Dto\DiscordMessage;
use App\Notifications\Dto\PushoverMessage;
use App\Notifications\Dto\SlackMessage;
use Illuminate\Notifications\Messages\MailMessage;
class ServerPatchCheck extends CustomEmailNotification
{
public string $serverUrl;
public function __construct(public Server $server, public array $patchData)
{
$this->onQueue('high');
$this->serverUrl = route('server.security.patches', ['server_uuid' => $this->server->uuid]);
if (isDev()) {
$this->serverUrl = 'https://staging-but-dev.coolify.io/server/'.$this->server->uuid.'/security/patches';
}
}
public function via(object $notifiable): array
{
return $notifiable->getEnabledChannels('server_patch');
}
public function toMail($notifiable = null): MailMessage
{
$mail = new MailMessage;
$totalUpdates = $this->patchData['total_updates'] ?? 0;
$mail->subject("Coolify: [ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}");
$mail->view('emails.server-patches', [
'name' => $this->server->name,
'total_updates' => $totalUpdates,
'updates' => $this->patchData['updates'] ?? [],
'osId' => $this->patchData['osId'] ?? 'unknown',
'package_manager' => $this->patchData['package_manager'] ?? 'unknown',
'server_url' => $this->serverUrl,
]);
return $mail;
}
public function toDiscord(): DiscordMessage
{
$totalUpdates = $this->patchData['total_updates'] ?? 0;
$updates = $this->patchData['updates'] ?? [];
$osId = $this->patchData['osId'] ?? 'unknown';
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
$description = "**{$totalUpdates} package updates** available for server {$this->server->name}\n\n";
$description .= "**Summary:**\n";
$description .= '• OS: '.ucfirst($osId)."\n";
$description .= "• Package Manager: {$packageManager}\n";
$description .= "• Total Updates: {$totalUpdates}\n\n";
// Show first few packages
if (count($updates) > 0) {
$description .= "**Sample Updates:**\n";
$sampleUpdates = array_slice($updates, 0, 5);
foreach ($sampleUpdates as $update) {
$description .= "{$update['package']}: {$update['current_version']}{$update['new_version']}\n";
}
if (count($updates) > 5) {
$description .= '• ... and '.(count($updates) - 5)." more packages\n";
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
if ($criticalPackages->count() > 0) {
$description .= "\n **Critical packages detected** ({$criticalPackages->count()} packages may require restarts)";
}
$description .= "\n [Manage Server Patches]($this->serverUrl)";
}
return new DiscordMessage(
title: ':warning: Coolify: [ACTION REQUIRED] Server patches available on '.$this->server->name,
description: $description,
color: DiscordMessage::errorColor(),
);
}
public function toTelegram(): array
{
$totalUpdates = $this->patchData['total_updates'] ?? 0;
$updates = $this->patchData['updates'] ?? [];
$osId = $this->patchData['osId'] ?? 'unknown';
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
$message = "🔧 Coolify: [ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n";
$message .= "📊 Summary:\n";
$message .= '• OS: '.ucfirst($osId)."\n";
$message .= "• Package Manager: {$packageManager}\n";
$message .= "• Total Updates: {$totalUpdates}\n\n";
if (count($updates) > 0) {
$message .= "📦 Sample Updates:\n";
$sampleUpdates = array_slice($updates, 0, 5);
foreach ($sampleUpdates as $update) {
$message .= "{$update['package']}: {$update['current_version']}{$update['new_version']}\n";
}
if (count($updates) > 5) {
$message .= '• ... and '.(count($updates) - 5)." more packages\n";
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
if ($criticalPackages->count() > 0) {
$message .= "\n⚠️ Critical packages detected: {$criticalPackages->count()} packages may require restarts\n";
foreach ($criticalPackages->take(3) as $package) {
$message .= "{$package['package']}: {$package['current_version']}{$package['new_version']}\n";
}
if ($criticalPackages->count() > 3) {
$message .= '• ... and '.($criticalPackages->count() - 3)." more critical packages\n";
}
}
}
return [
'message' => $message,
'buttons' => [
[
'text' => 'Manage Server Patches',
'url' => $this->serverUrl,
],
],
];
}
public function toPushover(): PushoverMessage
{
$totalUpdates = $this->patchData['total_updates'] ?? 0;
$updates = $this->patchData['updates'] ?? [];
$osId = $this->patchData['osId'] ?? 'unknown';
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
$message = "[ACTION REQUIRED] {$totalUpdates} server patches available on {$this->server->name}!\n\n";
$message .= "Summary:\n";
$message .= '• OS: '.ucfirst($osId)."\n";
$message .= "• Package Manager: {$packageManager}\n";
$message .= "• Total Updates: {$totalUpdates}\n\n";
if (count($updates) > 0) {
$message .= "Sample Updates:\n";
$sampleUpdates = array_slice($updates, 0, 3);
foreach ($sampleUpdates as $update) {
$message .= "{$update['package']}: {$update['current_version']}{$update['new_version']}\n";
}
if (count($updates) > 3) {
$message .= '• ... and '.(count($updates) - 3)." more packages\n";
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
if ($criticalPackages->count() > 0) {
$message .= "\nCritical packages detected: {$criticalPackages->count()} may require restarts";
}
}
return new PushoverMessage(
title: 'Server patches available',
level: 'error',
message: $message,
buttons: [
[
'text' => 'Manage Server Patches',
'url' => $this->serverUrl,
],
],
);
}
public function toSlack(): SlackMessage
{
$totalUpdates = $this->patchData['total_updates'] ?? 0;
$updates = $this->patchData['updates'] ?? [];
$osId = $this->patchData['osId'] ?? 'unknown';
$packageManager = $this->patchData['package_manager'] ?? 'unknown';
$description = "{$totalUpdates} server patches available on '{$this->server->name}'!\n\n";
$description .= "*Summary:*\n";
$description .= '• OS: '.ucfirst($osId)."\n";
$description .= "• Package Manager: {$packageManager}\n";
$description .= "• Total Updates: {$totalUpdates}\n\n";
if (count($updates) > 0) {
$description .= "*Sample Updates:*\n";
$sampleUpdates = array_slice($updates, 0, 5);
foreach ($sampleUpdates as $update) {
$description .= "• `{$update['package']}`: {$update['current_version']}{$update['new_version']}\n";
}
if (count($updates) > 5) {
$description .= '• ... and '.(count($updates) - 5)." more packages\n";
}
// Check for critical packages
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
if ($criticalPackages->count() > 0) {
$description .= "\n:warning: *Critical packages detected:* {$criticalPackages->count()} packages may require restarts\n";
foreach ($criticalPackages->take(3) as $package) {
$description .= "• `{$package['package']}`: {$package['current_version']}{$package['new_version']}\n";
}
if ($criticalPackages->count() > 3) {
$description .= '• ... and '.($criticalPackages->count() - 3)." more critical packages\n";
}
}
}
$description .= "\n:link: <{$this->serverUrl}|Manage Server Patches>";
return new SlackMessage(
title: 'Coolify: [ACTION REQUIRED] Server patches available',
description: $description,
color: SlackMessage::errorColor()
);
}
}

View File

@@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// Add server patch notification fields to email notification settings
Schema::table('email_notification_settings', function (Blueprint $table) {
$table->boolean('server_patch_email_notifications')->default(true);
});
// Add server patch notification fields to discord notification settings
Schema::table('discord_notification_settings', function (Blueprint $table) {
$table->boolean('server_patch_discord_notifications')->default(true);
});
// Add server patch notification fields to telegram notification settings
Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->boolean('server_patch_telegram_notifications')->default(true);
$table->string('telegram_notifications_server_patch_thread_id')->nullable();
});
// Add server patch notification fields to slack notification settings
Schema::table('slack_notification_settings', function (Blueprint $table) {
$table->boolean('server_patch_slack_notifications')->default(true);
});
// Add server patch notification fields to pushover notification settings
Schema::table('pushover_notification_settings', function (Blueprint $table) {
$table->boolean('server_patch_pushover_notifications')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
// Remove server patch notification fields from email notification settings
Schema::table('email_notification_settings', function (Blueprint $table) {
$table->dropColumn('server_patch_email_notifications');
});
// Remove server patch notification fields from discord notification settings
Schema::table('discord_notification_settings', function (Blueprint $table) {
$table->dropColumn('server_patch_discord_notifications');
});
// Remove server patch notification fields from telegram notification settings
Schema::table('telegram_notification_settings', function (Blueprint $table) {
$table->dropColumn([
'server_patch_telegram_notifications',
'telegram_notifications_server_patch_thread_id',
]);
});
// Remove server patch notification fields from slack notification settings
Schema::table('slack_notification_settings', function (Blueprint $table) {
$table->dropColumn('server_patch_slack_notifications');
});
// Remove server patch notification fields from pushover notification settings
Schema::table('pushover_notification_settings', function (Blueprint $table) {
$table->dropColumn('server_patch_pushover_notifications');
});
}
};

View File

@@ -0,0 +1,53 @@
<x-emails.layout>
{{ $total_updates }} package updates are available for your server {{ $name }}.
## Summary
- Operating System: {{ ucfirst($osId) }}
- Package Manager: {{ $package_manager }}
- Total Updates: {{ $total_updates }}
## Available Updates
@if ($total_updates > 0)
@foreach ($updates as $update)
Package: {{ $update['package'] }} ({{ $update['architecture'] }}), from version {{ $update['current_version'] }} to {{ $update['new_version'] }} at repository {{ $update['repository'] ?? 'Unknown' }}
@endforeach
## Security Considerations
Some of these updates may include important security patches. We recommend reviewing and applying these updates promptly.
### Critical packages that may require container/server/service restarts:
@php
$criticalPackages = collect($updates)->filter(function ($update) {
return str_contains(strtolower($update['package']), 'docker') ||
str_contains(strtolower($update['package']), 'kernel') ||
str_contains(strtolower($update['package']), 'openssh') ||
str_contains(strtolower($update['package']), 'ssl');
});
@endphp
@if ($criticalPackages->count() > 0)
@foreach ($criticalPackages as $package)
- {{ $package['package'] }}: {{ $package['current_version'] }} {{ $package['new_version'] }}
@endforeach
@else
No critical packages requiring container restarts detected.
@endif
## Next Steps
1. Review the available updates
2. Plan maintenance window if critical packages are involved
3. Apply updates through the Coolify dashboard
4. Monitor services after updates are applied
@else
Your server is up to date! No packages require updating at this time.
@endif
---
You can manage server patches in your [Coolify Dashboard]({{ $server_url }}).
</x-emails.layout>

View File

@@ -78,6 +78,8 @@
label="Server Reachable" /> label="Server Reachable" />
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableDiscordNotifications" <x-forms.checkbox instantSave="saveModel" id="serverUnreachableDiscordNotifications"
label="Server Unreachable" /> label="Server Unreachable" />
<x-forms.checkbox instantSave="saveModel" id="serverPatchDiscordNotifications"
label="Server Patching" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -155,6 +155,8 @@
label="Server Reachable" /> label="Server Reachable" />
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableEmailNotifications" <x-forms.checkbox instantSave="saveModel" id="serverUnreachableEmailNotifications"
label="Server Unreachable" /> label="Server Unreachable" />
<x-forms.checkbox instantSave="saveModel" id="serverPatchEmailNotifications"
label="Server Patching" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -80,6 +80,8 @@
label="Server Reachable" /> label="Server Reachable" />
<x-forms.checkbox instantSave="saveModel" id="serverUnreachablePushoverNotifications" <x-forms.checkbox instantSave="saveModel" id="serverUnreachablePushoverNotifications"
label="Server Unreachable" /> label="Server Unreachable" />
<x-forms.checkbox instantSave="saveModel" id="serverPatchPushoverNotifications"
label="Server Patching" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -24,8 +24,8 @@
<x-forms.checkbox instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" /> <x-forms.checkbox instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" />
</div> </div>
<x-forms.input type="password" <x-forms.input type="password"
helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>" required helper="Create a Slack APP and generate a Incoming Webhook URL. <br><a class='inline-block underline dark:text-white' href='https://api.slack.com/apps' target='_blank'>Create Slack APP</a>"
id="slackWebhookUrl" label="Webhook" /> required id="slackWebhookUrl" label="Webhook" />
</form> </form>
<h2 class="mt-4">Notification Settings</h2> <h2 class="mt-4">Notification Settings</h2>
<p class="mb-4"> <p class="mb-4">
@@ -73,6 +73,7 @@
label="Server Reachable" /> label="Server Reachable" />
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableSlackNotifications" <x-forms.checkbox instantSave="saveModel" id="serverUnreachableSlackNotifications"
label="Server Unreachable" /> label="Server Unreachable" />
<x-forms.checkbox instantSave="saveModel" id="serverPatchSlackNotifications" label="Server Patching" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -27,8 +27,9 @@
<x-forms.input type="password" autocomplete="new-password" <x-forms.input type="password" autocomplete="new-password"
helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram." helper="Get it from the <a class='inline-block underline dark:text-white' href='https://t.me/botfather' target='_blank'>BotFather Bot</a> on Telegram."
required id="telegramToken" label="Bot API Token" /> required id="telegramToken" label="Bot API Token" />
<x-forms.input type="password" autocomplete="new-password" helper="Add your bot to a group chat and add its Chat ID here." required <x-forms.input type="password" autocomplete="new-password"
id="telegramChatId" label="Chat ID" /> helper="Add your bot to a group chat and add its Chat ID here." required id="telegramChatId"
label="Chat ID" />
</div> </div>
</form> </form>
<h2 class="mt-4">Notification Settings</h2> <h2 class="mt-4">Notification Settings</h2>
@@ -151,7 +152,6 @@
id="telegramNotificationsServerReachableThreadId" /> id="telegramNotificationsServerReachableThreadId" />
</div> </div>
<div class="pl-1 flex gap-2"> <div class="pl-1 flex gap-2">
<div class="w-96"> <div class="w-96">
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableTelegramNotifications" <x-forms.checkbox instantSave="saveModel" id="serverUnreachableTelegramNotifications"
@@ -160,6 +160,15 @@
<x-forms.input type="password" placeholder="Custom Telegram Thread ID" <x-forms.input type="password" placeholder="Custom Telegram Thread ID"
id="telegramNotificationsServerUnreachableThreadId" /> id="telegramNotificationsServerUnreachableThreadId" />
</div> </div>
<div class="pl-1 flex gap-2">
<div class="w-96">
<x-forms.checkbox instantSave="saveModel" id="serverPatchTelegramNotifications"
label="Server Patching" />
</div>
<x-forms.input type="password" placeholder="Custom Telegram Thread ID"
id="telegramNotificationsServerPatchThreadId" />
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -23,7 +23,7 @@
<x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')"> <x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">
Check Now</x-forms.button> Check Now</x-forms.button>
</div> </div>
<div>Update your servers automatically.</div> <div>Update your servers semi-automatically.</div>
<div> <div>
<div class="flex flex-col gap-6 pt-4"> <div class="flex flex-col gap-6 pt-4">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -38,18 +38,20 @@
<div class="text-green-500">Your server is up to date.</div> <div class="text-green-500">Your server is up to date.</div>
@endif @endif
@if (isset($updates) && count($updates) > 0) @if (isset($updates) && count($updates) > 0)
<x-modal-confirmation title="Confirm package update?" <div class="pb-2">
buttonTitle="Update All <x-modal-confirmation title="Confirm package update?"
buttonTitle="Update All
Packages" Packages"
isHighlightedButton submitAction="updateAllPackages" dispatchAction isHighlightedButton submitAction="updateAllPackages" dispatchAction
:actions="[ :actions="[
'All packages will be updated to the latest version.', 'All packages will be updated to the latest version.',
'This action could restart your currently running containers if docker will be updated.', 'This action could restart your currently running containers if docker will be updated.',
]" confirmationText="Update All Packages" ]" confirmationText="Update All Packages"
confirmationLabel="Please confirm the execution of the actions by entering the name below" confirmationLabel="Please confirm the execution of the actions by entering the name below"
shortConfirmationLabel="Name" :confirmWithPassword=false shortConfirmationLabel="Name" :confirmWithPassword=false
step2ButtonText="Update All step2ButtonText="Update All
Packages" /> Packages" />
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -65,7 +67,7 @@
@foreach ($updates as $update) @foreach ($updates as $update)
<tr> <tr>
<td class="inline-flex gap-2 justify-center items-center"> <td class="inline-flex gap-2 justify-center items-center">
@if (data_get_str($update, 'package')->contains('docker')) @if (data_get_str($update, 'package')->contains('docker') || data_get_str($update, 'package')->contains('kernel'))
<x-helper :helper="'This package will restart your currently running containers'"> <x-helper :helper="'This package will restart your currently running containers'">
<x-slot:icon> <x-slot:icon>
<svg class="w-4 h-4 text-red-500 block" <svg class="w-4 h-4 text-red-500 block"