Site Details: 3 new tabs — vHost / SSL-TLS / SSH-FTP

vHost tab:
- HTTP-version segmented (HTTP/1.1 | HTTP/2 | HTTP/3 QUIC).
- vhost config grid: server_name, document_root, client_max_body_size,
  keepalive_timeout, worker_connections, access_log.
- 5 toggle-rows: HTTP→HTTPS redirect, HSTS, Brotli+gzip, server_tokens,
  HTTP/2 server push.
- Custom Headers kv-card (X-Frame-Options, X-Content-Type-Options,
  Referrer-Policy, Permissions-Policy).
- Rate Limit kv-card (/wp-login.php 5 req/min, /xmlrpc.php blocked).
- Actions: 'nginx -t (Syntax-Check)', 'vHost speichern + reload'.

SSL / TLS tab:
- Cert-Provider segmented (Let's Encrypt | Cloudflare Origin | Custom).
- CN + SAN + TLS-min (1.2 | 1.3) + Cipher Suite (Mozilla Modern/
  Intermediate/Old).
- 4 toggle-rows: OCSP Stapling, Auto-Renewal, CAA-Check, CT-Log.
- Cert Details card (Issuer, Signature ECDSA P-256, Key 256-bit,
  Issued/Expires dates, SSL-Labs A+).
- Bisherige Zertifikate table.
- Actions: download, renew now, save+reload.

SSH / FTP tab:
- SSH-Benutzer table (deploy/m.weber/ci-bot) with username, public-key
  fingerprint, shell, last-login, scope pill, row actions for rotate-key
  + delete.
- SFTP table (redakteur/backup-puller) with chroot, protocol pill,
  quota, last-upload.
- Zugriffs-Policy card: 5 toggle-rows (password-auth off, root-login no,
  fail2ban, IP-allowlist, 2FA-SSH).
- Actions: 'Public Key importieren', 'SSH/SFTP-Benutzer anlegen'.

Volt state: httpVersion, hsts, hstsMaxAge, forceHttps, brotli, tlsMin,
sslProvider — all wire:click-toggleable via $set/$toggle.

All 5 site-details Pest tests still green.
master
Boban Blaskovic 2026-06-01 00:18:14 +02:00
parent 4be0ea7bae
commit 9f8038d5f4
1 changed files with 314 additions and 1 deletions

View File

@ -18,6 +18,15 @@ new #[Layout('layouts.app')] class extends Component {
/** Set of dir paths currently expanded in file tree. */
public array $openDirs = ['htdocs', 'htdocs/wp-content', 'htdocs/wp-content/themes', 'htdocs/wp-content/themes/storefront-child', 'logs'];
/** vhost / http / tls settings (in-memory for now). */
public string $httpVersion = 'http2'; // http1 | http2 | http3
public bool $hsts = true;
public int $hstsMaxAge = 31536000;
public bool $forceHttps = true;
public bool $brotli = true;
public string $tlsMin = '1.3';
public string $sslProvider = 'letsencrypt'; // letsencrypt | custom | cloudflare
public function mount(Site $site): void
{
$this->site = $site->load(['server', 'certificate', 'crons']);
@ -320,7 +329,7 @@ new #[Layout('layouts.app')] class extends Component {
{{-- ── Tab bar ── --}}
<div class="clu-dtabs mb-[18px]" role="tablist">
@foreach (['overview' => ['Übersicht', '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'], 'wp' => ['WordPress', '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>'], 'php' => ['PHP / ini', '<path d="M4 7h16M4 12h16M4 17h10"/>'], 'files' => ['Datei-Manager', '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'], 'cron' => ['Cronjobs', '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'], 'logs' => ['Logs', '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 8h10M7 12h10M7 16h6"/>']] as $key => [$label, $svg])
@foreach (['overview' => ['Übersicht', '<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'], 'wp' => ['WordPress', '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/>'], 'vhost' => ['vHost', '<path d="M2 12h20M2 6h20M2 18h20"/>'], 'ssl' => ['SSL / TLS', '<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>'], 'access' => ['SSH / FTP', '<path d="M4 17l5-5-5-5M13 19h7"/>'], 'php' => ['PHP / ini', '<path d="M4 7h16M4 12h16M4 17h10"/>'], 'files' => ['Datei-Manager', '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>'], 'cron' => ['Cronjobs', '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'], 'logs' => ['Logs', '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M7 8h10M7 12h10M7 16h6"/>']] as $key => [$label, $svg])
<button type="button" role="tab"
wire:click="setTab('{{ $key }}')"
class="clu-dtab @if($tab === $key) active @endif">
@ -531,6 +540,310 @@ new #[Layout('layouts.app')] class extends Component {
</div>
@endif
{{-- ────── VHOST ────── --}}
@if ($tab === 'vhost')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h20M2 6h20M2 18h20"/></svg>
Virtual Host
</h3>
<span class="meta">{{ $site->domain }} · nginx</span>
</div>
<div class="clu-card-body">
{{-- HTTP version --}}
<div class="clu-field">
<label>HTTP-Version</label>
<div class="flex gap-[8px]">
@foreach (['http1' => 'HTTP/1.1', 'http2' => 'HTTP/2', 'http3' => 'HTTP/3 (QUIC)'] as $v => $lbl)
<button type="button" wire:click="$set('httpVersion', '{{ $v }}')"
class="clu-ghost-btn"
style="@if($httpVersion === $v) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
<span class="clu-tr-desc">HTTP/3 nutzt QUIC über UDP schneller bei verlustbehafteten Verbindungen.</span>
</div>
<div class="clu-set-grid">
<div class="clu-field">
<label>Server-Name</label>
<div class="clu-input"><input value="{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>Document Root</label>
<div class="clu-input"><input value="/var/www/bergmann/htdocs" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>client_max_body_size</label>
<div class="clu-input-group"><input value="128" type="text" /><span class="suffix">M</span></div>
</div>
<div class="clu-field">
<label>keepalive_timeout</label>
<div class="clu-input-group"><input value="65" type="text" /><span class="suffix">sek</span></div>
</div>
<div class="clu-field">
<label>worker_connections</label>
<div class="clu-input"><input value="2048" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>access_log</label>
<div class="clu-input"><input value="/var/log/nginx/bergmann.access.log" style="font-family:var(--font-mono);" type="text" /></div>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="$toggle('forceHttps')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTP HTTPS Redirect</div><div class="clu-tr-desc">alle :80 anfragen mit 301 auf https umleiten</div></div>
<span class="clu-toggle @if($forceHttps) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="$toggle('hsts')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HSTS (Strict-Transport-Security)</div><div class="clu-tr-desc">browser merkt sich https-only für {{ number_format($hstsMaxAge / 86400, 0, ',', '.') }} tage</div></div>
<span class="clu-toggle @if($hsts) on @endif"></span>
</div>
<div class="clu-toggle-row" wire:click="$toggle('brotli')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Brotli + gzip Kompression</div><div class="clu-tr-desc">text-assets vor auslieferung komprimieren</div></div>
<span class="clu-toggle @if($brotli) on @endif"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">server_tokens off</div><div class="clu-tr-desc">nginx-version aus header entfernen</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTP/2 Server Push</div><div class="clu-tr-desc">kritische assets im voraus an client streamen</div></div>
<span class="clu-toggle"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button">nginx -t (Syntax-Check)</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">vHost speichern + reload</button>
</div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Custom Headers</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="clu-kv"><span class="k">X-Frame-Options</span><span class="v">SAMEORIGIN</span></div>
<div class="clu-kv"><span class="k">X-Content-Type-Options</span><span class="v">nosniff</span></div>
<div class="clu-kv"><span class="k">Referrer-Policy</span><span class="v">strict-origin</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Permissions-Policy</span><span class="v">camera=(), mic=()</span></div>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Rate Limit</h3><span class="clu-pill success">aktiv</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="clu-kv"><span class="k">Zone-Größe</span><span class="v">10 MB</span></div>
<div class="clu-kv"><span class="k">/wp-login.php</span><span class="v">5 req / min</span></div>
<div class="clu-kv"><span class="k">/xmlrpc.php</span><span class="v">geblockt</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Burst</span><span class="v">20</span></div>
</div>
</div>
</div>
</div>
@endif
{{-- ────── SSL / TLS ────── --}}
@if ($tab === 'ssl')
<div class="clu-pane">
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
SSL / TLS Zertifikat
</h3>
<span class="clu-pill success">gültig · 54 tage</span>
</div>
<div class="clu-card-body">
<div class="clu-field">
<label>Cert-Provider</label>
<div class="flex gap-[8px] flex-wrap">
@foreach (['letsencrypt' => "Let's Encrypt", 'cloudflare' => 'Cloudflare Origin', 'custom' => 'Custom Upload'] as $v => $lbl)
<button type="button" wire:click="$set('sslProvider', '{{ $v }}')"
class="clu-ghost-btn"
style="@if($sslProvider === $v) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
</div>
<div class="clu-set-grid">
<div class="clu-field">
<label>Common Name (CN)</label>
<div class="clu-input"><input value="{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>Subject Alternative Names (SAN)</label>
<div class="clu-input"><input value="www.{{ $site->domain }}, *.{{ $site->domain }}" style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>TLS-Version (min)</label>
<div class="flex gap-[8px]">
@foreach (['1.2' => 'TLS 1.2', '1.3' => 'TLS 1.3'] as $v => $lbl)
<button type="button" wire:click="$set('tlsMin', '{{ $v }}')"
class="clu-ghost-btn"
style="@if($tlsMin === $v) background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent); @endif">
{{ $lbl }}
</button>
@endforeach
</div>
</div>
<div class="clu-field">
<label>Cipher Suite Profile</label>
<select class="clu-input" style="appearance:auto;font-family:var(--font-mono);">
<option>Mozilla Modern</option>
<option selected>Mozilla Intermediate</option>
<option>Mozilla Old (PCI)</option>
<option>Custom</option>
</select>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">OCSP Stapling</div><div class="clu-tr-desc">cert-revocation-status mit response cachen</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Auto-Renewal</div><div class="clu-tr-desc">erneuerung 30 tage vor ablauf</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">CAA-Record-Check</div><div class="clu-tr-desc">vor neuausstellung dns-caa prüfen</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Certificate Transparency Log</div><div class="clu-tr-desc">cert in öffentliches ct-log eintragen</div></div>
<span class="clu-toggle on"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button">Cert herunterladen</button>
<button class="clu-ghost-btn" type="button">Jetzt erneuern</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">Speichern + reload</button>
</div>
</div>
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Cert Details</h3></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:9px;">
<div class="clu-kv"><span class="k">Aussteller</span><span class="v">Let's Encrypt R3</span></div>
<div class="clu-kv"><span class="k">Signatur</span><span class="v">ECDSA P-256</span></div>
<div class="clu-kv"><span class="k">Schlüsselgröße</span><span class="v">256 bit</span></div>
<div class="clu-kv"><span class="k">Ausgestellt am</span><span class="v">2026-03-22</span></div>
<div class="clu-kv"><span class="k">Läuft ab am</span><span class="v" style="color:var(--color-success);">2026-06-20</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">SSL-Labs Rating</span><span class="v" style="color:var(--color-success);">A+</span></div>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Bisherige Zertifikate</h3></div>
<div class="clu-card-body tight">
<table class="clu-table">
<thead><tr><th>Ausgestellt</th><th>Abgelaufen</th><th>Status</th></tr></thead>
<tbody>
<tr><td class="font-mono">2026-03-22</td><td class="font-mono">2026-06-20</td><td><span class="clu-health">aktuell</span></td></tr>
<tr><td class="font-mono">2025-12-21</td><td class="font-mono">2026-03-21</td><td><span class="clu-health warn">ersetzt</span></td></tr>
<tr><td class="font-mono">2025-09-22</td><td class="font-mono">2025-12-21</td><td><span class="clu-health warn">ersetzt</span></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
@endif
{{-- ────── SSH / FTP ────── --}}
@if ($tab === 'access')
<div class="clu-pane">
{{-- SSH-Users --}}
<div class="clu-card">
<div class="clu-card-head">
<h3 class="flex items-center gap-[8px]">
<svg class="clu-hico" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l5-5-5-5M13 19h7"/></svg>
SSH-Benutzer
</h3>
<span class="meta">key-only · password-auth aus</span>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Username</th><th>Public Key</th><th>Shell</th><th>Letzter Login</th><th>Scope</th><th></th></tr></thead>
<tbody>
<tr><td class="font-semibold">deploy</td><td class="font-mono text-[11px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">ssh-ed25519 AAAAC3NzaC1l…7Q deploy@bergmann</td><td class="font-mono">/bin/bash</td><td class="font-mono">vor 14 min</td><td><span class="clu-pill accent">/var/www/bergmann</span></td><td><div class="clu-row-actions"><button type="button" title="Key tauschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
<tr><td class="font-semibold">m.weber</td><td class="font-mono text-[11px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">ssh-ed25519 AAAAC3NzaC1l…1F m.weber@laptop</td><td class="font-mono">/bin/bash</td><td class="font-mono">heute, 09:42</td><td><span class="clu-pill accent">/var/www/bergmann</span></td><td><div class="clu-row-actions"><button type="button" title="Key tauschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
<tr><td class="font-semibold">ci-bot</td><td class="font-mono text-[11px] text-(--color-muted)" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">ssh-rsa AAAAB3NzaC1y…XY github-actions</td><td class="font-mono">/usr/bin/rrsync</td><td class="font-mono">vor 2h</td><td><span class="clu-pill muted">deploy only</span></td><td><div class="clu-row-actions"><button type="button" title="Key tauschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
</tbody>
</table>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button">Public Key importieren</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
SSH-Benutzer anlegen
</button>
</div>
</div>
{{-- SFTP / FTP-Users --}}
<div class="clu-card">
<div class="clu-card-head">
<h3>SFTP / FTP-Benutzer</h3>
<span class="meta">chroot · explicit-tls erzwungen</span>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Username</th><th>Chroot</th><th>Protokoll</th><th>Quota</th><th>Letzter Upload</th><th></th></tr></thead>
<tbody>
<tr><td class="font-semibold">redakteur</td><td class="font-mono">/uploads/2026</td><td><span class="clu-pill success">SFTP</span></td><td class="font-mono">2.4 / 5 GB</td><td class="font-mono">heute, 11:02</td><td><div class="clu-row-actions"><button type="button" title="Passwort"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
<tr><td class="font-semibold">backup-puller</td><td class="font-mono">/backups</td><td><span class="clu-pill success">SFTP</span></td><td class="font-mono">128 GB</td><td class="font-mono">heute, 03:00</td><td><div class="clu-row-actions"><button type="button" title="Passwort"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></button><button type="button" title="Löschen"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/></svg></button></div></td></tr>
</tbody>
</table>
</div>
<div class="clu-form-actions">
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
SFTP-Benutzer anlegen
</button>
</div>
</div>
{{-- Security policy --}}
<div class="clu-card">
<div class="clu-card-head"><h3>Zugriffs-Policy</h3></div>
<div class="clu-card-body">
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Password-Auth deaktiviert</div><div class="clu-tr-desc">SSH nur über Public-Key zulassen</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Root-Login verbieten</div><div class="clu-tr-desc">PermitRootLogin no</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">fail2ban (3 Versuche 1h Ban)</div><div class="clu-tr-desc">automatische IP-Sperre nach fehlversuchen</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">IP-Allowlist</div><div class="clu-tr-desc">SSH nur aus 10.10.0.0/16 + büro-WAN</div></div>
<span class="clu-toggle"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">2FA für SSH (Google Authenticator)</div><div class="clu-tr-desc">zusätzlich zum public key</div></div>
<span class="clu-toggle"></span>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">Policy speichern</button>
</div>
</div>
</div>
@endif
{{-- ────── WP + PHP (kompakte Hardcoded-Konfig nach Template) ────── --}}
@if ($tab === 'wp')
<div class="clu-pane">