Site Details: 3 high-value tabs — Plugins / Redirects / Staging

User accepted top-3 from brainstorm. Tab bar now 12 tabs (wraps cleanly).

Plugins tab:
- 4 metrics: total / updates-pending / auto-update count / themes count.
- Plugins table with favicon-letter, name + slug, version pill (green
  current, orange 'x.y → x.z' on update available), active/inactive
  health label, per-row auto-update toggle, row actions (update +
  activate/deactivate + delete).
- Themes table (Storefront parent, Storefront Child active, TT4 inactive).
- Update-Policy card with 5 toggle-rows: security-only / minor / major
  auto, pre-update snapshot, staging-test-first.
- Bulk-update CTA banner when any update is pending.

Redirects tab:
- 4 metrics: active rules / 24h hits / 404 sources / loop count.
- 'Neue Regel' card with source + target input, status-code segmented
  (301/302/307/410), match-type select, query-string + case-insensitive
  toggles. CSV import button.
- Active rules table with from → to, code pill, hits, last-hit, edit/
  test/delete actions. Sorted by hits.

Staging tab:
- Subdomain input with .domain suffix, server select.
- 5 toggle-rows: HTTP-Basic-Auth (default on), robots.txt block,
  exclude media on sync, email-sandbox, payment-gateways-test-mode.
- Two big action cards: 'Production → Staging Clone' and
  'Staging → Production Push' (push uses clu-btn-danger gradient,
  includes table-include pills + auto-snapshot note).
- Recent sync operations feed with success/info/warning icons.

Volt state additions: stagingActive, stagingSubdomain,
stagingPasswordProtect, stagingExcludeMedia, stagingLastSync. Mocked
plugins/themes/redirects via computed properties (no DB yet, would
come from WP-API/Redirect model in prod).

5/5 SiteDetailsTest still green.
master
Boban Blaskovic 2026-06-01 00:24:30 +02:00
parent 9f8038d5f4
commit 0615e0969d
1 changed files with 408 additions and 1 deletions

View File

@ -27,6 +27,49 @@ new #[Layout('layouts.app')] class extends Component {
public string $tlsMin = '1.3';
public string $sslProvider = 'letsencrypt'; // letsencrypt | custom | cloudflare
/** Staging environment state. */
public bool $stagingActive = true;
public string $stagingSubdomain = 'staging';
public bool $stagingPasswordProtect = true;
public bool $stagingExcludeMedia = false;
public string $stagingLastSync = '2026-05-31 14:22';
/** Plugin / theme registry — mocked, would come from WP-API in prod. */
public function getPluginsProperty(): array
{
return [
['name' => 'Yoast SEO', 'slug' => 'wordpress-seo', 'version' => '22.5', 'latest' => '22.5', 'status' => 'active', 'auto' => true, 'updates' => false],
['name' => 'WooCommerce', 'slug' => 'woocommerce', 'version' => '8.9.2', 'latest' => '9.0.1', 'status' => 'active', 'auto' => false, 'updates' => true],
['name' => 'WP Rocket', 'slug' => 'wp-rocket', 'version' => '3.16.0', 'latest' => '3.16.0', 'status' => 'active', 'auto' => true, 'updates' => false],
['name' => 'Wordfence Security', 'slug' => 'wordfence', 'version' => '7.11.4', 'latest' => '7.11.6', 'status' => 'active', 'auto' => true, 'updates' => true],
['name' => 'Advanced Custom Fields', 'slug' => 'advanced-custom-fields', 'version' => '6.2.10', 'latest' => '6.3.1', 'status' => 'active', 'auto' => false, 'updates' => true],
['name' => 'Contact Form 7', 'slug' => 'contact-form-7', 'version' => '5.9.6', 'latest' => '5.9.6', 'status' => 'inactive', 'auto' => true, 'updates' => false],
['name' => 'Akismet Anti-Spam', 'slug' => 'akismet', 'version' => '5.3.2', 'latest' => '5.3.3', 'status' => 'active', 'auto' => true, 'updates' => true],
['name' => 'UpdraftPlus', 'slug' => 'updraftplus', 'version' => '1.24.0', 'latest' => '1.24.0', 'status' => 'inactive', 'auto' => false, 'updates' => false],
];
}
public function getThemesProperty(): array
{
return [
['name' => 'Storefront', 'version' => '4.6.0', 'latest' => '4.6.0', 'status' => 'parent'],
['name' => 'Storefront Child', 'version' => '1.2.1', 'latest' => '1.2.1', 'status' => 'active'],
['name' => 'Twenty Twenty-Four','version' => '1.2', 'latest' => '1.2', 'status' => 'inactive'],
];
}
public function getRedirectsProperty(): array
{
return [
['from' => '/alte-kategorie/', 'to' => '/produkte/kaffee/', 'code' => 301, 'hits' => 4218, 'last' => 'vor 12 min'],
['from' => '/2024-aktion', 'to' => '/aktuelle-angebote/', 'code' => 301, 'hits' => 2104, 'last' => 'vor 38 min'],
['from' => '/sale', 'to' => '/aktuelle-angebote/', 'code' => 302, 'hits' => 18642,'last' => 'vor 2 min'],
['from' => '/checkout-old', 'to' => '/warenkorb/', 'code' => 301, 'hits' => 12, 'last' => 'gestern'],
['from' => '/feed/atom', 'to' => '/feed/', 'code' => 301, 'hits' => 891, 'last' => 'vor 7 min'],
['from' => '/wp-content/uploads/2022/(.*)', 'to' => '/wp-content/uploads/legacy/$1', 'code' => 301, 'hits' => 56, 'last' => 'vor 4 std'],
];
}
public function mount(Site $site): void
{
$this->site = $site->load(['server', 'certificate', 'crons']);
@ -329,7 +372,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"/>'], '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])
@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"/>'], 'plugins' => ['Plugins', '<path d="M14 7V3H8v4M3 7h18v13H3zM12 11v6M9 14h6"/>'], 'redirects' => ['Redirects', '<path d="M3 12h14l-4-4M17 12l-4 4"/>'], 'staging' => ['Staging', '<path d="M3 12a9 9 0 1 0 9-9M3 4v5h5M21 12a9 9 0 1 0-9 9M21 20v-5h-5"/>'], '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">
@ -540,6 +583,370 @@ new #[Layout('layouts.app')] class extends Component {
</div>
@endif
{{-- ────── PLUGINS (Plugin- + Theme-Manager) ────── --}}
@if ($tab === 'plugins')
<div class="clu-pane">
<div class="grid gap-[14px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
@php
$totalPlugins = count($this->plugins);
$activePlugins = count(array_filter($this->plugins, fn($p) => $p['status'] === 'active'));
$updatesPending = count(array_filter($this->plugins, fn($p) => $p['updates']));
$autoUpdate = count(array_filter($this->plugins, fn($p) => $p['auto']));
@endphp
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Plugins gesamt</span></div><div class="clu-metric-value">{{ $totalPlugins }}</div><div class="clu-metric-delta">{{ $activePlugins }} aktiv</div></div>
<div class="clu-metric @if($updatesPending > 0) warning @endif"><div class="clu-metric-row"><span class="clu-metric-label">Updates verfügbar</span></div><div class="clu-metric-value">{{ $updatesPending }}</div><div class="clu-metric-delta @if($updatesPending > 0) warn @endif">{{ $updatesPending > 0 ? 'jetzt aktualisieren' : 'alles aktuell' }}</div></div>
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Auto-Update aktiv</span></div><div class="clu-metric-value">{{ $autoUpdate }}<span class="unit">/{{ $totalPlugins }}</span></div><div class="clu-metric-delta">automatisch gepflegt</div></div>
<div class="clu-metric success"><div class="clu-metric-row"><span class="clu-metric-label">Themes</span></div><div class="clu-metric-value">{{ count($this->themes) }}</div><div class="clu-metric-delta">1 aktiv (child)</div></div>
</div>
<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="M14 7V3H8v4M3 7h18v13H3zM12 11v6M9 14h6"/></svg>
Plugins
</h3>
<div class="flex gap-[8px]">
<button class="clu-ghost-btn" type="button">Plugin hochladen</button>
<button class="clu-btn-primary" type="button" style="height:32px;padding:0 12px;font-size:12px;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M12 5v14M5 12h14"/></svg>
Aus Repo
</button>
</div>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Plugin</th><th>Version</th><th>Status</th><th>Auto-Update</th><th></th></tr></thead>
<tbody>
@foreach ($this->plugins as $p)
<tr>
<td>
<div class="clu-site-cell">
<div class="clu-favicon" style="background:linear-gradient(135deg,#5a8bff,#8a63ff);">{{ mb_strtoupper(mb_substr($p['name'], 0, 1)) }}</div>
<div class="clu-site-meta">
<span class="clu-site-domain">{{ $p['name'] }}</span>
<span class="clu-site-server">{{ $p['slug'] }}</span>
</div>
</div>
</td>
<td>
@if ($p['updates'])
<span class="clu-pill warning">{{ $p['version'] }} {{ $p['latest'] }}</span>
@else
<span class="clu-pill success">{{ $p['version'] }}</span>
@endif
</td>
<td>
@if ($p['status'] === 'active')<span class="clu-health">aktiv</span>
@else <span class="clu-health warn">inaktiv</span>@endif
</td>
<td>
<span class="clu-toggle @if($p['auto']) on @endif" style="cursor:pointer;"></span>
</td>
<td>
<div class="clu-row-actions">
@if ($p['updates'])
<button type="button" title="Update auf {{ $p['latest'] }}"><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>
@endif
<button type="button" title="{{ $p['status'] === 'active' ? 'Deaktivieren' : 'Aktivieren' }}"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">@if($p['status']==='active')<rect x="6" y="6" width="12" height="12" rx="1"/>@else<polygon points="6 4 20 12 6 20 6 4"/>@endif</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>
@endforeach
</tbody>
</table>
</div>
@if ($updatesPending > 0)
<div class="clu-form-actions" style="background:var(--color-warning-soft);">
<span class="text-[12.5px]"><b>{{ $updatesPending }}</b> Updates verfügbar. Bulk-Update führt vorher automatisch einen Snapshot durch.</span>
<button class="clu-btn-primary" type="button" style="height:32px;padding:0 12px;font-size:12px;">Alle aktualisieren ({{ $updatesPending }})</button>
</div>
@endif
</div>
<div class="clu-card">
<div class="clu-card-head">
<h3>Themes</h3>
<button class="clu-ghost-btn" type="button">Theme hochladen</button>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Theme</th><th>Version</th><th>Status</th><th></th></tr></thead>
<tbody>
@foreach ($this->themes as $t)
<tr>
<td class="font-semibold">{{ $t['name'] }}</td>
<td><span class="clu-pill success">{{ $t['version'] }}</span></td>
<td>
@if ($t['status'] === 'active')<span class="clu-health">aktiv</span>
@elseif ($t['status'] === 'parent')<span class="clu-pill muted">parent</span>
@else <span class="clu-health warn">inaktiv</span>@endif
</td>
<td>
<div class="clu-row-actions">
<button type="button" title="Aktivieren"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6 4 20 12 6 20 6 4"/></svg></button>
<button type="button" title="Mehr"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg></button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Update-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">Auto-Update: Security-Patches</div><div class="clu-tr-desc">kritische sicherheits-updates automatisch einspielen</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-Update: Minor-Versionen</div><div class="clu-tr-desc">8.9.2 8.9.4 automatisch (gleiche major)</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-Update: Major-Versionen</div><div class="clu-tr-desc">8.x 9.x manuell empfohlen, breaking changes möglich</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">Snapshot vor jedem Update</div><div class="clu-tr-desc">automatisches backup vor plugin-update, rollback bei fehler</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">Staging-Test vor Production</div><div class="clu-tr-desc">update zuerst auf staging, smoke-test, dann auf prod</div></div>
<span class="clu-toggle"></span>
</div>
</div>
</div>
</div>
@endif
{{-- ────── REDIRECTS ────── --}}
@if ($tab === 'redirects')
<div class="clu-pane">
<div class="grid gap-[14px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
@php
$totalRedirects = count($this->redirects);
$totalHits = array_sum(array_column($this->redirects, 'hits'));
@endphp
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Aktive Regeln</span></div><div class="clu-metric-value">{{ $totalRedirects }}</div><div class="clu-metric-delta">301 + 302</div></div>
<div class="clu-metric success"><div class="clu-metric-row"><span class="clu-metric-label">Treffer / 24h</span></div><div class="clu-metric-value">{{ number_format($totalHits, 0, ',', '.') }}</div><div class="clu-metric-delta">erfolgreich weitergeleitet</div></div>
<div class="clu-metric warning"><div class="clu-metric-row"><span class="clu-metric-label">404-Quellen</span></div><div class="clu-metric-value">7</div><div class="clu-metric-delta warn">potenzielle redirect-targets</div></div>
<div class="clu-metric"><div class="clu-metric-row"><span class="clu-metric-label">Loop-Detection</span></div><div class="clu-metric-value">0</div><div class="clu-metric-delta">keine zirkelschlüsse</div></div>
</div>
<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="M3 12h14l-4-4M17 12l-4 4"/></svg>
Neue Redirect-Regel
</h3>
</div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field">
<label>Von (Source)</label>
<div class="clu-input"><input placeholder="/alte-url oder /pattern/(.*)" style="font-family:var(--font-mono);" type="text" /></div>
<span class="clu-tr-desc">Regex erlaubt mit Capture-Groups ($1, $2).</span>
</div>
<div class="clu-field">
<label>Nach (Target)</label>
<div class="clu-input"><input placeholder="/neue-url" style="font-family:var(--font-mono);" type="text" /></div>
<span class="clu-tr-desc">Externe URLs (https://...) erlaubt.</span>
</div>
<div class="clu-field">
<label>Status-Code</label>
<div class="flex gap-[8px]">
<button type="button" class="clu-ghost-btn" style="background:var(--color-accent-soft);border-color:var(--color-accent);color:var(--color-accent);">301 (Permanent)</button>
<button type="button" class="clu-ghost-btn">302 (Temporär)</button>
<button type="button" class="clu-ghost-btn">307 (Strict)</button>
<button type="button" class="clu-ghost-btn">410 (Gone)</button>
</div>
</div>
<div class="clu-field">
<label>Match-Typ</label>
<select class="clu-input" style="appearance:auto;font-family:var(--font-mono);">
<option>Exact</option>
<option>Prefix</option>
<option>Regex</option>
<option>Wildcard</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">Query-String beibehalten</div><div class="clu-tr-desc">?utm_source= überträgt sich auf target</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">Case-insensitive Match</div><div class="clu-tr-desc">/PRODUKT und /produkt matchen gleich</div></div>
<span class="clu-toggle"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
<button class="clu-ghost-btn" type="button">Aus CSV importieren</button>
<button class="clu-btn-primary" type="button" style="height:36px;padding:0 14px;">Regel hinzufügen</button>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head">
<h3>Aktive Regeln <span class="clu-pill muted">{{ $totalRedirects }}</span></h3>
<span class="meta">nach hits sortiert</span>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead><tr><th>Von</th><th></th><th>Nach</th><th>Code</th><th>Hits 24h</th><th>Letzter Treffer</th><th></th></tr></thead>
<tbody>
@foreach ($this->redirects as $r)
<tr>
<td class="font-mono text-[12px]" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ $r['from'] }}</td>
<td style="color:var(--color-muted-2);"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 5l7 7-7 7"/></svg></td>
<td class="font-mono text-[12px]" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ $r['to'] }}</td>
<td><span class="clu-pill @if($r['code'] === 301) success @else accent @endif">{{ $r['code'] }}</span></td>
<td class="font-mono">{{ number_format($r['hits'], 0, ',', '.') }}</td>
<td class="font-mono text-[11.5px] text-(--color-muted)">{{ $r['last'] }}</td>
<td>
<div class="clu-row-actions">
<button type="button" title="Bearbeiten"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4v16h16v-7M18 2l4 4-11 11H7v-4z"/></svg></button>
<button type="button" title="Test"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="6 4 20 12 6 20 6 4"/></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>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endif
{{-- ────── STAGING ────── --}}
@if ($tab === 'staging')
<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="M3 12a9 9 0 1 0 9-9M3 4v5h5M21 12a9 9 0 1 0-9 9M21 20v-5h-5"/></svg>
Staging-Environment
</h3>
@if ($stagingActive)
<span class="clu-pill success">aktiv</span>
@else
<span class="clu-pill muted">nicht angelegt</span>
@endif
</div>
<div class="clu-card-body">
<div class="clu-set-grid">
<div class="clu-field">
<label>Staging-Subdomain</label>
<div class="clu-input-group">
<input wire:model.live="stagingSubdomain" type="text" style="font-family:var(--font-mono);" />
<span class="suffix">.{{ $site->domain }}</span>
</div>
<span class="clu-tr-desc">URL: <b>{{ $stagingSubdomain }}.{{ $site->domain }}</b></span>
</div>
<div class="clu-field">
<label>Server</label>
<select class="clu-input" style="appearance:auto;">
<option value="{{ $site->server->name }}">{{ $site->server->name }} (gleicher Server)</option>
<option>staging-pool (dedicated)</option>
</select>
<span class="clu-tr-desc">Staging auf gleichem Server spart Ressourcen Lasttests ggf. eigenen Server.</span>
</div>
<div class="clu-field">
<label>Letzter Sync</label>
<div class="clu-input"><input value="{{ $stagingLastSync }}" disabled style="font-family:var(--font-mono);" type="text" /></div>
</div>
<div class="clu-field">
<label>Staging-Größe</label>
<div class="clu-input"><input value="2.8 GB · 142 Tabellen · 8.4k Dateien" disabled type="text" /></div>
</div>
</div>
<div style="margin-top:8px;">
<div class="clu-toggle-row" wire:click="$toggle('stagingPasswordProtect')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">HTTP-Basic-Auth</div><div class="clu-tr-desc">staging-zugriff per username + passwort schützen (verhindert google-indexierung)</div></div>
<span class="clu-toggle @if($stagingPasswordProtect) on @endif"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">robots.txt blockt alle Crawler</div><div class="clu-tr-desc">User-agent: * / Disallow: /</div></div>
<span class="clu-toggle on"></span>
</div>
<div class="clu-toggle-row" wire:click="$toggle('stagingExcludeMedia')" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Media-Dateien beim Sync ausschließen</div><div class="clu-tr-desc">spart bandwidth, lädt /uploads lazy on-demand</div></div>
<span class="clu-toggle @if($stagingExcludeMedia) on @endif"></span>
</div>
<div class="clu-toggle-row" style="cursor:pointer;">
<div class="clu-tr-body"><div class="clu-tr-title">Email-Sandbox (DISALLOW_OUTGOING_MAIL)</div><div class="clu-tr-desc">keine kunden-mails von staging versenden</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">Payment-Gateways auf Test-Mode</div><div class="clu-tr-desc">stripe + paypal automatisch im sandbox-mode</div></div>
<span class="clu-toggle on"></span>
</div>
</div>
</div>
<div class="clu-form-actions">
@if ($stagingActive)
<button class="clu-ghost-btn" type="button" style="color:var(--color-danger);border-color:rgba(219,59,59,0.3);">Staging löschen</button>
@endif
<button class="clu-ghost-btn" type="button">Settings speichern</button>
</div>
</div>
{{-- Sync actions --}}
<div class="grid gap-[14px]" style="grid-template-columns:1fr 1fr;">
<div class="clu-card">
<div class="clu-card-head"><h3>Production Staging</h3><span class="clu-pill accent">CLONE</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
<p class="clu-tr-desc">Vollständiger Clone von Production: DB + Files + Uploads. URLs werden automatisch auf staging.{{ $site->domain }} umgeschrieben.</p>
<div class="clu-kv"><span class="k">Schätzung</span><span class="v">~4 min · 2.8 GB</span></div>
<div class="clu-kv" style="border-bottom:0;"><span class="k">Letzter Lauf</span><span class="v">{{ $stagingLastSync }}</span></div>
<button class="clu-btn-primary" type="button" style="height:38px;padding:0 16px;align-self:flex-start;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 12a9 9 0 1 0 9-9M3 4v5h5"/></svg>
Jetzt klonen
</button>
</div>
</div>
<div class="clu-card">
<div class="clu-card-head"><h3>Staging Production</h3><span class="clu-pill warning">PUSH</span></div>
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:12px;">
<p class="clu-tr-desc">Selektive Übernahme: Code, DB-Tabellen oder beides. <b>Auto-Snapshot von Production</b> vor dem Push.</p>
<div class="flex flex-wrap gap-[6px]">
<span class="clu-pill muted"> Plugins</span>
<span class="clu-pill muted"> Themes</span>
<span class="clu-pill muted"> wp_options</span>
<span class="clu-pill muted"> wp_posts</span>
<span class="clu-pill muted"> wp_users</span>
</div>
<button class="clu-btn-primary clu-btn-danger" type="button" style="height:38px;padding:0 16px;align-self:flex-start;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M13 5l7 7-7 7"/></svg>
Push to Production
</button>
</div>
</div>
</div>
{{-- Recent activity --}}
<div class="clu-card">
<div class="clu-card-head"><h3>Letzte Sync-Operationen</h3></div>
<div class="clu-card-body tight">
<div class="clu-feed">
<div class="clu-feed-row"><div class="clu-feed-ico success"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Push: Plugins + Themes nach Production · 142 MB übertragen</div><div class="clu-feed-meta">heute, 14:22 · Marie Weber · 3m 18s</div></div></div>
<div class="clu-feed-row"><div class="clu-feed-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 9-9"/><path d="M3 4v5h5"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Clone: Production Staging · 2.8 GB · automatisch</div><div class="clu-feed-meta">heute, 06:00 · Scheduler · 4m 02s</div></div></div>
<div class="clu-feed-row"><div class="clu-feed-ico warning"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.3 3.86l-8.55 14.83A2 2 0 0 0 3.46 22h17.08a2 2 0 0 0 1.71-3.31L13.7 3.86a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Push abgebrochen: WP-Version-Mismatch (6.6 6.4)</div><div class="clu-feed-meta">gestern, 18:14 · Marie Weber</div></div></div>
<div class="clu-feed-row"><div class="clu-feed-ico success"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></div><div class="clu-feed-body"><div class="clu-feed-msg">Clone: Production Staging · WooCommerce-Update testen</div><div class="clu-feed-meta">gestern, 11:42 · Marie Weber · 3m 56s</div></div></div>
</div>
</div>
</div>
</div>
@endif
{{-- ────── VHOST ────── --}}
@if ($tab === 'vhost')
<div class="clu-pane">