@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' => __('servers.status_online'), 'warning' => __('servers.status_warning'), 'offline' => __('servers.status_offline'), 'pending' => __('servers.status_pending')][$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 = [ [__('servers.spec_arch'), $specs['arch'] ?? '—'], [__('servers.spec_kernel'), $specs['kernel'] ?? '—'], [__('servers.spec_virt'), $specs['virt'] ?? '—'], [__('servers.spec_cores'), $cores ?? '—'], [__('servers.spec_ram'), $ramGb !== null ? __('servers.gb', ['value' => $ramGb]) : '—'], [__('servers.spec_disk'), $diskGb !== null ? __('servers.gb', ['value' => $diskGb]) : '—'], ]; // 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[] = [__('servers.spec_system'), $os['familyLabel'] ?? '—']; $specRows[] = [__('servers.spec_package_manager'), $pm]; $specRows[] = [__('servers.spec_firewall'), $fw]; } $vitals = [ ['label' => __('servers.vital_cpu'), 'icon' => 'cpu', 'val' => (int) $server->cpu, 'sub' => ($loadAvg > 0 ? __('servers.load', ['value' => number_format($loadAvg, 2)]).' · ' : '').($cores ? __('servers.cores_count', ['count' => $cores]) : '—')], ['label' => __('servers.vital_ram'), 'icon' => 'activity', 'val' => (int) $server->mem, 'sub' => $ramUsed !== null ? __('servers.ram_used_of_total', ['used' => $ramUsed, 'total' => $ramGb]) : '—'], ['label' => __('servers.vital_disk'), 'icon' => 'server', 'val' => (int) $server->disk, 'sub' => $diskUsed !== null ? __('servers.disk_used_of_total', ['used' => $diskUsed, 'total' => $diskGb]) : '—'], ]; @endphp
{{ __('servers.back_to_fleet') }} {{-- Hero header --}}
$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', 'border-cyan/30 bg-cyan/10 text-cyan' => $server->status === 'pending', ])>

{{ $server->name }}

{{ $statusLabel }}

{{ $server->hostname }} · {{ $server->ip }} · {{ $server->os }} · {{ __('servers.uptime', ['value' => $server->uptime ?: '—']) }} · {{ __('servers.last_seen', ['value' => optional($server->last_seen_at)->diffForHumans() ?? '—']) }}

@can('manage-fleet') {{ __('servers.system_updates') }} @endcan {{ __('servers.files') }}
@if ($tab === 'docker') @elseif ($tab === 'terminal') @else @if ($ready && ! $connected)

{{ __('servers.offline_banner') }}

@endif {{-- SSH-Zugang (Credential) --}} @php $cred = $server->credential; $credStatus = $cred ? ($cred->disabled ? 'offline' : 'online') : null; $credStatusLabel = $cred ? ($cred->disabled ? __('servers.cred_locked') : __('servers.cred_active')) : null; $credAuthLabel = $cred ? ($cred->auth_type === 'key' ? __('servers.cred_auth_key') : __('servers.cred_auth_password')) : null; @endphp @if ($cred)
! $cred->disabled, 'border-offline/30 bg-offline/10 text-offline' => $cred->disabled, ])>

{{ $cred->name ?: $cred->username }}

{{ $credStatusLabel }} {{ $credAuthLabel }}

{{ $cred->username }}@{{ $server->ip }}

@can('manage-fleet')
{{ __('common.edit') }} {{ $cred->disabled ? __('common.unlock') : __('common.lock') }} {{ __('common.delete') }}
@endcan
@else

{{ __('servers.cred_none_title') }}

{{ __('servers.cred_none_hint') }}

@can('manage-fleet') {{ __('servers.cred_deposit') }} @endcan
@endif
{{-- Vitals: live gauges, full width, dense --}}
@foreach ($vitals as $v)

{{ $v['label'] }}

{{ $v['sub'] }}

@endforeach
{{-- Metric history graph — self-contained Alpine island (fetches /servers//history.json) --}}
CPU MEM DISK {{-- range selector (client-side, instant) --}}
{{-- empty / loading --}}

{{ __('servers.history_empty') }}

{{ __('servers.history_subtitle') }} …

{{-- chart --}}
100 75 50 25 0
{{-- hover tooltip (positioned at the crosshair) --}}

% CPU

% MEM

% DISK

@foreach ([0, 25, 50, 75, 100] as $g) @endforeach {{-- crosshair + per-series dots (guarded bindings; no
{{-- /relative chart box --}}
{{ __('dashboard.axis_now') }}
@if ($ready) {{-- Spezifikationen + Sicherheit --}}
@foreach ($specRows as [$key, $val])
{{ $key }}
{{ $val }}
@endforeach
{{-- Busy cue while the post-toggle security re-read runs (up to a few SSH round-trips). --}}
@foreach ($hardening as $check) @php $supported = $check['supported'] ?? true; // Neutral rows (auto-updates) are a preference, not a security // verdict: muted glyph + aktiv/inaktiv, never SICHER/OFFEN. $neutral = $check['neutral'] ?? false; @endphp {{-- One calm state signal: a lock glyph (geschlossen = sicher, offen = offen); unsupported/neutral items render muted, no warning tone. --}}
$supported && ! $neutral && $check['secure'], 'border-warning/25 bg-warning/10 text-warning' => $supported && ! $neutral && ! $check['secure'], 'border-line bg-raised text-ink-4' => ! $supported || $neutral, ])>

{{ $check['label'] }} @if (! $supported) {{ __('common.unsupported') }} @elseif ($neutral) {{ $check['featureOn'] ? __('common.active') : __('common.inactive') }} @else $check['secure'], 'text-warning' => ! $check['secure'], ])>{{ $check['secure'] ? __('servers.check_secure') : __('servers.check_open') }} @endif

{{ $supported ? $check['detail'] : $check['reason'] }}

{{-- Key-only warning: disabling password login leaves SSH-key access only. --}} @if ($supported && $check['key'] === 'ssh_password' && $check['featureOn'])

{{ __('servers.ssh_key_only_hint') }}

@endif
@if ($supported) @if ($check['key'] === 'fail2ban') @can('manage-network') @endcan @endif @if ($check['key'] === 'ssh_password' && $check['featureOn']) {{-- Safe auto-flow: generate+install a key, verify, switch the panel credential, THEN disable. --}} @can('manage-fleet') {{ __('servers.ssh_key_provision_action') }} @endcan @else @can('manage-network') {{-- Disabled while the post-apply re-read runs — prevents re-opening the modal against the stale pre-toggle state ("2-3 Versuche"-Effekt). --}} {{ $check['featureOn'] ? __('common.disable') : __('common.enable') }} @endcan @endif @endif
@endforeach
{{-- Firewall + fail2ban: only the tools actually installed, side-by-side on lg --}} @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']; $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)); // Show a panel when its tool is actually installed (active or inactive), OR // when the privileged read FAILED (readError) — a failed probe must stay // visible so the operator can tell it apart from a genuinely-absent tool // (both report installed=false). When truly absent, the Sicherheit checklist // row carries the "Aktivieren" install path — so the redundant "nicht // installiert" box is gone. $showFw = $fwSupported && ($fwInstalled || $fwReadError); $showF2b = $f2bSupported && ($f2bInstalled || $f2bReadError); $panelCount = ($showFw ? 1 : 0) + ($showF2b ? 1 : 0); // A single installed tool would otherwise sit in a capped, left-aligned box // beside a large void. Instead the lone panel spans the full content width and // splits its OWN body into two columns on lg — a rich, deliberate card rather // than a fragment. Two tools keep the side-by-side grid. $lone = $panelCount === 1; @endphp @if ($panelCount > 0) {{-- Two tools → side-by-side; a lone tool spans full width and lays its body out internally in two columns (see $lone), so neither case leaves dead space. --}}
$panelCount === 2, ])> @if ($showFw) {{-- Firewall-Regeln --}} {{-- Adding rules to an INACTIVE firewall would suggest they take effect — gate on active. --}} @if ($fwManageable && $fwActive) @can('manage-network') {{ __('servers.firewall_add_rule') }} @endcan @endif @if ($fwReadError)

{{ __('servers.firewall_read_error') }}

@elseif (! $fwActive) {{-- Inactive firewall (ufw OR firewalld): rules are not enforced, so showing the rules table would mislead — compact hint + Aktivieren instead. --}}

{{ __('servers.firewall_inactive_hint', ['tool' => $fwToolLabel]) }}

@can('manage-network') {{ __('common.enable') }} @endcan
@else {{-- Lone firewall: defaults/state in the left column, rules list in the right. Two-tool case keeps the single stacked column. --}}
$lone])>
@if ($fwTool === 'ufw') {{ __('servers.firewall_default') }} {{ __('servers.firewall_incoming', ['value' => $fwDefaults['incoming'] ?? '—']) }} {{ __('servers.firewall_outgoing', ['value' => $fwDefaults['outgoing'] ?? '—']) }} @else {{ __('servers.firewall_zone') }} {{ $fwDefaults['zone'] ?? '—' }} @endif
@if ($fwReadOnly)

{{ __('servers.firewall_readonly') }}

@endif
@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
$ruleTone === 'online', 'bg-warning' => $ruleTone === 'warning', 'bg-offline' => $ruleTone === 'offline', ])>

{{ $rule['label'] ?? ($rule['to'] ?? $rule['raw']) }}

@if ($fwTool === 'ufw' && ($rule['from'] ?? '') !== '' && ! \Illuminate\Support\Str::contains($rule['from'], 'Anywhere'))

{{ __('servers.firewall_rule_from', ['value' => $rule['from']]) }}

@endif
$ruleTone === 'online', 'text-warning' => $ruleTone === 'warning', 'text-offline' => $ruleTone === 'offline', ])>{{ strtolower($rule['action'] ?? 'allow') }} @unless ($fwReadOnly) @can('manage-network') @endcan @endunless
@empty

{{ __('servers.firewall_no_rules') }}

@endforelse
@endif
@endif @if ($showF2b) {{-- fail2ban-Status --}} @if ($f2bActive) @can('manage-network') {{ __('servers.fail2ban_ban_ip') }} @endcan @endif @if ($f2bReadError)

{{ __('servers.fail2ban_read_error') }}

@elseif (! $f2bActive)

{{ __('servers.fail2ban_state_installed_inactive') }}

@can('manage-network') {{ __('common.enable') }} @endcan
@else {{-- Lone fail2ban: status summary + "Gesperrte IPs ansehen" in the left column, the whitelist (ignoreip) management in the right. Two-tool case keeps the single stacked column. --}}
$lone])> {{-- Compact summary; the full jail/banned-IP list (which can grow very long) lives in a scrollable modal, opened via the button below. --}}
$lone, 'border-b border-line' => ! $lone, ])>

{{ __('common.active') }} · $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }} · {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }}

@can('manage-network') {{ __('servers.fail2ban_view_bans') }} @endcan
{{-- Whitelist (ignoreip) --}}
$lone, 'border-t border-line' => ! $lone, ])>

{{ __('servers.whitelist_label') }}

@foreach ($f2bIgnore as $ip) {{ $ip }} @unless (in_array($ip, ['127.0.0.1/8', '::1'], true)) @can('manage-network') @endcan @endunless @endforeach
@can('manage-network')
{{ __('common.add') }}
@endcan
@endif
@endif
@endif {{-- Volumes + Netzwerk-Interfaces --}}
@forelse ($volumes as $vol) @php $vt = $tone((int) $vol['used']); @endphp

{{ $vol['mount'] }}

{{ $vol['fs'] }} · {{ $vol['size'] }}

{{ $vol['used'] }}%
@empty

{{ __('servers.no_volumes') }}

@endforelse
@foreach ($interfaces as $if) @endforeach
{{ __('servers.col_name') }} {{ __('servers.col_ip') }} {{ __('servers.col_mac') }} {{ __('servers.col_rx') }} {{ __('servers.col_tx') }}
{{ $if['name'] }} {{ $if['ip'] }} {{ $if['mac'] }} {{ $if['rx'] }} {{ $if['tx'] }}
{{-- SSH keys --}} @can('manage-fleet') {{ __('servers.ssh_keys_add') }} @endcan
@forelse ($sshKeys as $key)

{{ $key['comment'] }}

{{ $key['type'] }}

{{ $key['fingerprint'] }}

@can('manage-fleet') @endcan
@empty

{{ __('servers.ssh_keys_none') }}

@endforelse
@else {{-- skeletons for the SSH-loaded sections --}}
@foreach ([__('servers.specs_title'), __('servers.security_title')] as $t)
@endforeach
@foreach ([__('servers.volumes_title'), __('servers.interfaces_title')] as $t)
@endforeach
@endif @endif {{-- /tab === overview --}}