From 6ea6d2742bfbd8f0373f71729fce82a2a8eef705 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 26 May 2025 14:03:59 +0200 Subject: [PATCH] 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. --- app/Actions/Server/CheckUpdates.php | 4 - app/Console/Kernel.php | 4 + app/Jobs/ServerPatchCheckJob.php | 66 +++++ app/Livewire/Notifications/Discord.php | 5 + app/Livewire/Notifications/Email.php | 5 + app/Livewire/Notifications/Pushover.php | 5 + app/Livewire/Notifications/Slack.php | 5 + app/Livewire/Notifications/Telegram.php | 10 + app/Models/DiscordNotificationSettings.php | 2 + app/Models/EmailNotificationSettings.php | 2 + app/Models/PushoverNotificationSettings.php | 2 + app/Models/SlackNotificationSettings.php | 2 + app/Models/TelegramNotificationSettings.php | 4 + .../Channels/TelegramChannel.php | 1 + app/Notifications/Server/ServerPatchCheck.php | 245 ++++++++++++++++++ ..._100258_add_server_patch_notifications.php | 74 ++++++ .../views/emails/server-patches.blade.php | 53 ++++ .../livewire/notifications/discord.blade.php | 2 + .../livewire/notifications/email.blade.php | 2 + .../livewire/notifications/pushover.blade.php | 2 + .../livewire/notifications/slack.blade.php | 5 +- .../livewire/notifications/telegram.blade.php | 15 +- .../server/security/patches.blade.php | 26 +- 23 files changed, 520 insertions(+), 21 deletions(-) create mode 100644 app/Jobs/ServerPatchCheckJob.php create mode 100644 app/Notifications/Server/ServerPatchCheck.php create mode 100644 database/migrations/2025_05_26_100258_add_server_patch_notifications.php create mode 100644 resources/views/emails/server-patches.blade.php diff --git a/app/Actions/Server/CheckUpdates.php b/app/Actions/Server/CheckUpdates.php index 55b06e863..a8b1be11d 100644 --- a/app/Actions/Server/CheckUpdates.php +++ b/app/Actions/Server/CheckUpdates.php @@ -83,8 +83,6 @@ class CheckUpdates $out = $this->parseDnfOutput($output); $out['osId'] = $osId; $out['package_manager'] = $packageManager; - $rebootRequired = instant_remote_process(['LANG=C dnf needs-restarting -r'], $server); - $out['reboot_required'] = $rebootRequired !== '0'; return $out; case 'apt': @@ -94,8 +92,6 @@ class CheckUpdates $out = $this->parseAptOutput($output); $out['osId'] = $osId; $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; default: diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c03475647..372a6c119 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,6 +12,7 @@ use App\Jobs\PullTemplatesFromCDN; use App\Jobs\RegenerateSslCertJob; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; +use App\Jobs\ServerPatchCheckJob; use App\Jobs\ServerStorageCheckJob; use App\Jobs\UpdateCoolifyJob; use App\Models\InstanceSettings; @@ -175,6 +176,9 @@ class Kernel extends ConsoleKernel } $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 // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); diff --git a/app/Jobs/ServerPatchCheckJob.php b/app/Jobs/ServerPatchCheckJob.php new file mode 100644 index 000000000..c88770d3a --- /dev/null +++ b/app/Jobs/ServerPatchCheckJob.php @@ -0,0 +1,66 @@ +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(), + ]); + } + } +} diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 9489eb128..e0425fa17 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -56,6 +56,9 @@ class Discord extends Component #[Validate(['boolean'])] public bool $serverUnreachableDiscordNotifications = true; + #[Validate(['boolean'])] + public bool $serverPatchDiscordNotifications = false; + #[Validate(['boolean'])] public bool $discordPingEnabled = true; @@ -89,6 +92,7 @@ class Discord extends Component $this->settings->server_disk_usage_discord_notifications = $this->serverDiskUsageDiscordNotifications; $this->settings->server_reachable_discord_notifications = $this->serverReachableDiscordNotifications; $this->settings->server_unreachable_discord_notifications = $this->serverUnreachableDiscordNotifications; + $this->settings->server_patch_discord_notifications = $this->serverPatchDiscordNotifications; $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->serverReachableDiscordNotifications = $this->settings->server_reachable_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; } diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index c5f518e16..4b5d1c6a0 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -98,6 +98,9 @@ class Email extends Component #[Validate(['boolean'])] public bool $serverUnreachableEmailNotifications = true; + #[Validate(['boolean'])] + public bool $serverPatchEmailNotifications = false; + #[Validate(['nullable', 'email'])] public ?string $testEmailAddress = null; @@ -146,6 +149,7 @@ class Email extends Component $this->settings->server_disk_usage_email_notifications = $this->serverDiskUsageEmailNotifications; $this->settings->server_reachable_email_notifications = $this->serverReachableEmailNotifications; $this->settings->server_unreachable_email_notifications = $this->serverUnreachableEmailNotifications; + $this->settings->server_patch_email_notifications = $this->serverPatchEmailNotifications; $this->settings->save(); } else { @@ -177,6 +181,7 @@ class Email extends Component $this->serverDiskUsageEmailNotifications = $this->settings->server_disk_usage_email_notifications; $this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications; $this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications; + $this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications; } } diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php index f1e4c464d..bd5ab79c8 100644 --- a/app/Livewire/Notifications/Pushover.php +++ b/app/Livewire/Notifications/Pushover.php @@ -64,6 +64,9 @@ class Pushover extends Component #[Validate(['boolean'])] public bool $serverUnreachablePushoverNotifications = true; + #[Validate(['boolean'])] + public bool $serverPatchPushoverNotifications = false; + public function mount() { try { @@ -95,6 +98,7 @@ class Pushover extends Component $this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications; $this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications; $this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications; + $this->settings->server_patch_pushover_notifications = $this->serverPatchPushoverNotifications; $this->settings->save(); refreshSession(); @@ -115,6 +119,7 @@ class Pushover extends Component $this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications; $this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications; $this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications; + $this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications; } } diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php index f47ad8a97..9c847ce57 100644 --- a/app/Livewire/Notifications/Slack.php +++ b/app/Livewire/Notifications/Slack.php @@ -61,6 +61,9 @@ class Slack extends Component #[Validate(['boolean'])] public bool $serverUnreachableSlackNotifications = true; + #[Validate(['boolean'])] + public bool $serverPatchSlackNotifications = false; + public function mount() { try { @@ -91,6 +94,7 @@ class Slack extends Component $this->settings->server_disk_usage_slack_notifications = $this->serverDiskUsageSlackNotifications; $this->settings->server_reachable_slack_notifications = $this->serverReachableSlackNotifications; $this->settings->server_unreachable_slack_notifications = $this->serverUnreachableSlackNotifications; + $this->settings->server_patch_slack_notifications = $this->serverPatchSlackNotifications; $this->settings->save(); refreshSession(); @@ -110,6 +114,7 @@ class Slack extends Component $this->serverDiskUsageSlackNotifications = $this->settings->server_disk_usage_slack_notifications; $this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications; $this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications; + $this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications; } } diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index 01afb4ac6..07393d4ea 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -64,6 +64,9 @@ class Telegram extends Component #[Validate(['boolean'])] public bool $serverUnreachableTelegramNotifications = true; + #[Validate(['boolean'])] + public bool $serverPatchTelegramNotifications = false; + #[Validate(['nullable', 'string'])] public ?string $telegramNotificationsDeploymentSuccessThreadId = null; @@ -100,6 +103,9 @@ class Telegram extends Component #[Validate(['nullable', 'string'])] public ?string $telegramNotificationsServerUnreachableThreadId = null; + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsServerPatchThreadId = null; + public function mount() { try { @@ -131,6 +137,7 @@ class Telegram extends Component $this->settings->server_disk_usage_telegram_notifications = $this->serverDiskUsageTelegramNotifications; $this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications; $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_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_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId; $this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId; + $this->settings->telegram_notifications_server_patch_thread_id = $this->telegramNotificationsServerPatchThreadId; $this->settings->save(); } else { @@ -163,6 +171,7 @@ class Telegram extends Component $this->serverDiskUsageTelegramNotifications = $this->settings->server_disk_usage_telegram_notifications; $this->serverReachableTelegramNotifications = $this->settings->server_reachable_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->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->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id; $this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id; + $this->telegramNotificationsServerPatchThreadId = $this->settings->telegram_notifications_server_patch_thread_id; } } diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php index 1ba16ccd8..34adfc997 100644 --- a/app/Models/DiscordNotificationSettings.php +++ b/app/Models/DiscordNotificationSettings.php @@ -28,6 +28,7 @@ class DiscordNotificationSettings extends Model 'server_disk_usage_discord_notifications', 'server_reachable_discord_notifications', 'server_unreachable_discord_notifications', + 'server_patch_discord_notifications', 'discord_ping_enabled', ]; @@ -46,6 +47,7 @@ class DiscordNotificationSettings extends Model 'server_disk_usage_discord_notifications' => 'boolean', 'server_reachable_discord_notifications' => 'boolean', 'server_unreachable_discord_notifications' => 'boolean', + 'server_patch_discord_notifications' => 'boolean', 'discord_ping_enabled' => 'boolean', ]; diff --git a/app/Models/EmailNotificationSettings.php b/app/Models/EmailNotificationSettings.php index 445987619..39617b4cf 100644 --- a/app/Models/EmailNotificationSettings.php +++ b/app/Models/EmailNotificationSettings.php @@ -35,6 +35,7 @@ class EmailNotificationSettings extends Model 'scheduled_task_success_email_notifications', 'scheduled_task_failure_email_notifications', 'server_disk_usage_email_notifications', + 'server_patch_email_notifications', ]; protected $casts = [ @@ -61,6 +62,7 @@ class EmailNotificationSettings extends Model 'scheduled_task_success_email_notifications' => 'boolean', 'scheduled_task_failure_email_notifications' => 'boolean', 'server_disk_usage_email_notifications' => 'boolean', + 'server_patch_email_notifications' => 'boolean', ]; public function team() diff --git a/app/Models/PushoverNotificationSettings.php b/app/Models/PushoverNotificationSettings.php index e3191dcc3..a75fd71d7 100644 --- a/app/Models/PushoverNotificationSettings.php +++ b/app/Models/PushoverNotificationSettings.php @@ -29,6 +29,7 @@ class PushoverNotificationSettings extends Model 'server_disk_usage_pushover_notifications', 'server_reachable_pushover_notifications', 'server_unreachable_pushover_notifications', + 'server_patch_pushover_notifications', ]; protected $casts = [ @@ -47,6 +48,7 @@ class PushoverNotificationSettings extends Model 'server_disk_usage_pushover_notifications' => 'boolean', 'server_reachable_pushover_notifications' => 'boolean', 'server_unreachable_pushover_notifications' => 'boolean', + 'server_patch_pushover_notifications' => 'boolean', ]; public function team() diff --git a/app/Models/SlackNotificationSettings.php b/app/Models/SlackNotificationSettings.php index 48153f2ea..2b52bfd5b 100644 --- a/app/Models/SlackNotificationSettings.php +++ b/app/Models/SlackNotificationSettings.php @@ -28,6 +28,7 @@ class SlackNotificationSettings extends Model 'server_disk_usage_slack_notifications', 'server_reachable_slack_notifications', 'server_unreachable_slack_notifications', + 'server_patch_slack_notifications', ]; protected $casts = [ @@ -45,6 +46,7 @@ class SlackNotificationSettings extends Model 'server_disk_usage_slack_notifications' => 'boolean', 'server_reachable_slack_notifications' => 'boolean', 'server_unreachable_slack_notifications' => 'boolean', + 'server_patch_slack_notifications' => 'boolean', ]; public function team() diff --git a/app/Models/TelegramNotificationSettings.php b/app/Models/TelegramNotificationSettings.php index 78bd841bd..94315ee30 100644 --- a/app/Models/TelegramNotificationSettings.php +++ b/app/Models/TelegramNotificationSettings.php @@ -29,6 +29,7 @@ class TelegramNotificationSettings extends Model 'server_disk_usage_telegram_notifications', 'server_reachable_telegram_notifications', 'server_unreachable_telegram_notifications', + 'server_patch_telegram_notifications', 'telegram_notifications_deployment_success_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_reachable_thread_id', 'telegram_notifications_server_unreachable_thread_id', + 'telegram_notifications_server_patch_thread_id', ]; protected $casts = [ @@ -59,6 +61,7 @@ class TelegramNotificationSettings extends Model 'server_disk_usage_telegram_notifications' => 'boolean', 'server_reachable_telegram_notifications' => 'boolean', 'server_unreachable_telegram_notifications' => 'boolean', + 'server_patch_telegram_notifications' => 'boolean', 'telegram_notifications_deployment_success_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_reachable_thread_id' => 'encrypted', 'telegram_notifications_server_unreachable_thread_id' => 'encrypted', + 'telegram_notifications_server_patch_thread_id' => 'encrypted', ]; public function team() diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index ea4ab7171..c2fa3ff10 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -34,6 +34,7 @@ class TelegramChannel \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\Reachable::class => $settings->telegram_notifications_server_reachable_thread_id, + \App\Notifications\Server\ServerPatchCheck::class => $settings->telegram_notifications_server_patch_thread_id, default => null, }; diff --git a/app/Notifications/Server/ServerPatchCheck.php b/app/Notifications/Server/ServerPatchCheck.php new file mode 100644 index 000000000..8453e0dd6 --- /dev/null +++ b/app/Notifications/Server/ServerPatchCheck.php @@ -0,0 +1,245 @@ +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() + ); + } +} diff --git a/database/migrations/2025_05_26_100258_add_server_patch_notifications.php b/database/migrations/2025_05_26_100258_add_server_patch_notifications.php new file mode 100644 index 000000000..6d6611173 --- /dev/null +++ b/database/migrations/2025_05_26_100258_add_server_patch_notifications.php @@ -0,0 +1,74 @@ +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'); + }); + } +}; diff --git a/resources/views/emails/server-patches.blade.php b/resources/views/emails/server-patches.blade.php new file mode 100644 index 000000000..37644eaa2 --- /dev/null +++ b/resources/views/emails/server-patches.blade.php @@ -0,0 +1,53 @@ + +{{ $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 }}). + diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php index 9b48c9793..a65be078f 100644 --- a/resources/views/livewire/notifications/discord.blade.php +++ b/resources/views/livewire/notifications/discord.blade.php @@ -78,6 +78,8 @@ label="Server Reachable" /> + diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 194cb3b43..e4d8bf31d 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -155,6 +155,8 @@ label="Server Reachable" /> + diff --git a/resources/views/livewire/notifications/pushover.blade.php b/resources/views/livewire/notifications/pushover.blade.php index c8dd777d8..697597acb 100644 --- a/resources/views/livewire/notifications/pushover.blade.php +++ b/resources/views/livewire/notifications/pushover.blade.php @@ -80,6 +80,8 @@ label="Server Reachable" /> + diff --git a/resources/views/livewire/notifications/slack.blade.php b/resources/views/livewire/notifications/slack.blade.php index 28bfe509f..85b9a2de7 100644 --- a/resources/views/livewire/notifications/slack.blade.php +++ b/resources/views/livewire/notifications/slack.blade.php @@ -24,8 +24,8 @@ + helper="Create a Slack APP and generate a Incoming Webhook URL.
Create Slack APP" + required id="slackWebhookUrl" label="Webhook" />

Notification Settings

@@ -73,6 +73,7 @@ label="Server Reachable" /> + diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index ecc9360fa..b1e6eccf7 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -27,8 +27,9 @@ - +

Notification Settings

@@ -151,7 +152,6 @@ id="telegramNotificationsServerReachableThreadId" /> -
+ +
+
+ +
+ +
diff --git a/resources/views/livewire/server/security/patches.blade.php b/resources/views/livewire/server/security/patches.blade.php index 7dc2748fc..33a9bba54 100644 --- a/resources/views/livewire/server/security/patches.blade.php +++ b/resources/views/livewire/server/security/patches.blade.php @@ -23,7 +23,7 @@ Check Now -
Update your servers automatically.
+
Update your servers semi-automatically.
@@ -38,18 +38,20 @@
Your server is up to date.
@endif @if (isset($updates) && count($updates) > 0) - + +
@@ -65,7 +67,7 @@ @foreach ($updates as $update)
- @if (data_get_str($update, 'package')->contains('docker')) + @if (data_get_str($update, 'package')->contains('docker') || data_get_str($update, 'package')->contains('kernel'))