From a6f71909fd684d760a50d960db1909fa912b39c2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:43:04 +0200 Subject: [PATCH 01/12] feat(ui): add Open server links on destination cards Link primary and additional destination server cards to their server pages via wire:navigate, and cover the markup in a feature test. --- .../views/livewire/project/shared/destination.blade.php | 6 ++++++ tests/Feature/ApplicationDestinationStatusBadgeTest.php | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/resources/views/livewire/project/shared/destination.blade.php b/resources/views/livewire/project/shared/destination.blade.php index 720e85bdc..a1f7923d3 100644 --- a/resources/views/livewire/project/shared/destination.blade.php +++ b/resources/views/livewire/project/shared/destination.blade.php @@ -33,6 +33,8 @@
+ Open server @if ($hasAdditionalDestinations)
+ Open server @if ($destinationStatus->startsWith('running')) @elseif ($destinationStatus->startsWith(['starting', 'restarting'])) @@ -211,6 +215,8 @@
+ Open server @if ($primaryStatus->startsWith('running')) @elseif ($primaryStatus->startsWith(['starting', 'restarting'])) diff --git a/tests/Feature/ApplicationDestinationStatusBadgeTest.php b/tests/Feature/ApplicationDestinationStatusBadgeTest.php index 11eeba515..9dfbedb56 100644 --- a/tests/Feature/ApplicationDestinationStatusBadgeTest.php +++ b/tests/Feature/ApplicationDestinationStatusBadgeTest.php @@ -8,3 +8,12 @@ it('uses the shared status summary in the primary application server card', func ->toContain('') ->not->toContain('toContain("route('server.show', ['server_uuid' => data_get(\$resource, 'destination.server.uuid')])") + ->toContain("route('server.show', ['server_uuid' => data_get(\$destination, 'server.uuid')])") + ->toContain('Open server'); +}); From 8f7eb2d79051cc0d82f1b11904c8fdff0296d557 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:21:18 +0200 Subject: [PATCH 02/12] 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. --- app/Actions/Server/StartSentinel.php | 5 +- app/Livewire/Server/Sentinel.php | 3 +- app/Livewire/Server/ValidateAndInstall.php | 5 ++ app/Models/ServerSetting.php | 17 ++++- bootstrap/helpers/remoteProcess.php | 2 +- .../views/livewire/activity-monitor.blade.php | 2 +- .../views/livewire/boarding/index.blade.php | 8 +-- .../views/livewire/server/charts.blade.php | 4 -- .../views/livewire/server/create.blade.php | 63 ++++++++++--------- .../views/livewire/server/new/by-ip.blade.php | 52 +++++++++------ .../views/livewire/server/sentinel.blade.php | 9 +-- .../views/livewire/server/show.blade.php | 15 ++--- .../server/validate-and-install.blade.php | 54 ++++++++++------ tests/Feature/BoardingActionsLayoutTest.php | 16 +++++ .../Livewire/SentinelComponentTest.php | 17 +++++ tests/Feature/ServerBuildRoleHelperTest.php | 9 +++ tests/Feature/ServerCreatePageLayoutTest.php | 10 +++ .../ServerCreationBuildRoleLayoutTest.php | 22 +++++++ tests/Feature/ServerMetricsEmptyStateTest.php | 10 +++ tests/Feature/ServerValidationDialogTest.php | 55 ++++++++++++++++ tests/Unit/LogTimestampDisplayTest.php | 9 +++ .../StartSentinelEndpointFallbackTest.php | 16 +++++ 22 files changed, 306 insertions(+), 97 deletions(-) create mode 100644 tests/Feature/BoardingActionsLayoutTest.php create mode 100644 tests/Feature/ServerBuildRoleHelperTest.php create mode 100644 tests/Feature/ServerCreatePageLayoutTest.php create mode 100644 tests/Feature/ServerCreationBuildRoleLayoutTest.php create mode 100644 tests/Feature/ServerMetricsEmptyStateTest.php create mode 100644 tests/Feature/ServerValidationDialogTest.php create mode 100644 tests/Unit/LogTimestampDisplayTest.php create mode 100644 tests/Unit/StartSentinelEndpointFallbackTest.php diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index 6350a5f37..cec90288e 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -23,13 +23,10 @@ class StartSentinel $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); $token = $server->settings->ensureValidSentinelToken(); - $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $endpoint = $server->settings->ensureSentinelUrl(); $debug = data_get($server, 'settings.is_sentinel_debug_enabled'); $mountDir = '/data/coolify/sentinel'; $image = coolifyRegistryUrl().'/coollabsio/sentinel:'.$version; - if (! $endpoint) { - throw new \RuntimeException('You should set FQDN in Instance Settings.'); - } $environments = [ 'TOKEN' => $token, 'DEBUG' => $debug ? 'true' : 'false', diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index 909ed54f9..cd05002aa 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -114,9 +114,10 @@ class Sentinel extends Component return; } - $this->isSentinelEnabled = true; $customImage = isDev() ? $this->sentinelCustomDockerImage : null; StartSentinel::run($this->server, true, null, $customImage); + $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->isSentinelEnabled = true; } else { $this->isSentinelEnabled = false; $this->isMetricsEnabled = false; diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index c7181ebcf..9e6108de0 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -39,6 +39,8 @@ class ValidateAndInstall extends Component public bool $ask = false; + public bool $isInstalling = false; + protected $listeners = [ 'init', 'validateConnection', @@ -51,6 +53,7 @@ class ValidateAndInstall extends Component public function init(int $data = 0) { + $this->isInstalling = false; $this->uptime = null; $this->supported_os_type = null; $this->prerequisites_installed = null; @@ -172,6 +175,7 @@ class ValidateAndInstall extends Component if ($this->number_of_tries <= $this->max_tries) { $this->installationStep = 'Prerequisites'; $activity = $this->server->installPrerequisites(); + $this->isInstalling = true; $this->number_of_tries++; $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs"); } @@ -208,6 +212,7 @@ class ValidateAndInstall extends Component if ($this->number_of_tries <= $this->max_tries) { $this->installationStep = 'Docker'; $activity = $this->server->installDocker(); + $this->isInstalling = true; $this->number_of_tries++; $this->dispatch('activityMonitor', $activity->id, 'init', $this->number_of_tries, "{$this->installationStep} Installation Logs"); } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 0453dc793..3bd39aeb1 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -219,7 +219,22 @@ class ServerSetting extends Model return $token; } - public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false) + public function ensureSentinelUrl(): string + { + $url = $this->sentinel_custom_url; + + if (blank($url)) { + $url = $this->generateSentinelUrl(ignoreEvent: true); + } + + if (blank($url)) { + throw new \RuntimeException('Set an instance FQDN or public IP before enabling Sentinel.'); + } + + return $url; + } + + public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false): ?string { $domain = null; $settings = InstanceSettings::get(); diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 84522a5e1..fee3f376f 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -253,7 +253,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d } catch (Exception) { $timestamp->setTimezone('UTC'); } - data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s.u')); + data_set($i, 'timestamp', $timestamp->format('Y-M-d H:i:s')); return $i; }) diff --git a/resources/views/livewire/activity-monitor.blade.php b/resources/views/livewire/activity-monitor.blade.php index f3e79db0e..495d5cf52 100644 --- a/resources/views/livewire/activity-monitor.blade.php +++ b/resources/views/livewire/activity-monitor.blade.php @@ -1,7 +1,7 @@ @php use App\Actions\CoolifyTask\RunRemoteProcess; @endphp
$fullHeight, - 'h-full overflow-hidden' => !$fullHeight, + 'overflow-hidden' => !$fullHeight, ])> @if ($activity) @if (isset($header)) diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php index 62048602f..dca26797d 100644 --- a/resources/views/livewire/boarding/index.blade.php +++ b/resources/views/livewire/boarding/index.blade.php @@ -507,16 +507,16 @@ @endif - + Server validation - Start validation - +
@@ -659,7 +659,7 @@
@if ($currentState !== 'welcome' && $currentState !== 'create-resource') -
+
- - - - @endcan - @endif +
+

New server

+
+ @if ($selectedType) + + Change method + + @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) + + + + + + + @endcan + @endif +
@if (!$selectedType)
- +
+
@can('viewAny', App\Models\CloudProviderToken::class)
- +
+
@else
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index d7fe02178..95e674ab5 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -20,28 +20,17 @@ -
- - -
- -
+
- -
-
- - -
- - +
+
+
+ +
@can('create', App\Models\PrivateKey::class)
+ +
+ + +
+ +
+ + +
+
+ + +
+ +
+
@endif diff --git a/resources/views/livewire/server/sentinel.blade.php b/resources/views/livewire/server/sentinel.blade.php index 2354b79cd..e5041fb4b 100644 --- a/resources/views/livewire/server/sentinel.blade.php +++ b/resources/views/livewire/server/sentinel.blade.php @@ -13,19 +13,14 @@ helper="Monitor server and container health while collecting historical metrics.">
- @if (!$isSentinelEnabled) Enable Sentinel @else + diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index c3e6a35b3..dbe82c5ee 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -192,17 +192,17 @@
@endif - + Validate and configure - - + + {{ $server->isFunctional() ? 'Revalidate connection' : 'Validate connection' }} - +
@if ($this->limaStartCommand) @@ -261,10 +261,11 @@ @if ($isBuildServerLocked) + label="Use as a dedicated build server" /> @else @endif
diff --git a/resources/views/livewire/server/validate-and-install.blade.php b/resources/views/livewire/server/validate-and-install.blade.php index 0bba6652c..09dc7aec6 100644 --- a/resources/views/livewire/server/validate-and-install.blade.php +++ b/resources/views/livewire/server/validate-and-install.blade.php @@ -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 -
+
@if ($ask)
@@ -77,28 +83,36 @@ Continue @else -
-
-
-

Validation checkpoints

-
-
-
-
- @foreach ($checkpoints as $checkpoint) - @continue(! $checkpoint['visible']) - - @endforeach -
+
+
+

Validation checkpoints

-
+
+ @foreach ($checkpoints as $checkpoint) + + @endforeach +
+
-
-
- + @if ($validationComplete) +
+
+ + Validation complete +
+ + Close +
-
+ @elseif ($isInstalling) +
+
+ +
+
+ @endif @isset($error)
toContain('class="mx-auto mt-6 flex w-full max-w-3xl flex-col items-center gap-3"'); +}); + +test('server validation opens in the centered process dialog', function () { + $view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php')); + + expect($view) + ->toContain('') + ->toContain('@click="processDialogOpen = true"') + ->not->toContain(''); +}); diff --git a/tests/Feature/Livewire/SentinelComponentTest.php b/tests/Feature/Livewire/SentinelComponentTest.php index 47cad4e22..41b3f2b49 100644 --- a/tests/Feature/Livewire/SentinelComponentTest.php +++ b/tests/Feature/Livewire/SentinelComponentTest.php @@ -19,3 +19,20 @@ it('dispatches a server navbar refresh after toggling sentinel', function () { expect($matches['body'] ?? '') ->toContain("\$this->dispatch('refreshServerShow');"); }); + +it('only marks sentinel enabled after startup succeeds', function () { + $componentSource = file_get_contents(app_path('Livewire/Server/Sentinel.php')); + + preg_match('/public function toggleSentinel\([^)]*\).*?\{(?.*?)\n \}/s', $componentSource, $matches); + $toggleBody = $matches['body'] ?? ''; + + expect(strpos($toggleBody, 'StartSentinel::run'))->toBeLessThan( + strpos($toggleBody, '$this->isSentinelEnabled = true;') + ); +}); + +it('does not repeat a disabled status badge in the sentinel empty state', function () { + $view = file_get_contents(resource_path('views/livewire/server/sentinel.blade.php')); + + expect($view)->not->toContain("? 'Disabled'"); +}); diff --git a/tests/Feature/ServerBuildRoleHelperTest.php b/tests/Feature/ServerBuildRoleHelperTest.php new file mode 100644 index 000000000..3cb85e608 --- /dev/null +++ b/tests/Feature/ServerBuildRoleHelperTest.php @@ -0,0 +1,9 @@ +toContain('label="Use as a dedicated build server"') + ->toContain('helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."'); +}); diff --git a/tests/Feature/ServerCreatePageLayoutTest.php b/tests/Feature/ServerCreatePageLayoutTest.php new file mode 100644 index 000000000..66d2c50f3 --- /dev/null +++ b/tests/Feature/ServerCreatePageLayoutTest.php @@ -0,0 +1,10 @@ +toContain('

New server

') + ->not->toContain('Back to servers') + ->not->toContain('title="Add a server"'); +}); diff --git a/tests/Feature/ServerCreationBuildRoleLayoutTest.php b/tests/Feature/ServerCreationBuildRoleLayoutTest.php new file mode 100644 index 000000000..2f5eaf1fa --- /dev/null +++ b/tests/Feature/ServerCreationBuildRoleLayoutTest.php @@ -0,0 +1,22 @@ +toContain('class="flex items-end gap-3"') + ->toContain('x-data="{ advancedOpen: false }"') + ->toContain('x-show="advancedOpen" x-cloak') + ->toContain('Advanced settings') + ->toContain('label="Use as a dedicated build server"') + ->toContain('helper="Build servers compile applications but do not host deployments. Enabling this makes the server build-only."'); +}); + +test('server creation places the IP address and private key before optional details', function () { + $view = file_get_contents(resource_path('views/livewire/server/new/by-ip.blade.php')); + + expect($view) + ->toContain('class="mb-5"') + ->and(strpos($view, 'id="ip"'))->toBeLessThan(strpos($view, 'id="private_key_id"')) + ->and(strpos($view, 'id="private_key_id"'))->toBeLessThan(strpos($view, 'id="name"')); +}); diff --git a/tests/Feature/ServerMetricsEmptyStateTest.php b/tests/Feature/ServerMetricsEmptyStateTest.php new file mode 100644 index 000000000..e3c34065b --- /dev/null +++ b/tests/Feature/ServerMetricsEmptyStateTest.php @@ -0,0 +1,10 @@ +after('@else')->before('@endif')->toString(); + + expect($sentinelRequiredState) + ->toContain('title="Sentinel is required"') + ->not->toContain('status="Unavailable"'); +}); diff --git a/tests/Feature/ServerValidationDialogTest.php b/tests/Feature/ServerValidationDialogTest.php new file mode 100644 index 000000000..bf6c139f3 --- /dev/null +++ b/tests/Feature/ServerValidationDialogTest.php @@ -0,0 +1,55 @@ +toContain('') + ->toContain(':isHighlighted="! $server->isFunctional()"') + ->toContain('@click="processDialogOpen = true" wire:click.prevent="validateServer"'); +}); + +test('completed server validation shows a close action instead of empty logs', function () { + $view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php')); + + expect($view) + ->toContain('$validationComplete') + ->toContain('mt-auto') + ->toContain('') + ->toContain('@click="processDialogOpen = false"') + ->toContain('Validation complete') + ->toContain('Close'); +}); + +test('installation logs are only shown after an installation starts', function () { + $view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php')); + $component = file_get_contents(app_path('Livewire/Server/ValidateAndInstall.php')); + + expect($view)->toContain('@elseif ($isInstalling)') + ->and($component) + ->toContain('public bool $isInstalling = false;') + ->toContain('$this->isInstalling = true;'); +}); + +test('server validation content scrolls within the dialog', function () { + $view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php')); + $activityMonitor = file_get_contents(resource_path('views/livewire/activity-monitor.blade.php')); + + expect($view)->toContain('class="flex h-full min-h-0 flex-col gap-4 overflow-y-auto scrollbar"') + ->and($activityMonitor)->toContain("'overflow-hidden' => !\$fullHeight") + ->and($activityMonitor)->not->toContain("'h-full overflow-hidden' => !\$fullHeight"); +}); + +test('validation checkpoints use the standard bordered list treatment', function () { + $view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php')); + + expect($view) + ->toContain('data-validation-checkpoints') + ->toContain('overflow-hidden rounded-[10px] border border-neutral-200 dark:border-white/[0.08]'); +}); + +test('all validation checkpoints remain visible while only the current phase runs', function () { + $view = file_get_contents(resource_path('views/livewire/server/validate-and-install.blade.php')); + + expect($view)->not->toContain("@continue(! \$checkpoint['visible'])"); +}); diff --git a/tests/Unit/LogTimestampDisplayTest.php b/tests/Unit/LogTimestampDisplayTest.php new file mode 100644 index 000000000..d2846065e --- /dev/null +++ b/tests/Unit/LogTimestampDisplayTest.php @@ -0,0 +1,9 @@ +toContain("->format('Y-M-d H:i:s')") + ->not->toContain("->format('Y-M-d H:i:s.u')"); +}); diff --git a/tests/Unit/StartSentinelEndpointFallbackTest.php b/tests/Unit/StartSentinelEndpointFallbackTest.php new file mode 100644 index 000000000..7cb913682 --- /dev/null +++ b/tests/Unit/StartSentinelEndpointFallbackTest.php @@ -0,0 +1,16 @@ +toContain('ensureSentinelUrl()') + ->and($component) + ->toContain('$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;') + ->and(file_get_contents(dirname(__DIR__, 2).'/app/Models/ServerSetting.php')) + ->toContain('generateSentinelUrl(ignoreEvent: true)') + ->toContain('Set an instance FQDN or public IP before enabling Sentinel.') + ->and($component) + ->toContain('$this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url;'); +}); From 28f57aafb8f7ef5c4d232dbd82899d107c4351f3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:24:41 +0200 Subject: [PATCH 03/12] fix(server): keep resources nav active and show tab loading Use $activeMenu for Resources so Livewire updates do not clear the sidebar highlight, and disable managed/unmanaged tabs with spinners while containers load. --- resources/views/components/server/sidebar.blade.php | 2 +- resources/views/livewire/server/resources.blade.php | 10 ++++++---- tests/Feature/ServerResourcesTableMobileLayoutTest.php | 10 ++++++++++ tests/Feature/SidebarNavigationMarkupTest.php | 8 ++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 5450e4310..1d6f07310 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -76,7 +76,7 @@ [ 'label' => 'Resources', 'route' => 'server.resources', - 'active' => request()->routeIs('server.resources'), + 'active' => $activeMenu === 'resources', 'icon' => 'projects', 'group' => 'Platform', ], diff --git a/resources/views/livewire/server/resources.blade.php b/resources/views/livewire/server/resources.blade.php index 6d9031c18..1ab1744b5 100644 --- a/resources/views/livewire/server/resources.blade.php +++ b/resources/views/livewire/server/resources.blade.php @@ -13,12 +13,14 @@ flush>
- -
diff --git a/tests/Feature/ServerResourcesTableMobileLayoutTest.php b/tests/Feature/ServerResourcesTableMobileLayoutTest.php index 41834ae4b..7d35c7f10 100644 --- a/tests/Feature/ServerResourcesTableMobileLayoutTest.php +++ b/tests/Feature/ServerResourcesTableMobileLayoutTest.php @@ -55,3 +55,13 @@ test('unmanaged container names use the same typeface as managed resource names' ->toContain('min-w-0 truncate text-[12px] font-medium text-neutral-950 dark:text-fg') ->not->toContain('truncate font-mono text-[12px] text-neutral-950 dark:text-fg'); }); + +test('server resource tabs show a loading state while switching', function () { + $view = file_get_contents(resource_path('views/livewire/server/resources.blade.php')); + + expect(substr_count($view, 'wire:loading.attr="disabled" wire:target="loadManagedContainers,loadUnmanagedContainers"')) + ->toBe(2) + ->and($view) + ->toContain('') + ->toContain(''); +}); diff --git a/tests/Feature/SidebarNavigationMarkupTest.php b/tests/Feature/SidebarNavigationMarkupTest.php index ffd5cfdf1..2e07f06f2 100644 --- a/tests/Feature/SidebarNavigationMarkupTest.php +++ b/tests/Feature/SidebarNavigationMarkupTest.php @@ -15,6 +15,14 @@ it('keeps server submenu state independent from the Livewire update route', func ->and($proxyLogs)->toContain('activeSubMenu="logs"'); }); +it('keeps the server resources menu active during Livewire updates', function () { + $sidebar = file_get_contents(resource_path('views/components/server/sidebar.blade.php')); + + expect($sidebar) + ->toContain("'label' => 'Resources',\n 'route' => 'server.resources',\n 'active' => \$activeMenu === 'resources'") + ->not->toContain("'active' => request()->routeIs('server.resources')"); +}); + it('initializes persisted sidebar state before enabling layout transitions', function () { $layout = file_get_contents(resource_path('views/layouts/app.blade.php')); From de4e74b4cd8aaaf323fcea5c102d3b1046ac0a18 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Aug 2026 15:41:39 +0200 Subject: [PATCH 04/12] feat(server): fall back Sentinel URL to request host Use the current request scheme/host when no FQDN or public IP is set, skipping loopback hosts. Polish boarding/server create cards, optional listbox tooltips, and cover the layout and Sentinel URL behavior in tests. --- app/Models/ServerSetting.php | 27 +++++- .../views/components/forms/listbox.blade.php | 3 +- .../views/livewire/boarding/index.blade.php | 84 +++++++------------ .../views/livewire/server/create.blade.php | 23 +---- .../views/livewire/server/index.blade.php | 11 +-- .../server/new/by-digital-ocean.blade.php | 12 ++- .../views/livewire/server/new/by-ip.blade.php | 2 +- .../livewire/server/new/by-vultr.blade.php | 12 ++- tests/Feature/BoardingActionsLayoutTest.php | 58 +++++++++++++ tests/Feature/SentinelTokenValidationTest.php | 36 ++++++++ tests/Feature/ServerCreatePageLayoutTest.php | 10 +++ .../ServerCreationBuildRoleLayoutTest.php | 1 + tests/Feature/ServerIndexViewSwitcherTest.php | 10 +++ .../StartSentinelEndpointFallbackTest.php | 3 +- 14 files changed, 203 insertions(+), 89 deletions(-) diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 3bd39aeb1..93f040e4a 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -228,7 +228,7 @@ class ServerSetting extends Model } if (blank($url)) { - throw new \RuntimeException('Set an instance FQDN or public IP before enabling Sentinel.'); + throw new \RuntimeException('Set an instance FQDN, public IP, or reachable Coolify URL before enabling Sentinel.'); } return $url; @@ -246,6 +246,8 @@ class ServerSetting extends Model $domain = 'http://'.$settings->public_ipv4.':8000'; } elseif ($settings->public_ipv6) { $domain = 'http://'.$settings->public_ipv6.':8000'; + } else { + $domain = $this->sentinelUrlFromCurrentRequest(); } $this->sentinel_custom_url = $domain; if ($save) { @@ -259,6 +261,29 @@ class ServerSetting extends Model return $domain; } + private function sentinelUrlFromCurrentRequest(): ?string + { + if (! app()->bound('request')) { + return null; + } + + $request = request(); + $host = strtolower($request->getHost()); + + if ( + $host === 'localhost' || + str_ends_with($host, '.localhost') || + $host === '::1' || + $host === '::' || + $host === '0.0.0.0' || + str_starts_with($host, '127.') + ) { + return null; + } + + return $request->getSchemeAndHttpHost(); + } + public function server() { return $this->belongsTo(Server::class); diff --git a/resources/views/components/forms/listbox.blade.php b/resources/views/components/forms/listbox.blade.php index eb32de577..050bea8b3 100644 --- a/resources/views/components/forms/listbox.blade.php +++ b/resources/views/components/forms/listbox.blade.php @@ -11,6 +11,7 @@ 'wire' => true, // false = purely client-side value (no Livewire binding) 'value' => null, // initial value when wire=false 'disabled' => false, + 'tooltip' => true, ])
@@ -53,7 +54,7 @@ @click.outside="open = false" @keydown.escape="open = false">
- -

- Group related resources (apps, databases, - services) - into logical projects. -

-

- Each project includes a production environment by - default. - Add staging, development, or custom environments as needed. -

-

- Projects inherit team permissions and can be managed - collaboratively. -

-
@elseif ($currentState === 'create-resource') diff --git a/resources/views/livewire/server/create.blade.php b/resources/views/livewire/server/create.blade.php index fea01333e..9b42400bd 100644 --- a/resources/views/livewire/server/create.blade.php +++ b/resources/views/livewire/server/create.blade.php @@ -3,7 +3,7 @@ New Server | Coolify -
+

New server

@if ($selectedType) @@ -11,27 +11,6 @@ Change method @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) - - - - - - - @endcan - @endif
diff --git a/resources/views/livewire/server/index.blade.php b/resources/views/livewire/server/index.blade.php index d1dda5920..9faae18c5 100644 --- a/resources/views/livewire/server/index.blade.php +++ b/resources/views/livewire/server/index.blade.php @@ -24,7 +24,6 @@ 'uuid' => $server->uuid, 'name' => $server->name, 'description' => $server->description ?: 'No description', - 'address' => $server->ip, 'href' => route('server.show', ['server_uuid' => $server->uuid]), 'status' => $isReady ? 'Ready' : ($server->settings->force_disabled ? 'Disabled' : 'Validation required'), 'statusType' => $isReady ? 'success' : 'error', @@ -41,7 +40,7 @@ const query = this.search.trim().toLowerCase(); if (!query) return this.servers; return this.servers.filter(server => - [server.name, server.description, server.address, server.status] + [server.name, server.description, server.status] .some(value => String(value || '').toLowerCase().includes(query)) ); }, @@ -109,7 +108,7 @@

+ x-text="server.description">

@@ -126,14 +125,13 @@
+ class="grid min-w-[480px] grid-cols-[minmax(0,1fr)_9.5rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
Server
-
Address
Status