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['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:

View File

@@ -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();

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'])]
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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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',
];

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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,
};

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" />
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableDiscordNotifications"
label="Server Unreachable" />
<x-forms.checkbox instantSave="saveModel" id="serverPatchDiscordNotifications"
label="Server Patching" />
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -24,8 +24,8 @@
<x-forms.checkbox instantSave="instantSaveSlackEnabled" id="slackEnabled" label="Enabled" />
</div>
<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
id="slackWebhookUrl" label="Webhook" />
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 id="slackWebhookUrl" label="Webhook" />
</form>
<h2 class="mt-4">Notification Settings</h2>
<p class="mb-4">
@@ -73,6 +73,7 @@
label="Server Reachable" />
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableSlackNotifications"
label="Server Unreachable" />
<x-forms.checkbox instantSave="saveModel" id="serverPatchSlackNotifications" label="Server Patching" />
</div>
</div>
</div>

View File

@@ -27,8 +27,9 @@
<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."
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
id="telegramChatId" label="Chat ID" />
<x-forms.input type="password" autocomplete="new-password"
helper="Add your bot to a group chat and add its Chat ID here." required id="telegramChatId"
label="Chat ID" />
</div>
</form>
<h2 class="mt-4">Notification Settings</h2>
@@ -151,7 +152,6 @@
id="telegramNotificationsServerReachableThreadId" />
</div>
<div class="pl-1 flex gap-2">
<div class="w-96">
<x-forms.checkbox instantSave="saveModel" id="serverUnreachableTelegramNotifications"
@@ -160,6 +160,15 @@
<x-forms.input type="password" placeholder="Custom Telegram Thread ID"
id="telegramNotificationsServerUnreachableThreadId" />
</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>

View File

@@ -23,7 +23,7 @@
<x-forms.button type="button" wire:click="$dispatch('checkForUpdatesDispatch')">
Check Now</x-forms.button>
</div>
<div>Update your servers automatically.</div>
<div>Update your servers semi-automatically.</div>
<div>
<div class="flex flex-col gap-6 pt-4">
<div class="flex flex-col">
@@ -38,6 +38,7 @@
<div class="text-green-500">Your server is up to date.</div>
@endif
@if (isset($updates) && count($updates) > 0)
<div class="pb-2">
<x-modal-confirmation title="Confirm package update?"
buttonTitle="Update All
Packages"
@@ -50,6 +51,7 @@
shortConfirmationLabel="Name" :confirmWithPassword=false
step2ButtonText="Update All
Packages" />
</div>
<table>
<thead>
<tr>
@@ -65,7 +67,7 @@
@foreach ($updates as $update)
<tr>
<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-slot:icon>
<svg class="w-4 h-4 text-red-500 block"