249 lines
12 KiB
PHP
249 lines
12 KiB
PHP
<?php
|
|
|
|
use App\Models\Activity;
|
|
use App\Models\SecurityEvent;
|
|
use App\Models\Server;
|
|
use App\Models\Site;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Volt\Component;
|
|
|
|
new #[Layout('layouts.app')] class extends Component {
|
|
public function with(): array
|
|
{
|
|
$sitesTotal = Site::count();
|
|
$sitesOk = Site::where('status', 'ok')->count();
|
|
$avgResponseMs = (int) Site::avg('response_ms');
|
|
$updatesPending = (int) Site::sum('updates_pending');
|
|
$blocked24h = SecurityEvent::where('occurred_at', '>=', now()->subDay())->count();
|
|
$serversTotal = Server::count();
|
|
|
|
$sites = Site::query()
|
|
->with('server')
|
|
->orderByDesc('visitors_24h')
|
|
->limit(8)
|
|
->get();
|
|
|
|
$activities = Activity::query()
|
|
->orderByDesc('occurred_at')
|
|
->limit(8)
|
|
->get();
|
|
|
|
return compact(
|
|
'sitesTotal', 'sitesOk', 'avgResponseMs', 'updatesPending',
|
|
'blocked24h', 'serversTotal', 'sites', 'activities',
|
|
);
|
|
}
|
|
}; ?>
|
|
|
|
<div>
|
|
{{-- ───── Topbar ───── --}}
|
|
<div class="clu-topbar">
|
|
<x-clu.burger />
|
|
<div>
|
|
<h1>Dashboard</h1>
|
|
<div class="crumb">Acme Cluster · eu-central-1</div>
|
|
</div>
|
|
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> Alle Systeme aktiv</span>
|
|
<div class="spacer"></div>
|
|
<div class="clu-search">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-(--color-muted-2)" aria-hidden="true">
|
|
<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
|
|
</svg>
|
|
<input type="search" placeholder="Sites, Server, Logs durchsuchen…" />
|
|
<span class="kbd">⌘ K</span>
|
|
</div>
|
|
<button class="clu-icon-btn" aria-label="Benachrichtigungen">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>
|
|
</svg>
|
|
<span class="badge">3</span>
|
|
</button>
|
|
<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="M12 5v14M5 12h14"/>
|
|
</svg>
|
|
Cluster anlegen
|
|
</button>
|
|
</div>
|
|
|
|
{{-- ───── Metric tiles (4) ───── --}}
|
|
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:repeat(auto-fit,minmax(240px,1fr));">
|
|
<div class="clu-metric success">
|
|
<div class="clu-metric-row">
|
|
<span class="clu-metric-label">Sites Online</span>
|
|
<span class="clu-metric-ico">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20M12 2a15 15 0 0 0 0 20"/></svg>
|
|
</span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $sitesOk }}<span class="unit">/{{ $sitesTotal }}</span></div>
|
|
<div class="clu-metric-delta">● {{ number_format($sitesOk / max($sitesTotal, 1) * 100, 1, ',', '.') }}% uptime · 24h</div>
|
|
</div>
|
|
|
|
<div class="clu-metric">
|
|
<div class="clu-metric-row">
|
|
<span class="clu-metric-label">Ø Response</span>
|
|
<span class="clu-metric-ico">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 8v4l3 2"/><circle cx="12" cy="12" r="10"/></svg>
|
|
</span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $avgResponseMs }}<span class="unit">ms</span></div>
|
|
<div class="clu-metric-delta">↓ 8,4% vs. 7d</div>
|
|
</div>
|
|
|
|
<div class="clu-metric warning">
|
|
<div class="clu-metric-row">
|
|
<span class="clu-metric-label">Updates ausstehend</span>
|
|
<span class="clu-metric-ico">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg>
|
|
</span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $updatesPending }}</div>
|
|
<div class="clu-metric-delta warn">{{ $serversTotal }} Server betroffen</div>
|
|
</div>
|
|
|
|
<div class="clu-metric danger">
|
|
<div class="clu-metric-row">
|
|
<span class="clu-metric-label">Blocked / 24h</span>
|
|
<span class="clu-metric-ico">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/></svg>
|
|
</span>
|
|
</div>
|
|
<div class="clu-metric-value">{{ $blocked24h }}</div>
|
|
<div class="clu-metric-delta bad">↑ 23% Brute-force</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- ───── Traffic chart (ChartJS) ───── --}}
|
|
<div class="clu-card mb-[18px]" wire:ignore>
|
|
<div class="clu-card-head">
|
|
<h3>Traffic · letzte 24h</h3>
|
|
<span class="meta">requests / minute</span>
|
|
</div>
|
|
<div class="clu-card-body" style="position:relative;height:240px;">
|
|
<canvas id="clu-traffic-chart"></canvas>
|
|
</div>
|
|
</div>
|
|
@push('scripts')
|
|
<script>
|
|
(function initTrafficChart() {
|
|
const el = document.getElementById('clu-traffic-chart');
|
|
if (!el || !window.Chart) return;
|
|
if (el._chart) el._chart.destroy();
|
|
const ctx = el.getContext('2d');
|
|
|
|
// 24 buckets, hour labels
|
|
const labels = Array.from({length: 24}, (_, i) => ((24 + new Date().getHours() - 23 + i) % 24).toString().padStart(2, '0') + ':00');
|
|
// mock 24h traffic curve (peak midday)
|
|
const data = [42, 38, 32, 28, 22, 19, 25, 48, 92, 134, 168, 192, 210, 218, 204, 185, 162, 148, 135, 118, 96, 78, 64, 52];
|
|
|
|
const grad = ctx.createLinearGradient(0, 0, 0, 240);
|
|
grad.addColorStop(0, 'rgba(59,111,242,0.35)');
|
|
grad.addColorStop(1, 'rgba(59,111,242,0.00)');
|
|
|
|
el._chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels,
|
|
datasets: [{
|
|
data,
|
|
borderColor: '#3b6ff2',
|
|
borderWidth: 2,
|
|
backgroundColor: grad,
|
|
fill: true,
|
|
tension: 0.35,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
pointHoverBackgroundColor: '#3b6ff2',
|
|
}],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { grid: { color: 'rgba(30,35,60,0.06)' }, ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: '#9596a6' } },
|
|
y: { grid: { color: 'rgba(30,35,60,0.06)' }, ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: '#9596a6' }, beginAtZero: true },
|
|
},
|
|
},
|
|
});
|
|
})();
|
|
document.addEventListener('livewire:navigated', () => initTrafficChart());
|
|
</script>
|
|
@endpush
|
|
|
|
{{-- ───── 2-col: Sites table + Activity feed ───── --}}
|
|
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:minmax(0,2fr) minmax(280px,1fr);">
|
|
|
|
{{-- Sites table --}}
|
|
<div class="clu-card">
|
|
<div class="clu-card-head">
|
|
<h3>Top Sites</h3>
|
|
<a href="{{ route('sites.index') }}" wire:navigate class="meta">Alle ansehen →</a>
|
|
</div>
|
|
<div class="clu-card-body tight">
|
|
<table class="clu-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Site</th>
|
|
<th>WP / PHP</th>
|
|
<th>Besucher 24h</th>
|
|
<th>Response</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach ($sites as $site)
|
|
<tr>
|
|
<td>
|
|
<a href="{{ route('sites.show', $site) }}" wire:navigate class="clu-site-cell" style="text-decoration:none;color:inherit;">
|
|
<div class="clu-favicon">{{ mb_substr($site->domain, 0, 1) }}</div>
|
|
<div class="clu-site-meta">
|
|
<span class="clu-site-domain">{{ $site->domain }}</span>
|
|
<span class="clu-site-server">{{ $site->server->name }} · {{ $site->server->region }}</span>
|
|
</div>
|
|
</a>
|
|
</td>
|
|
<td><span class="clu-pill muted">WP {{ $site->wp_version }} · PHP {{ $site->php_version }}</span></td>
|
|
<td class="font-mono">{{ number_format($site->visitors_24h, 0, ',', '.') }}</td>
|
|
<td class="font-mono">{{ $site->response_ms }}ms</td>
|
|
<td><span class="clu-health @if($site->status === 'warn') warn @elseif($site->status === 'bad') bad @endif">{{ $site->status }}</span></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Activity feed --}}
|
|
<div class="clu-card">
|
|
<div class="clu-card-head">
|
|
<h3>Aktivität</h3>
|
|
<span class="meta">live</span>
|
|
</div>
|
|
<div class="clu-card-body tight">
|
|
<div class="clu-feed">
|
|
@foreach ($activities as $a)
|
|
<div class="clu-feed-row">
|
|
<div class="clu-feed-ico {{ $a->level }}">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
@switch($a->type)
|
|
@case('deploy') <path d="M22 2L11 13"/><path d="M22 2l-7 20-4-9-9-4 20-7z"/> @break
|
|
@case('backup') <path d="M21 12c0 5-9 9-9 9s-9-4-9-9V5l9-3 9 3v7z"/> @break
|
|
@case('update') <path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/> @break
|
|
@case('security') <path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7l8-4z"/> @break
|
|
@default <circle cx="12" cy="12" r="9"/><path d="M12 8v4l3 2"/>
|
|
@endswitch
|
|
</svg>
|
|
</div>
|
|
<div class="clu-feed-body">
|
|
<div class="clu-feed-msg">{{ $a->message }}</div>
|
|
<div class="clu-feed-meta">{{ $a->subject ?? '—' }} · {{ $a->occurred_at?->diffForHumans() }}</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|