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

@@ -176,6 +176,7 @@
'project.application.configuration' => array_values(array_filter([
['id' => 'application-details-section', 'label' => 'Application details'],
['id' => 'public-access-section', 'label' => 'Public access'],
$isComposeApp ? null : ['id' => 'internal-access-section', 'label' => 'Internal access'],
['id' => 'build-pipeline-section', 'label' => 'Build pipeline'],
$isComposeApp ? null : ['id' => 'container-image-section', 'label' => 'Container image'],
$isComposeApp ? null : ['id' => 'networking-section', 'label' => 'Networking'],
@@ -1,6 +1,6 @@
@props([
'diff' => null,
'compact' => false,
'compact' => true,
])
@php
@@ -8,18 +8,26 @@
@endphp
@if ($changes->isNotEmpty())
<div class="space-y-5">
<div @class(['space-y-3' => $compact, 'space-y-5' => ! $compact])>
@foreach ($changes->groupBy('section_label') as $sectionLabel => $sectionChanges)
<div>
<h4
class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-neutral-500 dark:text-fg-faint">
@class([
'font-semibold uppercase tracking-wide text-neutral-500 dark:text-fg-faint',
'mb-1 text-[10px]' => $compact,
'mb-2 text-[11px]' => ! $compact,
])>
{{ $sectionLabel }}
</h4>
<div class="overflow-x-auto rounded-lg ring-1 ring-neutral-200 dark:ring-white/[0.08]">
<div class="min-w-[640px]">
<div class="overflow-x-auto rounded-md ring-1 ring-neutral-200 dark:ring-white/[0.08]">
<div class="min-w-[560px]">
<div
class="grid grid-cols-[minmax(10rem,0.8fr)_minmax(14rem,1fr)_1.5rem_minmax(14rem,1fr)_1.75rem] items-center gap-3 bg-neutral-100 px-3 py-2 text-[11px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint">
@class([
'grid grid-cols-[minmax(8rem,0.75fr)_minmax(12rem,1fr)_1.25rem_minmax(12rem,1fr)_1.5rem] items-center text-[10px] font-medium text-neutral-500 dark:bg-white/[0.04] dark:text-fg-faint bg-neutral-100',
'gap-2 px-2 py-1' => $compact,
'gap-3 px-3 py-2' => ! $compact,
])>
<div>Field</div>
<div>Current</div>
<div></div>
@@ -42,7 +50,11 @@
@endphp
<div
class="grid grid-cols-[minmax(10rem,0.8fr)_minmax(14rem,1fr)_1.5rem_minmax(14rem,1fr)_1.75rem] items-start gap-3 px-3 py-2.5 text-sm transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]">
@class([
'grid grid-cols-[minmax(8rem,0.75fr)_minmax(12rem,1fr)_1.25rem_minmax(12rem,1fr)_1.5rem] items-start transition-colors hover:bg-neutral-50 dark:hover:bg-white/[0.025]',
'gap-2 px-2 py-1.5 text-xs' => $compact,
'gap-3 px-3 py-2.5 text-sm' => ! $compact,
])>
<div class="min-w-0 font-medium text-neutral-900 dark:text-fg">
@if ($rowExpandable)
<div class="break-words"
@@ -66,7 +78,7 @@
</div>
<div class="flex justify-center pt-0.5 text-neutral-400 dark:text-fg-faint">
<x-reicon name="arrow-right" class="size-3.5" />
<x-reicon name="arrow-right" class="size-3" />
</div>
<div class="min-w-0 text-emerald-600 dark:text-emerald-400">
@@ -86,13 +98,13 @@
x-on:click="expandedRows['{{ $changeKey }}'] = ! expandedRows['{{ $changeKey }}']"
:aria-expanded="!! expandedRows['{{ $changeKey }}']"
title="Toggle full value"
class="flex size-6 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
class="flex size-5 items-center justify-center rounded text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
<x-reicon name="eye"
x-show="! expandedRows['{{ $changeKey }}']"
class="size-3.5" />
class="size-3" />
<x-reicon name="eye-off2"
x-show="expandedRows['{{ $changeKey }}']"
x-cloak class="size-3.5" />
x-cloak class="size-3" />
</button>
@endif
</div>
@@ -7,27 +7,27 @@
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="translate-y-0 opacity-100"
x-transition:leave-end="translate-y-3 opacity-0"
class="fixed bottom-4 right-4 z-999 w-[calc(100%-2rem)] max-w-md">
<div class="relative flex items-start gap-3 rounded-lg p-4 pr-12"
class="fixed bottom-4 right-4 z-999 w-[calc(100%-2rem)] max-w-sm">
<div class="relative flex items-start gap-2.5 rounded-lg p-3 pr-10"
style="background: var(--coollabs-elevated); box-shadow: 0 0 0 1px var(--coollabs-line), var(--shadow-modal);">
@isset($icon)
<div
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-amber-100 text-amber-700 dark:bg-warning/10 dark:text-warning">
{{ $icon }}
</div>
@endisset
<div class="min-w-0 flex-1 pt-0.5">
<h4 class="text-sm font-semibold leading-5 text-neutral-950 dark:text-fg">
<div class="min-w-0 flex-1">
<h4 class="text-[13px] font-semibold leading-4 text-neutral-950 dark:text-fg">
{{ $title }}
</h4>
<div class="mt-1 text-xs leading-5 text-neutral-600 dark:text-fg-dim">
<div class="mt-0.5 text-[11px] leading-4 text-neutral-600 dark:text-fg-dim">
{{ $description }}
</div>
</div>
<button type="button" @click="bannerVisible = false" aria-label="Dismiss"
class="absolute right-3 top-3 flex size-7 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-black/5 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
class="absolute right-2 top-2 flex size-6 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-black/5 hover:text-neutral-700 dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg">
<x-reicon name="x" class="size-3.5" />
</button>
</div>
@@ -1,5 +1,6 @@
{{-- Dev-only Server-Timing HUD. Injected by AddServerTimingHeaders on full HTML responses.
JS docks into #server-timing-hud-slot in the desktop top bar (lg+); otherwise floats bottom-left. --}}
JS docks into the top bar: #server-timing-hud-slot (lg+) or #server-timing-hud-slot-mobile (<lg);
floats bottom-left only if no navbar slot is available. --}}
<div id="server-timing-hud" data-server-timing-hud data-metrics='@json($metrics)' data-path="{{ $path }}"
data-sth-mode="float"
style="position:fixed;bottom:12px;left:12px;z-index:2147483000;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;color:#e5e5e5;pointer-events:auto">
@@ -52,16 +53,21 @@
function pickSlot() {
const desktop = document.getElementById('server-timing-hud-slot');
// Match Tailwind `lg` (1024px): only dock into the desktop top bar.
// On mobile the header is cramped — float bottom-left instead of docking.
const mobile = document.getElementById('server-timing-hud-slot-mobile');
// Match Tailwind `lg` (1024px): desktop top bar vs mobile top bar.
const isLg = window.matchMedia('(min-width: 1024px)').matches;
if (isLg && desktop) {
return desktop;
}
return null;
if (!isLg && mobile) {
return mobile;
}
// Fallback if one breakpoint's slot is missing from the shell.
return desktop || mobile || null;
}
function applyDockedStyles(root) {
const isMobileSlot = root.parentElement && root.parentElement.id === 'server-timing-hud-slot-mobile';
root.setAttribute('data-sth-mode', 'docked');
root.style.cssText = [
'position:relative',
@@ -71,15 +77,35 @@
'right:auto',
'z-index:60',
'display:block',
'flex-shrink:0',
'flex-shrink:1',
'min-width:0',
'max-width:100%',
'visibility:visible',
'opacity:1',
'margin-right:8px',
isMobileSlot ? 'margin-right:4px' : 'margin-right:8px',
'font:11px/1.3 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace',
'color:#e5e5e5',
'pointer-events:auto',
].join(';');
const toggle = qs(root, '[data-sth-toggle]');
if (toggle) {
if (isMobileSlot) {
// Compact pill: just "83ms" — no full metric breakdown in the bar.
toggle.style.padding = '4px 8px';
toggle.style.fontSize = '11px';
toggle.style.maxWidth = 'none';
toggle.style.overflow = 'visible';
toggle.style.whiteSpace = 'nowrap';
} else {
toggle.style.padding = '';
toggle.style.fontSize = '';
toggle.style.maxWidth = '100%';
toggle.style.overflow = 'hidden';
toggle.style.textOverflow = 'ellipsis';
}
}
const panel = qs(root, '[data-sth-panel]');
if (panel) {
// Dropdown below the pill inside the top bar.
@@ -89,6 +115,10 @@
panel.style.top = 'calc(100% + 8px)';
panel.style.bottom = 'auto';
panel.style.zIndex = '2147483001';
if (isMobileSlot) {
// Keep panel on-screen: open near the right edge of the viewport.
panel.style.width = 'min(420px, calc(100vw - 16px))';
}
}
}
@@ -563,7 +593,13 @@
const q = latest.metrics.queries !== undefined ? Math.round(Number(latest.metrics.queries)) + 'q' : '—';
const db = latest.metrics.db !== undefined ? Number(latest.metrics.db).toFixed(0) + 'ms db' : '—';
const n = history.length;
summary.textContent = 'ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : '');
// Mobile navbar is tight — show only total app time; full breakdown lives in the panel.
const compactSummary = root.getAttribute('data-sth-mode') === 'docked'
&& root.parentElement
&& root.parentElement.id === 'server-timing-hud-slot-mobile';
summary.textContent = compactSummary
? app
: ('ST ' + app + ' · ' + db + ' · ' + q + (n > 1 ? ' · ×' + n : ''));
if (countEl) {
countEl.textContent = n + (n === 1 ? ' request' : ' requests');
}
@@ -64,13 +64,15 @@
</div>
@endif
@if ($server->proxySet())
<button type="button" class="listbox-option justify-start! gap-2.5!" wire:click="checkProxyStatus"
wire:loading.attr="disabled" wire:target="checkProxyStatus" role="menuitem">
<button type="button"
class="listbox-option justify-start! min-h-0! h-7! gap-1.5! px-2! py-1! text-[11px] text-neutral-500 dark:text-fg-muted"
wire:click="checkProxyStatus" wire:loading.attr="disabled" wire:target="checkProxyStatus"
role="menuitem">
<span class="contents" wire:loading.remove wire:target="checkProxyStatus">
<x-reicon name="refresh" class="size-3 opacity-70" />
<x-reicon name="refresh" class="size-2.5 opacity-70" />
Refresh status
</span>
<span class="hidden items-center gap-2.5" wire:loading.flex wire:target="checkProxyStatus">
<span class="hidden items-center gap-1.5" wire:loading.flex wire:target="checkProxyStatus">
<x-loading compact />
Refreshing status
</span>
@@ -127,7 +127,7 @@
'active' => $storage->uuid === $currentStorage->uuid,
])">
<x-slot:meta>
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg"
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg"
x-data="{ usable: @js((bool) $currentStorage->is_usable) }"
@storage-status-changed.window="usable = $event.detail.isUsable">
<span class="size-1.5 rounded-full" :class="usable ? 'bg-[#3fb950]' : 'bg-red-500'"></span>
@@ -151,7 +151,7 @@
'active' => $source->getMorphClass() === $currentSource->getMorphClass() && $source->uuid === $currentSource->uuid,
])">
<x-slot:meta>
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg">
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg">
<span @class([
'size-1.5 rounded-full',
'bg-[#3fb950]' => $sourceConnected,
@@ -170,7 +170,7 @@
'active' => $destination->getMorphClass() === $currentDestination->getMorphClass() && $destination->uuid === $currentDestination->uuid,
])">
<x-slot:meta>
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg">
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg">
<span @class([
'size-1.5 rounded-full',
'bg-[#3fb950]' => $currentDestination->getMorphClass() === 'App\\Models\\StandaloneDocker',
@@ -266,7 +266,7 @@
<span class="shrink-0 text-neutral-300 dark:text-fg-faint px-0.5">/</span>
<span class="flex min-w-0 shrink items-center gap-2 h-8 px-2">
<span class="min-w-0 truncate font-semibold text-black dark:text-fg">{{ $currentApplication->name }}</span>
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-neutral-100 px-2.5 text-xs font-medium text-black dark:bg-white/[0.08] dark:text-fg"
<span class="inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full border border-neutral-200 bg-neutral-100 px-2.5 text-xs font-medium text-black dark:border-white/[0.12] dark:bg-white/[0.08] dark:text-fg"
title="{{ $currentApplication->status }}">
<span class="size-1.5 rounded-full {{ $statusDotClass }}"></span>
{{ $statusLabel }}