From 923f0349dcc49a6c22670a4f4613ac885f187aea Mon Sep 17 00:00:00 2001 From: M1 Date: Mon, 16 Mar 2026 17:10:12 +0400 Subject: [PATCH] feat: fully SSE-driven detail/home pages, kill polling intervals --- apps/web/src/views/detail.ejs | 59 ++++++++++++++++++++++++++++------- apps/web/src/views/home.ejs | 19 +++++++++-- 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs index 2abe85a..f29bdbd 100644 --- a/apps/web/src/views/detail.ejs +++ b/apps/web/src/views/detail.ejs @@ -221,6 +221,11 @@ const avgLatency = latencies.length ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : null; const uptime = results.length ? Math.round((upPings.length / results.length) * 100) : null; + // Seed running stats for SSE incremental updates + _totalPings = results.length; + _upPings = upPings.length; + _latencies = latencies.slice(-100); + document.getElementById('stat-status').innerHTML = lastPing ? (lastPing.up ? 'Up' : 'Down') : ''; @@ -353,32 +358,64 @@ }); load(); - setInterval(load, 60000); // fallback poll, less frequent now that SSE handles updates + // No interval poll — SSE handles all live updates + + // Track running stats in memory for incremental updates + let _totalPings = 0, _upPings = 0, _latencies = []; // SSE: live ping updates const id = window.location.pathname.split('/').pop(); watchMonitor(id, (ping) => { - // Update stat bar - document.getElementById('stat-last').innerHTML = timeAgo(ping.checked_at); - if (ping.latency_ms) document.getElementById('stat-latency').textContent = ping.latency_ms + 'ms'; + // ── Stats ─────────────────────────────────────────────────── + _totalPings++; + if (ping.up) _upPings++; + if (ping.latency_ms != null) { + _latencies.push(ping.latency_ms); + if (_latencies.length > 100) _latencies.shift(); + const avg = Math.round(_latencies.reduce((a,b)=>a+b,0) / _latencies.length); + document.getElementById('stat-latency').textContent = avg + 'ms'; + } - // Prepend new row to ping table + document.getElementById('stat-last').innerHTML = timeAgo(ping.checked_at); + document.getElementById('stat-status').innerHTML = ping.up + ? 'Up' + : 'Down'; + + if (_totalPings > 0) { + const pct = Math.round((_upPings / _totalPings) * 100); + document.getElementById('stat-uptime').textContent = pct + '%'; + } + + // ── Status history bar — prepend a segment ─────────────────── + const bar = document.getElementById('status-bar'); + if (bar) { + const seg = document.createElement('div'); + seg.className = `flex-1 ${ping.up ? 'bg-green-500/70' : 'bg-red-500/70'}`; + seg.title = `${new Date(ping.checked_at).toLocaleString()} — ${ping.up ? 'Up' : 'Down'}${ping.latency_ms ? ' ' + ping.latency_ms + 'ms' : ''}`; + bar.prepend(seg); + while (bar.children.length > 60) bar.removeChild(bar.lastChild); + } + + // ── Latency chart ───────────────────────────────────────────── + if (ping.latency_ms != null) { + renderLatencyChart([...(_latencies.map((ms, i) => ({ + latency_ms: ms, checked_at: new Date(Date.now() - (_latencies.length - i) * 10000).toISOString() + })))]); + } + + // ── Ping table ─────────────────────────────────────────────── const tbody = document.getElementById('pings-table'); if (tbody) { - const upBadge = ping.up - ? 'Up' - : 'Down'; const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-800/50'; tr.innerHTML = ` - ${upBadge} + ${ping.up ? 'Up' : 'Down'} ${ping.status_code ?? '—'} ${ping.latency_ms ? ping.latency_ms + 'ms' : '—'} ${timeAgo(ping.checked_at)} - ${ping.error ?? ''} + ${ping.error ? escapeHtml(ping.error) : ''} `; tbody.prepend(tr); - // Trim to keep table manageable while (tbody.children.length > 100) tbody.removeChild(tbody.lastChild); } }); diff --git a/apps/web/src/views/home.ejs b/apps/web/src/views/home.ejs index 3a3f6eb..80e1d79 100644 --- a/apps/web/src/views/home.ejs +++ b/apps/web/src/views/home.ejs @@ -92,7 +92,11 @@ } load(); - setInterval(load, 30000); + // No interval poll — SSE handles live updates. Only reload on tab focus if stale. + let lastLoad = Date.now(); + document.addEventListener('visibilitychange', () => { + if (!document.hidden && Date.now() - lastLoad > 60000) { load(); lastLoad = Date.now(); } + }); // SSE: subscribe to all monitors after load so cards update in real time const sseConnections = []; @@ -107,7 +111,18 @@ // Status dot const statusDot = card.querySelector('.status-dot'); - if (statusDot) statusDot.className = `status-dot w-2.5 h-2.5 rounded-full ${ping.up ? 'bg-green-500' : 'bg-red-500'}`; + if (statusDot) { + const wasUp = statusDot.classList.contains('bg-green-500'); + statusDot.className = `status-dot w-2.5 h-2.5 rounded-full ${ping.up ? 'bg-green-500' : 'bg-red-500'}`; + // If status changed, recount and update summary + if (wasUp !== ping.up) { + const dots = document.querySelectorAll('.status-dot'); + const upNow = [...dots].filter(d => d.classList.contains('bg-green-500')).length; + const downNow = [...dots].filter(d => d.classList.contains('bg-red-500')).length; + const summary = document.getElementById('summary'); + if (summary) summary.innerHTML = `${upNow} up · ${downNow} down · ${dots.length} total`; + } + } // Latency text if (ping.latency_ms) card.querySelector('.stat-latency').textContent = `${ping.latency_ms}ms`;