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

@@ -0,0 +1,16 @@
<?php
test('boarding utility actions are centered', function () {
$view = file_get_contents(resource_path('views/livewire/boarding/index.blade.php'));
expect($view)->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('<x-process-dialog closeWithX size="xl">')
->toContain('@click="processDialogOpen = true"')
->not->toContain('<x-slide-over closeWithX fullScreen>');
});
@@ -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\([^)]*\).*?\{(?<body>.*?)\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'");
});
@@ -0,0 +1,9 @@
<?php
test('server settings explain dedicated build server mode', function () {
$view = file_get_contents(resource_path('views/livewire/server/show.blade.php'));
expect($view)
->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."');
});
@@ -0,0 +1,10 @@
<?php
test('server creation uses the standard page title without redundant navigation', function () {
$view = file_get_contents(resource_path('views/livewire/server/create.blade.php'));
expect($view)
->toContain('<h1 class="min-w-0 text-[24px]! leading-7! font-semibold! tracking-tight!">New server</h1>')
->not->toContain('Back to servers')
->not->toContain('title="Add a server"');
});
@@ -0,0 +1,22 @@
<?php
test('server creation keeps private key actions together and advanced options collapsed', function () {
$view = file_get_contents(resource_path('views/livewire/server/new/by-ip.blade.php'));
expect($view)
->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"'));
});
@@ -0,0 +1,10 @@
<?php
test('sentinel-required metrics state does not repeat an unavailable badge', function () {
$view = file_get_contents(resource_path('views/livewire/server/charts.blade.php'));
$sentinelRequiredState = str($view)->after('@else')->before('@endif')->toString();
expect($sentinelRequiredState)
->toContain('title="Sentinel is required"')
->not->toContain('status="Unavailable"');
});
@@ -0,0 +1,55 @@
<?php
test('server revalidation opens in the centered process dialog', function () {
$view = file_get_contents(resource_path('views/livewire/server/show.blade.php'));
expect($view)
->toContain('<x-process-dialog closeWithX size="xl">')
->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('<x-forms.button type="button" @click="processDialogOpen = false">')
->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'])");
});
+9
View File
@@ -0,0 +1,9 @@
<?php
test('deployment log timestamps omit microseconds', function () {
$helper = file_get_contents(dirname(__DIR__, 2).'/bootstrap/helpers/remoteProcess.php');
expect($helper)
->toContain("->format('Y-M-d H:i:s')")
->not->toContain("->format('Y-M-d H:i:s.u')");
});
@@ -0,0 +1,16 @@
<?php
test('sentinel startup regenerates an empty endpoint from instance settings', function () {
$action = file_get_contents(dirname(__DIR__, 2).'/app/Actions/Server/StartSentinel.php');
$component = file_get_contents(dirname(__DIR__, 2).'/app/Livewire/Server/Sentinel.php');
expect($action)
->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;');
});