diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 8671a5f27..2f2e2096b 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -13,7 +13,7 @@ class StartProxy { use AsAction; - public function handle(Server $server, bool $async = true, bool $force = false): string|Activity + public function handle(Server $server, bool $async = true, bool $force = false, bool $restarting = false): string|Activity { $proxyType = $server->proxyType(); if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { @@ -22,7 +22,10 @@ public function handle(Server $server, bool $async = true, bool $force = false): $server->proxy->set('status', 'starting'); $server->save(); $server->refresh(); - ProxyStatusChangedUI::dispatch($server->team_id); + + if (! $restarting) { + ProxyStatusChangedUI::dispatch($server->team_id); + } $commands = collect([]); $proxy_path = $server->proxyPath(); diff --git a/app/Actions/Proxy/StopProxy.php b/app/Actions/Proxy/StopProxy.php index 29cc63b40..a11754cd0 100644 --- a/app/Actions/Proxy/StopProxy.php +++ b/app/Actions/Proxy/StopProxy.php @@ -12,13 +12,16 @@ class StopProxy { use AsAction; - public function handle(Server $server, bool $forceStop = true, int $timeout = 30) + public function handle(Server $server, bool $forceStop = true, int $timeout = 30, bool $restarting = false) { try { $containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; $server->proxy->status = 'stopping'; $server->save(); - ProxyStatusChangedUI::dispatch($server->team_id); + + if (! $restarting) { + ProxyStatusChangedUI::dispatch($server->team_id); + } instant_remote_process(command: [ "docker stop --time=$timeout $containerName", @@ -32,7 +35,10 @@ public function handle(Server $server, bool $forceStop = true, int $timeout = 30 return handleError($e); } finally { ProxyDashboardCacheService::clearCache($server); - ProxyStatusChanged::dispatch($server->id); + + if (! $restarting) { + ProxyStatusChanged::dispatch($server->id); + } } } } diff --git a/app/Console/Commands/CheckTraefikVersionCommand.php b/app/Console/Commands/CheckTraefikVersionCommand.php new file mode 100644 index 000000000..48cc78093 --- /dev/null +++ b/app/Console/Commands/CheckTraefikVersionCommand.php @@ -0,0 +1,30 @@ +info('Checking Traefik versions on all servers...'); + + try { + CheckTraefikVersionJob::dispatch(); + $this->info('Traefik version check job dispatched successfully.'); + $this->info('Notifications will be sent to teams with outdated Traefik versions.'); + + return Command::SUCCESS; + } catch (\Exception $e) { + $this->error('Failed to dispatch Traefik version check job: '.$e->getMessage()); + + return Command::FAILURE; + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c2ea27274..832bed5ae 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -5,6 +5,7 @@ use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckHelperImageJob; +use App\Jobs\CheckTraefikVersionJob; use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\PullChangelog; use App\Jobs\PullTemplatesFromCDN; @@ -83,6 +84,8 @@ protected function schedule(Schedule $schedule): void $this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily(); + $this->scheduleInstance->job(new CheckTraefikVersionJob)->weekly()->sundays()->at('00:00')->timezone($this->instanceTimezone)->onOneServer(); + $this->scheduleInstance->command('cleanup:database --yes')->daily(); $this->scheduleInstance->command('uploads:clear')->everyTwoMinutes(); } diff --git a/app/Data/ServerMetadata.php b/app/Data/ServerMetadata.php index d95944b15..cdd9c8c08 100644 --- a/app/Data/ServerMetadata.php +++ b/app/Data/ServerMetadata.php @@ -10,6 +10,8 @@ class ServerMetadata extends Data { public function __construct( public ?ProxyTypes $type, - public ?ProxyStatus $status + public ?ProxyStatus $status, + public ?string $last_saved_settings = null, + public ?string $last_applied_settings = null ) {} } diff --git a/app/Jobs/CheckTraefikVersionJob.php b/app/Jobs/CheckTraefikVersionJob.php new file mode 100644 index 000000000..925c8ba7d --- /dev/null +++ b/app/Jobs/CheckTraefikVersionJob.php @@ -0,0 +1,225 @@ + $branches]); + + // Query all servers with Traefik proxy that are reachable + $servers = Server::whereNotNull('proxy') + ->whereRelation('settings', 'is_reachable', true) + ->whereRelation('settings', 'is_usable', true) + ->get() + ->filter(fn ($server) => $server->proxyType() === ProxyTypes::TRAEFIK->value); + + $serverCount = $servers->count(); + Log::info("CheckTraefikVersionJob: Found {$serverCount} server(s) with Traefik proxy"); + + if ($serverCount === 0) { + Log::info('CheckTraefikVersionJob: No Traefik servers found, job completed'); + + return; + } + + $outdatedServers = collect(); + + // Phase 1: Scan servers and detect versions + Log::info('CheckTraefikVersionJob: Phase 1 - Scanning servers and detecting versions'); + + foreach ($servers as $server) { + $currentVersion = getTraefikVersionFromDockerCompose($server); + + Log::info("CheckTraefikVersionJob: Server '{$server->name}' - Detected version: ".($currentVersion ?? 'unable to detect')); + + // Update detected version in database + $server->update(['detected_traefik_version' => $currentVersion]); + + if (! $currentVersion) { + Log::warning("CheckTraefikVersionJob: Server '{$server->name}' - Unable to detect version, skipping"); + + continue; + } + + // Check if image tag is 'latest' by inspecting the image + $imageTag = instant_remote_process([ + "docker inspect coolify-proxy --format '{{.Config.Image}}' 2>/dev/null", + ], $server, false); + + if (str_contains(strtolower(trim($imageTag)), ':latest')) { + Log::info("CheckTraefikVersionJob: Server '{$server->name}' uses 'latest' tag, skipping notification (UI warning only)"); + + continue; + } + + // Parse current version to extract major.minor.patch + $current = ltrim($currentVersion, 'v'); + if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) { + Log::warning("CheckTraefikVersionJob: Server '{$server->name}' - Invalid version format '{$current}', skipping"); + + continue; + } + + $currentBranch = $matches[1]; // e.g., "3.6" + $currentPatch = $matches[2]; // e.g., "0" + + Log::debug("CheckTraefikVersionJob: Server '{$server->name}' - Parsed branch: {$currentBranch}, patch: {$currentPatch}"); + + // Find the latest version for this branch + $latestForBranch = $traefikVersions["v{$currentBranch}"] ?? null; + + if (! $latestForBranch) { + // User is on a branch we don't track - check if newer branches exist + Log::debug("CheckTraefikVersionJob: Server '{$server->name}' - Branch v{$currentBranch} not tracked, checking for newer branches"); + + $newestBranch = null; + $newestVersion = null; + + foreach ($traefikVersions as $branch => $version) { + $branchNum = ltrim($branch, 'v'); + if (version_compare($branchNum, $currentBranch, '>')) { + if (! $newestVersion || version_compare($version, $newestVersion, '>')) { + $newestBranch = $branchNum; + $newestVersion = $version; + } + } + } + + if ($newestVersion) { + Log::info("CheckTraefikVersionJob: Server '{$server->name}' is outdated - on {$current}, newer branch {$newestBranch} with version {$newestVersion} available"); + $server->outdatedInfo = [ + 'current' => $current, + 'latest' => $newestVersion, + 'type' => 'minor_upgrade', + ]; + $outdatedServers->push($server); + } else { + Log::info("CheckTraefikVersionJob: Server '{$server->name}' on {$current} - no newer branches available"); + } + + continue; + } + + // Compare patch version within the same branch + $latest = ltrim($latestForBranch, 'v'); + + if (version_compare($current, $latest, '<')) { + Log::info("CheckTraefikVersionJob: Server '{$server->name}' is outdated - current: {$current}, latest for branch: {$latest}"); + $server->outdatedInfo = [ + 'current' => $current, + 'latest' => $latest, + 'type' => 'patch_update', + ]; + $outdatedServers->push($server); + } else { + // Check if newer branches exist (user is up to date on their branch, but branch might be old) + $newestBranch = null; + $newestVersion = null; + + foreach ($traefikVersions as $branch => $version) { + $branchNum = ltrim($branch, 'v'); + if (version_compare($branchNum, $currentBranch, '>')) { + if (! $newestVersion || version_compare($version, $newestVersion, '>')) { + $newestBranch = $branchNum; + $newestVersion = $version; + } + } + } + + if ($newestVersion) { + Log::info("CheckTraefikVersionJob: Server '{$server->name}' up to date on branch {$currentBranch} ({$current}), but newer branch {$newestBranch} available ({$newestVersion})"); + $server->outdatedInfo = [ + 'current' => $current, + 'latest' => $newestVersion, + 'type' => 'minor_upgrade', + ]; + $outdatedServers->push($server); + } else { + Log::info("CheckTraefikVersionJob: Server '{$server->name}' is fully up to date - version: {$current}"); + } + } + } + + $outdatedCount = $outdatedServers->count(); + Log::info("CheckTraefikVersionJob: Phase 1 complete - Found {$outdatedCount} outdated server(s)"); + + if ($outdatedCount === 0) { + Log::info('CheckTraefikVersionJob: All servers are up to date, no notifications to send'); + + return; + } + + // Phase 2: Group by team and send notifications + Log::info('CheckTraefikVersionJob: Phase 2 - Grouping by team and sending notifications'); + + $serversByTeam = $outdatedServers->groupBy('team_id'); + $teamCount = $serversByTeam->count(); + + Log::info("CheckTraefikVersionJob: Grouped outdated servers into {$teamCount} team(s)"); + + foreach ($serversByTeam as $teamId => $teamServers) { + $team = Team::find($teamId); + if (! $team) { + Log::warning("CheckTraefikVersionJob: Team ID {$teamId} not found, skipping"); + + continue; + } + + $serverNames = $teamServers->pluck('name')->join(', '); + Log::info("CheckTraefikVersionJob: Sending notification to team '{$team->name}' for {$teamServers->count()} server(s): {$serverNames}"); + + // Send one notification per team with all outdated servers (with per-server info) + $team->notify(new TraefikVersionOutdated($teamServers)); + + Log::info("CheckTraefikVersionJob: Notification sent to team '{$team->name}'"); + } + + Log::info('CheckTraefikVersionJob: Job completed successfully'); + } catch (\Throwable $e) { + Log::error('CheckTraefikVersionJob: Error checking Traefik versions: '.$e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + throw $e; + } + } +} diff --git a/app/Jobs/RestartProxyJob.php b/app/Jobs/RestartProxyJob.php index dba4f4ac8..e3e809c8d 100644 --- a/app/Jobs/RestartProxyJob.php +++ b/app/Jobs/RestartProxyJob.php @@ -31,12 +31,12 @@ public function __construct(public Server $server) {} public function handle() { try { - StopProxy::run($this->server); + StopProxy::run($this->server, restarting: true); $this->server->proxy->force_stop = false; $this->server->save(); - StartProxy::run($this->server, force: true); + StartProxy::run($this->server, force: true, restarting: true); } catch (\Throwable $e) { return handleError($e); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 7912c4b85..9f1eac4d2 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -347,6 +347,8 @@ public function selectProxy(?string $proxyType = null) } $this->createdServer->proxy->type = $proxyType; $this->createdServer->proxy->status = 'exited'; + $this->createdServer->proxy->last_saved_settings = null; + $this->createdServer->proxy->last_applied_settings = null; $this->createdServer->save(); $this->getProjects(); } diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 28d1cb866..b914fbd94 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -62,6 +62,9 @@ class Discord extends Component #[Validate(['boolean'])] public bool $serverPatchDiscordNotifications = false; + #[Validate(['boolean'])] + public bool $traefikOutdatedDiscordNotifications = true; + #[Validate(['boolean'])] public bool $discordPingEnabled = true; @@ -98,6 +101,7 @@ public function syncData(bool $toModel = false) $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->traefik_outdated_discord_notifications = $this->traefikOutdatedDiscordNotifications; $this->settings->discord_ping_enabled = $this->discordPingEnabled; @@ -120,6 +124,7 @@ public function syncData(bool $toModel = false) $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->traefikOutdatedDiscordNotifications = $this->settings->traefik_outdated_discord_notifications; $this->discordPingEnabled = $this->settings->discord_ping_enabled; } diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index d62a08417..847f10765 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -104,6 +104,9 @@ class Email extends Component #[Validate(['boolean'])] public bool $serverPatchEmailNotifications = false; + #[Validate(['boolean'])] + public bool $traefikOutdatedEmailNotifications = true; + #[Validate(['nullable', 'email'])] public ?string $testEmailAddress = null; @@ -155,6 +158,7 @@ public function syncData(bool $toModel = false) $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->traefik_outdated_email_notifications = $this->traefikOutdatedEmailNotifications; $this->settings->save(); } else { @@ -187,6 +191,7 @@ public function syncData(bool $toModel = false) $this->serverReachableEmailNotifications = $this->settings->server_reachable_email_notifications; $this->serverUnreachableEmailNotifications = $this->settings->server_unreachable_email_notifications; $this->serverPatchEmailNotifications = $this->settings->server_patch_email_notifications; + $this->traefikOutdatedEmailNotifications = $this->settings->traefik_outdated_email_notifications; } } diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php index 9c7ff64ad..d79eea87b 100644 --- a/app/Livewire/Notifications/Pushover.php +++ b/app/Livewire/Notifications/Pushover.php @@ -70,6 +70,9 @@ class Pushover extends Component #[Validate(['boolean'])] public bool $serverPatchPushoverNotifications = false; + #[Validate(['boolean'])] + public bool $traefikOutdatedPushoverNotifications = true; + public function mount() { try { @@ -104,6 +107,7 @@ public function syncData(bool $toModel = false) $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->traefik_outdated_pushover_notifications = $this->traefikOutdatedPushoverNotifications; $this->settings->save(); refreshSession(); @@ -125,6 +129,7 @@ public function syncData(bool $toModel = false) $this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications; $this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications; $this->serverPatchPushoverNotifications = $this->settings->server_patch_pushover_notifications; + $this->traefikOutdatedPushoverNotifications = $this->settings->traefik_outdated_pushover_notifications; } } diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php index d21399c42..fa8c97ae9 100644 --- a/app/Livewire/Notifications/Slack.php +++ b/app/Livewire/Notifications/Slack.php @@ -67,6 +67,9 @@ class Slack extends Component #[Validate(['boolean'])] public bool $serverPatchSlackNotifications = false; + #[Validate(['boolean'])] + public bool $traefikOutdatedSlackNotifications = true; + public function mount() { try { @@ -100,6 +103,7 @@ public function syncData(bool $toModel = false) $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->traefik_outdated_slack_notifications = $this->traefikOutdatedSlackNotifications; $this->settings->save(); refreshSession(); @@ -120,6 +124,7 @@ public function syncData(bool $toModel = false) $this->serverReachableSlackNotifications = $this->settings->server_reachable_slack_notifications; $this->serverUnreachableSlackNotifications = $this->settings->server_unreachable_slack_notifications; $this->serverPatchSlackNotifications = $this->settings->server_patch_slack_notifications; + $this->traefikOutdatedSlackNotifications = $this->settings->traefik_outdated_slack_notifications; } } diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index ca9df47c1..fc3966cf6 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -70,6 +70,9 @@ class Telegram extends Component #[Validate(['boolean'])] public bool $serverPatchTelegramNotifications = false; + #[Validate(['boolean'])] + public bool $traefikOutdatedTelegramNotifications = true; + #[Validate(['nullable', 'string'])] public ?string $telegramNotificationsDeploymentSuccessThreadId = null; @@ -109,6 +112,9 @@ class Telegram extends Component #[Validate(['nullable', 'string'])] public ?string $telegramNotificationsServerPatchThreadId = null; + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsTraefikOutdatedThreadId = null; + public function mount() { try { @@ -143,6 +149,7 @@ public function syncData(bool $toModel = false) $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->traefik_outdated_telegram_notifications = $this->traefikOutdatedTelegramNotifications; $this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId; $this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId; @@ -157,6 +164,7 @@ public function syncData(bool $toModel = false) $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->telegram_notifications_traefik_outdated_thread_id = $this->telegramNotificationsTraefikOutdatedThreadId; $this->settings->save(); } else { @@ -177,6 +185,7 @@ public function syncData(bool $toModel = false) $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->traefikOutdatedTelegramNotifications = $this->settings->traefik_outdated_telegram_notifications; $this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id; $this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id; @@ -191,6 +200,7 @@ public function syncData(bool $toModel = false) $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; + $this->telegramNotificationsTraefikOutdatedThreadId = $this->settings->telegram_notifications_traefik_outdated_thread_id; } } diff --git a/app/Livewire/Notifications/Webhook.php b/app/Livewire/Notifications/Webhook.php index cf4e71105..8af70c6eb 100644 --- a/app/Livewire/Notifications/Webhook.php +++ b/app/Livewire/Notifications/Webhook.php @@ -62,6 +62,9 @@ class Webhook extends Component #[Validate(['boolean'])] public bool $serverPatchWebhookNotifications = false; + #[Validate(['boolean'])] + public bool $traefikOutdatedWebhookNotifications = true; + public function mount() { try { @@ -95,6 +98,7 @@ public function syncData(bool $toModel = false) $this->settings->server_reachable_webhook_notifications = $this->serverReachableWebhookNotifications; $this->settings->server_unreachable_webhook_notifications = $this->serverUnreachableWebhookNotifications; $this->settings->server_patch_webhook_notifications = $this->serverPatchWebhookNotifications; + $this->settings->traefik_outdated_webhook_notifications = $this->traefikOutdatedWebhookNotifications; $this->settings->save(); refreshSession(); @@ -115,6 +119,7 @@ public function syncData(bool $toModel = false) $this->serverReachableWebhookNotifications = $this->settings->server_reachable_webhook_notifications; $this->serverUnreachableWebhookNotifications = $this->settings->server_unreachable_webhook_notifications; $this->serverPatchWebhookNotifications = $this->settings->server_patch_webhook_notifications; + $this->traefikOutdatedWebhookNotifications = $this->settings->traefik_outdated_webhook_notifications; } } diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php index 6baa54672..a759232cc 100644 --- a/app/Livewire/Server/Navbar.php +++ b/app/Livewire/Server/Navbar.php @@ -5,7 +5,6 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StopProxy; -use App\Jobs\RestartProxyJob; use App\Models\Server; use App\Services\ProxyDashboardCacheService; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -61,7 +60,13 @@ public function restart() { try { $this->authorize('manageProxy', $this->server); - RestartProxyJob::dispatch($this->server); + StopProxy::run($this->server, restarting: true); + + $this->server->proxy->force_stop = false; + $this->server->save(); + + $activity = StartProxy::run($this->server, force: true, restarting: true); + $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { return handleError($e, $this); } @@ -118,19 +123,28 @@ public function checkProxyStatus() public function showNotification() { + $previousStatus = $this->proxyStatus; $this->server->refresh(); $this->proxyStatus = $this->server->proxy->status ?? 'unknown'; switch ($this->proxyStatus) { case 'running': $this->loadProxyConfiguration(); - $this->dispatch('success', 'Proxy is running.'); + // Only show "Proxy is running" notification when transitioning from a stopped/error state + // Don't show during normal start/restart flows (starting, restarting, stopping) + if (in_array($previousStatus, ['exited', 'stopped', 'unknown', null])) { + $this->dispatch('success', 'Proxy is running.'); + } break; case 'restarting': $this->dispatch('info', 'Initiating proxy restart.'); break; case 'exited': - $this->dispatch('info', 'Proxy has exited.'); + // Only show "Proxy has exited" notification when transitioning from running state + // Don't show during normal stop/restart flows (stopping, restarting) + if (in_array($previousStatus, ['running'])) { + $this->dispatch('info', 'Proxy has exited.'); + } break; case 'stopping': $this->dispatch('info', 'Proxy is stopping.'); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index bc7e9bde4..e95eb4d3b 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -4,8 +4,10 @@ use App\Actions\Proxy\GetProxyConfiguration; use App\Actions\Proxy\SaveProxyConfiguration; +use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Facades\File; use Livewire\Component; class Proxy extends Component @@ -144,4 +146,125 @@ public function loadProxyConfiguration() return handleError($e, $this); } } + + public function getLatestTraefikVersionProperty(): ?string + { + try { + $versionsPath = base_path('versions.json'); + if (! File::exists($versionsPath)) { + return null; + } + + $versions = json_decode(File::get($versionsPath), true); + $traefikVersions = data_get($versions, 'traefik'); + + if (! $traefikVersions) { + return null; + } + + // Handle new structure (array of branches) + if (is_array($traefikVersions)) { + $currentVersion = $this->server->detected_traefik_version; + + // If we have a current version, try to find matching branch + if ($currentVersion && $currentVersion !== 'latest') { + $current = ltrim($currentVersion, 'v'); + if (preg_match('/^(\d+\.\d+)/', $current, $matches)) { + $branch = "v{$matches[1]}"; + if (isset($traefikVersions[$branch])) { + $version = $traefikVersions[$branch]; + + return str_starts_with($version, 'v') ? $version : "v{$version}"; + } + } + } + + // Return the newest available version + $newestVersion = collect($traefikVersions) + ->map(fn ($v) => ltrim($v, 'v')) + ->sortBy(fn ($v) => $v, SORT_NATURAL) + ->last(); + + return $newestVersion ? "v{$newestVersion}" : null; + } + + // Handle old structure (simple string) for backward compatibility + return str_starts_with($traefikVersions, 'v') ? $traefikVersions : "v{$traefikVersions}"; + } catch (\Throwable $e) { + return null; + } + } + + public function getIsTraefikOutdatedProperty(): bool + { + if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value) { + return false; + } + + $currentVersion = $this->server->detected_traefik_version; + if (! $currentVersion || $currentVersion === 'latest') { + return false; + } + + $latestVersion = $this->latestTraefikVersion; + if (! $latestVersion) { + return false; + } + + // Compare versions (strip 'v' prefix) + $current = ltrim($currentVersion, 'v'); + $latest = ltrim($latestVersion, 'v'); + + return version_compare($current, $latest, '<'); + } + + public function getNewerTraefikBranchAvailableProperty(): ?string + { + try { + if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value) { + return null; + } + + $currentVersion = $this->server->detected_traefik_version; + if (! $currentVersion || $currentVersion === 'latest') { + return null; + } + + $versionsPath = base_path('versions.json'); + if (! File::exists($versionsPath)) { + return null; + } + + $versions = json_decode(File::get($versionsPath), true); + $traefikVersions = data_get($versions, 'traefik'); + + if (! is_array($traefikVersions)) { + return null; + } + + // Extract current branch (e.g., "3.5" from "3.5.6") + $current = ltrim($currentVersion, 'v'); + if (! preg_match('/^(\d+\.\d+)/', $current, $matches)) { + return null; + } + + $currentBranch = $matches[1]; + + // Find the newest branch that's greater than current + $newestVersion = null; + foreach ($traefikVersions as $branch => $version) { + $branchNum = ltrim($branch, 'v'); + if (version_compare($branchNum, $currentBranch, '>')) { + $cleanVersion = ltrim($version, 'v'); + if (! $newestVersion || version_compare($cleanVersion, $newestVersion, '>')) { + $newestVersion = $cleanVersion; + } + } + } + + return $newestVersion ? "v{$newestVersion}" : null; + } catch (\Throwable $e) { + return null; + } + } } diff --git a/app/Models/DiscordNotificationSettings.php b/app/Models/DiscordNotificationSettings.php index 34adfc997..23e1f0f12 100644 --- a/app/Models/DiscordNotificationSettings.php +++ b/app/Models/DiscordNotificationSettings.php @@ -29,6 +29,7 @@ class DiscordNotificationSettings extends Model 'server_reachable_discord_notifications', 'server_unreachable_discord_notifications', 'server_patch_discord_notifications', + 'traefik_outdated_discord_notifications', 'discord_ping_enabled', ]; @@ -48,6 +49,7 @@ class DiscordNotificationSettings extends Model 'server_reachable_discord_notifications' => 'boolean', 'server_unreachable_discord_notifications' => 'boolean', 'server_patch_discord_notifications' => 'boolean', + 'traefik_outdated_discord_notifications' => 'boolean', 'discord_ping_enabled' => 'boolean', ]; diff --git a/app/Models/EmailNotificationSettings.php b/app/Models/EmailNotificationSettings.php index 39617b4cf..ee31a49b6 100644 --- a/app/Models/EmailNotificationSettings.php +++ b/app/Models/EmailNotificationSettings.php @@ -36,6 +36,7 @@ class EmailNotificationSettings extends Model 'scheduled_task_failure_email_notifications', 'server_disk_usage_email_notifications', 'server_patch_email_notifications', + 'traefik_outdated_email_notifications', ]; protected $casts = [ @@ -63,6 +64,7 @@ class EmailNotificationSettings extends Model 'scheduled_task_failure_email_notifications' => 'boolean', 'server_disk_usage_email_notifications' => 'boolean', 'server_patch_email_notifications' => 'boolean', + 'traefik_outdated_email_notifications' => 'boolean', ]; public function team() diff --git a/app/Models/PushoverNotificationSettings.php b/app/Models/PushoverNotificationSettings.php index a75fd71d7..189d05dd4 100644 --- a/app/Models/PushoverNotificationSettings.php +++ b/app/Models/PushoverNotificationSettings.php @@ -30,6 +30,7 @@ class PushoverNotificationSettings extends Model 'server_reachable_pushover_notifications', 'server_unreachable_pushover_notifications', 'server_patch_pushover_notifications', + 'traefik_outdated_pushover_notifications', ]; protected $casts = [ @@ -49,6 +50,7 @@ class PushoverNotificationSettings extends Model 'server_reachable_pushover_notifications' => 'boolean', 'server_unreachable_pushover_notifications' => 'boolean', 'server_patch_pushover_notifications' => 'boolean', + 'traefik_outdated_pushover_notifications' => 'boolean', ]; public function team() diff --git a/app/Models/Server.php b/app/Models/Server.php index e39526949..52dcce44f 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -167,6 +167,7 @@ protected static function booted() 'hetzner_server_id', 'hetzner_server_status', 'is_validating', + 'detected_traefik_version', ]; protected $guarded = []; diff --git a/app/Models/SlackNotificationSettings.php b/app/Models/SlackNotificationSettings.php index 2b52bfd5b..128b25221 100644 --- a/app/Models/SlackNotificationSettings.php +++ b/app/Models/SlackNotificationSettings.php @@ -29,6 +29,7 @@ class SlackNotificationSettings extends Model 'server_reachable_slack_notifications', 'server_unreachable_slack_notifications', 'server_patch_slack_notifications', + 'traefik_outdated_slack_notifications', ]; protected $casts = [ @@ -47,6 +48,7 @@ class SlackNotificationSettings extends Model 'server_reachable_slack_notifications' => 'boolean', 'server_unreachable_slack_notifications' => 'boolean', 'server_patch_slack_notifications' => 'boolean', + 'traefik_outdated_slack_notifications' => 'boolean', ]; public function team() diff --git a/app/Models/Team.php b/app/Models/Team.php index 6c30389ee..5cb186942 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -49,7 +49,9 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen protected static function booted() { static::created(function ($team) { - $team->emailNotificationSettings()->create(); + $team->emailNotificationSettings()->create([ + 'use_instance_email_settings' => isDev(), + ]); $team->discordNotificationSettings()->create(); $team->slackNotificationSettings()->create(); $team->telegramNotificationSettings()->create(); diff --git a/app/Models/TelegramNotificationSettings.php b/app/Models/TelegramNotificationSettings.php index 94315ee30..73889910e 100644 --- a/app/Models/TelegramNotificationSettings.php +++ b/app/Models/TelegramNotificationSettings.php @@ -30,6 +30,7 @@ class TelegramNotificationSettings extends Model 'server_reachable_telegram_notifications', 'server_unreachable_telegram_notifications', 'server_patch_telegram_notifications', + 'traefik_outdated_telegram_notifications', 'telegram_notifications_deployment_success_thread_id', 'telegram_notifications_deployment_failure_thread_id', @@ -43,6 +44,7 @@ class TelegramNotificationSettings extends Model 'telegram_notifications_server_reachable_thread_id', 'telegram_notifications_server_unreachable_thread_id', 'telegram_notifications_server_patch_thread_id', + 'telegram_notifications_traefik_outdated_thread_id', ]; protected $casts = [ @@ -62,6 +64,7 @@ class TelegramNotificationSettings extends Model 'server_reachable_telegram_notifications' => 'boolean', 'server_unreachable_telegram_notifications' => 'boolean', 'server_patch_telegram_notifications' => 'boolean', + 'traefik_outdated_telegram_notifications' => 'boolean', 'telegram_notifications_deployment_success_thread_id' => 'encrypted', 'telegram_notifications_deployment_failure_thread_id' => 'encrypted', @@ -75,6 +78,7 @@ class TelegramNotificationSettings extends Model 'telegram_notifications_server_reachable_thread_id' => 'encrypted', 'telegram_notifications_server_unreachable_thread_id' => 'encrypted', 'telegram_notifications_server_patch_thread_id' => 'encrypted', + 'telegram_notifications_traefik_outdated_thread_id' => 'encrypted', ]; public function team() diff --git a/app/Models/WebhookNotificationSettings.php b/app/Models/WebhookNotificationSettings.php index 4ca89e0d3..79bd0ae62 100644 --- a/app/Models/WebhookNotificationSettings.php +++ b/app/Models/WebhookNotificationSettings.php @@ -29,6 +29,7 @@ class WebhookNotificationSettings extends Model 'server_reachable_webhook_notifications', 'server_unreachable_webhook_notifications', 'server_patch_webhook_notifications', + 'traefik_outdated_webhook_notifications', ]; protected function casts(): array @@ -49,6 +50,7 @@ protected function casts(): array 'server_reachable_webhook_notifications' => 'boolean', 'server_unreachable_webhook_notifications' => 'boolean', 'server_patch_webhook_notifications' => 'boolean', + 'traefik_outdated_webhook_notifications' => 'boolean', ]; } diff --git a/app/Notifications/Server/TraefikVersionOutdated.php b/app/Notifications/Server/TraefikVersionOutdated.php new file mode 100644 index 000000000..61c2d2497 --- /dev/null +++ b/app/Notifications/Server/TraefikVersionOutdated.php @@ -0,0 +1,186 @@ +onQueue('high'); + } + + public function via(object $notifiable): array + { + return $notifiable->getEnabledChannels('traefik_outdated'); + } + + private function formatVersion(string $version): string + { + // Add 'v' prefix if not present for consistent display + return str_starts_with($version, 'v') ? $version : "v{$version}"; + } + + public function toMail($notifiable = null): MailMessage + { + $mail = new MailMessage; + $count = $this->servers->count(); + + $mail->subject("Coolify: Traefik proxy outdated on {$count} server(s)"); + $mail->view('emails.traefik-version-outdated', [ + 'servers' => $this->servers, + 'count' => $count, + ]); + + return $mail; + } + + public function toDiscord(): DiscordMessage + { + $count = $this->servers->count(); + $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade'); + + $description = "**{$count} server(s)** running outdated Traefik proxy. Update recommended for security and features.\n\n"; + $description .= "*Based on actual running container version*\n\n"; + $description .= "**Affected servers:**\n"; + + foreach ($this->servers as $server) { + $info = $server->outdatedInfo ?? []; + $current = $this->formatVersion($info['current'] ?? 'unknown'); + $latest = $this->formatVersion($info['latest'] ?? 'unknown'); + $type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)'; + $description .= "⢠{$server->name}: {$current} ā {$latest} {$type}\n"; + } + + $description .= "\nā ļø It is recommended to test before switching the production version."; + + if ($hasUpgrades) { + $description .= "\n\nš **For major/minor upgrades**: Read the Traefik changelog before upgrading to understand breaking changes."; + } + + return new DiscordMessage( + title: ':warning: Coolify: Traefik proxy outdated', + description: $description, + color: DiscordMessage::warningColor(), + ); + } + + public function toTelegram(): array + { + $count = $this->servers->count(); + $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade'); + + $message = "ā ļø Coolify: Traefik proxy outdated on {$count} server(s)!\n\n"; + $message .= "Update recommended for security and features.\n"; + $message .= "ā¹ļø Based on actual running container version\n\n"; + $message .= "š Affected servers:\n"; + + foreach ($this->servers as $server) { + $info = $server->outdatedInfo ?? []; + $current = $this->formatVersion($info['current'] ?? 'unknown'); + $latest = $this->formatVersion($info['latest'] ?? 'unknown'); + $type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)'; + $message .= "⢠{$server->name}: {$current} ā {$latest} {$type}\n"; + } + + $message .= "\nā ļø It is recommended to test before switching the production version."; + + if ($hasUpgrades) { + $message .= "\n\nš For major/minor upgrades: Read the Traefik changelog before upgrading to understand breaking changes."; + } + + return [ + 'message' => $message, + 'buttons' => [], + ]; + } + + public function toPushover(): PushoverMessage + { + $count = $this->servers->count(); + $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade'); + + $message = "Traefik proxy outdated on {$count} server(s)!\n"; + $message .= "Based on actual running container version\n\n"; + $message .= "Affected servers:\n"; + + foreach ($this->servers as $server) { + $info = $server->outdatedInfo ?? []; + $current = $this->formatVersion($info['current'] ?? 'unknown'); + $latest = $this->formatVersion($info['latest'] ?? 'unknown'); + $type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)'; + $message .= "⢠{$server->name}: {$current} ā {$latest} {$type}\n"; + } + + $message .= "\nIt is recommended to test before switching the production version."; + + if ($hasUpgrades) { + $message .= "\n\nFor major/minor upgrades: Read the Traefik changelog before upgrading."; + } + + return new PushoverMessage( + title: 'Traefik proxy outdated', + level: 'warning', + message: $message, + ); + } + + public function toSlack(): SlackMessage + { + $count = $this->servers->count(); + $hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade'); + + $description = "Traefik proxy outdated on {$count} server(s)!\n"; + $description .= "_Based on actual running container version_\n\n"; + $description .= "*Affected servers:*\n"; + + foreach ($this->servers as $server) { + $info = $server->outdatedInfo ?? []; + $current = $this->formatVersion($info['current'] ?? 'unknown'); + $latest = $this->formatVersion($info['latest'] ?? 'unknown'); + $type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)'; + $description .= "⢠`{$server->name}`: {$current} ā {$latest} {$type}\n"; + } + + $description .= "\n:warning: It is recommended to test before switching the production version."; + + if ($hasUpgrades) { + $description .= "\n\n:book: For major/minor upgrades: Read the Traefik changelog before upgrading to understand breaking changes."; + } + + return new SlackMessage( + title: 'Coolify: Traefik proxy outdated', + description: $description, + color: SlackMessage::warningColor() + ); + } + + public function toWebhook(): array + { + $servers = $this->servers->map(function ($server) { + $info = $server->outdatedInfo ?? []; + + return [ + 'name' => $server->name, + 'uuid' => $server->uuid, + 'current_version' => $info['current'] ?? 'unknown', + 'latest_version' => $info['latest'] ?? 'unknown', + 'update_type' => $info['type'] ?? 'patch_update', + ]; + })->toArray(); + + return [ + 'success' => false, + 'message' => 'Traefik proxy outdated', + 'event' => 'traefik_version_outdated', + 'affected_servers_count' => $this->servers->count(), + 'servers' => $servers, + ]; + } +} diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index b8274a3b3..beba22ca7 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -334,3 +334,94 @@ function generateDefaultProxyConfiguration(Server $server, array $custom_command return $config; } + +function getExactTraefikVersionFromContainer(Server $server): ?string +{ + try { + Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Checking for exact version"); + + // Method A: Execute traefik version command (most reliable) + $versionCommand = "docker exec coolify-proxy traefik version 2>/dev/null | grep -oP 'Version:\s+\K\d+\.\d+\.\d+'"; + Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Running: {$versionCommand}"); + + $output = instant_remote_process([$versionCommand], $server, false); + + if (! empty(trim($output))) { + $version = trim($output); + Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Detected exact version from command: {$version}"); + + return $version; + } + + // Method B: Try OCI label as fallback + $labelCommand = "docker inspect coolify-proxy --format '{{index .Config.Labels \"org.opencontainers.image.version\"}}' 2>/dev/null"; + Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Trying OCI label"); + + $label = instant_remote_process([$labelCommand], $server, false); + + if (! empty(trim($label))) { + // Extract version number from label (might have 'v' prefix) + if (preg_match('/(\d+\.\d+\.\d+)/', trim($label), $matches)) { + Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Detected from OCI label: {$matches[1]}"); + + return $matches[1]; + } + } + + Log::debug("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Could not detect exact version"); + + return null; + } catch (\Exception $e) { + Log::error("getExactTraefikVersionFromContainer: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage()); + + return null; + } +} + +function getTraefikVersionFromDockerCompose(Server $server): ?string +{ + try { + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Starting version detection"); + + // Try to get exact version from running container (e.g., "3.6.0") + $exactVersion = getExactTraefikVersionFromContainer($server); + if ($exactVersion) { + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Using exact version: {$exactVersion}"); + + return $exactVersion; + } + + // Fallback: Check image tag (current method) + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Falling back to image tag detection"); + + $containerName = 'coolify-proxy'; + $inspectCommand = "docker inspect {$containerName} --format '{{.Config.Image}}' 2>/dev/null"; + + $image = instant_remote_process([$inspectCommand], $server, false); + + if (empty(trim($image))) { + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Container '{$containerName}' not found or not running"); + + return null; + } + + $image = trim($image); + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Running container image: {$image}"); + + // Extract version from image string (e.g., "traefik:v3.6" or "traefik:3.6.0" or "traefik:latest") + if (preg_match('/traefik:(v?\d+\.\d+(?:\.\d+)?|latest)/i', $image, $matches)) { + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Extracted version from image tag: {$matches[1]}"); + + return $matches[1]; + } + + Log::debug("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Image format doesn't match expected pattern: {$image}"); + + return null; + } catch (\Exception $e) { + Log::error("getTraefikVersionFromDockerCompose: Server '{$server->name}' (ID: {$server->id}) - Error: ".$e->getMessage()); + ray('Error getting Traefik version from running container: '.$e->getMessage()); + + return null; + } +} diff --git a/database/migrations/2025_11_12_130931_add_traefik_version_tracking_to_servers_table.php b/database/migrations/2025_11_12_130931_add_traefik_version_tracking_to_servers_table.php new file mode 100644 index 000000000..3bab33368 --- /dev/null +++ b/database/migrations/2025_11_12_130931_add_traefik_version_tracking_to_servers_table.php @@ -0,0 +1,28 @@ +string('detected_traefik_version')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('detected_traefik_version'); + }); + } +}; diff --git a/database/migrations/2025_11_12_131252_add_traefik_outdated_to_email_notification_settings.php b/database/migrations/2025_11_12_131252_add_traefik_outdated_to_email_notification_settings.php new file mode 100644 index 000000000..ac509dc71 --- /dev/null +++ b/database/migrations/2025_11_12_131252_add_traefik_outdated_to_email_notification_settings.php @@ -0,0 +1,28 @@ +boolean('traefik_outdated_email_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('email_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_email_notifications'); + }); + } +}; diff --git a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_discord_notification_settings.php b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_discord_notification_settings.php new file mode 100644 index 000000000..1be15a105 --- /dev/null +++ b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_discord_notification_settings.php @@ -0,0 +1,28 @@ +boolean('traefik_outdated_discord_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('discord_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_discord_notifications'); + }); + } +}; diff --git a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_pushover_notification_settings.php b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_pushover_notification_settings.php new file mode 100644 index 000000000..0b689cfb3 --- /dev/null +++ b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_pushover_notification_settings.php @@ -0,0 +1,28 @@ +boolean('traefik_outdated_pushover_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pushover_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_pushover_notifications'); + }); + } +}; diff --git a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_slack_notification_settings.php b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_slack_notification_settings.php new file mode 100644 index 000000000..6ac58ebbf --- /dev/null +++ b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_slack_notification_settings.php @@ -0,0 +1,28 @@ +boolean('traefik_outdated_slack_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('slack_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_slack_notifications'); + }); + } +}; diff --git a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_telegram_notification_settings.php b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_telegram_notification_settings.php new file mode 100644 index 000000000..6df3a9a6b --- /dev/null +++ b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_telegram_notification_settings.php @@ -0,0 +1,28 @@ +boolean('traefik_outdated_telegram_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('telegram_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_telegram_notifications'); + }); + } +}; diff --git a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_webhook_notification_settings.php b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_webhook_notification_settings.php new file mode 100644 index 000000000..7d9dd8730 --- /dev/null +++ b/database/migrations/2025_11_12_131253_add_traefik_outdated_to_webhook_notification_settings.php @@ -0,0 +1,28 @@ +boolean('traefik_outdated_webhook_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('webhook_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_webhook_notifications'); + }); + } +}; diff --git a/database/migrations/2025_11_12_133400_add_traefik_outdated_thread_id_to_telegram_notification_settings.php b/database/migrations/2025_11_12_133400_add_traefik_outdated_thread_id_to_telegram_notification_settings.php new file mode 100644 index 000000000..b7d69e634 --- /dev/null +++ b/database/migrations/2025_11_12_133400_add_traefik_outdated_thread_id_to_telegram_notification_settings.php @@ -0,0 +1,28 @@ +text('telegram_notifications_traefik_outdated_thread_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('telegram_notification_settings', function (Blueprint $table) { + $table->dropColumn('telegram_notifications_traefik_outdated_thread_id'); + }); + } +}; diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index adada458e..511af1a9f 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -113,6 +113,8 @@ public function run(): void $server_details['proxy'] = ServerMetadata::from([ 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, + 'last_saved_settings' => null, + 'last_applied_settings' => null, ]); $server = Server::create($server_details); $server->settings->is_reachable = true; @@ -177,6 +179,8 @@ public function run(): void $server_details['proxy'] = ServerMetadata::from([ 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, + 'last_saved_settings' => null, + 'last_applied_settings' => null, ]); $server = Server::create($server_details); $server->settings->is_reachable = true; diff --git a/resources/views/components/callout.blade.php b/resources/views/components/callout.blade.php index e65dad63b..67da3ba7f 100644 --- a/resources/views/components/callout.blade.php +++ b/resources/views/components/callout.blade.php @@ -1,13 +1,13 @@ -@props(['type' => 'warning', 'title' => 'Warning', 'class' => '']) +@props(['type' => 'warning', 'title' => 'Warning', 'class' => '', 'dismissible' => false, 'onDismiss' => null]) @php $icons = [ 'warning' => '', - + 'danger' => '', - + 'info' => '', - + 'success' => '' ]; @@ -42,12 +42,12 @@ $icon = $icons[$type] ?? $icons['warning']; @endphp -