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
parent
9f8038d5f4
commit
0615e0969d
|
|
@ -27,6 +27,49 @@ new #[Layout('layouts.app')] class extends Component {
|
||||||
public string $tlsMin = '1.3';
|
public string $tlsMin = '1.3';
|
||||||
public string $sslProvider = 'letsencrypt'; // letsencrypt | custom | cloudflare
|
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
|
public function mount(Site $site): void
|
||||||
{
|
{
|
||||||
$this->site = $site->load(['server', 'certificate', 'crons']);
|
$this->site = $site->load(['server', 'certificate', 'crons']);
|
||||||
|
|
@ -329,7 +372,7 @@ new #[Layout('layouts.app')] class extends Component {
|
||||||
|
|
||||||
{{-- ── Tab bar ── --}}
|
{{-- ── Tab bar ── --}}
|
||||||
<div class="clu-dtabs mb-[18px]" role="tablist">
|
<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"
|
<button type="button" role="tab"
|
||||||
wire:click="setTab('{{ $key }}')"
|
wire:click="setTab('{{ $key }}')"
|
||||||
class="clu-dtab @if($tab === $key) active @endif">
|
class="clu-dtab @if($tab === $key) active @endif">
|
||||||
|
|
@ -540,6 +583,370 @@ new #[Layout('layouts.app')] class extends Component {
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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 ────── --}}
|
{{-- ────── VHOST ────── --}}
|
||||||
@if ($tab === 'vhost')
|
@if ($tab === 'vhost')
|
||||||
<div class="clu-pane">
|
<div class="clu-pane">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue