From fed9973899d1fce27a6730bb3bc29532a67a8fae Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 17 Jun 2026 16:18:50 -0400 Subject: [PATCH] fix(dashboard): downsample history graphs + readable tooltip time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent reports every few seconds, so multi-hour history series were hundreds– thousands of points — a dense, noisy line (esp. CPU). Bucket-average server-side to ~120 points (keeps the shape, drops the noise) for both the history-graph widget and the host-detail charts. Also fix the chart tooltip title showing the raw epoch-ms (e.g. 1,781,720,471,459) — format it as HH:MM. Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/host_agent/routes.py | 28 ++++++++++++++++++- plugins/host_agent/templates/host_detail.html | 5 +++- .../host_agent/templates/widget_history.html | 5 +++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/plugins/host_agent/routes.py b/plugins/host_agent/routes.py index b73237d..848c05a 100644 --- a/plugins/host_agent/routes.py +++ b/plugins/host_agent/routes.py @@ -461,11 +461,35 @@ async def widget_history(): key = "disk_root" if p.resource_name != host.name else p.metric_name series[key].append([int(p.recorded_at.timestamp() * 1000), round(p.value, 2)]) + # Downsample: the agent reports every few seconds, so a multi-hour window is + # hundreds–thousands of points — a noisy, unreadable line. Bucket-average to a + # readable point count, keeping the shape. + series = {k: _downsample(v) for k, v in series.items()} return await render_template( "widget_history.html", host=host, series=series, hours=hours, wid=wid, ) +def _downsample(series: list[list], target: int = 120) -> list[list]: + """Bucket-average a time series [[epoch_ms, value], …] down to ~target points. + + Consecutive samples are grouped into equal buckets and averaged (x = bucket + midpoint). Cheap, order-preserving, and smooths the high-frequency noise that + makes dense agent series hard to read. + """ + n = len(series) + if n <= target: + return series + bucket = (n + target - 1) // target + out: list[list] = [] + for i in range(0, n, bucket): + chunk = series[i:i + bucket] + mid = chunk[len(chunk) // 2][0] + avg = sum(p[1] for p in chunk) / len(chunk) + out.append([mid, round(avg, 2)]) + return out + + # Host-level metrics charted on the detail page (sub-resources are shown as # current-value lists, not time series, to keep the page readable). HISTORY_METRICS = ( @@ -520,7 +544,9 @@ async def _history_for_host(session, host_name: str, since) -> dict[str, list]: series: dict[str, list] = {m: [] for m in HISTORY_METRICS} for p in rows: series[p.metric_name].append([int(p.recorded_at.timestamp() * 1000), round(p.value, 2)]) - return series + # Downsample to a readable point count (see _downsample) — raw agent cadence + # is too dense to read over a multi-hour window. + return {m: _downsample(v) for m, v in series.items()} @host_agent_bp.get("//") diff --git a/plugins/host_agent/templates/host_detail.html b/plugins/host_agent/templates/host_detail.html index c34fda9..42df49e 100644 --- a/plugins/host_agent/templates/host_detail.html +++ b/plugins/host_agent/templates/host_detail.html @@ -216,7 +216,10 @@ x: { type: "linear", ticks: { callback: (v) => fmtTime(v), maxTicksLimit: 8, color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } }, y: { beginAtZero: true, max: ymax, ticks: { color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } }, }, - plugins: { legend: { labels: { color: "#b8b8b0", boxWidth: 10, font: { size: 11 } } } }, + plugins: { + legend: { labels: { color: "#b8b8b0", boxWidth: 10, font: { size: 11 } } }, + tooltip: { callbacks: { title: function (items) { return items.length ? fmtTime(items[0].parsed.x) : ""; } } }, + }, }); const mk = (id, datasets, ymax) => { const el = document.getElementById(id); diff --git a/plugins/host_agent/templates/widget_history.html b/plugins/host_agent/templates/widget_history.html index 5d1a464..1414ad5 100644 --- a/plugins/host_agent/templates/widget_history.html +++ b/plugins/host_agent/templates/widget_history.html @@ -48,7 +48,10 @@ // still show the real %, so the values stay honest. y: { beginAtZero: true, grace: "8%", ticks: { color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } }, }, - plugins: { legend: { labels: { color: "#b8b8b0", boxWidth: 10, font: { size: 11 } } } }, + plugins: { + legend: { labels: { color: "#b8b8b0", boxWidth: 10, font: { size: 11 } } }, + tooltip: { callbacks: { title: function (items) { return items.length ? fmtTime(items[0].parsed.x) : ""; } } }, + }, }, }); })();