From 688245b0c26aeca5fca1dc3bf5b0fcbffe6afb5e Mon Sep 17 00:00:00 2001 From: nate Date: Wed, 18 Mar 2026 19:31:41 +0400 Subject: [PATCH] fix: match client-side sparkline behavior to SSR region-aware rendering --- apps/web/src/utils/sparkline.ts | 6 +-- apps/web/src/views/home.ejs | 75 +++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/apps/web/src/utils/sparkline.ts b/apps/web/src/utils/sparkline.ts index bec8386..17e72ea 100644 --- a/apps/web/src/utils/sparkline.ts +++ b/apps/web/src/utils/sparkline.ts @@ -1,4 +1,4 @@ -export function sparkline(values: number[], width = 120, height = 32, color = '#60a5fa'): string { +export function sparkline(values: number[], width = 120, height = 32, color = '#60a5fa', region = '__none__'): string { if (!values.length) return ''; const max = Math.max(...values, 1); const min = Math.min(...values, 0); @@ -9,7 +9,7 @@ export function sparkline(values: number[], width = 120, height = 32, color = '# const y = height - ((v - min) / range) * (height - 4) - 2; return `${x},${y}`; }).join(' '); - return ``; + return ``; } // Given pings with region+latency, pick the region with the lowest avg latency @@ -42,5 +42,5 @@ export function sparklineFromPings(pings: Array<{latency_ms?: number|null, regio } const color = COLORS[bestRegion] || '#60a5fa'; - return sparkline(byRegion[bestRegion], width, height, color); + return sparkline(byRegion[bestRegion], width, height, color, bestRegion); } diff --git a/apps/web/src/views/home.ejs b/apps/web/src/views/home.ejs index a73842e..bc549bd 100644 --- a/apps/web/src/views/home.ejs +++ b/apps/web/src/views/home.ejs @@ -76,6 +76,50 @@ } catch {} }, 30000); + const REGION_COLORS = { + 'eu-central': '#3b82f6', + 'us-east': '#10b981', + 'us-west': '#f59e0b', + 'ap-southeast': '#a78bfa', + }; + + // Per-monitor, per-region latency tracking for sparkline (mirrors SSR sparklineFromPings) + const sparkData = {}; + // Seed from SSR data-vals and data-region + document.querySelectorAll('[data-monitor-id]').forEach(card => { + const mid = card.dataset.monitorId; + const svg = card.querySelector('.stat-sparkline svg'); + if (!svg) return; + const region = svg.dataset.region || '__none__'; + const vals = svg.dataset.vals ? svg.dataset.vals.split(',').map(Number) : []; + sparkData[mid] = {}; + sparkData[mid][region] = vals; + }); + + function redrawSparkline(card, monitorId) { + const regions = sparkData[monitorId] || {}; + // Pick region with lowest average latency (same as SSR) + let bestRegion = '__none__', bestAvg = Infinity; + for (const [region, vals] of Object.entries(regions)) { + if (!vals.length) continue; + const avg = vals.reduce((a, b) => a + b, 0) / vals.length; + if (avg < bestAvg) { bestAvg = avg; bestRegion = region; } + } + const vals = regions[bestRegion]; + if (!vals || !vals.length) return; + const color = REGION_COLORS[bestRegion] || '#60a5fa'; + const W = 120, H = 32; + 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 points = vals.map((v, i) => `${i * step},${H - ((v - min) / range) * (H - 4) - 2}`).join(' '); + const sparkEl = card.querySelector('.stat-sparkline'); + if (sparkEl) { + sparkEl.innerHTML = ``; + } + } + // SSE: on each ping, update text fields and redraw sparkline in place watchAccount((ping) => { const card = document.querySelector(`[data-monitor-id="${ping.monitor_id}"]`); @@ -89,30 +133,15 @@ if (ping.latency_ms != null) card.querySelector('.stat-latency').textContent = ping.latency_ms + 'ms'; card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at); - // Sparkline — append point to existing polyline, drop oldest, no refetch + // Update sparkline data per region, then redraw picking best region 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); - } + const mid = ping.monitor_id; + const region = ping.region || '__none__'; + if (!sparkData[mid]) sparkData[mid] = {}; + if (!sparkData[mid][region]) sparkData[mid][region] = []; + sparkData[mid][region].push(ping.latency_ms); + if (sparkData[mid][region].length > 20) sparkData[mid][region].shift(); + redrawSparkline(card, mid); } });