198 lines
14 KiB
PHP
198 lines
14 KiB
PHP
{{-- Die Liste holt sich ihren Stand selbst.
|
|
|
|
Sie tat es nicht, und das fiel an zwei Stellen auf: Zustand, VPN und
|
|
Belegung standen so lange still, bis jemand neu lud — und ein entfernter
|
|
Host blieb sichtbar, obwohl er schon weg war. Beides sah aus, als sei
|
|
nichts passiert.
|
|
|
|
Schneller, solange etwas in Arbeit ist: `ConfirmRemoveHost` setzt den Host
|
|
auf `disabled` und wirft `PurgeHost` in die Warteschlange, die erst auf die
|
|
Runner-Sperre wartet. In dieser Zeit steht die Zeile noch da, und ohne
|
|
kurzes Nachfassen bliebe sie es gefühlt für immer. Läuft nichts, reicht die
|
|
Minute — die Seite soll nicht dauernd Abfragen erzeugen, nur weil sie offen
|
|
ist. --}}
|
|
<div class="space-y-5" @if ($busy) wire:poll.5s @else wire:poll.60s @endif>
|
|
<div class="flex items-start justify-between gap-4 animate-rise">
|
|
<div>
|
|
<h1 class="text-2xl font-bold tracking-tight text-ink">{{ __('hosts.title') }}</h1>
|
|
<p class="mt-1 text-sm text-muted">{{ __('hosts.subtitle') }}</p>
|
|
</div>
|
|
<a href="{{ route('admin.hosts.create') }}" wire:navigate>
|
|
<x-ui.button variant="primary" size="sm"><x-ui.icon name="plus" class="size-4" />{{ __('hosts.add') }}</x-ui.button>
|
|
</a>
|
|
</div>
|
|
|
|
{{-- Die Anleitung gehört hierher, nicht nur auf die Anlegen-Seite: ihre
|
|
ersten zwei Schritte passieren beim Anbieter, und wer erst auf „Host
|
|
hinzufügen" klickt, hat den Server womöglich noch gar nicht bestellt.
|
|
Zusammengeklappt, damit sie die Liste nicht verdrängt — aber auffindbar,
|
|
ohne dass man vorher etwas anlegen muss. --}}
|
|
<details class="group overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:20ms]">
|
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 px-6 py-3">
|
|
<span class="flex items-center gap-2.5 text-sm font-medium text-body">
|
|
<x-ui.icon name="life-buoy" class="size-4" />
|
|
{{ __('hosts.takeover.how_title') }}
|
|
<span class="hidden text-xs font-normal text-muted sm:inline">· {{ __('hosts.takeover.how_sub') }}</span>
|
|
</span>
|
|
<x-ui.icon name="chevron-down" class="size-4 shrink-0 text-muted transition group-open:rotate-180" />
|
|
</summary>
|
|
<x-admin.takeover-steps class="border-t border-line" />
|
|
</details>
|
|
|
|
{{-- Filter bar — scales to many hosts (search + datacenter + status). --}}
|
|
<div class="flex flex-wrap items-center gap-2 animate-rise [animation-delay:40ms]">
|
|
<div class="relative min-w-48 flex-1">
|
|
<input type="search" wire:model.live.debounce.300ms="search" placeholder="{{ __('hosts.search_placeholder') }}"
|
|
class="w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-muted" />
|
|
</div>
|
|
<select wire:model.live="datacenter" class="rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
|
<option value="">{{ __('hosts.all_datacenters') }}</option>
|
|
@foreach ($datacenters as $dc)
|
|
<option value="{{ $dc->code }}">{{ $dc->name }} ({{ $dc->code }})</option>
|
|
@endforeach
|
|
</select>
|
|
<select wire:model.live="status" class="rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
|
<option value="">{{ __('hosts.all_statuses') }}</option>
|
|
@foreach ($statuses as $s)
|
|
<option value="{{ $s }}">{{ __('hosts.status.'.$s) }}</option>
|
|
@endforeach
|
|
</select>
|
|
@if ($search !== '' || $datacenter !== '' || $status !== '')
|
|
<button type="button" wire:click="clearFilters" class="rounded-md border border-line px-3 py-2 text-sm text-muted hover:bg-surface-hover">{{ __('hosts.clear') }}</button>
|
|
@endif
|
|
<span class="ml-auto text-xs text-muted">{{ __('hosts.count', ['shown' => $hosts->count(), 'total' => $total]) }}</span>
|
|
</div>
|
|
|
|
@if ($hosts->isEmpty())
|
|
<div class="rounded-lg border border-dashed border-line-strong bg-surface p-10 text-center animate-rise">
|
|
<span class="mx-auto grid size-12 place-items-center rounded-lg bg-surface-2 text-muted"><x-ui.icon name="server" class="size-6" /></span>
|
|
<p class="mt-3 text-sm text-muted">{{ $total === 0 ? __('hosts.empty') : __('hosts.no_matches') }}</p>
|
|
</div>
|
|
@else
|
|
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:80ms]">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.host') }}</th>
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.datacenter') }}</th>
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.health') }}</th>
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.vpn') }}</th>
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.instances') }}</th>
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.capacity') }}</th>
|
|
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.status') }}</th>
|
|
{{-- Leer mit Absicht: die Spalte trägt nur den
|
|
Terminal-Knopf, und der ist nicht für jeden da
|
|
(`hosts.manage`). Eine Beschriftung, die
|
|
unabhängig davon immer dastünde, wäre eine
|
|
falsche Zusage.
|
|
|
|
Hier stand einmal „Berechtigung,
|
|
Betriebsbereitschaft" — Letzteres entscheidet
|
|
seit dem Entsperren des Knopfes nichts mehr,
|
|
und zwanzig Zeilen tiefer begründet der
|
|
Kommentar am Knopf ausführlich das Gegenteil. --}}
|
|
<th class="px-4 py-3 font-semibold"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach ($hosts as $host)
|
|
@php
|
|
$badge = ['pending' => 'pending', 'onboarding' => 'provisioning', 'active' => 'active', 'error' => 'failed', 'disabled' => 'suspended'][$host->status] ?? 'info';
|
|
$health = $host->healthState();
|
|
$hdot = ['online' => 'bg-success-bright', 'stale' => 'bg-warning', 'offline' => 'bg-danger'][$health];
|
|
$usedPct = $host->usedPct();
|
|
|
|
$vpnPeer = $host->vpnPeer;
|
|
$vpnState = $host->vpnState($vpnPeer);
|
|
$vpnIcon = ['not_configured' => 'unlock', 'connected' => 'lock', 'silent' => 'alert-triangle', 'unknown' => 'life-buoy'][$vpnState];
|
|
$vpnTone = ['not_configured' => 'text-muted', 'connected' => 'text-success', 'silent' => 'text-warning', 'unknown' => 'text-muted'][$vpnState];
|
|
@endphp
|
|
<tr wire:key="host-{{ $host->uuid }}" class="border-b border-line last:border-0 hover:bg-surface-hover">
|
|
<td class="px-4 py-3">
|
|
<a href="{{ route('admin.hosts.show', $host) }}" wire:navigate class="flex items-center gap-3">
|
|
<span class="grid size-9 shrink-0 place-items-center rounded-lg bg-surface-2 text-accent-text"><x-ui.icon name="server" class="size-4" /></span>
|
|
<span class="min-w-0">
|
|
<span class="block font-mono font-semibold text-ink">{{ $host->name }}</span>
|
|
<span class="block font-mono text-xs text-muted">{{ $host->public_ip }}</span>
|
|
</span>
|
|
</a>
|
|
</td>
|
|
<td class="px-4 py-3 font-mono text-xs text-body">{{ $host->datacenter }}</td>
|
|
{{-- `align-middle` an beiden: ein `inline-flex` holt sich seine
|
|
Grundlinie vom ERSTEN Kind, und das ist hier links ein
|
|
8px-Punkt und rechts ein 16px-Icon. Beide sitzen in einer
|
|
18,6px hohen Zeile mittig, ihre Unterkanten liegen deshalb
|
|
4px auseinander — und genau so weit standen „Online" und
|
|
„Verbunden" versetzt, obwohl beide Zellen gleich hoch sind.
|
|
Nachgemessen: 361,3 gegen 358,5; mit `align-middle` beide
|
|
361,6. --}}
|
|
<td class="px-4 py-3">
|
|
<span class="inline-flex items-center gap-1.5 align-middle text-xs text-body">
|
|
<span class="size-2 rounded-pill {{ $hdot }}" aria-hidden="true"></span>{{ __('hosts.health.'.$health) }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span class="inline-flex items-center gap-1.5 align-middle text-xs {{ $vpnTone }}">
|
|
<x-ui.icon name="{{ $vpnIcon }}" class="size-4" />
|
|
@if ($vpnState === 'silent' && $vpnPeer?->last_handshake_at)
|
|
{{ __('hosts.vpn.silent_since', ['time' => $vpnPeer->last_handshake_at->diffForHumans()]) }}
|
|
@else
|
|
{{ __('hosts.vpn.'.$vpnState) }}
|
|
@endif
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 font-mono text-body">{{ $host->instances_count }}</td>
|
|
<td class="px-4 py-3">
|
|
@if ($host->total_gb)
|
|
<div class="flex items-center gap-2">
|
|
<div class="h-1.5 w-24 overflow-hidden rounded-pill bg-surface-2">
|
|
<div class="h-full rounded-pill {{ $usedPct >= 80 ? 'bg-danger' : 'bg-accent' }}" style="width: {{ $usedPct }}%"></div>
|
|
</div>
|
|
<span class="font-mono text-xs text-muted">{{ $host->availableGb() }} / {{ $host->freeGb() }} GB</span>
|
|
</div>
|
|
@else
|
|
<span class="text-xs text-muted">{{ __('hosts.unknown') }}</span>
|
|
@endif
|
|
{{-- Steht getrennt da, statt in der Zahl darüber zu verschwinden:
|
|
diese Maschine zählt nicht zum allgemeinen Bestand (Host::placeableIn()). --}}
|
|
@if ($host->reserved_for_customer_id)
|
|
<span class="mt-1 flex items-center gap-1 text-xs text-muted">
|
|
<x-ui.icon name="lock" class="size-3.5 shrink-0" />{{ __('hosts.reserved_badge', ['name' => $host->reservedFor?->name ?? __('hosts.unknown')]) }}
|
|
</span>
|
|
@endif
|
|
</td>
|
|
<td class="px-4 py-3"><x-ui.badge :status="$badge">{{ __('hosts.status.'.$host->status) }}</x-ui.badge></td>
|
|
<td class="px-4 py-3 text-right">
|
|
{{-- Der Knopf steht auf JEDER Zeile, auch wenn dem Host noch die
|
|
Tunneladresse oder der Fingerabdruck fehlt.
|
|
|
|
Vorher verschwand er dann, mit der Begründung, ein Knopf, der
|
|
verlässlich in eine Ausnahme läuft, sei schlechter als gar
|
|
keiner. Das stimmte, solange die Seite dahinter mit einer
|
|
Laravel-Fehlerseite aufging. Sie tut es nicht mehr — sie
|
|
schreibt hin, was fehlt (`HostTerminal::mount()`,
|
|
`hosts.terminal.problem.*`). Damit ist der fehlende Knopf die
|
|
schlechtere Auskunft von beiden: er sah aus wie „hier gibt es
|
|
kein Terminal", nicht wie „hier noch nicht, und zwar deshalb". --}}
|
|
@can('hosts.manage')
|
|
{{-- href direkt am Knopf, nicht als <a> drumherum: interaktiver
|
|
Inhalt in einem Link ist ungültiges HTML, und Mittelklick/
|
|
Statuszeile hingen sonst an einem Element, auf dem der
|
|
Zeiger nie landet (components/ui/button.blade.php Zeile 6-9). --}}
|
|
<x-ui.button variant="secondary" size="sm"
|
|
:href="route('admin.hosts.terminal', ['host' => $host->uuid])"
|
|
target="_blank" rel="noopener" :title="__('hosts.terminal.hint')">
|
|
<x-ui.icon name="activity" class="size-4" />{{ __('hosts.terminal.open') }}
|
|
</x-ui.button>
|
|
@endcan
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|