197 lines
11 KiB
PHP
197 lines
11 KiB
PHP
<?php
|
||
|
||
use App\Models\Backup;
|
||
use App\Models\Site;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Volt\Component;
|
||
use Livewire\WithPagination;
|
||
|
||
new #[Layout('layouts.app')] class extends Component {
|
||
use WithPagination;
|
||
|
||
public bool $dailyEnabled = true;
|
||
public bool $snapshotsEnabled = true;
|
||
public bool $replicationEnabled = false;
|
||
|
||
public function with(): array
|
||
{
|
||
$last = Backup::orderByDesc('completed_at')->first();
|
||
$totalSizeBytes = (int) Backup::sum('size_bytes');
|
||
$failedCount = Backup::where('status', 'failed')->count();
|
||
$totalCount = Backup::count();
|
||
|
||
return [
|
||
'backups' => Backup::query()->with('site')->orderByDesc('completed_at')->paginate(10),
|
||
'lastBackupAt' => $last?->completed_at,
|
||
'totalSize' => $totalSizeBytes,
|
||
'totalSizeGb' => round($totalSizeBytes / 1024 / 1024 / 1024, 1),
|
||
'recoveryPoints' => $totalCount,
|
||
'failedCount' => $failedCount,
|
||
'storageQuotaGb' => 500,
|
||
'storageUsedPct' => min(100, (int) round(($totalSizeBytes / 1024 / 1024 / 1024) / 500 * 100)),
|
||
'totalSites' => Site::count(),
|
||
];
|
||
}
|
||
}; ?>
|
||
|
||
<div>
|
||
<div class="clu-topbar">
|
||
<div>
|
||
<h1 class="clu-page-title">Backups</h1>
|
||
<div class="clu-page-sub">{{ $recoveryPoints }} wiederherstellungspunkte · {{ $totalSites }} sites</div>
|
||
</div>
|
||
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> aktiv</span>
|
||
<div class="spacer"></div>
|
||
<button class="clu-btn-primary" style="height:36px;padding:0 14px;">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||
<path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/>
|
||
</svg>
|
||
Backup jetzt
|
||
</button>
|
||
</div>
|
||
|
||
{{-- Metrics --}}
|
||
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
|
||
<div class="clu-metric success">
|
||
<div class="clu-metric-row">
|
||
<span class="clu-metric-label">Letztes Backup</span>
|
||
<span class="clu-metric-ico"><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="M12 8v4l3 2"/></svg></span>
|
||
</div>
|
||
<div class="clu-metric-value">{{ $lastBackupAt?->diffForHumans() ?? '—' }}</div>
|
||
<div class="clu-metric-delta">letzter erfolgreicher Lauf</div>
|
||
</div>
|
||
<div class="clu-metric">
|
||
<div class="clu-metric-row">
|
||
<span class="clu-metric-label">Storage</span>
|
||
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.7 4 3 9 3s9-1.3 9-3V5M3 11v6c0 1.7 4 3 9 3s9-1.3 9-3v-6"/></svg></span>
|
||
</div>
|
||
<div class="clu-metric-value">{{ $totalSizeGb }}<span class="unit">GB</span></div>
|
||
<div class="clu-metric-delta">von {{ $storageQuotaGb }} GB ({{ $storageUsedPct }}%)</div>
|
||
</div>
|
||
<div class="clu-metric">
|
||
<div class="clu-metric-row">
|
||
<span class="clu-metric-label">Recovery-Points</span>
|
||
<span class="clu-metric-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 4v5h5M12 7v5l4 2"/></svg></span>
|
||
</div>
|
||
<div class="clu-metric-value">{{ $recoveryPoints }}</div>
|
||
<div class="clu-metric-delta">30 Tage Retention</div>
|
||
</div>
|
||
<div class="clu-metric @if($failedCount > 0) danger @endif">
|
||
<div class="clu-metric-row">
|
||
<span class="clu-metric-label">Fehlgeschlagen</span>
|
||
<span class="clu-metric-ico"><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="M15 9l-6 6M9 9l6 6"/></svg></span>
|
||
</div>
|
||
<div class="clu-metric-value">{{ $failedCount }}</div>
|
||
<div class="clu-metric-delta @if($failedCount > 0) bad @endif">@if($failedCount > 0)Aufmerksamkeit nötig @else alle OK @endif</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- 2-col: Recent backups + Sticky summary --}}
|
||
<div class="grid gap-[14px]" style="grid-template-columns:minmax(0,2fr) minmax(280px,1fr);">
|
||
<div class="clu-card">
|
||
<div class="clu-card-head">
|
||
<h3>Neueste Backups</h3>
|
||
<span class="meta">letzte 7 Tage</span>
|
||
</div>
|
||
<div class="clu-card-body tight" style="overflow-x:auto;">
|
||
<table class="clu-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Site</th>
|
||
<th>Typ</th>
|
||
<th>Größe</th>
|
||
<th>Wann</th>
|
||
<th>Status</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse ($backups as $b)
|
||
<tr>
|
||
<td>
|
||
<div class="clu-site-cell">
|
||
<div class="clu-favicon">{{ mb_substr($b->site->domain ?? '?', 0, 1) }}</div>
|
||
<div class="clu-site-meta">
|
||
<span class="clu-site-domain">{{ $b->site->domain ?? '—' }}</span>
|
||
<span class="clu-site-server">{{ $b->storage_location ?? 's3://clupilot-backups' }}</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td><span class="clu-pill muted">{{ ucfirst($b->type) }}</span></td>
|
||
<td class="font-mono">{{ $b->size_mb }} MB</td>
|
||
<td class="font-mono">{{ $b->completed_at?->diffForHumans() ?? '—' }}</td>
|
||
<td>
|
||
@if ($b->status === 'ok')
|
||
<span class="clu-health">ok</span>
|
||
@elseif ($b->status === 'failed')
|
||
<span class="clu-health bad">failed</span>
|
||
@else
|
||
<span class="clu-health warn">{{ $b->status }}</span>
|
||
@endif
|
||
</td>
|
||
<td>
|
||
<div class="clu-row-actions">
|
||
<button title="Restore" type="button">
|
||
<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>
|
||
</button>
|
||
<button title="Download" type="button">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v14M5 12l7 7 7-7M5 21h14"/></svg>
|
||
</button>
|
||
<button title="Mehr" type="button">
|
||
<svg width="14" height="14" 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>
|
||
@empty
|
||
<tr><td colspan="6" style="text-align:center;padding:40px;color:var(--color-muted);">Noch keine Backups.</td></tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="clu-pager">
|
||
<span class="clu-pager-meta">{{ $backups->firstItem() ?? 0 }}–{{ $backups->lastItem() ?? 0 }} von {{ $backups->total() }}</span>
|
||
<div class="flex gap-[6px]">
|
||
<button class="clu-ghost-btn" wire:click="previousPage" @if($backups->onFirstPage()) disabled @endif>← Zurück</button>
|
||
<button class="clu-ghost-btn" wire:click="nextPage" @if(! $backups->hasMorePages()) disabled @endif>Weiter →</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Sticky summary --}}
|
||
<div class="clu-card" style="height:fit-content;position:sticky;top:90px;">
|
||
<div class="clu-card-head"><h3>Konfiguration</h3></div>
|
||
<div class="clu-card-body" style="display:flex;flex-direction:column;gap:14px;">
|
||
<label class="clu-checkbox justify-between" style="width:100%;justify-content:space-between;">
|
||
<span>Daily Backups</span>
|
||
<input type="checkbox" wire:model.live="dailyEnabled" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
<label class="clu-checkbox" style="width:100%;justify-content:space-between;">
|
||
<span>Hourly Snapshots</span>
|
||
<input type="checkbox" wire:model.live="snapshotsEnabled" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
<label class="clu-checkbox" style="width:100%;justify-content:space-between;">
|
||
<span>Multi-Region Replication</span>
|
||
<input type="checkbox" wire:model.live="replicationEnabled" />
|
||
<span class="box"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 12l5 5L20 7"/></svg></span>
|
||
</label>
|
||
|
||
<div style="border-top:1px solid var(--color-hairline);padding-top:14px;">
|
||
<div class="clu-stat-label">Storage</div>
|
||
<div class="clu-plan-meter mt-2"><span style="width:{{ $storageUsedPct }}%;"></span></div>
|
||
<div class="clu-plan-stats">
|
||
<span>{{ $totalSizeGb }} GB used</span>
|
||
<span>{{ $storageUsedPct }}%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="clu-feed-meta" style="line-height:1.5;">
|
||
AES-256 verschlüsselt · S3 Frankfurt · 30 Tage Retention
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|