feat(ui): show internal hostname and densify config UI

Add an application Internal access section that loads the running
container hostname (skipped for Compose apps), densify configuration
diff and popup-small defaults, dock the server-timing HUD into the
mobile top bar, restyle the 2FA challenge with the auth shell, and
enforce 16px mobile form fonts to prevent iOS zoom.
This commit is contained in:
Andras Bacsai committed 2026-08-05 21:24:25 +02:00
1 parent 8046e5a28d
commit 4d35c63d89
22 files changed
+369 -213

No files matched your search

+9 -6
View File
@@ -109,23 +109,26 @@ test('removes stale Content-Length after injecting the HUD', function () {
expect($response->headers->has('Content-Length'))->toBeFalse();
});
test('app shell exposes Server-Timing HUD dock slot in the desktop top bar only', function () {
test('app shell exposes Server-Timing HUD dock slots in desktop and mobile top bars', function () {
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
expect($layout)
->toContain('id="server-timing-hud-slot"')
->toContain('data-server-timing-hud-slot')
->not->toContain('id="server-timing-hud-slot-mobile"');
->toContain('id="server-timing-hud-slot-mobile"')
->toContain('data-server-timing-hud-slot');
});
test('Server-Timing HUD docks on desktop only and floats on smaller viewports', function () {
test('Server-Timing HUD docks into navbar slots and floats only as fallback', function () {
$hud = file_get_contents(resource_path('views/components/server-timing-hud.blade.php'));
expect($hud)
->toContain('server-timing-hud-slot')
->toContain('server-timing-hud-slot-mobile')
->toContain("matchMedia('(min-width: 1024px)')")
->toContain('float bottom-left instead of docking')
->not->toContain('server-timing-hud-slot-mobile');
->toContain('floats bottom-left only if no navbar slot is available')
// Mobile pill is compact (app ms only); full "ST · db · q" breakdown stays desktop/float.
->toContain("parentElement.id === 'server-timing-hud-slot-mobile'")
->toContain('compactSummary');
});
test('does not inject HUD into non-HTML or fragment responses', function () {
@@ -0,0 +1,36 @@
<?php
it('shows internal Docker access details in application general settings', function () {
$generalSettings = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
$generalComponent = file_get_contents(app_path('Livewire/Project/Application/General.php'));
$configurationSidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($generalSettings)
->toContain('id="internal-access-section"')
->toContain('title="Internal access"')
->toContain('Internal hostname')
->toContain('Docker network')
->toContain('Exposed ports')
->toContain('Network aliases')
->toContain('wire:init="loadCurrentInternalHostname"')
->toContain('$currentInternalHostname')
->not->toContain('Changes with each deployment')
->toContain("window.scrollToSettingsSection?.('networking-section')")
->and($generalComponent)
->toContain('public ?string $currentInternalHostname = null;')
->toContain('public function loadCurrentInternalHostname(): void')
->toContain('getCurrentApplicationContainerStatus(')
->toContain("data_get(\$currentContainer, 'Names')")
->and($configurationSidebar)
->toContain("['id' => 'internal-access-section', 'label' => 'Internal access']");
});
it('does not show the internal access section for Docker Compose applications', function () {
$generalSettings = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
$configurationSidebar = file_get_contents(resource_path('views/components/application/configuration-sidebar.blade.php'));
expect($generalSettings)
->toContain("@if (\$buildPack !== 'dockercompose')")
->and($configurationSidebar)
->toContain("\$isComposeApp ? null : ['id' => 'internal-access-section', 'label' => 'Internal access']");
});
@@ -67,7 +67,7 @@ it('renders the changed configuration labels without a second backend request',
Livewire::test(ConfigurationChecker::class, ['resource' => $application->refresh()])
->assertSee('The latest configuration has not been applied')
->assertSee('A rebuild is required.')
->assertSee('Rebuild required.')
->assertSee('Build command');
$view = file_get_contents(resource_path('views/livewire/project/shared/configuration-checker.blade.php'));
@@ -114,7 +114,7 @@ it('shows an unapplied configuration warning after a directory mount is added',
->dispatch('configurationChanged')
->assertSet('isConfigurationChanged', true)
->assertSee('The latest configuration has not been applied')
->assertSee('Please redeploy to apply the new configuration.')
->assertSee('Redeploy to apply.')
->assertSee('Directory mount');
});
+13
View File
@@ -32,3 +32,16 @@ test('confirm password page uses the shared auth shell', function () {
->not->toContain('bg-gray-50 dark:bg-base')
->not->toContain('!text-5xl font-extrabold');
});
test('two factor challenge page uses the shared auth shell', function () {
$challenge = file_get_contents(resource_path('views/auth/two-factor-challenge.blade.php'));
expect($challenge)
->toContain('x-auth.shell')
->toContain('title="Coolify"')
->toContain('x-auth.alert')
->toContain('auth-guidance')
->toContain('Verify and continue')
->not->toContain('bg-gray-50 dark:bg-base')
->not->toContain('!text-5xl font-extrabold');
});
@@ -21,7 +21,8 @@ it('collapses server subsystem badges into one status summary', function () {
->toContain('wire:loading.flex wire:target="checkProxyStatus"')
->toContain('Refreshing status')
->toContain('<x-loading compact />')
->toContain('name="refresh" class="size-3 opacity-70"')
->toContain('name="refresh" class="size-2.5 opacity-70"')
->toContain('min-h-0! h-7! gap-1.5! px-2! py-1! text-[11px]')
->not->toContain('@click="open = false" role="menuitem"');
expect($badgeView)
@@ -34,6 +34,12 @@ it('draws a single border between the desktop sidebar and main content', functio
->and($layout)->not->toContain('lg:border-l border-neutral-200');
});
it('separates the mobile sidebar from the page with a visible border', function () {
$layout = file_get_contents(resource_path('views/layouts/app.blade.php'));
expect($layout)->toContain('max-w-56 min-w-0 flex-col border-l border-neutral-200 bg-white shadow-xl dark:border-white/[0.12] dark:bg-panel');
});
it('shows section titles and descriptions above settings navigation on smaller screens', function (string $view, string $title, string $description) {
$layout = file_get_contents(resource_path("views/components/{$view}"));
$css = file_get_contents(resource_path('css/app.css'));
@@ -36,6 +36,14 @@ it('renders service container statuses as shared status badges', function () {
->not->toContain('badge-success');
});
it('uses bordered status badges in the top breadcrumb', function () {
$breadcrumb = file_get_contents(resource_path('views/components/top-breadcrumb.blade.php'));
$borderedBadgeClasses = 'rounded-full border border-neutral-200 bg-neutral-100';
expect(substr_count($breadcrumb, $borderedBadgeClasses))->toBe(4)
->and(substr_count($breadcrumb, 'rounded-full bg-neutral-100'))->toBe(0);
});
it('uses a shared refresh badge for resource status refresh actions', function () {
$statusIndex = file_get_contents(resource_path('views/components/status/index.blade.php'));
$serviceStatus = file_get_contents(resource_path('views/components/status/services.blade.php'));