Fix statistics graphPayloads never being populated and add redesigned telemetry partial

Made-with: Cursor
This commit is contained in:
2026-03-27 18:58:35 -04:00
parent 36255eebcf
commit 3cb201d766
2 changed files with 316 additions and 2 deletions

View File

@@ -906,7 +906,8 @@ class Statistics extends Component
// Reduced cache TTL from 60 to 30 seconds for fresher data and faster updates
$data = Cache::remember($cacheKey, 30, function () use ($timeRange, $aggregation) {
$proxmox = new Proxmox;
$stats = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/rrddata', data: [
$vmType = $this->server->node->vmType();
$stats = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $vmType . '/' . $this->server->vm_id . '/rrddata', data: [
'timeframe' => $timeRange,
'cf' => $aggregation === 'maximum' ? 'MAX' : 'AVERAGE',
]);
@@ -938,7 +939,9 @@ class Statistics extends Component
$data = [];
}
$this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $data);
$this->graphPayloads[$graphType] = is_array($data) ? $data : [];
$this->dispatch('proxmox:stats:' . $this->server->id . ':' . $graphType, $this->graphPayloads[$graphType]);
}
#[On('render-chart-events')]

View File

@@ -0,0 +1,311 @@
{{-- Live telemetry: ApexCharts targets (#cpu-chart, etc.) + wire bindings must stay stable for @script initializers --}}
<style>
.telemetry-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(0, 143, 251, 0.2);
border-top-color: #008FFB;
border-radius: 50%;
animation: telemetrySpin 0.8s linear infinite;
}
@keyframes telemetrySpin {
to { transform: rotate(360deg); }
}
.telemetry-shell {
border-radius: 1.25rem;
border: 1px solid hsl(var(--color-neutral) / 0.35);
background: linear-gradient(
145deg,
hsl(var(--color-background-secondary) / 0.95) 0%,
hsl(var(--color-background)) 55%,
hsl(var(--color-background-secondary) / 0.85) 100%
);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.04) inset,
0 12px 40px -24px rgba(0, 0, 0, 0.35);
}
.telemetry-card {
border-radius: 1rem;
border: 1px solid hsl(var(--color-neutral) / 0.28);
background: hsl(var(--color-background) / 0.55);
backdrop-filter: blur(8px);
}
.telemetry-chart-host {
min-height: 280px;
}
.telemetry-toolbar select {
background-color: hsl(var(--color-background));
border: 1px solid hsl(var(--color-neutral));
border-radius: 0.5rem;
padding: 0.375rem 0.625rem;
font-size: 0.8125rem;
line-height: 1.25rem;
outline: none;
}
.telemetry-toolbar select:focus {
outline: none;
box-shadow: 0 0 0 2px hsl(var(--color-secondary));
}
</style>
<section
wire:poll.120s="stats"
x-data="{ chartsLoaded: false }"
@proxmox-all-stats-loaded.window="chartsLoaded = true"
class="telemetry-shell relative overflow-hidden"
>
<div
x-show="!chartsLoaded"
x-transition:leave="transition ease-in duration-500"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="absolute inset-0 z-10 flex items-center justify-center bg-background/75 backdrop-blur-sm"
>
<div class="flex flex-col items-center gap-3">
<div class="telemetry-spinner"></div>
<span class="text-base/70 text-sm">Loading telemetry…</span>
</div>
</div>
<div class="p-6 sm:p-8 space-y-8">
<div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div class="space-y-1">
<div class="flex items-center gap-2">
<span class="flex size-9 items-center justify-center rounded-xl bg-primary/15 text-primary">
<x-ri-pulse-line class="size-5" />
</span>
<h2 class="text-lg font-semibold tracking-tight">Live telemetry</h2>
</div>
<p class="text-sm text-base/65 max-w-2xl">
RRD-backed series from your hypervisor: CPU, memory, network (with 95th percentile), and disk I/O.
Adjust range and aggregation per chart; data refreshes every two minutes and when you change filters.
</p>
</div>
<div class="flex flex-wrap items-center gap-2 text-xs text-base/55">
<x-ri-time-line class="size-4 shrink-0 opacity-70" />
<span>Polls VM status with the page header; graph series use Proxmox <code class="rounded bg-neutral/15 px-1 py-0.5">rrddata</code>.</span>
</div>
</div>
<div class="grid grid-cols-1 gap-6 xl:grid-cols-2">
{{-- CPU --}}
<div class="telemetry-card overflow-hidden">
<div class="flex flex-col gap-3 border-b border-neutral/25 p-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-2 min-w-0">
<span class="flex size-8 items-center justify-center rounded-lg bg-sky-500/15 text-sky-600 dark:text-sky-400">
<x-ri-cpu-line class="size-4" />
</span>
<div class="min-w-0">
<h3 class="text-sm font-semibold truncate">Processor</h3>
<p class="text-xs text-base/55 truncate">Utilization (%)</p>
</div>
</div>
<div class="telemetry-toolbar flex flex-wrap items-center gap-2">
<select wire:model.live="cpuTimeRange" aria-label="CPU time range">
<option value="hour">Hour</option>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<select wire:model.live="cpuAggregation" aria-label="CPU aggregation">
<option value="average">Average</option>
<option value="maximum">Maximum</option>
</select>
<button type="button" onclick="exportCpuChartToCSV()"
class="inline-flex items-center gap-1.5 rounded-lg border border-neutral bg-background px-2.5 py-1.5 text-xs font-semibold hover:bg-background/80"
title="Export CPU series to CSV">
<x-ri-download-line class="size-3.5" />
CSV
</button>
</div>
</div>
<details class="group border-b border-neutral/15 px-4 py-2 text-xs text-base/60 [&_summary::-webkit-details-marker]:hidden">
<summary class="cursor-pointer select-none list-none font-medium text-base/50 hover:text-base/70">About this chart</summary>
<p class="mt-2 max-w-prose leading-relaxed">CPU from RRD as a share of allocated cores (shown as percent). Export includes timestamps and values.</p>
</details>
<div class="relative p-3 sm:p-4">
<div class="relative telemetry-chart-host" x-data="{ ready: false }" x-init="
let n = 0;
const t = setInterval(() => {
n++;
if (document.querySelector('#cpu-chart .apexcharts-canvas')) { ready = true; clearInterval(t); }
else if (n >= 150) clearInterval(t);
}, 200);
">
<div class="absolute inset-0 z-[1] flex items-center justify-center rounded-lg border border-dashed border-neutral/20 bg-background/40"
x-show="!ready" x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<span class="text-xs text-base/45">Preparing chart…</span>
</div>
<div id="cpu-chart" wire:ignore class="min-h-[280px]"></div>
</div>
</div>
</div>
{{-- Network --}}
<div class="telemetry-card overflow-hidden">
<div class="flex flex-col gap-3 border-b border-neutral/25 p-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-2 min-w-0">
<span class="flex size-8 items-center justify-center rounded-lg bg-amber-500/15 text-amber-600 dark:text-amber-400">
<x-ri-arrow-left-right-line class="size-4" />
</span>
<div class="min-w-0">
<h3 class="text-sm font-semibold truncate">Network</h3>
<p class="text-xs text-base/55 truncate">Throughput + 95th percentile</p>
</div>
</div>
<div class="telemetry-toolbar flex flex-wrap items-center gap-2">
<select wire:model.live="networkTimeRange" aria-label="Network time range">
<option value="hour">Hour</option>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<select wire:model.live="networkAggregation" aria-label="Network aggregation">
<option value="average">Average</option>
<option value="maximum">Maximum</option>
</select>
<button type="button" onclick="exportNetworkChartToCSV()"
class="inline-flex items-center gap-1.5 rounded-lg border border-neutral bg-background px-2.5 py-1.5 text-xs font-semibold hover:bg-background/80"
title="Export network series to CSV">
<x-ri-download-line class="size-3.5" />
CSV
</button>
</div>
</div>
<details class="group border-b border-neutral/15 px-4 py-2 text-xs text-base/60">
<summary class="cursor-pointer select-none list-none font-medium text-base/50 hover:text-base/70">About this chart</summary>
<p class="mt-2 max-w-prose leading-relaxed">Inbound and outbound rates plus a dashed 95th percentile of combined traffic (common for capacity planning).</p>
</details>
<div class="relative p-3 sm:p-4">
<div class="relative telemetry-chart-host" x-data="{ ready: false }" x-init="
let n = 0;
const t = setInterval(() => {
n++;
if (document.querySelector('#network-chart .apexcharts-canvas')) { ready = true; clearInterval(t); }
else if (n >= 150) clearInterval(t);
}, 200);
">
<div class="absolute inset-0 z-[1] flex items-center justify-center rounded-lg border border-dashed border-neutral/20 bg-background/40"
x-show="!ready" x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<span class="text-xs text-base/45">Preparing chart…</span>
</div>
<div id="network-chart" wire:ignore class="min-h-[280px]"></div>
</div>
</div>
</div>
{{-- Memory --}}
<div class="telemetry-card overflow-hidden">
<div class="flex flex-col gap-3 border-b border-neutral/25 p-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-2 min-w-0">
<span class="flex size-8 items-center justify-center rounded-lg bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
<x-ri-ram-line class="size-4" />
</span>
<div class="min-w-0">
<h3 class="text-sm font-semibold truncate">Memory</h3>
<p class="text-xs text-base/55 truncate">Guest RAM (bytes)</p>
</div>
</div>
<div class="telemetry-toolbar flex flex-wrap items-center gap-2">
<select wire:model.live="memoryTimeRange" aria-label="Memory time range">
<option value="hour">Hour</option>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<select wire:model.live="memoryAggregation" aria-label="Memory aggregation">
<option value="average">Average</option>
<option value="maximum">Maximum</option>
</select>
<button type="button" onclick="exportMemoryChartToCSV()"
class="inline-flex items-center gap-1.5 rounded-lg border border-neutral bg-background px-2.5 py-1.5 text-xs font-semibold hover:bg-background/80"
title="Export memory series to CSV">
<x-ri-download-line class="size-3.5" />
CSV
</button>
</div>
</div>
<details class="group border-b border-neutral/15 px-4 py-2 text-xs text-base/60">
<summary class="cursor-pointer select-none list-none font-medium text-base/50 hover:text-base/70">About this chart</summary>
<p class="mt-2 max-w-prose leading-relaxed">Uses Proxmox RRD <code class="rounded bg-neutral/15 px-1">mem</code> with axis capped by current VM max memory when available.</p>
</details>
<div class="relative p-3 sm:p-4">
<div class="relative telemetry-chart-host" x-data="{ ready: false }" x-init="
let n = 0;
const t = setInterval(() => {
n++;
if (document.querySelector('#memory-chart .apexcharts-canvas')) { ready = true; clearInterval(t); }
else if (n >= 150) clearInterval(t);
}, 200);
">
<div class="absolute inset-0 z-[1] flex items-center justify-center rounded-lg border border-dashed border-neutral/20 bg-background/40"
x-show="!ready" x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<span class="text-xs text-base/45">Preparing chart…</span>
</div>
<div id="memory-chart" wire:ignore class="min-h-[280px]"></div>
</div>
</div>
</div>
{{-- Disk --}}
<div class="telemetry-card overflow-hidden">
<div class="flex flex-col gap-3 border-b border-neutral/25 p-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-2 min-w-0">
<span class="flex size-8 items-center justify-center rounded-lg bg-violet-500/15 text-violet-600 dark:text-violet-400">
<x-ri-hard-drive-line class="size-4" />
</span>
<div class="min-w-0">
<h3 class="text-sm font-semibold truncate">Disk I/O</h3>
<p class="text-xs text-base/55 truncate">Read / write rates</p>
</div>
</div>
<div class="telemetry-toolbar flex flex-wrap items-center gap-2">
<select wire:model.live="diskTimeRange" aria-label="Disk time range">
<option value="hour">Hour</option>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<select wire:model.live="diskAggregation" aria-label="Disk aggregation">
<option value="average">Average</option>
<option value="maximum">Maximum</option>
</select>
<button type="button" onclick="exportDiskChartToCSV()"
class="inline-flex items-center gap-1.5 rounded-lg border border-neutral bg-background px-2.5 py-1.5 text-xs font-semibold hover:bg-background/80"
title="Export disk series to CSV">
<x-ri-download-line class="size-3.5" />
CSV
</button>
</div>
</div>
<details class="group border-b border-neutral/15 px-4 py-2 text-xs text-base/60">
<summary class="cursor-pointer select-none list-none font-medium text-base/50 hover:text-base/70">About this chart</summary>
<p class="mt-2 max-w-prose leading-relaxed">Disk read and write bytes per second from RRD. Pair with task history when investigating spikes.</p>
</details>
<div class="relative p-3 sm:p-4">
<div class="relative telemetry-chart-host" x-data="{ ready: false }" x-init="
let n = 0;
const t = setInterval(() => {
n++;
if (document.querySelector('#disk-chart .apexcharts-canvas')) { ready = true; clearInterval(t); }
else if (n >= 150) clearInterval(t);
}, 200);
">
<div class="absolute inset-0 z-[1] flex items-center justify-center rounded-lg border border-dashed border-neutral/20 bg-background/40"
x-show="!ready" x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<span class="text-xs text-base/45">Preparing chart…</span>
</div>
<div id="disk-chart" wire:ignore class="min-h-[280px]"></div>
</div>
</div>
</div>
</div>
</div>
</section>