166 lines
10 KiB
PHP
166 lines
10 KiB
PHP
@php
|
|
$svcLabel = ['online' => __('dashboard.svc_online'), 'warning' => __('dashboard.svc_warning'), 'offline' => __('dashboard.svc_offline')];
|
|
|
|
// Build SVG polyline paths from the real CPU/MEM history series (cache-backed, polled).
|
|
$chart = function (array $s) {
|
|
$w = 300; $h = 100; $pad = 6; $n = count($s); $pts = [];
|
|
foreach ($s as $i => $v) {
|
|
$x = $n > 1 ? round($i / ($n - 1) * $w, 2) : 0;
|
|
$y = round($h - $pad - ($v / 100) * ($h - 2 * $pad), 2);
|
|
$pts[] = "$x,$y";
|
|
}
|
|
$line = implode(' ', $pts);
|
|
return ['line' => $line, 'area' => "0,$h ".$line." $w,$h"];
|
|
};
|
|
$cpuP = $chart($cpuSeries);
|
|
$memP = $chart($memSeries);
|
|
@endphp
|
|
|
|
<div class="space-y-4" wire:poll.10s wire:init="load">
|
|
{{-- Header --}}
|
|
<div class="flex flex-wrap items-end justify-between gap-3">
|
|
<div>
|
|
@if ($active)
|
|
<a href="{{ route('servers.show', $active) }}" wire:navigate class="transition-colors hover:text-accent">
|
|
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-accent-text">{{ $active->name }}</p>
|
|
</a>
|
|
@else
|
|
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-accent-text">—</p>
|
|
@endif
|
|
<h2 class="mt-0.5 font-display text-xl font-semibold text-ink sm:text-2xl">{{ __('dashboard.heading') }}</h2>
|
|
</div>
|
|
@if ($active)
|
|
<a href="{{ route('servers.show', $active) }}" wire:navigate class="transition-opacity hover:opacity-80">
|
|
<x-status-pill :status="$active->status">{{ $active->ip }}</x-status-pill>
|
|
</a>
|
|
@else
|
|
<x-status-pill :status="'offline'">—</x-status-pill>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- KPI tiles with sparklines (active server) --}}
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
<x-metric label="CPU" :value="$cpu" unit="%" icon="cpu" :tone="$cpuTone" :series="$cpuSeries" :trend="$cpuTrend" />
|
|
<x-metric label="Memory" :value="$mem" unit="%" icon="activity" :tone="$memTone" :series="$memSeries" :trend="$memTrend" :sub="$memSub" />
|
|
<x-metric label="Disk" :value="$disk" unit="%" icon="server" :tone="$diskTone" :series="$diskSeries" :trend="$diskTrend" :sub="$diskSub" />
|
|
<x-metric label="Load avg" :value="number_format($load, 2)" icon="activity" :tone="$loadTone" :series="$loadSeries" :trend="$loadTrend" :sub="$loadSub" />
|
|
</div>
|
|
|
|
{{-- Big dual-series chart --}}
|
|
<x-panel :title="__('dashboard.utilization')" :subtitle="__('dashboard.utilization_subtitle')">
|
|
<x-slot:actions>
|
|
<span class="inline-flex items-center gap-1.5 font-mono text-[11px]"><span class="h-2 w-2 rounded-xs bg-accent"></span><span class="text-ink-2">CPU</span></span>
|
|
<span class="inline-flex items-center gap-1.5 font-mono text-[11px]"><span class="h-2 w-2 rounded-xs bg-cyan"></span><span class="text-ink-2">MEM</span></span>
|
|
<x-badge tone="accent">Live</x-badge>
|
|
</x-slot:actions>
|
|
|
|
<div class="relative pl-7">
|
|
{{-- Y axis --}}
|
|
<div class="pointer-events-none absolute inset-y-0 left-0 w-6 font-mono text-[9px] text-ink-4">
|
|
<span class="absolute right-0 top-0 -translate-y-1/2">100</span>
|
|
<span class="absolute right-0 top-1/4 -translate-y-1/2">75</span>
|
|
<span class="absolute right-0 top-1/2 -translate-y-1/2">50</span>
|
|
<span class="absolute right-0 top-3/4 -translate-y-1/2">25</span>
|
|
<span class="absolute right-0 top-full -translate-y-1/2">0</span>
|
|
</div>
|
|
|
|
<div class="h-44 w-full">
|
|
<svg viewBox="0 0 300 100" preserveAspectRatio="none" class="h-full w-full" aria-hidden="true">
|
|
{{-- grid --}}
|
|
<g class="text-line-soft">
|
|
@foreach ([0, 25, 50, 75, 100] as $g)
|
|
<line x1="0" x2="300" y1="{{ 100 * (1 - $g / 100) }}" y2="{{ 100 * (1 - $g / 100) }}" stroke="currentColor" stroke-width="1" vector-effect="non-scaling-stroke" />
|
|
@endforeach
|
|
</g>
|
|
{{-- MEM (cyan) --}}
|
|
<g class="text-cyan">
|
|
<polyline points="{{ $memP['area'] }}" fill="currentColor" fill-opacity="0.10" stroke="none" />
|
|
<polyline points="{{ $memP['line'] }}" fill="none" stroke="currentColor" stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
|
|
</g>
|
|
{{-- CPU (accent) --}}
|
|
<g class="text-accent">
|
|
<polyline points="{{ $cpuP['area'] }}" fill="currentColor" fill-opacity="0.13" stroke="none" />
|
|
<polyline points="{{ $cpuP['line'] }}" fill="none" stroke="currentColor" stroke-width="1.75" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- X axis OUTSIDE the relative wrapper (mirrors servers/show.blade.php): keeps the
|
|
y-rail's inset-y-0 spanning exactly the plot, so the "0" tick lands on the bottom
|
|
gridline instead of colliding with "-15 Min" down here. --}}
|
|
<div class="mt-3 flex justify-between pl-7 font-mono text-[9px] text-ink-4">
|
|
<span>{{ __('dashboard.axis_minus_15min') }}</span><span>-10</span><span>-5</span><span>{{ __('dashboard.axis_now') }}</span>
|
|
</div>
|
|
</x-panel>
|
|
|
|
{{-- systemd table + audit --}}
|
|
<div class="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
|
<x-panel :title="__('dashboard.services_title')" :subtitle="$active?->name" class="lg:col-span-2" :padded="false">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-line">
|
|
<th class="px-4 py-2.5 text-left font-mono text-[10px] uppercase tracking-wider text-ink-3 sm:px-5">{{ __('dashboard.col_unit') }}</th>
|
|
<th class="px-4 py-2.5 text-left font-mono text-[10px] uppercase tracking-wider text-ink-3">{{ __('dashboard.col_status') }}</th>
|
|
<th class="px-4 py-2.5 text-right font-mono text-[10px] uppercase tracking-wider text-ink-3">{{ __('dashboard.col_state') }}</th>
|
|
<th class="hidden px-4 py-2.5 text-right font-mono text-[10px] uppercase tracking-wider text-ink-3 sm:table-cell">{{ __('dashboard.col_boot') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (! $ready)
|
|
@for ($i = 0; $i < 5; $i++)
|
|
<tr class="border-b border-line/60">
|
|
<td class="px-4 py-3 sm:px-5"><x-skeleton class="h-3 w-40 max-w-full" /><x-skeleton class="mt-1.5 h-2.5 w-56 max-w-full" /></td>
|
|
<td class="px-4 py-3"><x-skeleton class="h-5 w-16 rounded-full" /></td>
|
|
<td class="px-4 py-3 text-right"><x-skeleton class="ml-auto h-3 w-12" /></td>
|
|
<td class="hidden px-4 py-3 sm:table-cell"><x-skeleton class="ml-auto h-3 w-14" /></td>
|
|
</tr>
|
|
@endfor
|
|
@else
|
|
@forelse ($services as $svc)
|
|
<tr class="border-b border-line/60 transition-colors last:border-0 hover:bg-raised">
|
|
<td class="px-4 py-2.5 sm:px-5">
|
|
<p class="font-mono text-sm text-ink">{{ $svc['name'] }}</p>
|
|
<p class="truncate text-[11px] text-ink-3">{{ $svc['desc'] }}</p>
|
|
</td>
|
|
<td class="px-4 py-2.5"><x-status-pill :status="$svc['status']">{{ $svcLabel[$svc['status']] }}</x-status-pill></td>
|
|
<td class="px-4 py-2.5 text-right font-mono text-xs text-ink-2">{{ $svc['sub'] }}</td>
|
|
<td class="hidden px-4 py-2.5 text-right font-mono text-xs sm:table-cell {{ $svc['enabled'] ? 'text-ink-2' : 'text-ink-4' }}">{{ $svc['enabled'] ? 'enabled' : 'disabled' }}</td>
|
|
</tr>
|
|
@empty
|
|
<tr><td colspan="4" class="px-5 py-8 text-center font-mono text-[11px] text-ink-3">{{ __('dashboard.no_service_data') }}</td></tr>
|
|
@endforelse
|
|
@endif
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</x-panel>
|
|
|
|
<x-panel :title="__('dashboard.audit_title')" :subtitle="__('dashboard.audit_subtitle')" :padded="false">
|
|
<div class="divide-y divide-line">
|
|
@forelse ($events as $e)
|
|
<div class="flex items-start gap-3 px-4 py-2.5 sm:px-5">
|
|
<span @class([
|
|
'mt-0.5 grid h-7 w-7 shrink-0 place-items-center rounded-sm',
|
|
'bg-offline/10 text-offline' => $e->is_error,
|
|
'bg-raised text-ink-3' => ! $e->is_error,
|
|
])><x-icon :name="$e->is_error ? 'alert' : 'audit'" class="h-3.5 w-3.5" /></span>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm text-ink">
|
|
<span class="text-ink-2">{{ $e->actor }}</span>
|
|
<span class="text-ink-4">·</span>
|
|
<span @class(['font-medium', 'text-offline' => $e->is_error, 'text-ink' => ! $e->is_error])>{{ $e->action_label }}</span>
|
|
</p>
|
|
<p class="truncate font-mono text-[11px] text-ink-3">{{ $e->target }}</p>
|
|
</div>
|
|
<span class="shrink-0 font-mono text-[11px] text-ink-4">{{ $e->created_at->diffForHumans(null, true) }}</span>
|
|
</div>
|
|
@empty
|
|
<p class="px-5 py-8 text-center font-mono text-[11px] text-ink-3">{{ __('dashboard.no_events') }}</p>
|
|
@endforelse
|
|
</div>
|
|
</x-panel>
|
|
</div>
|
|
</div>
|