fix: update Hetzner server status handling to prevent unnecessary database updates and improve UI responsiveness

This commit is contained in:
Andras Bacsai 2025-10-10 09:35:54 +02:00
parent 77dcabe51c
commit bbaef03602
2 changed files with 82 additions and 57 deletions

View file

@ -388,11 +388,13 @@ public function checkHetznerServerStatus(bool $manual = false)
$this->hetznerServerStatus = $serverData['status'] ?? null; $this->hetznerServerStatus = $serverData['status'] ?? null;
// Save status to database // Save status to database without triggering model events
$this->server->update(['hetzner_server_status' => $this->hetznerServerStatus]); if ($this->server->hetzner_server_status !== $this->hetznerServerStatus) {
$this->server->hetzner_server_status = $this->hetznerServerStatus;
if ($manual) { $this->server->update(['hetzner_server_status' => $this->hetznerServerStatus]);
$this->dispatch('success', 'Server status refreshed: '.ucfirst($this->hetznerServerStatus ?? 'unknown')); if ($manual) {
$this->dispatch('success', 'Server status refreshed: '.ucfirst($this->hetznerServerStatus ?? 'unknown'));
}
} }
// If Hetzner server is off but Coolify thinks it's still reachable, update Coolify's state // If Hetzner server is off but Coolify thinks it's still reachable, update Coolify's state

View file

@ -10,19 +10,38 @@
<div class="flex gap-2"> <div class="flex gap-2">
<h2>General</h2> <h2>General</h2>
@if ($server->hetzner_server_id) @if ($server->hetzner_server_id)
<div @class([ <div class="flex items-center">
'flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded transition-all cursor-pointer hover:opacity-80', <div @class([
'bg-white dark:bg-coolgray-100 dark:text-white', 'flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded transition-all',
]) 'bg-white dark:bg-coolgray-100 dark:text-white',
@if (in_array($hetznerServerStatus, ['starting', 'initializing'])) wire:poll.5s="checkHetznerServerStatus" @endif ])
wire:click="checkHetznerServerStatus(true)" title="Click to refresh status"> @if (in_array($hetznerServerStatus, ['starting', 'initializing'])) wire:poll.5s="checkHetznerServerStatus" @endif>
<svg class="w-4 h-4" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#D50C2D" rx="8" /> <rect width="200" height="200" fill="#D50C2D" rx="8" />
<path d="M40 40 H60 V90 H140 V40 H160 V160 H140 V110 H60 V160 H40 Z" fill="white" /> <path d="M40 40 H60 V90 H140 V40 H160 V160 H140 V110 H60 V160 H40 Z"
</svg> fill="white" />
@if ($hetznerServerStatus) </svg>
<span class="pl-1.5"> @if ($hetznerServerStatus)
@if (in_array($hetznerServerStatus, ['starting', 'initializing'])) <span class="pl-1.5">
@if (in_array($hetznerServerStatus, ['starting', 'initializing']))
<svg class="inline animate-spin h-3 w-3 mr-1 text-coollabs dark:text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
@endif
<span @class([
'text-green-500' => $hetznerServerStatus === 'running',
'text-red-500' => $hetznerServerStatus === 'off',
])>
{{ ucfirst($hetznerServerStatus) }}
</span>
</span>
@else
<span class="pl-1.5">
<svg class="inline animate-spin h-3 w-3 mr-1 text-coollabs dark:text-yellow-500" <svg class="inline animate-spin h-3 w-3 mr-1 text-coollabs dark:text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" <circle class="opacity-25" cx="12" cy="12" r="10"
@ -31,27 +50,26 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path> </path>
</svg> </svg>
@endif <span>Checking status...</span>
<span @class([
'text-green-500' => $hetznerServerStatus === 'running',
'text-red-500' => $hetznerServerStatus === 'off',
])>
{{ ucfirst($hetznerServerStatus) }}
</span> </span>
</span> @endif
@else </div>
<span class="pl-1.5"> <button wire:loading.remove wire:target="checkHetznerServerStatus" title="Refresh Status"
<svg class="inline animate-spin h-3 w-3 mr-1 text-coollabs dark:text-yellow-500" wire:click.prevent='checkHetznerServerStatus(true)'
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
<circle class="opacity-25" cx="12" cy="12" r="10" <svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
stroke="currentColor" stroke-width="4"></circle> <path
<path class="opacity-75" fill="currentColor" d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> </svg>
</path> </button>
</svg> <button wire:loading wire:target="checkHetznerServerStatus" title="Refreshing Status"
<span>Checking status...</span> class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
</span> <svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24"
@endif xmlns="http://www.w3.org/2000/svg">
<path
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
</svg>
</button>
</div> </div>
@if ($server->cloudProviderToken && !$server->isFunctional() && $hetznerServerStatus === 'off') @if ($server->cloudProviderToken && !$server->isFunctional() && $hetznerServerStatus === 'off')
<x-forms.button wire:click.prevent='startHetznerServer' isHighlighted canGate="update" <x-forms.button wire:click.prevent='startHetznerServer' isHighlighted canGate="update"
@ -311,32 +329,34 @@ class="w-full input opacity-50 cursor-not-allowed"
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
@if ($server->isSentinelLive()) @if ($server->isSentinelLive())
<x-status.running status="In sync" noLoading title="{{ $sentinelUpdatedAt }}" /> <x-status.running status="In sync" noLoading title="{{ $sentinelUpdatedAt }}" />
<x-forms.button type="submit" canGate="update" <x-forms.button type="submit" canGate="update" :canResource="$server"
:canResource="$server">Save</x-forms.button> :disabled="$isValidating">Save</x-forms.button>
<x-forms.button wire:click='restartSentinel' canGate="update" <x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server"
:canResource="$server">Restart</x-forms.button> :disabled="$isValidating">Restart</x-forms.button>
<x-slide-over fullScreen> <x-slide-over fullScreen>
<x-slot:title>Sentinel Logs</x-slot:title> <x-slot:title>Sentinel Logs</x-slot:title>
<x-slot:content> <x-slot:content>
<livewire:project.shared.get-logs :server="$server" <livewire:project.shared.get-logs :server="$server"
container="coolify-sentinel" lazy /> container="coolify-sentinel" lazy />
</x-slot:content> </x-slot:content>
<x-forms.button @click="slideOverOpen=true">Logs</x-forms.button> <x-forms.button @click="slideOverOpen=true"
:disabled="$isValidating">Logs</x-forms.button>
</x-slide-over> </x-slide-over>
@else @else
<x-status.stopped status="Out of sync" noLoading <x-status.stopped status="Out of sync" noLoading
title="{{ $sentinelUpdatedAt }}" /> title="{{ $sentinelUpdatedAt }}" />
<x-forms.button type="submit" canGate="update" <x-forms.button type="submit" canGate="update" :canResource="$server"
:canResource="$server">Save</x-forms.button> :disabled="$isValidating">Save</x-forms.button>
<x-forms.button wire:click='restartSentinel' canGate="update" <x-forms.button wire:click='restartSentinel' canGate="update" :canResource="$server"
:canResource="$server">Sync</x-forms.button> :disabled="$isValidating">Sync</x-forms.button>
<x-slide-over fullScreen> <x-slide-over fullScreen>
<x-slot:title>Sentinel Logs</x-slot:title> <x-slot:title>Sentinel Logs</x-slot:title>
<x-slot:content> <x-slot:content>
<livewire:project.shared.get-logs :server="$server" <livewire:project.shared.get-logs :server="$server"
container="coolify-sentinel" lazy /> container="coolify-sentinel" lazy />
</x-slot:content> </x-slot:content>
<x-forms.button @click="slideOverOpen=true">Logs</x-forms.button> <x-forms.button @click="slideOverOpen=true"
:disabled="$isValidating">Logs</x-forms.button>
</x-slide-over> </x-slide-over>
@endif @endif
</div> </div>
@ -345,14 +365,14 @@ class="w-full input opacity-50 cursor-not-allowed"
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="w-96"> <div class="w-96">
<x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled" <x-forms.checkbox canGate="update" :canResource="$server" wire:model.live="isSentinelEnabled"
label="Enable Sentinel" /> label="Enable Sentinel" :disabled="$isValidating" />
@if ($server->isSentinelEnabled()) @if ($server->isSentinelEnabled())
@if (isDev()) @if (isDev())
<x-forms.checkbox canGate="update" :canResource="$server" id="isSentinelDebugEnabled" <x-forms.checkbox canGate="update" :canResource="$server" id="isSentinelDebugEnabled"
label="Enable Sentinel (with debug)" instantSave /> label="Enable Sentinel (with debug)" instantSave :disabled="$isValidating" />
@endif @endif
<x-forms.checkbox canGate="update" :canResource="$server" instantSave <x-forms.checkbox canGate="update" :canResource="$server" instantSave
id="isMetricsEnabled" label="Enable Metrics" /> id="isMetricsEnabled" label="Enable Metrics" :disabled="$isValidating" />
@else @else
@if (isDev()) @if (isDev())
<x-forms.checkbox id="isSentinelDebugEnabled" label="Enable Sentinel (with debug)" <x-forms.checkbox id="isSentinelDebugEnabled" label="Enable Sentinel (with debug)"
@ -379,26 +399,29 @@ class="w-full input opacity-50 cursor-not-allowed"
@if ($server->isSentinelEnabled()) @if ($server->isSentinelEnabled())
<div class="flex flex-wrap gap-2 sm:flex-nowrap items-end"> <div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
<x-forms.input canGate="update" :canResource="$server" type="password" id="sentinelToken" <x-forms.input canGate="update" :canResource="$server" type="password" id="sentinelToken"
label="Sentinel token" required helper="Token for Sentinel." /> label="Sentinel token" required helper="Token for Sentinel." :disabled="$isValidating" />
<x-forms.button canGate="update" :canResource="$server" <x-forms.button canGate="update" :canResource="$server"
wire:click="regenerateSentinelToken">Regenerate</x-forms.button> wire:click="regenerateSentinelToken" :disabled="$isValidating">Regenerate</x-forms.button>
</div> </div>
<x-forms.input canGate="update" :canResource="$server" id="sentinelCustomUrl" required <x-forms.input canGate="update" :canResource="$server" id="sentinelCustomUrl" required
label="Coolify URL" label="Coolify URL"
helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance." /> helper="URL to your Coolify instance. If it is empty that means you do not have a FQDN set for your Coolify instance."
:disabled="$isValidating" />
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex flex-wrap gap-2 sm:flex-nowrap"> <div class="flex flex-wrap gap-2 sm:flex-nowrap">
<x-forms.input canGate="update" :canResource="$server" <x-forms.input canGate="update" :canResource="$server"
id="sentinelMetricsRefreshRateSeconds" label="Metrics rate (seconds)" required id="sentinelMetricsRefreshRateSeconds" label="Metrics rate (seconds)" required
helper="Interval used for gathering metrics. Lower values result in more disk space usage." /> helper="Interval used for gathering metrics. Lower values result in more disk space usage."
:disabled="$isValidating" />
<x-forms.input canGate="update" :canResource="$server" id="sentinelMetricsHistoryDays" <x-forms.input canGate="update" :canResource="$server" id="sentinelMetricsHistoryDays"
label="Metrics history (days)" required label="Metrics history (days)" required
helper="Number of days to retain metrics data for." /> helper="Number of days to retain metrics data for." :disabled="$isValidating" />
<x-forms.input canGate="update" :canResource="$server" <x-forms.input canGate="update" :canResource="$server"
id="sentinelPushIntervalSeconds" label="Push interval (seconds)" required id="sentinelPushIntervalSeconds" label="Push interval (seconds)" required
helper="Interval at which metrics data is sent to the collector." /> helper="Interval at which metrics data is sent to the collector."
:disabled="$isValidating" />
</div> </div>
</div> </div>
@endif @endif