From 017d489e2e3370899f237e6cf246a6c1710f8cf7 Mon Sep 17 00:00:00 2001 From: M1 Date: Tue, 17 Mar 2026 07:44:09 +0400 Subject: [PATCH] fix: mutate polyline points in place on SSE ping, no fetch, no flicker --- apps/web/src/utils/sparkline.ts | 2 +- apps/web/src/views/home.ejs | 44 +++++++++++++++++++-------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/apps/web/src/utils/sparkline.ts b/apps/web/src/utils/sparkline.ts index c5d4cb5..6aa076c 100644 --- a/apps/web/src/utils/sparkline.ts +++ b/apps/web/src/utils/sparkline.ts @@ -9,5 +9,5 @@ export function sparkline(values: number[], width = 120, height = 32): string { const y = height - ((v - min) / range) * (height - 4) - 2; return `${x},${y}`; }).join(' '); - return ``; + return ``; } diff --git a/apps/web/src/views/home.ejs b/apps/web/src/views/home.ejs index 32303cc..df9ee3b 100644 --- a/apps/web/src/views/home.ejs +++ b/apps/web/src/views/home.ejs @@ -75,9 +75,8 @@ } catch {} }, 30000); - // SSE: on each ping, update text fields and fetch fresh sparkline - const _fetchingSparkline = new Set(); - watchAccount(async (ping) => { + // SSE: on each ping, update text fields and redraw sparkline in place + watchAccount((ping) => { const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`); if (!card) return; @@ -89,22 +88,31 @@ if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms'; card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at); - // Sparkline - const sparkEl = card.querySelector('.stat-sparkline'); - if (!sparkEl || _fetchingSparkline.has(ping.monitor_id)) return; - _fetchingSparkline.add(ping.monitor_id); - try { - const res = await fetch(`/dashboard/monitors/${ping.monitor_id}/sparkline`, { credentials: 'same-origin' }); - if (res.ok) { - const tmp = document.createElement('div'); - tmp.innerHTML = await res.text(); - const newSvg = tmp.firstElementChild; - const oldSvg = sparkEl.querySelector('svg'); - if (newSvg && oldSvg) oldSvg.replaceWith(newSvg); - else if (newSvg) sparkEl.appendChild(newSvg); + // Sparkline — append point to existing polyline, drop oldest, no refetch + if (ping.latency_ms != null) { + const sparkEl = card.querySelector('.stat-sparkline'); + const polyline = sparkEl?.querySelector('polyline'); + if (polyline) { + const pts = polyline.getAttribute('points').trim().split(' ').filter(Boolean); + const W = 120, H = 32; + // Parse existing points + let coords = pts.map(p => p.split(',').map(Number)); + // Extract just y-values (latencies are encoded in y relative to scale) + // Easier: maintain a data attr on the svg with the raw values + const svg = sparkEl.querySelector('svg'); + let vals = svg?.dataset.vals ? svg.dataset.vals.split(',').map(Number) : []; + vals.push(ping.latency_ms); + if (vals.length > 20) vals.shift(); + if (svg) svg.dataset.vals = vals.join(','); + // Redraw polyline in place + const max = Math.max(...vals, 1); + const min = Math.min(...vals, 0); + const range = max - min || 1; + const step = W / Math.max(vals.length - 1, 1); + const newPts = vals.map((v, i) => `${i * step},${H - ((v - min) / range) * (H - 4) - 2}`).join(' '); + polyline.setAttribute('points', newPts); } - } catch {} - _fetchingSparkline.delete(ping.monitor_id); + } });