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);
}
});