fix(server): surface pending proxy and outdated Traefik states

This commit is contained in:
Andras Bacsai
2026-08-18 13:53:40 +02:00
parent 3cca2a0bfe
commit 861a904992
18 changed files with 398 additions and 24 deletions
+7 -1
View File
@@ -33,10 +33,11 @@ class CheckTraefikVersionForServerJob implements ShouldBeEncrypted, ShouldQueue
*/
public function handle(): void
{
$this->clearOutdatedInfo();
// Detect current version (makes SSH call)
$currentVersion = getTraefikVersionFromDockerCompose($this->server);
// Update detected version in database
$this->server->update(['detected_traefik_version' => $currentVersion]);
if (! $currentVersion) {
@@ -113,6 +114,11 @@ class CheckTraefikVersionForServerJob implements ShouldBeEncrypted, ShouldQueue
ProxyStatusChangedUI::dispatch($this->server->team_id);
}
private function clearOutdatedInfo(): void
{
$this->server->update(['traefik_outdated_info' => null]);
}
/**
* Get information about newer branches if available.
*/
+14 -1
View File
@@ -29,7 +29,10 @@ class ActivityMonitor extends Component
public static $eventDispatched = false;
protected $listeners = ['activityMonitor' => 'newMonitorActivity'];
protected $listeners = [
'activityMonitor' => 'newMonitorActivity',
'processDialogClosed' => 'clearActivity',
];
public function newMonitorActivity($activityId, $eventToDispatch = 'activityFinished', $eventData = null, $header = null)
{
@@ -50,6 +53,16 @@ class ActivityMonitor extends Component
$this->isPollingActive = true;
}
public function clearActivity(): void
{
$this->activityId = null;
$this->activity = null;
$this->isPollingActive = false;
$this->eventToDispatch = 'activityFinished';
$this->eventData = null;
self::$eventDispatched = false;
}
public function hydrateActivity()
{
if ($this->activityId === null) {
+16 -3
View File
@@ -163,6 +163,7 @@ class Navbar extends Component
$previousStatus = $this->proxyStatus;
$this->server->refresh();
$this->proxyStatus = $this->server->proxy->status ?? 'unknown';
$this->dispatchProxyConfigurationState();
// If event contains activityId, open activity monitor
if ($event && isset($event['activityId'])) {
@@ -227,6 +228,16 @@ class Navbar extends Component
{
$this->server->refresh();
$this->server->load('settings');
$this->dispatchProxyConfigurationState();
}
private function dispatchProxyConfigurationState(): void
{
$this->dispatch(
'proxy-configuration-state-changed',
pending: $this->server->hasPendingProxyConfiguration(),
traefikOutdated: $this->server->hasCurrentTraefikOutdatedInfo(),
);
}
public function refreshSentinelStatus($event = null): void
@@ -248,10 +259,12 @@ class Navbar extends Component
return false;
}
// Check if server has outdated info stored
$outdatedInfo = $this->server->traefik_outdated_info;
return $this->server->hasCurrentTraefikOutdatedInfo();
}
return ! empty($outdatedInfo) && isset($outdatedInfo['type']);
public function getHasPendingProxyConfigurationProperty(): bool
{
return $this->server->hasPendingProxyConfiguration();
}
public function render()
+5 -1
View File
@@ -161,6 +161,7 @@ class Proxy extends Component
$this->server->proxy->redirect_url = $this->redirectUrl;
$this->server->save();
$this->server->setupDefaultRedirect();
$this->dispatch('refreshServerShow');
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -175,6 +176,7 @@ class Proxy extends Component
$this->proxySettings = GetProxyConfiguration::run($this->server, forceRegenerate: true);
SaveProxyConfiguration::run($this->server, $this->proxySettings);
$this->server->save();
$this->dispatch('refreshServerShow');
$this->dispatch('success', 'Proxy configuration reset to default.');
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -276,7 +278,9 @@ class Proxy extends Component
// 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') {
$storedCurrentVersion = ltrim((string) data_get($outdatedInfo, 'current'), 'v');
$detectedCurrentVersion = ltrim($currentVersion, 'v');
if ($storedCurrentVersion === $detectedCurrentVersion && data_get($outdatedInfo, 'type') === 'minor_upgrade') {
// Use the upgrade_target field if available (e.g., "v3.6")
if (isset($outdatedInfo['upgrade_target'])) {
return str_starts_with($outdatedInfo['upgrade_target'], 'v')
+27
View File
@@ -858,6 +858,33 @@ $siteAddress {
return data_get($this->proxy, 'type');
}
public function hasPendingProxyConfiguration(): bool
{
if ($this->proxy->get('status') !== 'running') {
return false;
}
$savedSettings = $this->proxy->get('last_saved_settings');
$appliedSettings = $this->proxy->get('last_applied_settings');
return filled($savedSettings) && filled($appliedSettings) && $savedSettings !== $appliedSettings;
}
public function hasCurrentTraefikOutdatedInfo(): bool
{
if ($this->proxyType() !== ProxyTypes::TRAEFIK->value) {
return false;
}
$detectedVersion = ltrim((string) $this->detected_traefik_version, 'v');
$storedVersion = ltrim((string) data_get($this->traefik_outdated_info, 'current'), 'v');
$type = data_get($this->traefik_outdated_info, 'type');
return filled($detectedVersion)
&& $storedVersion === $detectedVersion
&& in_array($type, ['patch_update', 'minor_upgrade'], true);
}
public function scopeWithProxy(): Builder
{
return $this->proxy->modelScope();
+1 -2
View File
@@ -126,8 +126,7 @@
}
@utility button {
/* h-9 matches input-select; nowrap + shrink-0 keep side-by-side action rows equal height */
@apply inline-flex shrink-0 gap-1.5 justify-center items-center whitespace-nowrap px-2.5 h-9 min-h-9 text-[13px] text-black normal-case rounded-md border outline-0 cursor-pointer font-medium transition-colors bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-white/[0.06] dark:text-fg dark:hover:text-fg dark:hover:bg-white/[0.1] dark:border-white/[0.08] hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-fg-faint disabled:border-neutral-200 dark:disabled:border-white/[0.06] disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent;
@apply inline-flex shrink-0 gap-1.5 justify-center items-center whitespace-nowrap px-2.5 h-8 min-h-8 text-[13px] text-black normal-case rounded-md border outline-0 cursor-pointer font-medium transition-colors bg-white border-neutral-200 hover:bg-neutral-100 dark:bg-white/[0.06] dark:text-fg dark:hover:text-fg dark:hover:bg-white/[0.1] dark:border-white/[0.08] hover:text-black disabled:cursor-not-allowed min-w-fit dark:disabled:text-fg-faint disabled:border-neutral-200 dark:disabled:border-white/[0.06] disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent;
}
@utility button-highlighted {
@@ -0,0 +1,37 @@
@props(['canRestart' => false])
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape.window="open = false">
<button type="button" aria-label="Proxy configuration changes not applied" aria-haspopup="dialog"
:aria-expanded="open" @click="open = !open"
class="flex h-8 items-center justify-center gap-1.5 rounded-lg px-2 text-amber-700 transition-colors hover:bg-amber-100 dark:text-warning dark:hover:bg-warning/10">
<x-reicon name="alert-triangle" class="size-4" />
<span class="hidden text-xs font-medium lg:inline">Changes pending</span>
</button>
<div x-show="open" x-cloak x-transition.opacity role="dialog"
class="fixed top-14 left-1/2 z-[1100] w-[calc(100vw-2rem)] max-w-sm -translate-x-1/2 rounded-lg p-3 lg:absolute lg:top-full lg:right-0 lg:left-auto lg:mt-2 lg:translate-x-0"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
<div class="flex items-start gap-2.5">
<span
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
<x-reicon name="alert-triangle" class="size-4" />
</span>
<div class="min-w-0 flex-1">
<p class="text-[13px] leading-4 font-semibold text-neutral-950 dark:text-fg">
The saved proxy configuration has not been applied
</p>
<p class="mt-0.5 text-[11px] leading-4 text-neutral-600 dark:text-fg-dim">
Restart the proxy to apply these changes.
@if ($canRestart)
<button type="button"
class="ml-0.5 inline-flex items-center gap-0.5 font-semibold text-coollabs transition-colors hover:text-coollabs-100 dark:text-warning dark:hover:text-warning/80"
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
Restart proxy
<x-reicon name="arrow-right" class="size-2.5" />
</button>
@endif
</p>
</div>
</div>
</div>
</div>
@@ -55,6 +55,8 @@
'icon' => 'network',
'group' => 'Platform',
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
'warning' => $server->hasCurrentTraefikOutdatedInfo(),
'tracks_proxy_configuration' => true,
'children' => [
['label' => 'Configuration', 'route' => 'server.proxy', 'active' => $activeSubMenu === 'configuration', 'icon' => 'settings'],
['label' => 'Dynamic Configurations', 'route' => 'server.proxy.dynamic-confs', 'active' => $activeSubMenu === 'dynamic-confs', 'icon' => 'sliders', 'visible' => $server->proxySet()],
@@ -167,7 +169,15 @@
$groupedServerMenuItems = $serverMenuItems->groupBy('group');
@endphp
<aside class="application-settings-navigation min-w-0 xl:self-start">
<aside class="application-settings-navigation min-w-0 xl:self-start"
x-data="{
proxyConfigurationPending: @js($server->hasPendingProxyConfiguration()),
traefikOutdated: @js($server->hasCurrentTraefikOutdatedInfo())
}"
@proxy-configuration-state-changed.window="
proxyConfigurationPending = $event.detail.pending;
traefikOutdated = $event.detail.traefikOutdated;
">
<nav aria-label="Server configuration sections"
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
@foreach ($groupedServerMenuItems as $groupLabel => $groupItems)
@@ -186,6 +196,14 @@
href="{{ route($menuItem['route'], $serverRouteParameters) }}">
<x-reicon :name="$menuItem['icon']" class="menu-item-icon" />
<span class="menu-item-label">{{ $menuItem['label'] }}</span>
@if ($menuItem['tracks_proxy_configuration'] ?? false)
<x-reicon name="alert-triangle" x-cloak
x-show="proxyConfigurationPending || traefikOutdated"
class="ml-auto size-3.5 shrink-0 text-orange-500 dark:text-warning" />
@elseif ($menuItem['warning'] ?? false)
<x-reicon name="alert-triangle"
class="ml-auto size-3.5 shrink-0 text-orange-500 dark:text-warning" />
@endif
</a>
@if ($menuItem['active'] && isset($menuItem['children']))
<div class="col-span-full grid grid-cols-2 gap-0.5 border-l border-neutral-200 pl-2 sm:grid-cols-3 xl:grid-cols-1 dark:border-white/[0.08]">
+28 -11
View File
@@ -126,24 +126,41 @@
<div x-cloak x-show="viewMode === 'grid'"
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
<template x-for="server in filteredServers" :key="server.uuid">
<a :href="server.href" {{ wireNavigate() }}
@foreach ($servers as $server)
@php
$serverRow = $serverRows->firstWhere('uuid', $server->uuid);
@endphp
<a x-cloak
x-show="filteredServers.some(server => server.uuid === @js($server->uuid))"
href="{{ $serverRow['href'] }}" {{ wireNavigate() }}
class="group relative flex min-h-28 flex-col rounded-xl border border-neutral-200 bg-white p-3 shadow-sm transition-all hover:-translate-y-px hover:border-neutral-300 hover:no-underline hover:shadow-md dark:border-white/[0.08] dark:bg-white/[0.025] dark:hover:border-white/[0.14]">
<div class="flex items-start gap-3">
<div :title="server.status" :aria-label="`Server status: ${server.status}`"
class="flex size-8 shrink-0 items-center justify-center rounded-lg border bg-neutral-50 text-neutral-500 dark:bg-white/[0.04] dark:text-fg-dim"
:class="server.statusType === 'success' ? 'border-emerald-500/70' : server.statusType === 'warning' ? 'border-amber-500/70' : 'border-red-500/70'">
@if ($server->isMetricsEnabled())
<livewire:dashboard.server-metrics-chart :server="$server"
:key="'server-index-metrics-'.$server->uuid" />
@endif
<div class="pointer-events-none relative z-10 flex items-start gap-3">
<div title="{{ $serverRow['status'] }}"
aria-label="Server status: {{ $serverRow['status'] }}"
@class([
'flex size-8 shrink-0 items-center justify-center rounded-lg border bg-neutral-50 text-neutral-500 dark:bg-white/[0.04] dark:text-fg-dim',
'border-emerald-500/70' => $serverRow['statusType'] === 'success',
'border-amber-500/70' => $serverRow['statusType'] === 'warning',
'border-red-500/70' => $serverRow['statusType'] === 'error',
])>
<x-reicon name="servers" class="size-4" />
</div>
<div class="min-w-0 flex-1">
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg"
x-text="server.name"></h2>
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint"
x-text="server.description"></p>
<h2 class="truncate text-[13px]! leading-4! font-semibold! text-black dark:text-fg">
{{ $serverRow['name'] }}
</h2>
<p class="mt-0.5 truncate text-[11px] text-neutral-500 dark:text-fg-faint">
{{ $serverRow['description'] }}
</p>
</div>
</div>
</a>
</template>
@endforeach
</div>
<div x-show="viewMode === 'table'"
@@ -43,7 +43,7 @@
'route' => 'server.proxy',
'active' => request()->routeIs('server.proxy', 'server.proxy.*'),
'visible' => ! $server->isSwarmWorker() && ! $server->settings->is_build_server,
'warning' => $this->hasTraefikOutdated,
'warning' => $this->hasTraefikOutdated || $this->hasPendingProxyConfiguration,
],
[
'label' => 'Sentinel',
@@ -89,6 +89,20 @@
$proxyCanBeStopped = in_array($proxyStatus, ['running', 'starting', 'restarting'], true);
@endphp
@if ($this->hasPendingProxyConfiguration)
@teleport('#configuration-warning-hud-slot')
<div>
<x-proxy-configuration-warning :can-restart="auth()->user()?->can('manageProxy', $server) ?? false" />
</div>
@endteleport
@teleport('#configuration-warning-hud-slot-mobile')
<div>
<x-proxy-configuration-warning :can-restart="auth()->user()?->can('manageProxy', $server) ?? false" />
</div>
@endteleport
@endif
@teleport('#server-topbar-context')
<div data-testid="server-topbar-context"
class="flex min-w-0 items-center gap-1 text-[13px]">
@@ -6,6 +6,9 @@
<form wire:submit="submit" class="contents">
<x-unsaved-bar action="submit" />
<fieldset class="contents" wire:loading.attr="disabled"
wire:target="submit,resetProxyConfiguration">
<x-application.settings-section id="server-proxy-overview-section" title="Proxy configuration"
helper="Configure the reverse proxy and request handling for this server.">
<x-slot:actions>
@@ -126,7 +129,16 @@
@endif
@if ($proxySettings)
<div class="mt-4">
<div class="relative mt-4" wire:loading.class="pointer-events-none opacity-50"
wire:target="submit,resetProxyConfiguration" aria-live="polite">
<div wire:loading.flex wire:target="submit,resetProxyConfiguration"
class="absolute inset-0 z-20 hidden items-center justify-center rounded-lg bg-white/75 backdrop-blur-[1px] dark:bg-black/55">
<div
class="flex items-center gap-2 rounded-lg bg-white px-3 py-2 text-xs font-medium text-neutral-700 shadow-sm ring-1 ring-neutral-200 dark:bg-coolgray-100 dark:text-fg dark:ring-white/10">
<x-loading />
Updating proxy configuration…
</div>
</div>
<x-forms.textarea canGate="update" :canResource="$server" useMonacoEditor
monacoEditorLanguage="yaml"
label="Configuration file · {{ $this->configurationFilePath }}"
@@ -135,6 +147,7 @@
@endif
</x-application.settings-section>
@endif
</fieldset>
</form>
@elseif($selectedProxy === 'NONE')
<x-application.settings-section title="Custom proxy"
@@ -130,3 +130,23 @@ test('activityId property is locked and cannot be set from client', function ()
->set('activityId', $otherActivity->id)
->assertStatus(500);
})->throws(CannotUpdateLockedPropertyException::class);
test('closing a process dialog clears the previous activity logs', function () {
$activity = Activity::create([
'log_name' => 'default',
'description' => 'proxy restart activity',
'properties' => ['team_id' => $this->team->id],
]);
$this->actingAs($this->user);
session(['currentTeam' => ['id' => $this->team->id]]);
Livewire::test(ActivityMonitor::class)
->call('newMonitorActivity', $activity->id)
->assertSet('activityId', $activity->id)
->assertSet('isPollingActive', true)
->dispatch('processDialogClosed')
->assertSet('activityId', null)
->assertSet('activity', null)
->assertSet('isPollingActive', false);
});
@@ -1,10 +1,10 @@
<?php
it('uses the standard button height inside application forms and workspaces', function () {
it('uses the compact height for all standard buttons', function () {
$utilities = file_get_contents(resource_path('css/utilities.css'));
$applicationStyles = file_get_contents(resource_path('css/app.css'));
expect($utilities)->toContain('px-2.5 h-9 min-h-9')
expect($utilities)->toContain('px-2.5 h-8 min-h-8')
->and($applicationStyles)
->not->toContain('.application-settings-workspace .button')
->not->toContain('.application-settings-form .button');
@@ -1,6 +1,7 @@
<?php
use App\Livewire\Dashboard;
use App\Livewire\Server\Index as ServerIndex;
use App\Models\InstanceSettings;
use App\Models\PrivateKey;
use App\Models\Server;
@@ -45,6 +46,24 @@ it('renders a metrics chart only for servers with metrics enabled', function ()
->assertDontSeeHtml("dashboard-server-metrics-{$disabledServer->uuid}");
});
it('renders dashboard metrics charts on the server index grid', function () {
$enabledServer = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
]);
$enabledServer->settings->update(['is_metrics_enabled' => true]);
$disabledServer = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $this->privateKey->id,
]);
$disabledServer->settings->update(['is_metrics_enabled' => false]);
Livewire::test(ServerIndex::class)
->assertSeeHtml("dashboard-server-metrics-{$enabledServer->uuid}")
->assertDontSeeHtml("dashboard-server-metrics-{$disabledServer->uuid}");
});
it('configures the dashboard chart as a ten minute cpu and memory sparkline with hover details', function () {
$chart = file_get_contents(resource_path('views/livewire/dashboard/server-metrics-chart.blade.php'));
$component = file_get_contents(app_path('Livewire/Dashboard/ServerMetricsChart.php'));
+59
View File
@@ -72,6 +72,65 @@ test('admin can see proxy restart and stop buttons', function () {
->assertSee('Stop Proxy');
});
test('running proxy shows pending configuration warning when saved settings differ from applied settings', function () {
[$user, $team, $server] = setupProxyUser('admin');
makeServerProxyRunning($server);
$server->proxy->last_saved_settings = 'saved-hash';
$server->proxy->last_applied_settings = 'applied-hash';
$server->detected_traefik_version = '3.6.23';
$server->traefik_outdated_info = [
'current' => '3.6.23',
'latest' => '3.7.8',
'type' => 'minor_upgrade',
'upgrade_target' => 'v3.7',
];
$server->save();
expect($server->fresh()->hasPendingProxyConfiguration())->toBeTrue();
$this->actingAs($user);
session(['currentTeam' => $team]);
$component = Livewire::test('server.navbar', ['server' => $server->fresh()])
->assertSee('Changes pending')
->assertSee('The saved proxy configuration has not been applied')
->assertSee('Restart proxy');
$server->refresh();
$server->proxy->last_applied_settings = 'saved-hash';
$server->traefik_outdated_info = null;
$server->save();
$component->call('showNotification')
->assertDispatched('proxy-configuration-state-changed', pending: false, traefikOutdated: false)
->assertDontSee('The saved proxy configuration has not been applied');
});
test('running proxy hides pending configuration warning when saved settings match applied settings', function () {
[$user, $team, $server] = setupProxyUser('admin');
makeServerProxyRunning($server);
$server->proxy->last_saved_settings = 'matching-hash';
$server->proxy->last_applied_settings = 'matching-hash';
$server->save();
expect($server->fresh()->hasPendingProxyConfiguration())->toBeFalse();
$this->actingAs($user);
session(['currentTeam' => $team]);
$component = Livewire::test('server.navbar', ['server' => $server->fresh()])
->assertDontSee('The saved proxy configuration has not been applied');
$server->refresh();
$server->proxy->last_saved_settings = 'new-saved-hash';
$server->save();
$component->dispatch('refreshServerShow')
->assertDispatched('proxy-configuration-state-changed', pending: true, traefikOutdated: false)
->assertSee('Changes pending')
->assertSee('The saved proxy configuration has not been applied');
});
test('admin can stop a proxy while it is starting', function () {
[$user, $team, $server] = setupProxyUser('admin');
@@ -0,0 +1,12 @@
<?php
it('disables proxy configuration controls and covers the editor while saving', function () {
$view = file_get_contents(resource_path('views/livewire/server/proxy.blade.php'));
expect($view)
->toContain('wire:loading.attr="disabled"')
->toContain('wire:target="submit,resetProxyConfiguration"')
->toContain('wire:loading.class="pointer-events-none opacity-50"')
->toContain('Updating proxy configuration')
->toContain('aria-live="polite"');
});
+14
View File
@@ -46,3 +46,17 @@ it('uses the network reicon for proxy in the server sidebar', function () {
->toMatch("/'label' => 'Proxy'[\s\S]*?'icon' => 'network'/")
->not->toMatch("/'label' => 'Proxy'[\s\S]*?'icon' => 'settings',\s*'group' => 'Platform'/");
});
it('shows a warning icon when a server menu item requires attention', function () {
$contents = file_get_contents(resource_path('views/components/server/sidebar.blade.php'));
expect($contents)
->toContain('proxyConfigurationPending: @js($server->hasPendingProxyConfiguration())')
->toContain('proxyConfigurationPending: @js($server->hasPendingProxyConfiguration()),')
->toContain('traefikOutdated: @js($server->hasCurrentTraefikOutdatedInfo())')
->toContain('@proxy-configuration-state-changed.window')
->toContain("\$menuItem['warning'] ?? false")
->toContain('name="alert-triangle"');
expect(substr_count($contents, 'traefikOutdated: @js($server->hasCurrentTraefikOutdatedInfo())'))->toBe(1);
});
+89
View File
@@ -0,0 +1,89 @@
<?php
use App\Enums\ProxyTypes;
use App\Jobs\CheckTraefikVersionForServerJob;
use App\Livewire\Server\Proxy;
use App\Models\Server;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
uses(RefreshDatabase::class);
it('ignores stale minor upgrade information for the detected Traefik version', function () {
Cache::put('coolify:versions:all', [
'traefik' => [
'v3.7' => '3.7.8',
'v3.6' => '3.6.23',
],
]);
$server = Server::factory()->make([
'proxy' => ['type' => ProxyTypes::TRAEFIK->value],
'detected_traefik_version' => '3.7.8',
'traefik_outdated_info' => [
'current' => '3.6.23',
'latest' => '3.7.8',
'type' => 'minor_upgrade',
'upgrade_target' => 'v3.7',
],
]);
$component = new Proxy;
$component->server = $server;
expect($component->getNewerTraefikBranchAvailableProperty())->toBeNull();
});
it('does not mark stale Traefik outdated information as a current warning', function () {
$server = Server::factory()->make([
'proxy' => ['type' => ProxyTypes::TRAEFIK->value],
'detected_traefik_version' => '3.7.8',
'traefik_outdated_info' => [
'current' => '3.6.23',
'latest' => '3.7.8',
'type' => 'minor_upgrade',
'upgrade_target' => 'v3.7',
],
]);
expect($server->hasCurrentTraefikOutdatedInfo())->toBeFalse();
});
it('marks matching Traefik outdated information as a current warning', function () {
$server = Server::factory()->make([
'proxy' => ['type' => ProxyTypes::TRAEFIK->value],
'detected_traefik_version' => 'v3.6.23',
'traefik_outdated_info' => [
'current' => '3.6.23',
'latest' => '3.7.8',
'type' => 'minor_upgrade',
'upgrade_target' => 'v3.7',
],
]);
expect($server->hasCurrentTraefikOutdatedInfo())->toBeTrue();
});
it('clears stale outdated information before detecting the current version', function () {
$team = Team::factory()->create();
$server = Server::factory()->create([
'team_id' => $team->id,
'detected_traefik_version' => '3.6.23',
'traefik_outdated_info' => [
'current' => '3.6.23',
'latest' => '3.7.8',
'type' => 'minor_upgrade',
'upgrade_target' => 'v3.7',
],
]);
$job = new CheckTraefikVersionForServerJob($server, ['v3.7' => '3.7.8']);
$method = new ReflectionMethod($job, 'clearOutdatedInfo');
$method->invoke($job);
$server->refresh();
expect($server->detected_traefik_version)->toBe('3.6.23')
->and($server->traefik_outdated_info)->toBeNull();
});