CluPilotCloud/resources/views/livewire/admin/host-detail.blade.php

268 lines
16 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="refreshLoad" @else wire:poll.60s="refreshLoad" @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];
$htone = ['online' => 'text-success', 'stale' => 'text-warning', 'offline' => 'text-danger'][$health];
$usedPct = $host->usedPct();
@endphp
{{-- Vier Spalten, die Last belegt zwei: eine Zeitreihe braucht Breite. Auf
einem Drittel der Zeile blieben nach Legende und Zeitachse rund dreißig
Pixel Zeichenfläche, und eine Kurve, die man nicht lesen kann, ist
schlimmer als die Zahl, die vorher dort stand. --}}
<div class="grid grid-cols-1 gap-4 lg:grid-cols-4 animate-rise">
{{-- Health --}}
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs">
<p class="text-xs font-semibold text-muted">{{ __('hosts.detail.health') }}</p>
<p class="mt-2 flex items-center gap-2 text-lg font-semibold {{ $htone }}">
<span class="size-2.5 rounded-pill {{ $hdot }} {{ $health === 'online' ? 'animate-pulse' : '' }}" aria-hidden="true"></span>
{{ __('hosts.health.'.$health) }}
</p>
<p class="mt-1 text-xs text-muted">
{{ $host->last_seen_at ? __('hosts.detail.last_seen', ['time' => $host->last_seen_at->diffForHumans()]) : __('hosts.detail.never_seen') }}
</p>
</div>
{{-- Storage capacity --}}
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs">
<p class="text-xs font-semibold text-muted">{{ __('hosts.detail.storage') }}</p>
@if ($host->total_gb)
<p class="mt-2 font-mono text-lg font-semibold text-ink">{{ $host->availableGb() }} <span class="text-sm font-normal text-muted">/ {{ $host->freeGb() }} GB {{ __('hosts.free') }}</span></p>
<div class="mt-2 h-2 overflow-hidden rounded-pill bg-surface-2">
<div class="h-full rounded-pill {{ $usedPct >= 80 ? 'bg-danger' : 'bg-accent' }}" style="width: {{ $usedPct }}%"></div>
</div>
{{-- Belegung auf eine eigene Zeile über der Eingabe: in der
schmaleren Spalte brach der frühere Einzeiler mitten im
Satz um, und „· Reserve" stand allein unter der Zahl. --}}
<div class="mt-2" x-data="{ r: {{ $host->reserve_pct }} }">
<p class="text-xs text-muted">{{ __('hosts.detail.committed', ['gb' => $host->committedGb()]) }}</p>
<div class="mt-1.5 flex items-center gap-2">
<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>
</div>
</div>
@else
<p class="mt-2 text-sm text-muted">{{ __('hosts.unknown') }}</p>
@endif
</div>
{{-- Load. Die Karte, die vorher „Rechenleistung" hieß und die
Ausstattung zeigte: 12 Kerne ändern sich nie und sagten nichts
darüber, wie es dem Host geht. --}}
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs lg:col-span-2">
<div class="flex items-baseline justify-between gap-2">
<p class="text-xs font-semibold text-muted">{{ __('hosts.detail.load') }}</p>
<p class="text-xs text-muted">{{ __('hosts.detail.load_hint') }}</p>
</div>
@if ($load['available'])
@php
// Der jüngste Messwert, der wirklich einer ist: die letzten
// Punkte können Lücken sein, und eine Lücke ist keine Null.
$lastOf = fn (array $s) => collect($s)->filter(fn ($v) => $v !== null)->last();
$cpuNow = $lastOf($load['cpu']);
$ramNow = $lastOf($load['ram']);
@endphp
{{-- Beschriftete Zahlen, nicht nur Farbe: der Akzentton liegt
unter 3:1 gegen die Fläche, und die Kurve allein dürfte die
Identität der Reihen deshalb nicht tragen. --}}
<div class="mt-2 flex items-baseline gap-5">
<p class="font-mono text-lg font-semibold text-ink">
{{ $cpuNow !== null ? round($cpuNow).' %' : '—' }}
<span class="ml-1 text-xs font-normal text-muted">{{ __('hosts.detail.cpu') }}</span>
</p>
<p class="font-mono text-lg font-semibold text-ink">
{{ $ramNow !== null ? round($ramNow).' %' : '—' }}
<span class="ml-1 text-xs font-normal text-muted">{{ __('hosts.detail.ram') }}</span>
</p>
</div>
<div class="mt-3 h-36" wire:ignore>
<x-ui.chart :config="$loadChart" update-on="host-load" class="h-36"
:label="__('hosts.detail.load').' — '.__('hosts.detail.load_hint')" />
</div>
@else
<p class="mt-2 text-sm text-muted">{{ __('hosts.detail.no_load') }}</p>
@endif
</div>
</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-5">
@foreach ([
[__('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, statt der Bau-Kennung, in der
sie vorher abgeschnitten wurde. --}}
<dd class="mt-0.5 truncate font-mono text-sm font-semibold text-ink">Proxmox VE {{ $version['version'] }}</dd>
@if ($version['build'])
<dd class="truncate font-mono text-[11px] text-muted" title="{{ $host->pve_version }}">{{ __('hosts.detail.build') }} {{ $version['build'] }}</dd>
@endif
@else
<dd class="mt-0.5 truncate font-mono text-sm font-semibold text-ink">{{ __('hosts.unknown') }}</dd>
@endif
</div>
</dl>
</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>