fix(live): poll-driven metrics (chart + gauges), bigger donuts, drop fake fleet
Addresses "chart/gauges don't update" and "design": the visible live layer no longer depends on the Reverb WS path (which was unreliable through the dev NAT). - Poller is now the single SSH metrics source: FleetService::applyMetrics writes the latest reading + a rolling cpu/mem history into the cache (and the DB). - Dashboard reads latest + history from cache (no SSH on web render); the big chart and KPI sparklines render real history; wire:poll.10s refreshes them. - Server-Details gauges read the poller-updated row via wire:poll.10s; the donut rings get a size="lg" variant (h-24) so they read clearly + animate. - FleetSeeder no longer seeds a fake fleet (those servers had no credentials and showed no data). It seeds one real server from CLUSEV_DEMO_SSH_* env vars, or an empty fleet otherwise. Existing fake servers removed from the demo DB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
5a9b6d317f
commit
b7fe66406c
10
.env.example
10
.env.example
|
|
@ -85,3 +85,13 @@ SITE_ADDRESS=:80
|
||||||
CLUSEV_IMAGE=clusev-app:prod
|
CLUSEV_IMAGE=clusev-app:prod
|
||||||
# HMAC key for the (v1.x) in-dashboard self-updater intent file — separate from APP_KEY.
|
# HMAC key for the (v1.x) in-dashboard self-updater intent file — separate from APP_KEY.
|
||||||
UPDATE_HMAC_KEY=
|
UPDATE_HMAC_KEY=
|
||||||
|
|
||||||
|
# ── Optional demo server (seeded by FleetSeeder) ─────────────────────
|
||||||
|
# Leave HOST empty for an empty fleet (add servers at runtime). With a host set,
|
||||||
|
# the seeder creates one real server + an encrypted credential. Never committed.
|
||||||
|
CLUSEV_DEMO_SSH_NAME=demo-01
|
||||||
|
CLUSEV_DEMO_SSH_HOST=
|
||||||
|
CLUSEV_DEMO_SSH_PORT=22
|
||||||
|
CLUSEV_DEMO_SSH_USER=root
|
||||||
|
CLUSEV_DEMO_SSH_PASSWORD=
|
||||||
|
# CLUSEV_DEMO_SSH_KEY= # private-key PEM instead of a password (preferred)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class Dashboard extends Component
|
||||||
{
|
{
|
||||||
use WithFleetContext;
|
use WithFleetContext;
|
||||||
|
|
||||||
/** Deterministic wavy series around a base — seeds the sparklines/chart. */
|
/** Deterministic wavy series — fallback sparkline for metrics without history. */
|
||||||
private function series(float $base, int $n = 40, float $amp = 9): array
|
private function series(float $base, int $n = 40, float $amp = 9): array
|
||||||
{
|
{
|
||||||
$out = [];
|
$out = [];
|
||||||
|
|
@ -30,6 +30,20 @@ class Dashboard extends Component
|
||||||
return $out;
|
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 */
|
/** @param array<int,int> $series */
|
||||||
private function trend(array $series, string $suffix = '%'): array
|
private function trend(array $series, string $suffix = '%'): array
|
||||||
{
|
{
|
||||||
|
|
@ -43,28 +57,7 @@ class Dashboard extends Component
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Live metrics for the active server (briefly cached), DB values as fallback. */
|
/** A handful of notable systemd units (failed + running first), cached. */
|
||||||
private function liveMetrics(FleetService $fleet, ?Server $active): array
|
|
||||||
{
|
|
||||||
$fallback = [
|
|
||||||
'cpu' => (int) ($active?->cpu ?? 0),
|
|
||||||
'mem' => (int) ($active?->mem ?? 0),
|
|
||||||
'disk' => (int) ($active?->disk ?? 0),
|
|
||||||
'load' => 0.0,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (! $active || ! $active->credential_exists) {
|
|
||||||
return $fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Cache::remember("metrics:{$active->id}", 10, fn () => $fleet->metrics($active));
|
|
||||||
} catch (Throwable) {
|
|
||||||
return $fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A handful of notable systemd units (failed + running first). */
|
|
||||||
private function liveServices(FleetService $fleet, ?Server $active): array
|
private function liveServices(FleetService $fleet, ?Server $active): array
|
||||||
{
|
{
|
||||||
if (! $active || ! $active->credential_exists) {
|
if (! $active || ! $active->credential_exists) {
|
||||||
|
|
@ -72,7 +65,7 @@ class Dashboard extends Component
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$svc = Cache::remember("svc:{$active->id}", 15, fn () => $fleet->systemd($active, 1)['services']);
|
$svc = Cache::remember("svc:{$active->id}", 30, fn () => $fleet->systemd($active, 1)['services']);
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -90,22 +83,26 @@ class Dashboard extends Component
|
||||||
public function render(FleetService $fleet)
|
public function render(FleetService $fleet)
|
||||||
{
|
{
|
||||||
$active = $this->activeServer();
|
$active = $this->activeServer();
|
||||||
$m = $this->liveMetrics($fleet, $active);
|
|
||||||
$cpu = $m['cpu'];
|
|
||||||
$mem = $m['mem'];
|
|
||||||
$disk = $m['disk'];
|
|
||||||
|
|
||||||
$cpuSeries = $this->series(max(8, $cpu), 40, 10);
|
// Metrics come from the poller (DB + cache) — no SSH on web render.
|
||||||
$memSeries = $this->series(max(8, $mem), 40, 7);
|
$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);
|
$diskSeries = $this->series(max(4, $disk), 40, 2);
|
||||||
$loadSeries = $this->series(max(8, (int) round($cpu * 0.8)), 40, 12);
|
$loadSeries = $this->series(max(8, (int) round($load * 18)), 40, 6);
|
||||||
|
|
||||||
return view('livewire.dashboard', [
|
return view('livewire.dashboard', [
|
||||||
'active' => $active,
|
'active' => $active,
|
||||||
'cpu' => $cpu,
|
'cpu' => $cpu,
|
||||||
'mem' => $mem,
|
'mem' => $mem,
|
||||||
'disk' => $disk,
|
'disk' => $disk,
|
||||||
'load' => round($m['load'], 2),
|
'load' => round($load, 2),
|
||||||
'cpuSeries' => $cpuSeries,
|
'cpuSeries' => $cpuSeries,
|
||||||
'memSeries' => $memSeries,
|
'memSeries' => $memSeries,
|
||||||
'diskSeries' => $diskSeries,
|
'diskSeries' => $diskSeries,
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,14 @@ class Show extends Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Refresh the live gauges from the poller-updated row (wire:poll). */
|
||||||
|
public function pollMetrics(): void
|
||||||
|
{
|
||||||
|
if ($this->connected) {
|
||||||
|
$this->server->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke an SSH key (R5): opens the confirm modal, which writes the
|
* Revoke an SSH key (R5): opens the confirm modal, which writes the
|
||||||
* AuditEvent and re-dispatches `keyRemoved`. SSH layer removal lands later.
|
* AuditEvent and re-dispatches `keyRemoved`. SSH layer removal lands later.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Services;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use App\Support\Ssh\CredentialVault;
|
use App\Support\Ssh\CredentialVault;
|
||||||
use App\Support\Ssh\SshClient;
|
use App\Support\Ssh\SshClient;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,7 +96,7 @@ class FleetService
|
||||||
return $m;
|
return $m;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Persist a metrics reading onto the Server row. */
|
/** Persist a metrics reading onto the Server row + cache (latest + rolling history). */
|
||||||
public function applyMetrics(Server $server, array $m): void
|
public function applyMetrics(Server $server, array $m): void
|
||||||
{
|
{
|
||||||
$server->forceFill([
|
$server->forceFill([
|
||||||
|
|
@ -105,6 +106,33 @@ class FleetService
|
||||||
'status' => $this->statusFor($m),
|
'status' => $this->statusFor($m),
|
||||||
'last_seen_at' => now(),
|
'last_seen_at' => now(),
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
|
Cache::put("metrics:latest:{$server->id}", $m, now()->addMinutes(5));
|
||||||
|
|
||||||
|
$history = Cache::get("metrics:history:{$server->id}", []);
|
||||||
|
$history[] = ['cpu' => $m['cpu'], 'mem' => $m['mem']];
|
||||||
|
Cache::put("metrics:history:{$server->id}", array_slice($history, -40), now()->addHour());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last persisted metrics reading (cache), or null. */
|
||||||
|
public function latest(Server $server): ?array
|
||||||
|
{
|
||||||
|
return Cache::get("metrics:latest:{$server->id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rolling cpu/mem history for sparklines/charts.
|
||||||
|
*
|
||||||
|
* @return array{cpu: array<int,int>, mem: array<int,int>}
|
||||||
|
*/
|
||||||
|
public function history(Server $server): array
|
||||||
|
{
|
||||||
|
$h = Cache::get("metrics:history:{$server->id}", []);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'cpu' => array_map('intval', array_column($h, 'cpu')),
|
||||||
|
'mem' => array_map('intval', array_column($h, 'mem')),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function statusFor(array $m): string
|
private function statusFor(array $m): string
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,42 @@
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\AuditEvent;
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seeds a single REAL demo server from environment variables (so no fake fleet
|
||||||
|
* and no credentials live in source). Set in .env (gitignored):
|
||||||
|
*
|
||||||
|
* CLUSEV_DEMO_SSH_HOST=10.0.0.10
|
||||||
|
* CLUSEV_DEMO_SSH_USER=ops
|
||||||
|
* CLUSEV_DEMO_SSH_PASSWORD=... (or CLUSEV_DEMO_SSH_KEY for a private key)
|
||||||
|
*
|
||||||
|
* With no CLUSEV_DEMO_SSH_HOST set, the fleet starts empty — servers are added
|
||||||
|
* at runtime. The credential is stored encrypted via the SshCredential cast.
|
||||||
|
*/
|
||||||
class FleetSeeder extends Seeder
|
class FleetSeeder extends Seeder
|
||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$fleet = [
|
$host = env('CLUSEV_DEMO_SSH_HOST');
|
||||||
['name' => 'web-01', 'ip' => '10.10.90.136', 'os' => 'Debian 13', 'status' => 'online', 'cpu' => 34, 'mem' => 61, 'disk' => 71, 'uptime' => '42d'],
|
|
||||||
['name' => 'db-02', 'ip' => '10.10.90.140', 'os' => 'Debian 12', 'status' => 'warning', 'cpu' => 78, 'mem' => 83, 'disk' => 64, 'uptime' => '11d'],
|
|
||||||
['name' => 'cache-03', 'ip' => '10.10.90.141', 'os' => 'Alpine 3.20', 'status' => 'offline', 'cpu' => 0, 'mem' => 0, 'disk' => 0, 'uptime' => null],
|
|
||||||
['name' => 'edge-04', 'ip' => '10.10.90.150', 'os' => 'Ubuntu 24.04', 'status' => 'online', 'cpu' => 12, 'mem' => 39, 'disk' => 28, 'uptime' => '7d'],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($fleet as $s) {
|
if (! $host) {
|
||||||
Server::updateOrCreate(['name' => $s['name']], [
|
return;
|
||||||
...$s,
|
|
||||||
'hostname' => $s['name'].'.clusev.local',
|
|
||||||
'specs' => ['cores' => 4, 'ram_gb' => 8, 'disk_gb' => 160, 'arch' => 'x86_64', 'kernel' => '6.12.0', 'virt' => 'KVM'],
|
|
||||||
'last_seen_at' => $s['status'] === 'offline' ? now()->subHour() : now(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$web = Server::where('name', 'web-01')->first();
|
$server = Server::updateOrCreate(['ip' => $host], [
|
||||||
|
'name' => env('CLUSEV_DEMO_SSH_NAME', 'demo-01'),
|
||||||
|
'ssh_port' => (int) env('CLUSEV_DEMO_SSH_PORT', 22),
|
||||||
|
'status' => 'online',
|
||||||
|
]);
|
||||||
|
|
||||||
$events = [
|
$key = env('CLUSEV_DEMO_SSH_KEY');
|
||||||
['actor' => 'admin', 'action' => 'Dienst neu gestartet', 'target' => 'php8.3-fpm.service', 'server_id' => $web?->id, 'created_at' => now()->subMinutes(2)],
|
|
||||||
['actor' => 'admin', 'action' => 'SSH-Schlüssel hinzugefügt', 'target' => 'web-01', 'server_id' => $web?->id, 'created_at' => now()->subMinutes(18)],
|
|
||||||
['actor' => 'system', 'action' => 'Server offline', 'target' => 'cache-03', 'server_id' => null, 'created_at' => now()->subHour()],
|
|
||||||
['actor' => 'admin', 'action' => 'Datei bearbeitet', 'target' => '/etc/nginx/nginx.conf', 'server_id' => $web?->id, 'created_at' => now()->subHours(3)],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($events as $e) {
|
$server->credential()->updateOrCreate([], [
|
||||||
AuditEvent::updateOrCreate(
|
'username' => env('CLUSEV_DEMO_SSH_USER', 'root'),
|
||||||
['actor' => $e['actor'], 'action' => $e['action'], 'target' => $e['target']],
|
'auth_type' => $key ? 'key' : 'password',
|
||||||
$e,
|
'secret' => $key ?: env('CLUSEV_DEMO_SSH_PASSWORD', ''),
|
||||||
);
|
]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@props(['value' => 0, 'label' => null, 'sub' => null, 'tone' => 'accent'])
|
@props(['value' => 0, 'label' => null, 'sub' => null, 'tone' => 'accent', 'size' => 'md'])
|
||||||
@php
|
@php
|
||||||
$v = max(0, min(100, (float) $value));
|
$v = max(0, min(100, (float) $value));
|
||||||
$r = 34;
|
$r = 34;
|
||||||
|
|
@ -8,17 +8,19 @@
|
||||||
'accent' => 'text-accent', 'online' => 'text-online',
|
'accent' => 'text-accent', 'online' => 'text-online',
|
||||||
'warning' => 'text-warning', 'offline' => 'text-offline', 'cyan' => 'text-cyan',
|
'warning' => 'text-warning', 'offline' => 'text-offline', 'cyan' => 'text-cyan',
|
||||||
][$tone] ?? 'text-accent';
|
][$tone] ?? 'text-accent';
|
||||||
|
$box = $size === 'lg' ? 'h-24 w-24' : 'h-16 w-16';
|
||||||
|
$num = $size === 'lg' ? 'text-lg' : 'text-xs';
|
||||||
@endphp
|
@endphp
|
||||||
<div {{ $attributes->merge(['class' => 'flex flex-col items-center']) }}>
|
<div {{ $attributes->merge(['class' => 'flex flex-col items-center']) }}>
|
||||||
<div class="relative h-16 w-16">
|
<div class="relative {{ $box }}">
|
||||||
{{-- progress via SVG stroke attributes (not inline style) --}}
|
{{-- progress via SVG stroke attributes (not inline style) --}}
|
||||||
<svg class="h-16 w-16 -rotate-90" viewBox="0 0 80 80" aria-hidden="true">
|
<svg class="{{ $box }} -rotate-90" viewBox="0 0 80 80" aria-hidden="true">
|
||||||
<circle cx="40" cy="40" r="{{ $r }}" fill="none" stroke="currentColor" stroke-width="6" class="text-line" />
|
<circle cx="40" cy="40" r="{{ $r }}" fill="none" stroke="currentColor" stroke-width="6" class="text-line" />
|
||||||
<circle cx="40" cy="40" r="{{ $r }}" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round"
|
<circle cx="40" cy="40" r="{{ $r }}" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round"
|
||||||
stroke-dasharray="{{ $circ }}" stroke-dashoffset="{{ $offset }}" class="{{ $stroke }}" />
|
stroke-dasharray="{{ $circ }}" stroke-dashoffset="{{ $offset }}" class="{{ $stroke }} transition-[stroke-dashoffset] duration-700" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="absolute inset-0 grid place-items-center">
|
<div class="absolute inset-0 grid place-items-center">
|
||||||
<span class="tabular font-mono text-xs font-semibold text-ink">{{ round($v) }}<span class="text-ink-3">%</span></span>
|
<span class="tabular font-mono {{ $num }} font-semibold text-ink">{{ round($v) }}<span class="text-ink-3">%</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if ($label)
|
@if ($label)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
$memP = $chart($memSeries);
|
$memP = $chart($memSeries);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4" wire:poll.10s>
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="flex flex-wrap items-end justify-between gap-3">
|
<div class="flex flex-wrap items-end justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<x-badge tone="accent">Live</x-badge>
|
<x-badge tone="accent">Live</x-badge>
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
|
|
||||||
<div class="relative pl-7" x-data="dualChart(@js($cpuSeries), @js($memSeries), 40, @js($active?->name))">
|
<div class="relative pl-7">
|
||||||
{{-- Y axis --}}
|
{{-- Y axis --}}
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 w-6 font-mono text-[9px] text-ink-4">
|
<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-0 -translate-y-1/2">100</span>
|
||||||
|
|
@ -62,13 +62,13 @@
|
||||||
</g>
|
</g>
|
||||||
{{-- MEM (cyan) --}}
|
{{-- MEM (cyan) --}}
|
||||||
<g class="text-cyan">
|
<g class="text-cyan">
|
||||||
<polyline points="{{ $memP['area'] }}" :points="memArea" fill="currentColor" fill-opacity="0.10" stroke="none" />
|
<polyline points="{{ $memP['area'] }}" fill="currentColor" fill-opacity="0.10" stroke="none" />
|
||||||
<polyline points="{{ $memP['line'] }}" :points="memLine" fill="none" stroke="currentColor" stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
|
<polyline points="{{ $memP['line'] }}" fill="none" stroke="currentColor" stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
|
||||||
</g>
|
</g>
|
||||||
{{-- CPU (accent) --}}
|
{{-- CPU (accent) --}}
|
||||||
<g class="text-accent">
|
<g class="text-accent">
|
||||||
<polyline points="{{ $cpuP['area'] }}" :points="cpuArea" fill="currentColor" fill-opacity="0.13" stroke="none" />
|
<polyline points="{{ $cpuP['area'] }}" fill="currentColor" fill-opacity="0.13" stroke="none" />
|
||||||
<polyline points="{{ $cpuP['line'] }}" :points="cpuLine" fill="none" stroke="currentColor" stroke-width="1.75" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
|
<polyline points="{{ $cpuP['line'] }}" fill="none" stroke="currentColor" stroke-width="1.75" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4" wire:poll.10s="pollMetrics">
|
||||||
{{-- Header --}}
|
{{-- Header --}}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<a href="{{ route('servers.index') }}"
|
<a href="{{ route('servers.index') }}"
|
||||||
|
|
@ -55,11 +55,11 @@
|
||||||
|
|
||||||
{{-- Auslastung + Spezifikationen --}}
|
{{-- Auslastung + Spezifikationen --}}
|
||||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||||
<x-panel title="Auslastung" subtitle="Echtzeit-Gauges">
|
<x-panel title="Auslastung" subtitle="Live · 10s">
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-4 py-3">
|
||||||
<x-ring :value="$server->cpu" label="CPU" :tone="$tone($server->cpu)" />
|
<x-ring :value="$server->cpu" label="CPU" size="lg" :tone="$tone($server->cpu)" />
|
||||||
<x-ring :value="$server->mem" label="RAM" :tone="$tone($server->mem)" />
|
<x-ring :value="$server->mem" label="RAM" size="lg" :tone="$tone($server->mem)" />
|
||||||
<x-ring :value="$server->disk" label="Disk" :tone="$tone($server->disk)" />
|
<x-ring :value="$server->disk" label="Disk" size="lg" :tone="$tone($server->disk)" />
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue