From 662e3d2cdd089a4579359214432887b59bff53e1 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sun, 29 Mar 2026 10:37:33 +0300 Subject: [PATCH] feat(webui): combined GPU charts (load/memload/power/temp all GPUs per chart) Replace per-GPU cards with 4 combined charts showing all GPUs as separate series. Add gpu-all-load/memload/power/temp endpoints. Co-Authored-By: Claude Sonnet 4.6 --- audit/internal/webui/pages.go | 80 +++++++++++++++------------------- audit/internal/webui/server.go | 75 ++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 45 deletions(-) diff --git a/audit/internal/webui/pages.go b/audit/internal/webui/pages.go index f5b40a0..f646657 100644 --- a/audit/internal/webui/pages.go +++ b/audit/internal/webui/pages.go @@ -392,12 +392,6 @@ func renderMetrics() string { -
-
Temperature — GPUs
-
- GPU temperature -
-
Temperature — Ambient Sensors
@@ -421,50 +415,47 @@ func renderMetrics() string {
-
+
+
GPU — Compute Load
+
+ GPU compute load +
+
+
+
GPU — Memory Load
+
+ GPU memory load +
+
+
+
GPU — Power
+
+ GPU power +
+
+
+
GPU — Temperature
+
+ GPU temperature +
+
+
` diff --git a/audit/internal/webui/server.go b/audit/internal/webui/server.go index c9461c8..7ea24a0 100644 --- a/audit/internal/webui/server.go +++ b/audit/internal/webui/server.go @@ -449,7 +449,80 @@ func (h *handler) handleMetricsChartSVG(w http.ResponseWriter, r *http.Request) yMin = floatPtr(0) yMax = autoMax120(datasets...) - // ── GPU sub-charts ──────────────────────────────────────────────────── + // ── Combined GPU charts (all GPUs on one chart) ─────────────────────── + case path == "gpu-all-load": + title = "GPU Compute Load" + h.ringsMu.Lock() + for idx, gr := range h.gpuRings { + if gr == nil { + continue + } + vUtil, l := gr.Util.snapshot() + datasets = append(datasets, vUtil) + names = append(names, fmt.Sprintf("GPU %d %%", idx)) + if len(labels) == 0 { + labels = l + } + } + h.ringsMu.Unlock() + yMin = floatPtr(0) + yMax = floatPtr(100) + + case path == "gpu-all-memload": + title = "GPU Memory Load" + h.ringsMu.Lock() + for idx, gr := range h.gpuRings { + if gr == nil { + continue + } + vMem, l := gr.MemUtil.snapshot() + datasets = append(datasets, vMem) + names = append(names, fmt.Sprintf("GPU %d %%", idx)) + if len(labels) == 0 { + labels = l + } + } + h.ringsMu.Unlock() + yMin = floatPtr(0) + yMax = floatPtr(100) + + case path == "gpu-all-power": + title = "GPU Power" + h.ringsMu.Lock() + for idx, gr := range h.gpuRings { + if gr == nil { + continue + } + vPow, l := gr.Power.snapshot() + datasets = append(datasets, vPow) + names = append(names, fmt.Sprintf("GPU %d W", idx)) + if len(labels) == 0 { + labels = l + } + } + h.ringsMu.Unlock() + yMin = floatPtr(0) + yMax = autoMax120(datasets...) + + case path == "gpu-all-temp": + title = "GPU Temperature" + h.ringsMu.Lock() + for idx, gr := range h.gpuRings { + if gr == nil { + continue + } + vTemp, l := gr.Temp.snapshot() + datasets = append(datasets, vTemp) + names = append(names, fmt.Sprintf("GPU %d °C", idx)) + if len(labels) == 0 { + labels = l + } + } + h.ringsMu.Unlock() + yMin = floatPtr(0) + yMax = autoMax120(datasets...) + + // ── Per-GPU sub-charts ──────────────────────────────────────────────── case strings.HasPrefix(path, "gpu/"): rest := strings.TrimPrefix(path, "gpu/") // rest is either "{idx}-load", "{idx}-temp", "{idx}-power", or legacy "{idx}"