131 lines
4.2 KiB
PHP
131 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Livewire\Concerns\WithFleetContext;
|
|
use App\Models\AuditEvent;
|
|
use App\Models\Server;
|
|
use App\Services\FleetService;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\Component;
|
|
use Throwable;
|
|
|
|
#[Layout('layouts.app')]
|
|
#[Title('Clusev — Dashboard')]
|
|
class Dashboard extends Component
|
|
{
|
|
use WithFleetContext;
|
|
|
|
public bool $ready = false;
|
|
|
|
/** @var array<int, array> notable systemd units (lazy-loaded over SSH) */
|
|
public array $svcRows = [];
|
|
|
|
/** Lazy: the SSH services read runs after the shell renders (wire:init). */
|
|
public function load(FleetService $fleet): void
|
|
{
|
|
$this->svcRows = $this->liveServices($fleet, $this->activeServer());
|
|
$this->ready = true;
|
|
}
|
|
|
|
/** Deterministic wavy series — fallback sparkline for metrics without history. */
|
|
private function series(float $base, int $n = 40, float $amp = 9): array
|
|
{
|
|
$out = [];
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$v = $base + sin($i / 3.0) * $amp + sin($i / 6.5) * ($amp * 0.55) + cos($i / 2.0) * 2;
|
|
$out[] = (int) max(2, min(98, round($v)));
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/** Pad a real history series to a minimum length for a stable sparkline. */
|
|
private function pad(array $values, int $current, int $min = 16): array
|
|
{
|
|
$values = array_map('intval', $values);
|
|
if ($values === []) {
|
|
$values = [$current];
|
|
}
|
|
while (count($values) < $min) {
|
|
array_unshift($values, $values[0]);
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/** @param array<int,int> $series */
|
|
private function trend(array $series, string $suffix = '%'): array
|
|
{
|
|
$cur = (int) (end($series) ?: 0);
|
|
$prev = (int) ($series[max(0, count($series) - 8)] ?? $cur);
|
|
$d = $cur - $prev;
|
|
|
|
return [
|
|
'dir' => $d > 1 ? 'up' : ($d < -1 ? 'down' : 'flat'),
|
|
'text' => ($d >= 0 ? '+' : '').$d.$suffix,
|
|
];
|
|
}
|
|
|
|
/** A handful of notable systemd units (failed + running first), cached. */
|
|
private function liveServices(FleetService $fleet, ?Server $active): array
|
|
{
|
|
if (! $active || ! $active->credential_exists) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$svc = Cache::remember("svc:{$active->id}", 30, fn () => $fleet->systemd($active, 1)['services']);
|
|
} catch (Throwable) {
|
|
return [];
|
|
}
|
|
|
|
$rank = fn (array $s): int => match ($s['sub']) {
|
|
'failed' => 0,
|
|
'running' => 1,
|
|
default => 2,
|
|
};
|
|
usort($svc, fn ($a, $b) => [$rank($a), $a['name']] <=> [$rank($b), $b['name']]);
|
|
|
|
return array_slice($svc, 0, 8);
|
|
}
|
|
|
|
public function render(FleetService $fleet)
|
|
{
|
|
$active = $this->activeServer();
|
|
|
|
// Metrics come from the poller (DB + cache) — no SSH on web render.
|
|
$latest = $active ? $fleet->latest($active) : null;
|
|
$cpu = (int) ($latest['cpu'] ?? $active?->cpu ?? 0);
|
|
$mem = (int) ($latest['mem'] ?? $active?->mem ?? 0);
|
|
$disk = (int) ($latest['disk'] ?? $active?->disk ?? 0);
|
|
$load = (float) ($latest['load'] ?? 0);
|
|
|
|
$hist = $active ? $fleet->history($active) : ['cpu' => [], 'mem' => []];
|
|
$cpuSeries = $this->pad($hist['cpu'], $cpu);
|
|
$memSeries = $this->pad($hist['mem'], $mem);
|
|
$diskSeries = $this->series(max(4, $disk), 40, 2);
|
|
$loadSeries = $this->series(max(8, (int) round($load * 18)), 40, 6);
|
|
|
|
return view('livewire.dashboard', [
|
|
'active' => $active,
|
|
'cpu' => $cpu,
|
|
'mem' => $mem,
|
|
'disk' => $disk,
|
|
'load' => round($load, 2),
|
|
'cpuSeries' => $cpuSeries,
|
|
'memSeries' => $memSeries,
|
|
'diskSeries' => $diskSeries,
|
|
'loadSeries' => $loadSeries,
|
|
'cpuTrend' => $this->trend($cpuSeries),
|
|
'memTrend' => $this->trend($memSeries),
|
|
'diskTrend' => $this->trend($diskSeries),
|
|
'loadTrend' => $this->trend($loadSeries, ''),
|
|
'services' => $this->svcRows,
|
|
'events' => AuditEvent::with('server')->latest()->limit(6)->get(),
|
|
]);
|
|
}
|
|
}
|