Fix statistics graphPayloads never being populated and add redesigned telemetry partial
Made-with: Cursor
This commit is contained in:
@@ -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')]
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user