282 lines
17 KiB
PHP
282 lines
17 KiB
PHP
{{-- Zwei Taktungen, ein Aufruf: während der Übernahme zählt jede Sekunde, danach
|
|
schreibt Proxmox seine Messwerte ohnehin nur einmal pro Minute fort. Der
|
|
Zwischenspeicher in HostLoadSeries hält die schnelle Taktung vom Host fern. --}}
|
|
<div class="space-y-5" @if ($run && in_array($run->status, ['pending', 'running', 'waiting'])) wire:poll.4s @else wire:poll.60s @endif>
|
|
@php
|
|
$badge = ['pending' => 'pending', 'onboarding' => 'provisioning', 'active' => 'active', 'error' => 'failed', 'disabled' => 'suspended'][$host->status] ?? 'info';
|
|
@endphp
|
|
|
|
<div class="flex flex-wrap items-start justify-between gap-4 animate-rise">
|
|
<div class="min-w-0">
|
|
<a href="{{ route('admin.hosts') }}" wire:navigate class="inline-flex items-center gap-1.5 text-xs font-medium text-muted hover:text-body">
|
|
<x-ui.icon name="arrow-left" class="size-4" />{{ __('hosts.back') }}
|
|
</a>
|
|
<div class="mt-2 flex items-center gap-3">
|
|
<h1 class="font-mono text-2xl font-bold tracking-tight text-ink">{{ $host->name }}</h1>
|
|
<x-ui.badge :status="$badge">{{ __('hosts.status.'.$host->status) }}</x-ui.badge>
|
|
</div>
|
|
<p class="mt-1 font-mono text-xs text-muted">{{ $host->public_ip }} · {{ $host->datacenter }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
@if ($run && $run->status === 'failed')
|
|
<x-ui.button variant="secondary" size="sm" wire:click="retry" wire:loading.attr="disabled">
|
|
<x-ui.icon name="rotate-ccw" class="size-4" />{{ __('hosts.retry') }}
|
|
</x-ui.button>
|
|
@endif
|
|
@if (in_array($host->status, ['active', 'disabled'], true))
|
|
<x-ui.button variant="secondary" size="sm" wire:click="toggleMaintenance" wire:loading.attr="disabled">
|
|
<x-ui.icon name="{{ $host->status === 'active' ? 'shield' : 'check' }}" class="size-4" />
|
|
{{ $host->status === 'active' ? __('hosts.detail.drain') : __('hosts.detail.activate') }}
|
|
</x-ui.button>
|
|
@endif
|
|
{{-- Die Bedingung steht am Bauteil, nicht hier: `issue()` prüft
|
|
dieselbe, und zwei Fassungen liefen auseinander. --}}
|
|
@if (\App\Livewire\Admin\ReissueTakeover::eligible($host))
|
|
<x-ui.button variant="secondary" size="sm"
|
|
x-on:click="$dispatch('openModal', { component: 'admin.reissue-takeover', arguments: { uuid: '{{ $host->uuid }}' } })">
|
|
<x-ui.icon name="refresh" class="size-4" />{{ __('hosts.reissue.action') }}
|
|
</x-ui.button>
|
|
@endif
|
|
<x-ui.button variant="danger" size="sm"
|
|
x-on:click="$dispatch('openModal', { component: 'admin.confirm-remove-host', arguments: { uuid: '{{ $host->uuid }}' } })">
|
|
<x-ui.icon name="trash-2" class="size-4" />{{ __('hosts.remove') }}
|
|
</x-ui.button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Health + resource overview --}}
|
|
@php
|
|
$hdot = ['online' => 'bg-success-bright', 'stale' => 'bg-warning', 'offline' => 'bg-danger'][$health];
|
|
// Die Scheibe hinter dem Punkt trägt dieselbe Aussage in Farbe. Vorher
|
|
// war sie bg-surface-2 auf weißem Grund — technisch da, sichtbar nicht,
|
|
// und damit eine Fläche, die nichts sagt.
|
|
$hdisc = ['online' => 'bg-success-bg', 'stale' => 'bg-warning-bg', 'offline' => 'bg-danger-bg'][$health];
|
|
$usedPct = $host->usedPct();
|
|
@endphp
|
|
{{-- Ein Kachelraster statt dreier ungleicher Kästen.
|
|
|
|
Vorher standen Zustand, Speicher und eine gemeinsame Kurve nebeneinander:
|
|
zwei davon halb leer, die dritte überfüllt, und die gemeinsame Kurve
|
|
brauchte eine Legende, um zu sagen, welche Linie welche ist.
|
|
|
|
Eine Reihe je Kachel löst alle drei Beschwerden auf einmal — die
|
|
Beschriftung der Kachel benennt die Reihe, also braucht es keine
|
|
Legende; die Höhen sind durch das Raster gleich statt zufällig; und der
|
|
leere Platz ist mit Messwerten gefüllt, die es ohnehin schon gab. --}}
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 animate-rise">
|
|
@php
|
|
$pct = fn (?float $v) => $v === null ? '—' : number_format($v, $v < 10 ? 1 : 0, ',', '.');
|
|
$mib = fn (?float $v) => $v === null ? '—' : number_format($v, 2, ',', '.');
|
|
@endphp
|
|
|
|
<x-ui.metric :label="__('hosts.detail.cpu_load')"
|
|
:value="$pct($now['cpu'])"
|
|
:unit="$now['cpu'] === null ? null : '%'"
|
|
:foot="$load['available'] ? __('hosts.detail.load_hint') : __('hosts.detail.no_load_short')">
|
|
@if ($load['available'])
|
|
<x-slot:visual><x-ui.spark :points="$load['cpu']" :min="0" :max="100" tone="muted" area width="120" height="40" /></x-slot:visual>
|
|
@endif
|
|
</x-ui.metric>
|
|
|
|
<x-ui.metric :label="__('hosts.detail.ram_load')"
|
|
:value="$pct($now['ram'])"
|
|
:unit="$now['ram'] === null ? null : '%'"
|
|
:foot="$load['available'] ? __('hosts.detail.load_hint') : __('hosts.detail.no_load_short')">
|
|
@if ($load['available'])
|
|
<x-slot:visual><x-ui.spark :points="$load['ram']" :min="0" :max="100" tone="muted" area width="120" height="40" /></x-slot:visual>
|
|
@endif
|
|
</x-ui.metric>
|
|
|
|
{{-- Die Ablage als Ring, wie die Vorlage die Festplatte zeichnet. Der
|
|
Ring zeigt BELEGT, die Zahl daneben FREI — beides steht dabei, weil
|
|
ein Ring ohne Beschriftung beides bedeuten kann. --}}
|
|
<x-ui.metric :label="__('hosts.detail.storage')"
|
|
:value="$host->total_gb ? $host->availableGb() : '—'"
|
|
:unit="$host->total_gb ? '/ '.$host->freeGb().' GB '.__('hosts.free') : null"
|
|
:foot="$host->total_gb ? __('hosts.detail.committed', ['gb' => $host->committedGb()]) : __('hosts.unknown')">
|
|
@if ($host->total_gb)
|
|
<x-slot:visual><x-ui.ring :percent="$usedPct" /></x-slot:visual>
|
|
@endif
|
|
</x-ui.metric>
|
|
|
|
<x-ui.metric :label="__('hosts.detail.net_in')"
|
|
:value="$mib($now['netin'])"
|
|
:unit="$now['netin'] === null ? null : 'MiB/s'"
|
|
:foot="$load['available'] ? __('hosts.detail.load_hint') : __('hosts.detail.no_load_short')">
|
|
@if ($load['available'])
|
|
<x-slot:visual><x-ui.spark :points="$load['netin']" :min="0" tone="muted" area width="120" height="40" /></x-slot:visual>
|
|
@endif
|
|
</x-ui.metric>
|
|
|
|
<x-ui.metric :label="__('hosts.detail.net_out')"
|
|
:value="$mib($now['netout'])"
|
|
:unit="$now['netout'] === null ? null : 'MiB/s'"
|
|
:foot="$load['available'] ? __('hosts.detail.load_hint') : __('hosts.detail.no_load_short')">
|
|
@if ($load['available'])
|
|
<x-slot:visual><x-ui.spark :points="$load['netout']" :min="0" tone="muted" area width="120" height="40" /></x-slot:visual>
|
|
@endif
|
|
</x-ui.metric>
|
|
|
|
<x-ui.metric :label="__('hosts.detail.health')"
|
|
:value="__('hosts.health.'.$health)"
|
|
:foot="$host->last_seen_at ? __('hosts.detail.last_seen', ['time' => $host->last_seen_at->diffForHumans()]) : __('hosts.detail.never_seen')">
|
|
<x-slot:visual>
|
|
{{-- Halb so groß wie der Ring nebenan, nicht gleich groß: eine
|
|
gefüllte Scheibe wiegt optisch weit mehr als ein dünner
|
|
Ring, und bei 62 px erschlug sie die Kachel. --}}
|
|
<span class="grid size-8 place-items-center rounded-pill {{ $hdisc }}">
|
|
<span class="size-2.5 rounded-pill {{ $hdot }} {{ $health === 'online' ? 'animate-pulse' : '' }}" aria-hidden="true"></span>
|
|
</span>
|
|
</x-slot:visual>
|
|
</x-ui.metric>
|
|
</div>
|
|
|
|
{{-- Ausstattung: was feststeht. Vorher standen diese Zahlen in Karten, die
|
|
wie die lebendigen aussahen — deshalb las sich eine Kernzahl wie eine
|
|
Messung. --}}
|
|
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
|
<h2 class="mb-3 text-xs font-semibold text-muted">{{ __('hosts.detail.spec') }}</h2>
|
|
<dl class="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-3 lg:grid-cols-6">
|
|
@foreach ([
|
|
[__('hosts.detail.public_ip'), $host->public_ip ?? __('hosts.unknown')],
|
|
[__('hosts.meta.cores'), $host->cpu_cores ? (string) $host->cpu_cores : '—'],
|
|
[__('hosts.meta.ram'), $host->total_ram_mb ? round($host->total_ram_mb / 1024).' GB' : '—'],
|
|
[__('hosts.detail.node'), $host->node ?? __('hosts.unknown')],
|
|
[__('hosts.meta.wg_ip'), $host->wg_ip ?? __('hosts.unknown')],
|
|
] as [$label, $value])
|
|
<div class="min-w-0">
|
|
<dt class="text-xs text-muted">{{ $label }}</dt>
|
|
<dd class="mt-0.5 truncate font-mono text-sm font-semibold text-ink">{{ $value }}</dd>
|
|
</div>
|
|
@endforeach
|
|
<div class="min-w-0">
|
|
<dt class="text-xs text-muted">{{ __('hosts.meta.version') }}</dt>
|
|
@if ($version['version'])
|
|
{{-- Die Zahl, die jemand sucht. Die Bau-Kennung steht nur noch
|
|
im Titel: als eigene Zeile zwang sie die ganze Tafel in
|
|
eine zweite Reihe, für eine Zeichenkette, die fast
|
|
niemand liest und niemand abtippt. --}}
|
|
<dd class="mt-0.5 truncate font-mono text-sm font-semibold text-ink" title="{{ $host->pve_version }}">Proxmox VE {{ $version['version'] }}</dd>
|
|
@else
|
|
<dd class="mt-0.5 truncate font-mono text-sm font-semibold text-ink">{{ __('hosts.unknown') }}</dd>
|
|
@endif
|
|
</div>
|
|
</dl>
|
|
|
|
{{-- Die Reserve gehört hierher und nicht auf eine Kachel: die Kacheln
|
|
zeigen, was gemessen wurde, diese Tafel, was eingestellt ist. Ein
|
|
Eingabefeld zwischen Messwerten sähe aus, als ließe sich eine
|
|
Messung ändern. --}}
|
|
@if ($host->total_gb)
|
|
<div class="mt-5 flex flex-wrap items-center gap-2 border-t border-line pt-4" x-data="{ r: {{ $host->reserve_pct }} }">
|
|
<span class="text-xs text-muted">{{ __('hosts.detail.reserve_label') }}</span>
|
|
<input type="number" min="0" max="90" x-model.number="r" aria-label="{{ __('hosts.detail.reserve_label') }}"
|
|
class="w-16 rounded-md border border-line-strong bg-surface px-2 py-1 text-xs text-ink" />
|
|
<span class="text-xs text-muted">%</span>
|
|
<button type="button" x-on:click="$wire.saveReserve(r)" class="rounded-md border border-line px-2 py-1 text-xs font-semibold text-body hover:border-accent-border hover:text-accent-text">{{ __('hosts.detail.reserve_save') }}</button>
|
|
<span class="text-xs text-muted">{{ __('hosts.detail.reserve_hint') }}</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Hosted instances. Die Anzahl steht in der Überschrift, statt als eigene
|
|
Kleinkarte neben der Liste, die sie ohnehin zeigt. --}}
|
|
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:100ms]">
|
|
<h2 class="mb-3 text-sm font-semibold text-ink">
|
|
{{ __('hosts.detail.hosted') }}
|
|
<span class="ml-1 font-mono text-xs font-normal text-muted">{{ $instances->count() }}</span>
|
|
</h2>
|
|
@if ($instances->isEmpty())
|
|
<p class="text-sm text-muted">{{ __('hosts.detail.no_instances') }}</p>
|
|
@else
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<tbody>
|
|
@foreach ($instances as $inst)
|
|
@php $ibadge = ['active' => 'active', 'provisioning' => 'provisioning', 'failed' => 'failed', 'suspended' => 'suspended', 'cancellation_scheduled' => 'warning'][$inst->status] ?? 'info'; @endphp
|
|
<tr class="border-b border-line last:border-0">
|
|
<td class="py-2.5 pr-4 font-mono text-ink">{{ $inst->subdomain ?? '—' }}</td>
|
|
<td class="py-2.5 pr-4 text-body">{{ __('billing.plan.'.$inst->plan) }}</td>
|
|
<td class="py-2.5 pr-4 font-mono text-xs text-muted">{{ $inst->disk_gb }} GB</td>
|
|
<td class="py-2.5 text-right"><x-ui.badge :status="$ibadge">{{ __('hosts.istatus.'.$inst->status) }}</x-ui.badge></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($run && $run->status === 'failed')
|
|
<div class="flex items-start gap-3 rounded-lg border border-danger-border bg-danger-bg p-4 animate-rise">
|
|
<x-ui.icon name="alert-triangle" class="size-5 shrink-0 text-danger" />
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-semibold text-danger">{{ __('hosts.error_title') }}</p>
|
|
<p class="mt-0.5 break-words text-sm text-body">{{ $run->error }}</p>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@php
|
|
// Zugeklappt, sobald die Übernahme durch ist: fünfzehn abgehakte
|
|
// Schritte sind auf einem seit Wochen laufenden Host kein Dauerinhalt.
|
|
// Aufklappbar, nicht weg — wer nachsehen will, warum ein Schritt
|
|
// wiederholt wurde, findet ihn noch.
|
|
$onboardingDone = $run && $run->status === \App\Models\ProvisioningRun::STATUS_COMPLETED;
|
|
@endphp
|
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-5">
|
|
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise lg:col-span-3">
|
|
@if ($onboardingDone)
|
|
{{-- <details> statt Alpine: ein Aufklapper braucht kein
|
|
JavaScript, und der Inhalt bleibt für die Suche im Browser
|
|
und für Screenreader erreichbar. --}}
|
|
<details class="group">
|
|
<summary class="flex cursor-pointer list-none items-center gap-2 text-sm font-semibold text-ink">
|
|
<x-ui.icon name="check" class="size-4 shrink-0 text-success" />
|
|
<span>{{ __('hosts.detail.onboarding_done') }}</span>
|
|
<span class="ml-auto font-mono text-xs font-normal text-muted">
|
|
{{ $run->updated_at?->local()->isoFormat('D. MMM, HH:mm') }}
|
|
</span>
|
|
</summary>
|
|
<div class="mt-4">
|
|
<x-ui.progress-stepper :steps="$steps" />
|
|
</div>
|
|
</details>
|
|
@else
|
|
<h2 class="mb-4 text-sm font-semibold text-ink">{{ __('hosts.progress') }}</h2>
|
|
@if ($run)
|
|
<x-ui.progress-stepper :steps="$steps" />
|
|
@else
|
|
<p class="text-sm text-muted">{{ __('hosts.no_run') }}</p>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
|
|
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise lg:col-span-2">
|
|
<h2 class="mb-4 text-sm font-semibold text-ink">{{ __('hosts.events') }}</h2>
|
|
@if ($events->isEmpty())
|
|
<p class="text-sm text-muted">{{ __('hosts.no_run') }}</p>
|
|
@else
|
|
<ol class="space-y-3">
|
|
@foreach ($events as $event)
|
|
@php
|
|
$tone = ['advanced' => 'text-success', 'retry' => 'text-warning', 'failed' => 'text-danger', 'info' => 'text-muted'][$event->outcome] ?? 'text-muted';
|
|
@endphp
|
|
<li class="flex items-start gap-2.5 text-sm">
|
|
<span class="mt-1.5 size-1.5 shrink-0 rounded-pill bg-current {{ $tone }}" aria-hidden="true"></span>
|
|
<div class="min-w-0">
|
|
<p class="text-body">{{ __('hosts.step.'.$event->step) }}</p>
|
|
<p class="text-xs text-muted">
|
|
{{ __('hosts.outcome.'.$event->outcome) }}
|
|
· {{ __('hosts.attempt') }} {{ $event->attempt }}
|
|
@if ($event->message) · <span class="break-words">{{ $event->message }}</span> @endif
|
|
</p>
|
|
</div>
|
|
</li>
|
|
@endforeach
|
|
</ol>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|