clusev/resources/views/livewire/servers/show.blade.php

541 lines
33 KiB
PHP

@php
// Threshold tone for gauges/bars: >=90 offline, >=75 warning, else online.
$tone = fn (int $v): string => $v >= 90 ? 'offline' : ($v >= 75 ? 'warning' : 'online');
$barColor = ['online' => 'bg-online', 'warning' => 'bg-warning', 'offline' => 'bg-offline'];
$statusLabel = ['online' => 'Online', 'warning' => 'Warnung', 'offline' => 'Offline'][$server->status] ?? ucfirst($server->status);
$specs = $server->specs ?? [];
$cores = $specs['cores'] ?? null;
$ramGb = $specs['ram_gb'] ?? null;
$diskGb = $specs['disk_gb'] ?? null;
$ramUsed = $ramGb !== null ? round((float) $ramGb * $server->mem / 100, 1) : null;
$diskUsed = $diskGb !== null ? (int) round((float) $diskGb * $server->disk / 100) : null;
$specRows = [
['Architektur', $specs['arch'] ?? '—'],
['Kernel', $specs['kernel'] ?? '—'],
['Virtualisierung', $specs['virt'] ?? '—'],
['Kerne', $cores ?? '—'],
['RAM', $ramGb !== null ? $ramGb.' GB' : '—'],
['Disk', $diskGb !== null ? $diskGb.' GB' : '—'],
];
// Detected OS tooling (populated once the live profile loads).
if (! empty($os)) {
$pm = ($os['packageManager'] ?? 'none') !== 'none' ? $os['packageManager'] : '—';
$fw = ($os['firewallTool'] ?? 'none') !== 'none' ? $os['firewallTool'] : '—';
$specRows[] = ['System', $os['familyLabel'] ?? '—'];
$specRows[] = ['Paketverwaltung', $pm];
$specRows[] = ['Firewall', $fw];
}
$vitals = [
['label' => 'CPU', 'icon' => 'cpu', 'val' => (int) $server->cpu, 'sub' => ($loadAvg > 0 ? 'Last '.number_format($loadAvg, 2).' · ' : '').($cores ? $cores.' Kerne' : '—')],
['label' => 'RAM', 'icon' => 'activity', 'val' => (int) $server->mem, 'sub' => $ramUsed !== null ? $ramUsed.' / '.$ramGb.' GB' : '—'],
['label' => 'Disk', 'icon' => 'server', 'val' => (int) $server->disk, 'sub' => $diskUsed !== null ? $diskUsed.' / '.$diskGb.' GB' : '—'],
];
@endphp
<div class="space-y-5" wire:poll.10s="pollMetrics" wire:init="load">
<a href="{{ route('servers.index') }}"
class="inline-flex min-h-11 items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.2em] text-ink-3 hover:text-ink-2">
<x-icon name="switcher" class="h-3.5 w-3.5 rotate-90" />
Zurück zur Flotte
</a>
{{-- Hero header --}}
<div class="flex flex-wrap items-center justify-between gap-4 rounded-xl border border-line bg-surface p-5 shadow-panel">
<div class="flex min-w-0 items-center gap-4">
<span @class([
'grid h-12 w-12 shrink-0 place-items-center rounded-lg border',
'border-online/30 bg-online/10 text-online' => $server->status === 'online',
'border-warning/30 bg-warning/10 text-warning' => $server->status === 'warning',
'border-offline/30 bg-offline/10 text-offline' => $server->status === 'offline',
])>
<x-icon name="server" class="h-6 w-6" />
</span>
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2.5">
<h2 class="truncate font-display text-2xl font-semibold text-ink">{{ $server->name }}</h2>
<x-status-pill :status="$server->status">{{ $statusLabel }}</x-status-pill>
</div>
<p class="mt-1.5 flex flex-wrap items-center gap-x-2.5 gap-y-1 font-mono text-[11px] text-ink-3">
<span class="text-ink-2">{{ $server->hostname }}</span>
<span class="text-ink-4">·</span>
<span>{{ $server->ip }}</span>
<span class="text-ink-4">·</span>
<span>{{ $server->os }}</span>
<span class="text-ink-4">·</span>
<span>Uptime {{ $server->uptime ?: '—' }}</span>
<span class="text-ink-4">·</span>
<span>zuletzt {{ optional($server->last_seen_at)->diffForHumans() ?? '—' }}</span>
</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-2">
<x-btn variant="secondary" wire:click="$dispatch('openModal', { component: 'modals.system-update', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="rotate" class="h-3.5 w-3.5" /> System-Updates
</x-btn>
<x-btn variant="secondary" wire:click="openFiles">
<x-icon name="folder" class="h-3.5 w-3.5" /> Dateien
</x-btn>
</div>
</div>
@if ($ready && ! $connected)
<div class="flex items-center gap-2.5 rounded-lg border border-warning/25 bg-warning/10 px-4 py-3">
<x-icon name="alert" class="h-4 w-4 shrink-0 text-warning" />
<p class="text-sm text-ink-2">Keine Live-Verbindung gezeigt werden die zuletzt gespeicherten Werte. Zugang/Erreichbarkeit prüfen.</p>
</div>
@endif
{{-- SSH-Zugang (Credential) --}}
@php
$cred = $server->credential;
$credStatus = $cred ? ($cred->disabled ? 'offline' : 'online') : null;
$credStatusLabel = $cred ? ($cred->disabled ? 'Gesperrt' : 'Aktiv') : null;
$credAuthLabel = $cred ? ($cred->auth_type === 'key' ? 'SSH-Key' : 'Passwort') : null;
@endphp
<x-panel title="SSH-Zugang" subtitle="Hinterlegte Anmeldung für die Live-Steuerung" :padded="false">
@if ($cred)
<div class="flex flex-wrap items-center gap-x-4 gap-y-3 px-4 py-4 sm:px-5">
<span @class([
'grid h-10 w-10 shrink-0 place-items-center rounded-md border',
'border-online/30 bg-online/10 text-online' => ! $cred->disabled,
'border-offline/30 bg-offline/10 text-offline' => $cred->disabled,
])>
<x-icon name="shield" class="h-5 w-5" />
</span>
<div class="min-w-0 flex-1">
<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>
</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>
</p>
</div>
<div class="flex shrink-0 flex-wrap items-center gap-2">
<x-btn variant="secondary" wire:click="$dispatch('openModal', { component: 'modals.edit-credential', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="settings" class="h-3.5 w-3.5" /> Bearbeiten
</x-btn>
<x-btn variant="secondary" wire:click="toggleCredential" wire:target="toggleCredential" wire:loading.attr="disabled">
<x-icon name="power" class="h-3.5 w-3.5" wire:loading.remove wire:target="toggleCredential" />
<svg wire:loading wire:target="toggleCredential" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
{{ $cred->disabled ? 'Entsperren' : 'Sperren' }}
</x-btn>
<x-btn variant="ghost-danger" wire:click="confirmDeleteCredential">
<x-icon name="trash" class="h-3.5 w-3.5" /> Löschen
</x-btn>
</div>
</div>
@else
<div class="flex flex-wrap items-center gap-x-4 gap-y-3 px-4 py-4 sm:px-5">
<span class="grid h-10 w-10 shrink-0 place-items-center rounded-md border border-line bg-inset text-ink-3">
<x-icon name="shield" class="h-5 w-5" />
</span>
<div class="min-w-0 flex-1">
<p class="truncate font-display text-sm font-semibold text-ink">Kein Zugang hinterlegt</p>
<p class="mt-1.5 font-mono text-[11px] text-ink-3">Ohne SSH-Zugang ist keine Live-Steuerung möglich.</p>
</div>
<x-btn variant="accent" class="shrink-0" wire:click="$dispatch('openModal', { component: 'modals.edit-credential', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="plus" class="h-3.5 w-3.5" /> Zugang hinterlegen
</x-btn>
</div>
@endif
</x-panel>
{{-- Vitals: live gauges, full width, dense --}}
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
@foreach ($vitals as $v)
<div class="flex items-center gap-4 rounded-xl border border-line bg-surface p-4 shadow-panel">
<x-ring :value="$v['val']" size="lg" :tone="$tone($v['val'])" />
<div class="min-w-0">
<p class="flex items-center gap-1.5 font-mono text-xs uppercase tracking-[0.2em] text-accent-text">
<x-icon :name="$v['icon']" class="h-3.5 w-3.5" />{{ $v['label'] }}
</p>
<p class="mt-2 font-mono text-[11px] leading-relaxed text-ink-4">{{ $v['sub'] }}</p>
</div>
</div>
@endforeach
</div>
@if ($ready)
{{-- Spezifikationen + Sicherheit --}}
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
<x-panel title="Spezifikationen" subtitle="Hardware-Profil" :padded="false">
<dl class="divide-y divide-line">
@foreach ($specRows as [$key, $val])
<div class="flex items-center justify-between gap-3 px-4 py-2.5 sm:px-5">
<dt class="text-sm text-ink-2">{{ $key }}</dt>
<dd class="truncate font-mono text-sm text-ink">{{ $val }}</dd>
</div>
@endforeach
</dl>
</x-panel>
<x-panel title="Sicherheit" subtitle="Hardening-Checkliste" :padded="false">
<div class="divide-y divide-line">
@foreach ($hardening as $check)
@php $supported = $check['supported'] ?? true; @endphp
{{-- One calm state signal: a lock glyph (geschlossen = sicher, offen = offen);
unsupported items render muted with a German reason and no toggle. --}}
<div class="flex items-center gap-3.5 px-4 py-3 transition-colors hover:bg-raised/40 sm:px-5">
<span @class([
'grid h-9 w-9 shrink-0 place-items-center rounded-md border',
'border-online/25 bg-online/10 text-online' => $supported && $check['secure'],
'border-warning/25 bg-warning/10 text-warning' => $supported && ! $check['secure'],
'border-line bg-raised text-ink-4' => ! $supported,
])>
<x-icon :name="$supported && ! $check['secure'] ? 'lock-open' : 'lock'" class="h-4 w-4" />
</span>
<div class="min-w-0 flex-1">
<p class="flex items-center gap-2 truncate text-sm text-ink">
{{ $check['label'] }}
@if ($supported)
<span @class([
'shrink-0 font-mono text-[10px] uppercase tracking-wider',
'text-online' => $check['secure'],
'text-warning' => ! $check['secure'],
])>{{ $check['secure'] ? 'sicher' : 'offen' }}</span>
@else
<span class="shrink-0 font-mono text-[10px] uppercase tracking-wider text-ink-4">n. v.</span>
@endif
</p>
<p class="truncate font-mono text-[11px] text-ink-3">{{ $supported ? $check['detail'] : $check['reason'] }}</p>
</div>
@if ($supported)
@if ($check['key'] === 'fail2ban')
<x-btn variant="ghost" size="sm" icon class="shrink-0" title="fail2ban konfigurieren"
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-config', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="settings" class="h-3.5 w-3.5" />
</x-btn>
@endif
{{-- Uniform style across all rows (R10): always the bordered secondary button. --}}
<x-btn variant="secondary" size="sm" class="shrink-0"
wire:click="$dispatch('openModal', { component: 'modals.hardening-action', arguments: { serverId: {{ $server->id }}, action: '{{ $check['key'] }}', enable: {{ $check['featureOn'] ? 'false' : 'true' }} } })">
{{ $check['featureOn'] ? 'Deaktivieren' : 'Aktivieren' }}
</x-btn>
@endif
</div>
@endforeach
</div>
</x-panel>
</div>
{{-- Firewall-Regeln --}}
@php
$fw = $firewall ?? [];
$fwTool = $fw['tool'] ?? 'ufw';
$fwToolLabel = $fwTool === 'firewalld' ? 'firewalld' : 'UFW';
$fwSupported = $fw['supported'] ?? false;
$fwInstalled = $fw['installed'] ?? false;
$fwActive = $fw['active'] ?? false;
$fwManageable = $fw['manageable'] ?? false;
$fwReadOnly = $fw['readOnly'] ?? false;
$fwReadError = $fw['readError'] ?? false;
$fwRules = $fw['rules'] ?? [];
$fwDefaults = $fw['defaults'] ?? [];
$actTone = ['ALLOW' => 'online', 'LIMIT' => 'warning', 'DENY' => 'offline', 'REJECT' => 'offline'];
@endphp
<x-panel title="Firewall-Regeln"
:subtitle="$fwSupported ? ($fwToolLabel . ($fwInstalled ? ($fwActive ? ' · aktiv' : ' · inaktiv') : ' · nicht installiert')) : 'Nicht verfügbar'"
:padded="false">
@if ($fwSupported && $fwManageable)
<x-slot:actions>
<x-btn variant="accent" size="sm"
wire:click="$dispatch('openModal', { component: 'modals.firewall-rule', arguments: { serverId: {{ $server->id }}, tool: '{{ $fwTool }}' } })">
<x-icon name="plus" class="h-3.5 w-3.5" /> Regel
</x-btn>
</x-slot:actions>
@endif
@if (! $fwSupported)
<div class="px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ $fw['reason'] ?? 'Firewall-Verwaltung ist auf diesem System nicht verfügbar.' }}</p>
</div>
@elseif ($fwReadError)
<div class="flex flex-wrap items-center gap-2.5 px-4 py-4 sm:px-5">
<x-icon name="alert" class="h-4 w-4 shrink-0 text-warning" />
<p class="font-mono text-[11px] text-ink-3">Firewall-Status konnte nicht gelesen werden (Verbindung/Rechte). Bitte später erneut laden.</p>
</div>
@elseif (! $fwInstalled)
<div class="flex flex-wrap items-center justify-between gap-3 px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">{{ $fwToolLabel }} ist nicht installiert.</p>
<x-btn variant="secondary" size="sm"
wire:click="$dispatch('openModal', { component: 'modals.hardening-action', arguments: { serverId: {{ $server->id }}, action: 'firewall', enable: true } })">
Aktivieren
</x-btn>
</div>
@elseif ($fwTool === 'firewalld' && ! $fwActive)
<div class="flex flex-wrap items-center justify-between gap-3 px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">firewalld ist installiert, aber inaktiv.</p>
<x-btn variant="secondary" size="sm"
wire:click="$dispatch('openModal', { component: 'modals.hardening-action', arguments: { serverId: {{ $server->id }}, action: 'firewall', enable: true } })">
Aktivieren
</x-btn>
</div>
@else
<div class="flex flex-wrap items-center gap-2 border-b border-line px-4 py-2.5 sm:px-5">
@if ($fwTool === 'ufw')
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">Standard</span>
<x-badge tone="neutral">eingehend: {{ $fwDefaults['incoming'] ?? '—' }}</x-badge>
<x-badge tone="neutral">ausgehend: {{ $fwDefaults['outgoing'] ?? '—' }}</x-badge>
@else
<span class="font-mono text-[10px] uppercase tracking-wider text-ink-4">Zone</span>
<x-badge tone="neutral">{{ $fwDefaults['zone'] ?? '—' }}</x-badge>
@endif
</div>
@if ($fwReadOnly)
<div class="flex items-start gap-2.5 border-b border-line bg-inset px-4 py-2.5 sm:px-5">
<x-icon name="lock" class="mt-0.5 h-3.5 w-3.5 shrink-0 text-ink-4" />
<p class="font-mono text-[11px] leading-relaxed text-ink-3">firewalld: nur Anzeige Regeln bitte direkt am Server verwalten.</p>
</div>
@endif
<div class="divide-y divide-line">
@forelse ($fwRules as $rule)
{{-- $ruleTone, NOT $tone: the page-level $tone is a closure used by the
gauges/Volumes panel; reusing the name here would clobber it. --}}
@php $ruleTone = $actTone[$rule['action'] ?? 'ALLOW'] ?? 'online'; @endphp
<div class="flex items-center gap-3 px-4 py-2.5 sm:px-5">
<span @class([
'h-2 w-2 shrink-0 rounded-full',
'bg-online' => $ruleTone === 'online',
'bg-warning' => $ruleTone === 'warning',
'bg-offline' => $ruleTone === 'offline',
])></span>
<div class="min-w-0 flex-1">
<p class="truncate font-mono text-sm text-ink">{{ $rule['label'] ?? ($rule['to'] ?? $rule['raw']) }}</p>
@if ($fwTool === 'ufw' && ($rule['from'] ?? '') !== '' && ! \Illuminate\Support\Str::contains($rule['from'], 'Anywhere'))
<p class="truncate font-mono text-[11px] text-ink-3">von {{ $rule['from'] }}</p>
@endif
</div>
<span @class([
'shrink-0 font-mono text-[10px] uppercase tracking-wider',
'text-online' => $ruleTone === 'online',
'text-warning' => $ruleTone === 'warning',
'text-offline' => $ruleTone === 'offline',
])>{{ strtolower($rule['action'] ?? 'allow') }}</span>
@unless ($fwReadOnly)
<x-btn variant="ghost-danger" size="sm" icon class="shrink-0" title="Regel entfernen"
wire:click="confirmDeleteRule({{ $loop->index }})">
<x-icon name="trash" class="h-3.5 w-3.5" />
</x-btn>
@endunless
</div>
@empty
<div class="px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">Keine Regeln definiert.</p>
</div>
@endforelse
</div>
@endif
</x-panel>
{{-- fail2ban-Status --}}
@php
$f2b = $fail2ban ?? [];
$f2bSupported = $f2b['supported'] ?? false;
$f2bReadError = $f2b['readError'] ?? false;
$f2bInstalled = $f2b['installed'] ?? false;
$f2bActive = $f2b['active'] ?? false;
$f2bJails = $f2b['jails'] ?? [];
$f2bIgnore = $f2b['ignoreip'] ?? [];
$f2bBanned = array_sum(array_map(fn ($j) => $j['currentlyBanned'] ?? 0, $f2bJails));
@endphp
<x-panel title="fail2ban-Status"
:subtitle="$f2bSupported ? ($f2bInstalled ? ($f2bActive ? ('aktiv · ' . $f2bBanned . ' gesperrt') : 'installiert · inaktiv') : 'nicht installiert') : 'Nicht verfügbar'"
:padded="false">
@if ($f2bSupported && $f2bInstalled && $f2bActive)
<x-slot:actions>
<x-btn variant="ghost" size="sm" icon title="fail2ban konfigurieren"
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-config', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="settings" class="h-3.5 w-3.5" />
</x-btn>
<x-btn variant="accent" size="sm"
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-ban', arguments: { serverId: {{ $server->id }}, jails: {{ \Illuminate\Support\Js::from(array_map(fn ($j) => $j['name'], $f2bJails)) }} } })">
<x-icon name="lock" class="h-3.5 w-3.5" /> IP sperren
</x-btn>
</x-slot:actions>
@endif
@if (! $f2bSupported)
<div class="px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] leading-relaxed text-ink-3">{{ $f2b['reason'] ?? 'fail2ban ist auf diesem System nicht verfügbar.' }}</p>
</div>
@elseif ($f2bReadError)
<div class="flex flex-wrap items-center gap-2.5 px-4 py-4 sm:px-5">
<x-icon name="alert" class="h-4 w-4 shrink-0 text-warning" />
<p class="font-mono text-[11px] text-ink-3">fail2ban-Status konnte nicht gelesen werden. Bitte später erneut laden.</p>
</div>
@elseif (! $f2bInstalled || ! $f2bActive)
<div class="flex flex-wrap items-center justify-between gap-3 px-4 py-4 sm:px-5">
<p class="font-mono text-[11px] text-ink-3">fail2ban ist {{ $f2bInstalled ? 'installiert, aber inaktiv' : 'nicht installiert' }}.</p>
<x-btn variant="secondary" size="sm"
wire:click="$dispatch('openModal', { component: 'modals.hardening-action', arguments: { serverId: {{ $server->id }}, action: 'fail2ban', enable: true } })">
Aktivieren
</x-btn>
</div>
@else
<div class="divide-y divide-line">
@foreach ($f2bJails as $jail)
<div class="px-4 py-3 sm:px-5">
<div class="flex flex-wrap items-center justify-between gap-2">
<p class="font-mono text-sm text-ink">{{ $jail['name'] }}</p>
<p class="font-mono text-[11px] text-ink-3">
<span @class(['text-offline' => ($jail['currentlyBanned'] ?? 0) > 0])>gebannt: {{ $jail['currentlyBanned'] ?? 0 }}</span>
· Fehlversuche: {{ $jail['currentlyFailed'] ?? 0 }}
</p>
</div>
@if (count($jail['bannedIps'] ?? []))
<div class="mt-2 space-y-1">
@foreach ($jail['bannedIps'] as $ip)
<div class="flex items-center justify-between gap-2">
<span class="font-mono text-[11px] text-ink-2">{{ $ip }}</span>
<x-btn variant="ghost" size="sm" wire:click="unbanIp({{ \Illuminate\Support\Js::from($jail['name']) }}, {{ \Illuminate\Support\Js::from($ip) }})">Entsperren</x-btn>
</div>
@endforeach
</div>
@else
<p class="mt-1 font-mono text-[11px] text-ink-4">Keine gebannten IPs.</p>
@endif
</div>
@endforeach
</div>
{{-- Whitelist (ignoreip) --}}
<div class="border-t border-line px-4 py-3 sm:px-5">
<p class="mb-2 font-mono text-[10px] uppercase tracking-wider text-ink-4">Whitelist (ignoreip)</p>
<div class="flex flex-wrap gap-1.5">
@foreach ($f2bIgnore as $ip)
<span class="inline-flex items-center gap-1.5 rounded-sm border border-line bg-inset px-2 py-1 font-mono text-[11px] text-ink-2">
{{ $ip }}
@unless (in_array($ip, ['127.0.0.1/8', '::1'], true))
<button type="button" wire:click="removeIgnore({{ \Illuminate\Support\Js::from($ip) }})" class="text-ink-4 hover:text-offline" title="Entfernen">
<x-icon name="x" class="h-3 w-3" />
</button>
@endunless
</span>
@endforeach
</div>
<div class="mt-2.5 flex items-center gap-2">
<input wire:model="newIgnoreIp" wire:keydown.enter="addIgnore" type="text" placeholder="IP oder CIDR"
class="h-8 w-full max-w-xs rounded-md border border-line bg-inset px-2.5 font-mono text-[11px] text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
<x-btn variant="secondary" size="sm" wire:click="addIgnore">Hinzufügen</x-btn>
</div>
</div>
@endif
</x-panel>
{{-- Volumes + Netzwerk-Interfaces --}}
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
<x-panel title="Volumes" :subtitle="count($volumes) . ' Mountpoints'" :padded="false">
<div class="divide-y divide-line">
@forelse ($volumes as $vol)
@php $vt = $tone((int) $vol['used']); @endphp
<div class="px-4 py-3 sm:px-5">
<div class="flex items-center justify-between gap-3">
<p class="truncate font-mono text-sm text-ink">{{ $vol['mount'] }}</p>
<p class="shrink-0 font-mono text-[11px] text-ink-3">
<span class="text-ink-2">{{ $vol['fs'] }}</span> · {{ $vol['size'] }}
</p>
</div>
<div class="mt-2 flex items-center gap-3">
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-inset">
<div class="h-full rounded-full {{ $barColor[$vt] }}" style="width: {{ $vol['used'] }}%"></div>
</div>
<span class="tabular w-10 shrink-0 text-right font-mono text-[11px] text-ink-2">{{ $vol['used'] }}%</span>
</div>
</div>
@empty
<p class="px-5 py-8 text-center font-mono text-[11px] text-ink-3">Keine Volumes gelesen.</p>
@endforelse
</div>
</x-panel>
<x-panel title="Netzwerk-Interfaces" :subtitle="count($interfaces) . ' Schnittstellen'" :padded="false">
<div class="overflow-x-auto">
<table class="w-full min-w-[34rem] text-left">
<thead>
<tr class="border-b border-line font-mono text-[10px] uppercase tracking-wider text-ink-4">
<th class="px-4 py-2 font-medium sm:px-5">Name</th>
<th class="px-4 py-2 font-medium">IP</th>
<th class="px-4 py-2 font-medium">MAC</th>
<th class="px-4 py-2 text-right font-medium">RX</th>
<th class="px-4 py-2 pr-4 text-right font-medium sm:pr-5">TX</th>
</tr>
</thead>
<tbody class="divide-y divide-line">
@foreach ($interfaces as $if)
<tr class="font-mono text-[11px] text-ink-2">
<td class="px-4 py-2.5 text-ink sm:px-5">{{ $if['name'] }}</td>
<td class="px-4 py-2.5">{{ $if['ip'] }}</td>
<td class="px-4 py-2.5">{{ $if['mac'] }}</td>
<td class="tabular px-4 py-2.5 text-right">{{ $if['rx'] }}</td>
<td class="tabular px-4 py-2.5 pr-4 text-right sm:pr-5">{{ $if['tx'] }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</x-panel>
</div>
{{-- SSH-Schlüssel --}}
<x-panel title="SSH-Schlüssel" :subtitle="count($sshKeys) . ' autorisiert'" :padded="false">
<x-slot:actions>
<x-btn variant="accent" wire:click="$dispatch('openModal', { component: 'modals.add-ssh-key', arguments: { serverId: {{ $server->id }} } })">
<x-icon name="plus" class="h-3.5 w-3.5" /> Schlüssel hinzufügen
</x-btn>
</x-slot:actions>
<div class="divide-y divide-line">
@forelse ($sshKeys as $key)
<div class="flex items-center gap-3 px-4 py-3 sm:px-5">
<span class="grid h-7 w-7 shrink-0 place-items-center rounded-sm bg-raised text-ink-3">
<x-icon name="shield" class="h-3.5 w-3.5" />
</span>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<p class="truncate font-mono text-sm text-ink">{{ $key['comment'] }}</p>
<x-badge tone="neutral" class="shrink-0">{{ $key['type'] }}</x-badge>
</div>
<p class="truncate font-mono text-[11px] text-ink-3">{{ $key['fingerprint'] }}</p>
</div>
<x-btn variant="ghost-danger" icon wire:click="confirmKeyRemoval({{ $loop->index }})" title="Schlüssel entfernen">
<x-icon name="trash" class="h-3.5 w-3.5" />
</x-btn>
</div>
@empty
<p class="px-5 py-8 text-center font-mono text-[11px] text-ink-3">Keine autorisierten Schlüssel.</p>
@endforelse
</div>
</x-panel>
@else
{{-- skeletons for the SSH-loaded sections --}}
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
@foreach (['Spezifikationen', 'Sicherheit'] as $t)
<x-panel :title="$t" subtitle="lädt…" :padded="false">
<div class="space-y-3 p-4 sm:p-5">
<x-skeleton class="h-3 w-full" /><x-skeleton class="h-3 w-2/3" /><x-skeleton class="h-3 w-5/6" /><x-skeleton class="h-3 w-1/2" />
</div>
</x-panel>
@endforeach
</div>
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
@foreach (['Volumes', 'Netzwerk-Interfaces'] as $t)
<x-panel :title="$t" subtitle="lädt…" :padded="false">
<div class="space-y-3 p-4 sm:p-5">
<x-skeleton class="h-3 w-full" /><x-skeleton class="h-3 w-3/4" /><x-skeleton class="h-3 w-5/6" />
</div>
</x-panel>
@endforeach
</div>
<x-panel title="SSH-Schlüssel" subtitle="lädt…" :padded="false">
<div class="space-y-3 p-4 sm:p-5"><x-skeleton class="h-3 w-2/3" /><x-skeleton class="h-3 w-1/2" /></div>
</x-panel>
@endif
</div>