From 5071e340c71238a9ef6336239c29c1bb65ab1439 Mon Sep 17 00:00:00 2001 From: M1 Date: Mon, 16 Mar 2026 21:21:56 +0400 Subject: [PATCH] fix: SSE-driven chart/sparkline refresh, debounced server-side partials --- apps/web/src/routes/dashboard.ts | 21 +++++++++++++++++++++ apps/web/src/views/detail.ejs | 25 +++++++++++++++---------- apps/web/src/views/home.ejs | 19 ++++++++++++++++++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/apps/web/src/routes/dashboard.ts b/apps/web/src/routes/dashboard.ts index a6e4123..e66fcbe 100644 --- a/apps/web/src/routes/dashboard.ts +++ b/apps/web/src/routes/dashboard.ts @@ -196,5 +196,26 @@ export const dashboard = new Elysia() }); }) + // Sparkline partial — returns just the SVG for one monitor + .get("/dashboard/monitors/:id/sparkline", async ({ cookie, headers, params }) => { + const accountId = await getAccountId(cookie, headers); + if (!accountId) return new Response("Unauthorized", { status: 401 }); + + const [monitor] = await sql` + SELECT id FROM monitors WHERE id = ${params.id} AND account_id = ${accountId} + `; + if (!monitor) return new Response("Not found", { status: 404 }); + + const pings = await sql` + SELECT latency_ms FROM pings + WHERE monitor_id = ${params.id} AND latency_ms IS NOT NULL + ORDER BY checked_at DESC LIMIT 20 + `; + const latencies = pings.map((p: any) => p.latency_ms).reverse(); + return new Response(sparklineSSR(latencies), { + headers: { "content-type": "text/html; charset=utf-8" }, + }); + }) + // Docs .get("/docs", () => Bun.file(`${dashDir}/docs.html`)); diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs index 1763f8a..d1c1b3e 100644 --- a/apps/web/src/views/detail.ejs +++ b/apps/web/src/views/detail.ejs @@ -304,6 +304,9 @@ while (bar.children.length > 60) bar.removeChild(bar.lastChild); } + // Refresh latency chart (debounced) + scheduleChartRefresh(); + // Ping table — prepend plain HTML row const tbody = document.getElementById('pings-table'); if (tbody) { @@ -321,16 +324,18 @@ } }); - // Poll every 30s to refresh the latency chart from server - setInterval(async () => { - try { - const res = await fetch(`/dashboard/monitors/${monitorId}/chart`, { credentials: 'same-origin' }); - if (res.ok) { - const html = await res.text(); - document.getElementById('latency-chart').innerHTML = html; - } - } catch {} - }, 30000); + // Refresh latency chart from server (debounced — at most once per 5s) + let _chartRefreshTimer = null; + function scheduleChartRefresh() { + if (_chartRefreshTimer) return; + _chartRefreshTimer = setTimeout(async () => { + _chartRefreshTimer = null; + try { + const res = await fetch(`/dashboard/monitors/${monitorId}/chart`, { credentials: 'same-origin' }); + if (res.ok) document.getElementById('latency-chart').innerHTML = await res.text(); + } catch {} + }, 5000); + } <%~ include('./partials/foot') %> diff --git a/apps/web/src/views/home.ejs b/apps/web/src/views/home.ejs index 4f6491c..f4296ae 100644 --- a/apps/web/src/views/home.ejs +++ b/apps/web/src/views/home.ejs @@ -75,7 +75,20 @@ } catch {} }, 30000); - // SSE: subscribe to all monitors for live updates (minimal DOM patches only) + // Sparkline refresh — debounced per monitor (at most once per 5s) + const _sparklineTimers = {}; + function scheduleSparklineRefresh(mid, sparkEl) { + if (_sparklineTimers[mid]) return; + _sparklineTimers[mid] = setTimeout(async () => { + delete _sparklineTimers[mid]; + try { + const res = await fetch(`/dashboard/monitors/${mid}/sparkline`, { credentials: 'same-origin' }); + if (res.ok) sparkEl.innerHTML = await res.text(); + } catch {} + }, 5000); + } + + // SSE: subscribe to all monitors for live updates const sseConnections = []; function subscribeAll() { const monitorCards = document.querySelectorAll('[data-monitor-id]'); @@ -101,6 +114,10 @@ // Timestamp card.querySelector('.stat-last').innerHTML = timeAgo(ping.checked_at); + + // Sparkline — debounced fetch from server + const sparkEl = card.querySelector('.stat-sparkline'); + if (sparkEl) scheduleSparklineRefresh(mid, sparkEl); }); if (es) sseConnections.push(es); });