268 lines
16 KiB
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>
|