feat(server): auto-resolve Sentinel URL and polish setup UI

Ensure a Sentinel endpoint from instance FQDN/IP when enabling, use process dialogs for validation with install state, and tidy server create, boarding, metrics empty state, and log timestamps.
This commit is contained in:
Andras Bacsai committed 2026-08-07 15:21:18 +02:00
1 parent a6f71909fd
commit 8f7eb2d790
22 files changed
+306 -97

No files matched your search

@@ -280,10 +280,6 @@
@else
<x-application.settings-section id="server-metrics-overview-section" title="Metrics"
helper="Inspect recent CPU and memory usage reported by Sentinel.">
<x-slot:actions>
<x-status-badge status="Unavailable" type="warning" />
</x-slot:actions>
<x-empty size="sm" title="Sentinel is required"
description="Enable Sentinel before collecting CPU and memory metrics for this server."
icon-name="dashboard">
@@ -3,38 +3,42 @@
New Server | Coolify
</x-slot>
<div class="mb-4 flex flex-wrap items-center justify-end gap-2">
<a href="{{ $selectedType ? route('server.create') : route('server.index') }}" class="button"
{{ wireNavigate() }}>
{{ $selectedType ? 'Change method' : 'Back to servers' }}
</a>
@if ($selectedType && $selectedType !== 'manual' && ! $selectedTokenUuid)
@php
$tokenProvider = $selectedType === 'digital-ocean' ? 'digitalocean' : $selectedType;
$tokenProviderName = $selectedType === 'digital-ocean'
? 'DigitalOcean'
: str($selectedType)->headline();
@endphp
@can('create', App\Models\CloudProviderToken::class)
<x-modal-input title="New {{ $tokenProviderName }} token">
<x-slot:content>
<button type="button"
class="button button-highlighted">
<x-reicon name="plus" class="size-3.5" />
New token
</button>
</x-slot:content>
<livewire:security.cloud-provider-token-form :modal_mode="true" :provider="$tokenProvider"
wire:key="new-server-token-{{ $tokenProvider }}" />
</x-modal-input>
@endcan
@endif
<div class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">New server</h1>
<div class="flex flex-wrap items-center gap-2">
@if ($selectedType)
<a href="{{ route('server.create') }}" class="button" {{ wireNavigate() }}>
Change method
</a>
@endif
@if ($selectedType && $selectedType !== 'manual' && ! $selectedTokenUuid)
@php
$tokenProvider = $selectedType === 'digital-ocean' ? 'digitalocean' : $selectedType;
$tokenProviderName = $selectedType === 'digital-ocean'
? 'DigitalOcean'
: str($selectedType)->headline();
@endphp
@can('create', App\Models\CloudProviderToken::class)
<x-modal-input title="New {{ $tokenProviderName }} token">
<x-slot:content>
<button type="button"
class="button button-highlighted">
<x-reicon name="plus" class="size-3.5" />
New token
</button>
</x-slot:content>
<livewire:security.cloud-provider-token-form :modal_mode="true" :provider="$tokenProvider"
wire:key="new-server-token-{{ $tokenProvider }}" />
</x-modal-input>
@endcan
@endif
</div>
</div>
@if (!$selectedType)
<div class="application-settings-form">
<x-application.settings-section title="Add a server"
description="Provision with a cloud provider or connect any reachable Linux server." flush>
<section class="application-settings-section">
<div class="application-settings-section-body is-flush">
<div class="grid grid-cols-1 gap-3 p-3 sm:grid-cols-2 lg:grid-cols-4">
@can('viewAny', App\Models\CloudProviderToken::class)
<a href="{{ route('server.create.type', ['type' => 'hetzner']) }}"
@@ -115,7 +119,8 @@
</div>
</a>
</div>
</x-application.settings-section>
</div>
</section>
</div>
@else
<div class="application-settings-form">
@@ -20,28 +20,17 @@
</button>
</x-slot:actions>
<div class="grid gap-4 lg:grid-cols-2">
<x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" />
</div>
<div class="mt-5 grid gap-4 border-t border-neutral-200 pt-4 lg:grid-cols-3 dark:border-white/[0.08]">
<div class="mb-5">
<x-forms.input id="ip" label="IP address or domain" required
helper="For example 127.0.0.1 or server.example.com." />
<x-forms.input id="user" label="User" required
helper="Non-root SSH users are experimental." />
<x-forms.input type="number" id="port" label="Port" required />
</div>
<div class="mt-5 grid items-end gap-4 border-t border-neutral-200 pt-4 lg:grid-cols-2 dark:border-white/[0.08]">
<x-forms.listbox id="private_key_id" label="Private key"
placeholder="Select a private key" :options="$privateKeyOptions" />
<div class="flex items-center justify-between gap-3">
<x-forms.checkbox id="is_build_server"
helper="Build servers compile applications but do not host deployments."
label="Use as a build server" />
<div class="mb-5">
<div class="flex items-end gap-3">
<div class="min-w-0 flex-1">
<x-forms.listbox id="private_key_id" label="Private key"
placeholder="Select a private key" :options="$privateKeyOptions" />
</div>
@can('create', App\Models\PrivateKey::class)
<div x-data="{ dropdownOpen: false }" class="relative shrink-0"
@click.outside="dropdownOpen = false"
@@ -81,6 +70,33 @@
@endcan
</div>
</div>
<div class="grid gap-4 border-t border-neutral-200 pt-4 lg:grid-cols-2 dark:border-white/[0.08]">
<x-forms.input id="name" label="Name" required />
<x-forms.input id="description" label="Description" />
</div>
<div x-data="{ advancedOpen: false }"
class="mt-5 border-t border-neutral-200 pt-4 dark:border-white/[0.08]">
<button type="button" @click="advancedOpen = !advancedOpen"
class="flex w-full items-center justify-between gap-3 text-left text-[13px] font-medium text-neutral-600 dark:text-fg-dim"
:aria-expanded="advancedOpen">
Advanced settings
<x-reicon name="chevron-down" class="size-3.5 transition-transform"
x-bind:class="advancedOpen && 'rotate-180'" />
</button>
<div x-show="advancedOpen" x-cloak class="mt-4 flex flex-col gap-4">
<div class="grid gap-4 lg:grid-cols-2">
<x-forms.input id="user" label="User" required
helper="Non-root SSH users are experimental." />
<x-forms.input type="number" id="port" label="Port" required />
</div>
<x-forms.checkbox id="is_build_server"
helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."
label="Use as a dedicated build server" />
</div>
</div>
</x-application.settings-section>
</form>
@endif
@@ -13,19 +13,14 @@
helper="Monitor server and container health while collecting historical metrics.">
<x-slot:actions>
<div class="flex items-center gap-2">
<x-status-badge
:status="!$isSentinelEnabled
? 'Disabled'
: ($server->isSentinelLive() ? 'In sync' : 'Out of sync')"
:type="!$isSentinelEnabled
? 'neutral'
: ($server->isSentinelLive() ? 'success' : 'warning')" />
@if (!$isSentinelEnabled)
<x-forms.button canGate="update" :canResource="$server" isHighlighted
wire:click="toggleSentinel">
Enable Sentinel
</x-forms.button>
@else
<x-status-badge :status="$server->isSentinelLive() ? 'In sync' : 'Out of sync'"
:type="$server->isSentinelLive() ? 'success' : 'warning'" />
<x-forms.button wire:click="restartSentinel" canGate="update"
:canResource="$server">
<x-reicon name="refresh" class="size-3.5" />
@@ -192,17 +192,17 @@
</div>
@endif
<x-slide-over closeWithX fullScreen>
<x-process-dialog closeWithX size="xl">
<x-slot:title>Validate and configure</x-slot:title>
<x-slot:content>
<livewire:server.validate-and-install :server="$server" :ask="$server->isFunctional()" />
</x-slot:content>
<x-forms.button type="button"
@click="slideOverOpen=true" wire:click.prevent="validateServer">
<x-reicon name="refresh" class="size-3.5" />
<x-forms.button type="button" :isHighlighted="! $server->isFunctional()"
@click="processDialogOpen = true" wire:click.prevent="validateServer">
<x-reicon :name="$server->isFunctional() ? 'refresh' : 'alert-circle'" class="size-3.5" />
{{ $server->isFunctional() ? 'Revalidate connection' : 'Validate connection' }}
</x-forms.button>
</x-slide-over>
</x-process-dialog>
</x-slot:actions>
@if ($this->limaStartCommand)
@@ -261,10 +261,11 @@
@if ($isBuildServerLocked)
<x-forms.checkbox disabled id="isBuildServer"
helper="This server already hosts resources and cannot become build-only."
label="Use as a build server" />
label="Use as a dedicated build server" />
@else
<x-forms.checkbox canGate="update" :canResource="$server" instantSave
id="isBuildServer" label="Use as a build server"
id="isBuildServer" label="Use as a dedicated build server"
helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."
:disabled="$isValidating" />
@endif
</div>
@@ -21,6 +21,12 @@
$showDocker = (bool) ($uptime && $supported_os_type && $prerequisites_installed);
$showCompose = $showDocker;
$showVersion = (bool) ($showDocker && $docker_compose_installed);
$validationComplete = (bool) ($uptime
&& $supported_os_type
&& $prerequisites_installed
&& $docker_installed
&& $docker_compose_installed
&& $docker_version);
$checkpoints = [
[
@@ -66,7 +72,7 @@
];
@endphp
<div class="flex flex-col gap-4">
<div class="flex h-full min-h-0 flex-col gap-4 overflow-y-auto scrollbar">
@if ($ask)
<div
class="rounded-[10px] border border-neutral-200 bg-neutral-50 px-4 py-3 text-[13px] leading-5 text-neutral-600 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-dim">
@@ -77,28 +83,36 @@
Continue
</x-forms.button>
@else
<section class="application-settings-section">
<header>
<div class="flex items-center gap-2">
<h3>Validation checkpoints</h3>
</div>
</header>
<div class="application-settings-section-body is-flush">
<div class="divide-y divide-neutral-200 dark:divide-white/[0.07]">
@foreach ($checkpoints as $checkpoint)
@continue(! $checkpoint['visible'])
<x-checkpoint-item :title="$checkpoint['title']" :description="$checkpoint['description']"
:status="$checkpoint['status']" />
@endforeach
</div>
<div data-validation-checkpoints
class="overflow-hidden rounded-[10px] border border-neutral-200 dark:border-white/[0.08]">
<div class="border-b border-neutral-200 px-4 py-2.5 dark:border-white/[0.08]">
<h3 class="text-[13px] font-medium text-neutral-600 dark:text-fg-dim">Validation checkpoints</h3>
</div>
</section>
<div class="divide-y divide-neutral-200 dark:divide-white/[0.07]">
@foreach ($checkpoints as $checkpoint)
<x-checkpoint-item :title="$checkpoint['title']" :description="$checkpoint['description']"
:status="$checkpoint['status']" />
@endforeach
</div>
</div>
<section class="application-settings-section">
<div class="application-settings-section-body">
<livewire:activity-monitor :header="$installationStep.' installation logs'" :showWaiting="false" />
@if ($validationComplete)
<div class="mt-auto flex shrink-0 items-center justify-between gap-3 rounded-[10px] border border-emerald-500/20 bg-emerald-500/[0.06] px-4 py-3">
<div class="flex items-center gap-2 text-[13px] font-medium text-emerald-700 dark:text-emerald-300">
<x-reicon name="check-circle" class="size-4 shrink-0" />
Validation complete
</div>
<x-forms.button type="button" @click="processDialogOpen = false">
Close
</x-forms.button>
</div>
</section>
@elseif ($isInstalling)
<section class="application-settings-section">
<div class="application-settings-section-body">
<livewire:activity-monitor :header="$installationStep.' installation logs'" :showWaiting="false" />
</div>
</section>
@endif
@isset($error)
<div