diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index 1d3a345e1..4f2bfa68c 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -33,6 +33,9 @@ public function handle(): void // New version available $settings->update(['new_version_available' => true]); File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); + + // Invalidate cache to ensure fresh data is loaded + invalidate_versions_cache(); } else { $settings->update(['new_version_available' => false]); } diff --git a/app/Jobs/CheckTraefikVersionJob.php b/app/Jobs/CheckTraefikVersionJob.php index 3fb1d6601..5adbc7c09 100644 --- a/app/Jobs/CheckTraefikVersionJob.php +++ b/app/Jobs/CheckTraefikVersionJob.php @@ -9,7 +9,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\File; class CheckTraefikVersionJob implements ShouldQueue { @@ -19,16 +18,10 @@ class CheckTraefikVersionJob implements ShouldQueue public function handle(): void { - // Load versions from versions.json - $versionsPath = base_path('versions.json'); - if (! File::exists($versionsPath)) { - return; - } + // Load versions from cached data + $traefikVersions = get_traefik_versions(); - $allVersions = json_decode(File::get($versionsPath), true); - $traefikVersions = data_get($allVersions, 'traefik'); - - if (empty($traefikVersions) || ! is_array($traefikVersions)) { + if (empty($traefikVersions)) { return; } diff --git a/app/Livewire/Server/Navbar.php b/app/Livewire/Server/Navbar.php index a759232cc..7827f02b8 100644 --- a/app/Livewire/Server/Navbar.php +++ b/app/Livewire/Server/Navbar.php @@ -5,6 +5,7 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StopProxy; +use App\Enums\ProxyTypes; use App\Models\Server; use App\Services\ProxyDashboardCacheService; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; @@ -168,6 +169,22 @@ public function refreshServer() $this->server->load('settings'); } + /** + * Check if Traefik has any outdated version info (patch or minor upgrade). + * This shows a warning indicator in the navbar. + */ + public function getHasTraefikOutdatedProperty(): bool + { + if ($this->server->proxyType() !== ProxyTypes::TRAEFIK->value) { + return false; + } + + // Check if server has outdated info stored + $outdatedInfo = $this->server->traefik_outdated_info; + + return ! empty($outdatedInfo) && isset($outdatedInfo['type']); + } + public function render() { return view('livewire.server.navbar'); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index fb4da0c1b..c92f73f17 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -7,7 +7,6 @@ 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 @@ -26,6 +25,12 @@ class Proxy extends Component public bool $generateExactLabels = false; + /** + * Cache the versions.json file data in memory for this component instance. + * This avoids multiple file reads during a single request/render cycle. + */ + protected ?array $cachedVersionsFile = null; + public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -57,6 +62,34 @@ private function syncData(bool $toModel = false): void } } + /** + * Get Traefik versions from cached data with in-memory optimization. + * Returns array like: ['v3.5' => '3.5.6', 'v3.6' => '3.6.2'] + * + * This method adds an in-memory cache layer on top of the global + * get_traefik_versions() helper to avoid multiple calls during + * a single component lifecycle/render. + */ + protected function getTraefikVersions(): ?array + { + // In-memory cache for this component instance (per-request) + if ($this->cachedVersionsFile !== null) { + return data_get($this->cachedVersionsFile, 'traefik'); + } + + // Load from global cached helper (Redis + filesystem) + $versionsData = get_versions_data(); + $this->cachedVersionsFile = $versionsData; + + if (! $versionsData) { + return null; + } + + $traefikVersions = data_get($versionsData, 'traefik'); + + return is_array($traefikVersions) ? $traefikVersions : null; + } + public function getConfigurationFilePathProperty() { return $this->server->proxyPath().'docker-compose.yml'; @@ -147,49 +180,45 @@ public function loadProxyConfiguration() } } + /** + * Get the latest Traefik version for this server's current branch. + * + * This compares the server's detected version against available versions + * in versions.json to determine the latest patch for the current branch, + * or the newest available version if no current version is detected. + */ 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'); + $traefikVersions = $this->getTraefikVersions(); if (! $traefikVersions) { return null; } - // Handle new structure (array of branches) - if (is_array($traefikVersions)) { - $currentVersion = $this->server->detected_traefik_version; + // Get this server's current version + $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]; + // 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 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}"; + // 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; } catch (\Throwable $e) { return null; } @@ -218,6 +247,10 @@ public function getIsTraefikOutdatedProperty(): bool return version_compare($current, $latest, '<'); } + /** + * Check if a newer Traefik branch (minor version) is available for this server. + * Returns the branch identifier (e.g., "v3.6") if a newer branch exists. + */ public function getNewerTraefikBranchAvailableProperty(): ?string { try { @@ -225,12 +258,13 @@ public function getNewerTraefikBranchAvailableProperty(): ?string return null; } + // Get this server's current version $currentVersion = $this->server->detected_traefik_version; if (! $currentVersion || $currentVersion === 'latest') { return null; } - // Check if we have outdated info stored + // Check if we have outdated info stored for this server (faster than computing) $outdatedInfo = $this->server->traefik_outdated_info; if ($outdatedInfo && isset($outdatedInfo['type']) && $outdatedInfo['type'] === 'minor_upgrade') { // Use the upgrade_target field if available (e.g., "v3.6") @@ -241,15 +275,10 @@ public function getNewerTraefikBranchAvailableProperty(): ?string } } - $versionsPath = base_path('versions.json'); - if (! File::exists($versionsPath)) { - return null; - } + // Fallback: compute from cached versions data + $traefikVersions = $this->getTraefikVersions(); - $versions = json_decode(File::get($versionsPath), true); - $traefikVersions = data_get($versions, 'traefik'); - - if (! is_array($traefikVersions)) { + if (! $traefikVersions) { return null; } diff --git a/app/Models/Server.php b/app/Models/Server.php index 0f7db5ae4..e88af2b15 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -31,6 +31,51 @@ use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +/** + * @property array{ + * current: string, + * latest: string, + * type: 'patch_update'|'minor_upgrade', + * checked_at: string, + * newer_branch_target?: string, + * newer_branch_latest?: string, + * upgrade_target?: string + * }|null $traefik_outdated_info Traefik version tracking information. + * + * This JSON column stores information about outdated Traefik proxy versions on this server. + * The structure varies depending on the type of update available: + * + * **For patch updates** (e.g., 3.5.0 → 3.5.2): + * ```php + * [ + * 'current' => '3.5.0', // Current version (without 'v' prefix) + * 'latest' => '3.5.2', // Latest patch version available + * 'type' => 'patch_update', // Update type identifier + * 'checked_at' => '2025-11-14T10:00:00Z', // ISO8601 timestamp + * 'newer_branch_target' => 'v3.6', // (Optional) Available major/minor version + * 'newer_branch_latest' => '3.6.2' // (Optional) Latest version in that branch + * ] + * ``` + * + * **For minor/major upgrades** (e.g., 3.5.6 → 3.6.2): + * ```php + * [ + * 'current' => '3.5.6', // Current version + * 'latest' => '3.6.2', // Latest version in target branch + * 'type' => 'minor_upgrade', // Update type identifier + * 'upgrade_target' => 'v3.6', // Target branch (with 'v' prefix) + * 'checked_at' => '2025-11-14T10:00:00Z' // ISO8601 timestamp + * ] + * ``` + * + * **Null value**: Set to null when: + * - Server is fully up-to-date with the latest version + * - Traefik image uses the 'latest' tag (no fixed version tracking) + * - No Traefik version detected on the server + * + * @see \App\Jobs\CheckTraefikVersionForServerJob Where this data is populated + * @see \App\Livewire\Server\Proxy Where this data is read and displayed + */ #[OA\Schema( description: 'Server model', type: 'object', diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 39d847eac..9e69906ac 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -241,10 +241,9 @@ function get_latest_sentinel_version(): string function get_latest_version_of_coolify(): string { try { - $versions = File::get(base_path('versions.json')); - $versions = json_decode($versions, true); + $versions = get_versions_data(); - return data_get($versions, 'coolify.v4.version'); + return data_get($versions, 'coolify.v4.version', '0.0.0'); } catch (\Throwable $e) { return '0.0.0'; diff --git a/bootstrap/helpers/versions.php b/bootstrap/helpers/versions.php new file mode 100644 index 000000000..bb4694de5 --- /dev/null +++ b/bootstrap/helpers/versions.php @@ -0,0 +1,53 @@ + '3.5.6']) + */ +function get_traefik_versions(): ?array +{ + $versions = get_versions_data(); + + if (! $versions) { + return null; + } + + $traefikVersions = data_get($versions, 'traefik'); + + return is_array($traefikVersions) ? $traefikVersions : null; +} + +/** + * Invalidate the versions cache. + * Call this after updating versions.json to ensure fresh data is loaded. + */ +function invalidate_versions_cache(): void +{ + Cache::forget('coolify:versions:all'); +} diff --git a/database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php b/database/migrations/2025_10_10_120002_create_cloud_init_scripts_table.php similarity index 100% rename from database/migrations/2025_10_10_120000_create_cloud_init_scripts_table.php rename to database/migrations/2025_10_10_120002_create_cloud_init_scripts_table.php 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 deleted file mode 100644 index 1be15a105..000000000 --- a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_discord_notification_settings.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 0b689cfb3..000000000 --- a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_pushover_notification_settings.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 6ac58ebbf..000000000 --- a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_slack_notification_settings.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 6df3a9a6b..000000000 --- a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_telegram_notification_settings.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 7d9dd8730..000000000 --- a/database/migrations/2025_11_12_131253_add_traefik_outdated_to_webhook_notification_settings.php +++ /dev/null @@ -1,28 +0,0 @@ -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_17_092707_add_traefik_outdated_to_notification_settings.php b/database/migrations/2025_11_17_092707_add_traefik_outdated_to_notification_settings.php new file mode 100644 index 000000000..b5cad28b0 --- /dev/null +++ b/database/migrations/2025_11_17_092707_add_traefik_outdated_to_notification_settings.php @@ -0,0 +1,60 @@ +boolean('traefik_outdated_discord_notifications')->default(true); + }); + + Schema::table('slack_notification_settings', function (Blueprint $table) { + $table->boolean('traefik_outdated_slack_notifications')->default(true); + }); + + Schema::table('webhook_notification_settings', function (Blueprint $table) { + $table->boolean('traefik_outdated_webhook_notifications')->default(true); + }); + + Schema::table('telegram_notification_settings', function (Blueprint $table) { + $table->boolean('traefik_outdated_telegram_notifications')->default(true); + }); + + Schema::table('pushover_notification_settings', function (Blueprint $table) { + $table->boolean('traefik_outdated_pushover_notifications')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('discord_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_discord_notifications'); + }); + + Schema::table('slack_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_slack_notifications'); + }); + + Schema::table('webhook_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_webhook_notifications'); + }); + + Schema::table('telegram_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_telegram_notifications'); + }); + + Schema::table('pushover_notification_settings', function (Blueprint $table) { + $table->dropColumn('traefik_outdated_pushover_notifications'); + }); + } +}; diff --git a/resources/views/components/server/sidebar-proxy.blade.php b/resources/views/components/server/sidebar-proxy.blade.php index 9f47fde7f..ad6612a25 100644 --- a/resources/views/components/server/sidebar-proxy.blade.php +++ b/resources/views/components/server/sidebar-proxy.blade.php @@ -1,9 +1,9 @@ -@if ($server->proxySet()) -