New Blade layout, copy, and visual system for the monitoring tab; RRD hosts use data-statistics-chart hooks; tables use pxStatTableKit and data-statistics-data-row; network and comparison panels use new selectors; CSV helpers renamed (pxExport*); modals and telemetry refreshed. Made-with: Cursor
309 lines
23 KiB
PHP
309 lines
23 KiB
PHP
<style>
|
|
.px-stat-rrd-orbit {
|
|
width: 42px;
|
|
height: 42px;
|
|
border: 3px solid color-mix(in oklab, hsl(var(--color-primary)) 25%, transparent);
|
|
border-top-color: hsl(var(--color-primary));
|
|
border-radius: 50%;
|
|
animation: pxStatRrdSpin 0.85s linear infinite;
|
|
}
|
|
@keyframes pxStatRrdSpin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.px-stat-rrd-canvas-host { min-height: 288px; }
|
|
</style>
|
|
<div class="mt-10 rounded-2xl ring-1 ring-neutral/40 bg-background-secondary/80 p-6 shadow-sm relative overflow-hidden" data-statistics-panel="rrd-suite">
|
|
<section
|
|
wire:poll.120s="stats"
|
|
x-data="{ chartsLoaded: false }"
|
|
@proxmox-all-stats-loaded.window="chartsLoaded = true"
|
|
class="relative"
|
|
>
|
|
<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/80 backdrop-blur-sm rounded-xl"
|
|
>
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="px-stat-rrd-orbit" aria-hidden="true"></div>
|
|
<span class="text-sm text-base/70">Synthesizing RRD frames…</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-3 md:flex-row md:items-end md:justify-between mb-8">
|
|
<div>
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<h3 class="text-lg font-semibold tracking-tight">Node RRD studio</h3>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors" aria-label="Open RRD help">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
@click="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
|
style="display: none;">
|
|
</div>
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.self="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
|
<button type="button" @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors" aria-label="Close help">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-center gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/12">
|
|
<x-ri-line-chart-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight">Reading the RRD studio</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed text-base/90 mb-4">
|
|
Proxmox exposes canonical <code class="rounded bg-neutral/15 px-1 py-0.5 text-xs">rrddata</code> streams. Pick a horizon, choose average vs peak smoothing, and export any series as CSV for offline math.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">CPU pane:</span> Shows guest CPU share as a percent of entitlement; CSV lists ISO timestamps with load.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Network pane:</span> Plots ingress, egress, and a dashed P95 envelope of combined throughput.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Memory pane:</span> Bytes in use; axis caps at provisioned RAM when the API reports it.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Disk pane:</span> Read/write throughput—pair with the job console when chasing spikes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-base/70">Four synchronized canvases fed straight from the node RRD cache.</p>
|
|
</div>
|
|
<p class="text-xs text-base/50">Auto-refresh every two minutes while this screen stays mounted.</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-8 xl:grid-cols-2">
|
|
{{-- CPU --}}
|
|
<div class="rounded-2xl ring-1 ring-neutral/30 overflow-hidden bg-gradient-to-b from-sky-500/5 to-transparent">
|
|
<div class="flex flex-col gap-3 border-b border-neutral/30 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-sky-500/15 ring-1 ring-sky-500/25">
|
|
<x-ri-cpu-line class="size-5 text-sky-600 dark:text-sky-400" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<h4 class="text-sm font-semibold truncate">Processor load</h4>
|
|
<p class="text-xs text-base/50 truncate">Percent of allocated vCPU</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<select wire:model.live="cpuTimeRange" aria-label="CPU horizon" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<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 blend" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<option value="average">Average</option>
|
|
<option value="maximum">Maximum</option>
|
|
</select>
|
|
<button type="button" onclick="pxExportRrdCpuCsv()"
|
|
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="Download CPU series">
|
|
<x-ri-download-line class="size-3.5" />
|
|
CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="relative p-4">
|
|
<div class="relative px-stat-rrd-canvas-host" x-data="{ ready: false }" x-init="
|
|
let n = 0;
|
|
const t = setInterval(() => {
|
|
n++;
|
|
if (document.querySelector('[data-statistics-chart=\'cpu\'] .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-xl border border-dashed border-neutral/25 bg-background/50"
|
|
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/50">Mounting canvas…</span>
|
|
</div>
|
|
<div data-statistics-chart="cpu" wire:ignore class="min-h-[288px]"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Network --}}
|
|
<div class="rounded-2xl ring-1 ring-neutral/30 overflow-hidden bg-gradient-to-b from-amber-500/5 to-transparent">
|
|
<div class="flex flex-col gap-3 border-b border-neutral/30 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-amber-500/15 ring-1 ring-amber-500/25">
|
|
<x-ri-arrow-left-right-line class="size-5 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<h4 class="text-sm font-semibold truncate">NIC throughput</h4>
|
|
<p class="text-xs text-base/50 truncate">In / out / P95</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<select wire:model.live="networkTimeRange" aria-label="Network horizon" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<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 blend" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<option value="average">Average</option>
|
|
<option value="maximum">Maximum</option>
|
|
</select>
|
|
<button type="button" onclick="pxExportRrdNetworkCsv()"
|
|
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="Download network series">
|
|
<x-ri-download-line class="size-3.5" />
|
|
CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="relative p-4">
|
|
<div class="relative px-stat-rrd-canvas-host" x-data="{ ready: false }" x-init="
|
|
let n = 0;
|
|
const t = setInterval(() => {
|
|
n++;
|
|
if (document.querySelector('[data-statistics-chart=\'network\'] .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-xl border border-dashed border-neutral/25 bg-background/50"
|
|
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/50">Mounting canvas…</span>
|
|
</div>
|
|
<div data-statistics-chart="network" wire:ignore class="min-h-[288px]"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Memory --}}
|
|
<div class="rounded-2xl ring-1 ring-neutral/30 overflow-hidden bg-gradient-to-b from-emerald-500/5 to-transparent">
|
|
<div class="flex flex-col gap-3 border-b border-neutral/30 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-emerald-500/15 ring-1 ring-emerald-500/25">
|
|
<x-ri-ram-line class="size-5 text-emerald-600 dark:text-emerald-400" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<h4 class="text-sm font-semibold truncate">RAM footprint</h4>
|
|
<p class="text-xs text-base/50 truncate">Bytes from RRD</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<select wire:model.live="memoryTimeRange" aria-label="Memory horizon" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<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 blend" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<option value="average">Average</option>
|
|
<option value="maximum">Maximum</option>
|
|
</select>
|
|
<button type="button" onclick="pxExportRrdMemoryCsv()"
|
|
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="Download memory series">
|
|
<x-ri-download-line class="size-3.5" />
|
|
CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="relative p-4">
|
|
<div class="relative px-stat-rrd-canvas-host" x-data="{ ready: false }" x-init="
|
|
let n = 0;
|
|
const t = setInterval(() => {
|
|
n++;
|
|
if (document.querySelector('[data-statistics-chart=\'memory\'] .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-xl border border-dashed border-neutral/25 bg-background/50"
|
|
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/50">Mounting canvas…</span>
|
|
</div>
|
|
<div data-statistics-chart="memory" wire:ignore class="min-h-[288px]"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Disk --}}
|
|
<div class="rounded-2xl ring-1 ring-neutral/30 overflow-hidden bg-gradient-to-b from-violet-500/5 to-transparent">
|
|
<div class="flex flex-col gap-3 border-b border-neutral/30 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-xl bg-violet-500/15 ring-1 ring-violet-500/25">
|
|
<x-ri-hard-drive-line class="size-5 text-violet-600 dark:text-violet-400" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<h4 class="text-sm font-semibold truncate">Block throughput</h4>
|
|
<p class="text-xs text-base/50 truncate">Read vs write</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<select wire:model.live="diskTimeRange" aria-label="Disk horizon" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<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 blend" class="bg-background border border-neutral rounded-lg px-2 py-1.5 text-sm">
|
|
<option value="average">Average</option>
|
|
<option value="maximum">Maximum</option>
|
|
</select>
|
|
<button type="button" onclick="pxExportRrdDiskCsv()"
|
|
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="Download disk series">
|
|
<x-ri-download-line class="size-3.5" />
|
|
CSV
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="relative p-4">
|
|
<div class="relative px-stat-rrd-canvas-host" x-data="{ ready: false }" x-init="
|
|
let n = 0;
|
|
const t = setInterval(() => {
|
|
n++;
|
|
if (document.querySelector('[data-statistics-chart=\'disk\'] .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-xl border border-dashed border-neutral/25 bg-background/50"
|
|
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/50">Mounting canvas…</span>
|
|
</div>
|
|
<div data-statistics-chart="disk" wire:ignore class="min-h-[288px]"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|