UI consistency batch: docker names, chart axis, empty states, control heights

Docker container display names drop the compose replica suffix ("clusev-app-1"
-> "clusev-app") in the host page and per-server tab; the real name/id still
drives every action/log call and shows on hover. The logs modal title now
shows the display name instead of the raw hex id.

Dashboard chart: the x-axis label row moves outside the relative chart wrapper
so the y-rail spans only the plot -> the "0" tick lands on the bottom gridline
instead of colliding with "-15 Min", and the 75/50/25 ticks align to their
gridlines.

Uptime + Certificates empty states become a centered icon-chip stack
(matching the threats/audit pattern) instead of a centered title with a
left-anchored hint.

Server-detail tab strip: overflow-y-hidden so the -mb-px overhang can no
longer make it scroll vertically; only the x-axis scrolls.

App-wide badge/button height sweep: x-badge gains a size prop (md = h-8) so
status chips that sit beside h-8 pills/buttons line up (SSH auth-type chip,
WireGuard SSH-lock, settings role). x-btn gains a "field" size (h-9, no coarse
min-h-11 bump) for form-row submit buttons that must match their h-9 input on
every device (audit/system/health/certs/groups/webauthn/commands). Topbar
Ctrl-K + language switch pinned to h-8; the triplicated pagination prev/next
buttons switched from py-sizing to h-11/lg:h-7 to match the page-number
buttons in the same row.

Codex-reviewed (one finding: h-9 class override lost to the coarse min-h-11
on touch -> fixed with the dedicated "field" size). 761 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-07-08 21:20:18 +02:00
parent 4248c83c42
commit 182cb0fedd
25 changed files with 82 additions and 38 deletions

View File

@ -117,7 +117,7 @@ class Index extends Component
}
/** Open the read-only logs modal for a host container (ref re-validated in DockerService). */
public function viewLogs(string $id): void
public function viewLogs(string $id, string $name = ''): void
{
abort_unless(Auth::user()?->can('manage-fleet'), 403);
@ -125,6 +125,7 @@ class Index extends Component
// 0 is the sentinel the logs modal resolves to the Clusev host (no persisted id).
'serverId' => 0,
'ref' => $id,
'name' => $name,
]);
}

View File

@ -17,16 +17,20 @@ class ContainerLogs extends ModalComponent
public string $ref;
/** Display name for the modal title (the real ref stays the docker target). */
public string $name = '';
public string $logs = '';
public bool $loaded = false;
public ?string $error = null;
public function mount(int $serverId, string $ref): void
public function mount(int $serverId, string $ref, string $name = ''): void
{
$this->serverId = $serverId;
$this->ref = $ref;
$this->name = $name;
}
public static function modalMaxWidth(): string

View File

@ -88,13 +88,14 @@ class ServerDocker extends Component
}
/** Open the read-only logs modal for a container on this server (ref re-validated in DockerService). */
public function viewLogs(string $id): void
public function viewLogs(string $id, string $name = ''): void
{
abort_unless(Auth::user()?->can('operate'), 403);
$this->dispatch('openModal', component: 'modals.container-logs', arguments: [
'serverId' => (int) $this->server->id,
'ref' => $id,
'name' => $name,
]);
}

View File

@ -66,9 +66,11 @@ class DockerService
}
$p = explode("\t", $line);
$status = trim($p[3] ?? '');
$name = trim($p[1] ?? '');
$out[] = [
'id' => trim($p[0] ?? ''),
'name' => trim($p[1] ?? ''),
'name' => $name,
'display' => self::displayName($name),
'image' => trim($p[2] ?? ''),
'state' => $this->stateFromStatus($status),
'status' => $status,
@ -79,6 +81,15 @@ class DockerService
return $out;
}
/**
* Display-only container name: strip the compose replica suffix ("clusev-app-1" -> "clusev-app").
* NEVER use for docker commands actions/logs keep the real name/id.
*/
public static function displayName(string $name): string
{
return preg_replace('/-\d+$/', '', $name) ?: $name;
}
/** Derive a coarse state from docker's Status text (portable across versions that lack .State). */
private function stateFromStatus(string $status): string
{

View File

@ -1,4 +1,4 @@
@props(['tone' => 'neutral'])
@props(['tone' => 'neutral', 'size' => 'sm'])
@php
$c = [
'neutral' => 'text-ink-2 border-line bg-line',
@ -6,5 +6,8 @@
'cyan' => 'text-cyan-bright border-cyan/20 bg-cyan/10',
'warning' => 'text-warning border-warning/25 bg-warning/10',
][$tone] ?? 'text-ink-2 border-line bg-line';
// md = control-row height (matches x-status-pill/x-btn h-8) for badges sitting in a row
// with pills/buttons; sm stays the compact inline chip for badges next to plain text.
$s = $size === 'md' ? 'h-8 px-2.5 text-xs' : 'px-2 py-0.5 text-[11px]';
@endphp
<span {{ $attributes->merge(['class' => "inline-flex items-center gap-1 rounded-sm border px-2 py-0.5 font-mono text-[11px] $c"]) }}>{{ $slot }}</span>
<span {{ $attributes->merge(['class' => "inline-flex items-center gap-1 rounded-sm border font-mono $s $c"]) }}>{{ $slot }}</span>

View File

@ -14,10 +14,13 @@
// sm = compact toolbar/row buttons; lg = full-height primary CTA (≥44px touch target, R7).
// sm stays 32px visually on a fine pointer (mouse) for density, but grows to the 44px R7
// minimum on a COARSE pointer (touch) so row actions are tappable — desktop is unaffected.
// field = form-row button that must line up with an h-9 input on EVERY device: fixed h-9,
// NO coarse min-h-11 bump (which would make it taller than the field it sits beside on touch).
$sizes = [
'sm' => ($icon
? 'h-8 w-8 [@media(pointer:coarse)]:h-11 [@media(pointer:coarse)]:w-11'
: 'h-8 px-3 [@media(pointer:coarse)]:min-h-11').' text-xs',
'field' => 'h-9 px-3 text-xs',
'lg' => ($icon ? 'h-11 w-11' : 'h-11 px-4').' text-sm',
];
$classes = 'inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors '

View File

@ -15,14 +15,15 @@
<div wire:key="ctr-{{ $c['id'] }}" class="flex flex-wrap items-center gap-3 px-4 py-3 sm:px-5">
<x-status-dot :status="$pill" :ping="$c['state'] === 'running'" class="h-2.5 w-2.5" />
<div class="min-w-0 flex-1">
<p class="truncate font-mono text-sm text-ink">{{ $c['name'] }}</p>
{{-- Display name strips the compose replica suffix; hover shows the real name. --}}
<p class="truncate font-mono text-sm text-ink" title="{{ $c['name'] }}">{{ $c['display'] ?? $c['name'] }}</p>
<p class="truncate font-mono text-[11px] text-ink-3">{{ $c['image'] }}</p>
</div>
<x-status-pill :status="$pill" class="hidden sm:inline-flex">{{ $c['state'] ?: '—' }}</x-status-pill>
@if ($canOperate)
<div class="flex flex-wrap items-center gap-1.5 pl-5 sm:pl-0">
<x-btn variant="secondary" wire:click="viewLogs('{{ $c['id'] }}')">{{ __('docker.logs') }}</x-btn>
<x-btn variant="secondary" wire:click="viewLogs('{{ $c['id'] }}', '{{ $c['display'] ?? $c['name'] }}')">{{ __('docker.logs') }}</x-btn>
@if ($c['state'] === 'running')
<x-btn variant="secondary" wire:click="action('{{ $c['id'] }}', 'restart')" wire:loading.attr="disabled">{{ __('docker.restart') }}</x-btn>
<x-btn variant="danger-soft" wire:click="action('{{ $c['id'] }}', 'stop')" wire:loading.attr="disabled">{{ __('docker.stop') }}</x-btn>

View File

@ -1,7 +1,7 @@
{{-- DE/EN language switch (R16). Plain links full reload so the whole UI re-renders
in the chosen language; persists per-user + session via the locale route. --}}
@php $current = app()->getLocale(); @endphp
<div class="inline-flex shrink-0 items-center rounded-md border border-line bg-inset p-0.5 font-mono text-[10px] uppercase tracking-wider"
<div class="inline-flex h-8 shrink-0 items-center rounded-md border border-line bg-inset p-0.5 font-mono text-[10px] uppercase tracking-wider"
role="group" aria-label="Sprache / Language">
@foreach (['de' => 'DE', 'en' => 'EN'] as $code => $abbr)
<a href="{{ route('locale.set', $code) }}"

View File

@ -7,7 +7,9 @@
['key' => 'terminal', 'label' => __('servers.tab_terminal'), 'icon' => 'terminal', 'show' => (bool) auth()->user()?->can('operate')],
], fn ($t) => $t['show']);
@endphp
<div class="flex items-center gap-1 overflow-x-auto no-scrollbar border-b border-line" role="tablist" aria-label="{{ __('servers.tabs_aria') }}">
{{-- overflow-y-hidden: the buttons' -mb-px overhangs 1px below the strip, which would otherwise
make it scrollable on the Y axis too only X may ever scroll here. --}}
<div class="flex items-center gap-1 overflow-x-auto overflow-y-hidden no-scrollbar border-b border-line" role="tablist" aria-label="{{ __('servers.tabs_aria') }}">
@foreach ($tabs as $t)
<button type="button" wire:click="$set('tab', '{{ $t['key'] }}')" role="tab"
aria-selected="{{ $tab === $t['key'] ? 'true' : 'false' }}"

View File

@ -17,7 +17,7 @@
<div class="ml-auto flex min-w-0 items-center gap-2">
<x-lang-switch />
<button type="button" @click="$dispatch('cmdk-open')"
class="hidden items-center gap-2 rounded-md border border-line bg-inset px-2.5 py-1.5 text-ink-3 transition-colors hover:border-line-strong hover:text-ink sm:flex"
class="hidden h-8 items-center gap-2 rounded-md border border-line bg-inset px-2.5 text-ink-3 transition-colors hover:border-line-strong hover:text-ink sm:flex"
aria-label="{{ __('shell.commands_open_aria') }}" title="{{ __('shell.commands_title') }}">
<x-icon name="command" class="h-3.5 w-3.5" />
<span class="font-mono text-[10px] uppercase tracking-wider">{{ __('shell.shortcut_ctrl_k') }}</span>

View File

@ -37,7 +37,7 @@
autocomplete="off" autocapitalize="off" spellcheck="false"
placeholder="{{ __('audit.retention_unlimited') }}"
class="h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none sm:max-w-[12rem]" />
<x-btn variant="secondary" wire:click="saveRetention" wire:loading.attr="disabled" class="shrink-0">
<x-btn variant="secondary" size="field" wire:click="saveRetention" wire:loading.attr="disabled" class="shrink-0">
<x-icon name="save" class="h-3.5 w-3.5" /> {{ __('audit.retention_save') }}
</x-btn>
</div>
@ -123,7 +123,7 @@
@if ($events->lastPage() > 1)
<nav aria-label="{{ __('audit.pagination_nav') }}" class="flex items-center justify-between gap-3 border-t border-line px-4 py-3 sm:px-5">
<button type="button" wire:click="previousPage" @disabled($events->onFirstPage())
class="inline-flex items-center gap-1 rounded-md border border-line bg-raised px-2.5 py-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:py-1.5">
class="inline-flex h-11 items-center gap-1 rounded-md border border-line bg-raised px-2.5 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:h-7">
<x-icon name="chevron-left" class="h-3.5 w-3.5" />
<span class="hidden sm:inline">{{ __('audit.pagination_prev') }}</span>
</button>
@ -145,7 +145,7 @@
</div>
<button type="button" wire:click="nextPage" @disabled(! $events->hasMorePages())
class="inline-flex items-center gap-1 rounded-md border border-line bg-raised px-2.5 py-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:py-1.5">
class="inline-flex h-11 items-center gap-1 rounded-md border border-line bg-raised px-2.5 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:h-7">
<span class="hidden sm:inline">{{ __('audit.pagination_next') }}</span>
<x-icon name="chevron-right" class="h-3.5 w-3.5" />
</button>

View File

@ -29,16 +29,19 @@
<input wire:model="port" type="number" min="1" max="65535" class="{{ $field }} font-mono" />
@error('port') <p class="mt-1 font-mono text-[11px] text-offline">{{ $message }}</p> @enderror
</div>
<x-btn variant="primary" type="submit">{{ __('certs.add') }}</x-btn>
<x-btn variant="primary" size="field" type="submit">{{ __('certs.add') }}</x-btn>
</form>
</x-panel>
{{-- Endpoints --}}
@if ($endpoints->isEmpty())
<x-panel>
<div class="py-8 text-center">
<div class="flex flex-col items-center gap-2 py-8 text-center">
<span class="grid h-10 w-10 place-items-center rounded-md bg-raised text-ink-4">
<x-icon name="lock" class="h-5 w-5" />
</span>
<p class="font-display text-sm font-semibold text-ink">{{ __('certs.no_endpoints_title') }}</p>
<p class="mt-1 max-w-sm text-xs text-ink-3">{{ __('certs.no_endpoints') }}</p>
<p class="max-w-sm text-xs text-ink-3">{{ __('certs.no_endpoints') }}</p>
</div>
</x-panel>
@else

View File

@ -112,14 +112,14 @@
<p class="{{ $label }}">{{ __('commands.new_runbook') }}</p>
<form wire:submit="createRunbook" class="grid gap-3 sm:grid-cols-[minmax(0,14rem)_minmax(0,1fr)_auto] sm:items-start">
<div>
<input wire:model="runbookName" type="text" maxlength="80" placeholder="{{ __('commands.runbook_name_label') }}" class="{{ $field }} placeholder:text-ink-4" />
<input wire:model="runbookName" type="text" maxlength="80" placeholder="{{ __('commands.runbook_name_label') }}" class="{{ $field }} h-9 placeholder:text-ink-4" />
@error('runbookName') <p class="mt-1 font-mono text-[11px] text-offline">{{ $message }}</p> @enderror
</div>
<div>
<input wire:model="runbookCommand" type="text" maxlength="4000" placeholder="{{ __('commands.runbook_command_label') }}" class="{{ $field }} font-mono placeholder:text-ink-4" />
<input wire:model="runbookCommand" type="text" maxlength="4000" placeholder="{{ __('commands.runbook_command_label') }}" class="{{ $field }} h-9 font-mono placeholder:text-ink-4" />
@error('runbookCommand') <p class="mt-1 font-mono text-[11px] text-offline">{{ $message }}</p> @enderror
</div>
<x-btn variant="primary" type="submit">{{ __('commands.create') }}</x-btn>
<x-btn variant="primary" size="field" type="submit">{{ __('commands.create') }}</x-btn>
</form>
</div>
</x-panel>

View File

@ -84,10 +84,13 @@
</g>
</svg>
</div>
</div>
<div class="mt-1.5 flex justify-between font-mono text-[9px] text-ink-4">
<span>{{ __('dashboard.axis_minus_15min') }}</span><span>-10</span><span>-5</span><span>{{ __('dashboard.axis_now') }}</span>
</div>
{{-- X axis OUTSIDE the relative wrapper (mirrors servers/show.blade.php): keeps the
y-rail's inset-y-0 spanning exactly the plot, so the "0" tick lands on the bottom
gridline instead of colliding with "-15 Min" down here. --}}
<div class="mt-3 flex justify-between pl-7 font-mono text-[9px] text-ink-4">
<span>{{ __('dashboard.axis_minus_15min') }}</span><span>-10</span><span>-5</span><span>{{ __('dashboard.axis_now') }}</span>
</div>
</x-panel>

View File

@ -41,16 +41,19 @@
<input wire:model="port" type="number" min="1" max="65535" class="{{ $field }} font-mono" />
@error('port') <p class="mt-1 font-mono text-[11px] text-offline">{{ $message }}</p> @enderror
</div>
<x-btn variant="primary" type="submit">{{ __('health.add') }}</x-btn>
<x-btn variant="primary" size="field" type="submit">{{ __('health.add') }}</x-btn>
</form>
</x-panel>
{{-- Checks --}}
@if ($checks->isEmpty())
<x-panel>
<div class="py-8 text-center">
<div class="flex flex-col items-center gap-2 py-8 text-center">
<span class="grid h-10 w-10 place-items-center rounded-md bg-raised text-ink-4">
<x-icon name="activity" class="h-5 w-5" />
</span>
<p class="font-display text-sm font-semibold text-ink">{{ __('health.no_checks_title') }}</p>
<p class="mt-1 max-w-sm text-xs text-ink-3">{{ __('health.no_checks') }}</p>
<p class="max-w-sm text-xs text-ink-3">{{ __('health.no_checks') }}</p>
</div>
</x-panel>
@else

View File

@ -4,7 +4,7 @@
<x-icon name="box" class="h-4 w-4" />
</span>
<div class="min-w-0 flex-1">
<h2 class="truncate font-display text-base font-semibold text-ink" title="{{ $ref }}">{{ $ref }}</h2>
<h2 class="truncate font-display text-base font-semibold text-ink" title="{{ $ref }}">{{ $name !== '' ? $name : $ref }}</h2>
<p class="font-mono text-[11px] text-ink-3">{{ __('docker.logs_subtitle') }}</p>
</div>
</div>

View File

@ -41,7 +41,7 @@
@endforeach
</div>
</div>
<x-btn variant="primary" type="submit">{{ __('groups.create') }}</x-btn>
<x-btn variant="primary" size="field" type="submit">{{ __('groups.create') }}</x-btn>
</form>
</x-panel>

View File

@ -121,7 +121,7 @@
<div class="flex flex-wrap items-center gap-2">
<p class="truncate font-display text-sm font-semibold text-ink">{{ $cred->name ?: $cred->username }}</p>
<x-status-pill :status="$credStatus" class="shrink-0">{{ $credStatusLabel }}</x-status-pill>
<x-badge tone="neutral" class="shrink-0">{{ $credAuthLabel }}</x-badge>
<x-badge tone="neutral" size="md" class="shrink-0">{{ $credAuthLabel }}</x-badge>
</div>
<p class="mt-1.5 truncate font-mono text-[11px] text-ink-3">
<span class="text-ink-2">{{ $cred->username }}</span>@<span>{{ $server->ip }}</span>

View File

@ -38,7 +38,7 @@
class="inline-flex h-8 items-center gap-1.5 rounded-md border border-line px-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/40 hover:text-accent-text">
<x-icon name="help-circle" class="h-3.5 w-3.5" />{{ __('onboarding.relaunch') }}
</button>
<x-badge tone="neutral">{{ $u->role?->label() ?? __('settings.role_admin') }}</x-badge>
<x-badge tone="neutral" size="md">{{ $u->role?->label() ?? __('settings.role_admin') }}</x-badge>
<x-two-factor-badge :enabled="$twoFactorEnabled" />
</div>
</div>

View File

@ -31,7 +31,7 @@
<div class="flex flex-col gap-2 sm:flex-row" x-data>
<input wire:model="newName" type="text" maxlength="60" placeholder="{{ __('auth.webauthn_name_placeholder') }}"
class="h-9 w-full rounded-md border border-line bg-inset px-3 text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
<x-btn variant="accent" class="w-full shrink-0 sm:w-auto" x-on:click="window.clusevWebauthn?.register($wire)">
<x-btn variant="accent" size="field" class="w-full shrink-0 sm:w-auto" x-on:click="window.clusevWebauthn?.register($wire)">
<x-icon name="plus" class="h-3.5 w-3.5" /> {{ __('auth.webauthn_add') }}
</x-btn>
</div>

View File

@ -82,7 +82,7 @@
placeholder="{{ __('system.domain_placeholder') }}"
class="h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
@can('manage-panel')
<x-modal-trigger variant="primary" action="confirmDomain" class="shrink-0">
<x-modal-trigger variant="primary" size="field" action="confirmDomain" class="shrink-0">
<x-icon name="save" class="h-3.5 w-3.5" /> {{ __('common.save') }}
</x-modal-trigger>
@endcan

View File

@ -131,7 +131,7 @@
@if ($events->lastPage() > 1)
<nav aria-label="{{ __('threats.pagination_nav') }}" class="flex items-center justify-between gap-3 border-t border-line px-4 py-3 sm:px-5">
<button type="button" wire:click="previousPage" @disabled($events->onFirstPage())
class="inline-flex items-center gap-1 rounded-md border border-line bg-raised px-2.5 py-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:py-1.5">
class="inline-flex h-11 items-center gap-1 rounded-md border border-line bg-raised px-2.5 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:h-7">
<x-icon name="chevron-left" class="h-3.5 w-3.5" />
<span class="hidden sm:inline">{{ __('threats.pagination_prev') }}</span>
</button>
@ -153,7 +153,7 @@
</div>
<button type="button" wire:click="nextPage" @disabled(! $events->hasMorePages())
class="inline-flex items-center gap-1 rounded-md border border-line bg-raised px-2.5 py-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:py-1.5">
class="inline-flex h-11 items-center gap-1 rounded-md border border-line bg-raised px-2.5 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:h-7">
<span class="hidden sm:inline">{{ __('threats.pagination_next') }}</span>
<x-icon name="chevron-right" class="h-3.5 w-3.5" />
</button>

View File

@ -226,7 +226,7 @@
@if ($totalPages > 1)
<nav aria-label="{{ __('versions.pagination_nav') }}" class="flex items-center justify-between gap-3 border-t border-line px-4 py-3 sm:px-5">
<button type="button" wire:click="gotoChangelogPage({{ $changelogPage - 1 }})" @disabled($changelogPage <= 1)
class="inline-flex items-center gap-1 rounded-md border border-line bg-raised px-2.5 py-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:py-1.5">
class="inline-flex h-11 items-center gap-1 rounded-md border border-line bg-raised px-2.5 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:h-7">
<x-icon name="chevron-left" class="h-3.5 w-3.5" />
<span class="hidden sm:inline">{{ __('versions.pagination_prev') }}</span>
</button>
@ -248,7 +248,7 @@
</div>
<button type="button" wire:click="gotoChangelogPage({{ $changelogPage + 1 }})" @disabled($changelogPage >= $totalPages)
class="inline-flex items-center gap-1 rounded-md border border-line bg-raised px-2.5 py-3 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:py-1.5">
class="inline-flex h-11 items-center gap-1 rounded-md border border-line bg-raised px-2.5 font-mono text-[11px] text-ink-2 transition-colors hover:border-accent/30 hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:text-ink-2 lg:h-7">
<span class="hidden sm:inline">{{ __('versions.pagination_next') }}</span>
<x-icon name="chevron-right" class="h-3.5 w-3.5" />
</button>

View File

@ -111,12 +111,12 @@
<span class="font-mono text-[11px] text-ink-3">{{ __('wireguard.ssh_lock') }}</span>
@if ($status['gate']['ssh'])
<div class="flex items-center gap-2">
<x-badge tone="warning">{{ __('wireguard.ssh_lock_on_state') }}</x-badge>
<x-badge tone="warning" size="md">{{ __('wireguard.ssh_lock_on_state') }}</x-badge>
<x-btn variant="accent" size="sm" wire:click="confirmSshGate(false)">{{ __('wireguard.ssh_lock_turn_off') }}</x-btn>
</div>
@else
<div class="flex items-center gap-2">
<x-badge tone="neutral">{{ __('wireguard.ssh_lock_off_state') }}</x-badge>
<x-badge tone="neutral" size="md">{{ __('wireguard.ssh_lock_off_state') }}</x-badge>
<x-btn variant="danger-soft" size="sm" wire:click="confirmSshGate(true)">{{ __('wireguard.ssh_lock_turn_on') }}</x-btn>
</div>
@endif

View File

@ -47,11 +47,20 @@ class DockerServiceTest extends TestCase
$this->assertCount(3, $rows);
$this->assertSame('web', $rows[0]['name']);
$this->assertSame('web', $rows[0]['display']); // no replica suffix -> unchanged
$this->assertSame('running', $rows[0]['state']); // "Up ..."
$this->assertSame('exited', $rows[1]['state']); // "Exited ..."
$this->assertSame('paused', $rows[2]['state']); // "Up ... (Paused)"
}
public function test_display_name_strips_the_compose_replica_suffix_only(): void
{
$this->assertSame('clusev-app', DockerService::displayName('clusev-app-1'));
$this->assertSame('web', DockerService::displayName('web-12'));
$this->assertSame('unruffled_leavitt', DockerService::displayName('unruffled_leavitt'));
$this->assertSame('clusev-app', DockerService::displayName('clusev-app'));
}
public function test_containers_throws_the_real_error_when_docker_fails(): void
{
[$docker, $fleet] = $this->make();