clupilot/app/resources/views/livewire/pages/dashboard.blade.php

245 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 () {
function initTrafficChart() {
const el = document.getElementById('clu-traffic-chart');
if (!el) return;
// Wait until ChartJS is registered on window (app.js loads after page-parse)
if (!window.Chart) { setTimeout(initTrafficChart, 30); return; }
if (el._chart) el._chart.destroy();
const ctx = el.getContext('2d');
const labels = Array.from({length: 24}, (_, i) =>
((24 + new Date().getHours() - 23 + i) % 24).toString().padStart(2, '0') + ':00'
);
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 window.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 },
},
},
});
}
initTrafficChart();
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>