From 4149a4753eb7e53092b8a811a078491bc83e7ed5 Mon Sep 17 00:00:00 2001 From: nate Date: Thu, 19 Mar 2026 15:22:33 +0400 Subject: [PATCH] feat: add tooltip to status history blips --- apps/web/src/views/detail.ejs | 105 ++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 18 deletions(-) diff --git a/apps/web/src/views/detail.ejs b/apps/web/src/views/detail.ejs index 481bd81..8595d8a 100644 --- a/apps/web/src/views/detail.ejs +++ b/apps/web/src/views/detail.ejs @@ -15,10 +15,12 @@ for (const p of pings.slice(0, 120).reverse()) { const rid = p.run_id || p.checked_at; if (!runMap[rid]) { - runMap[rid] = { run_id: rid, up: 0, down: 0, checked_at: p.checked_at, latency_ms: p.latency_ms }; + runMap[rid] = { run_id: rid, up: 0, down: 0, total: 0, checked_at: p.checked_at, regions: [] }; barRuns.push(runMap[rid]); } + runMap[rid].total++; if (p.up) runMap[rid].up++; else runMap[rid].down++; + runMap[rid].regions.push({ region: p.region || '', up: p.up, latency_ms: p.latency_ms }); } // Keep last 60 runs const barPings = barRuns.slice(-60); @@ -90,17 +92,19 @@

Status History

-
- <% if (barPings.length > 0) { %> - <% barPings.forEach(function(c) { - const color = c.down === 0 ? 'bg-green-500/70' : (c.up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70'); - const label = c.down === 0 ? 'Up' : (c.up === 0 ? 'Down' : 'Partial'); - %> -
- <% }) %> - <% } else { %> -
No data
- <% } %> +
+
+ <% if (barPings.length > 0) { %> + <% barPings.forEach(function(c) { + const color = c.down === 0 ? 'bg-green-500/70' : (c.up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70'); + %> +
">
+ <% }) %> + <% } else { %> +
No data
+ <% } %> +
+
@@ -462,6 +466,66 @@ renderChart(); window.addEventListener('resize', renderChart); + // ── Status bar hover tooltip ────────────────────────────────────── + const statusBar = document.getElementById('status-bar'); + const statusTooltip = document.getElementById('status-tooltip'); + + statusBar.addEventListener('mousemove', (e) => { + const seg = e.target.closest('[data-run]'); + if (!seg) { statusTooltip.classList.add('hidden'); return; } + + const up = parseInt(seg.dataset.up || '0'); + const down = parseInt(seg.dataset.down || '0'); + const total = up + down; + const uptimePct = total > 0 ? Math.round((up / total) * 100) : 0; + const time = seg.dataset.time ? new Date(seg.dataset.time) : null; + const timeStr = time ? time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : ''; + const dateStr = time ? time.toLocaleDateString([], { month: 'short', day: 'numeric' }) : ''; + + const statusLabel = down === 0 ? 'All Up' + : up === 0 ? 'All Down' + : 'Partial'; + + let html = `
${dateStr} ${timeStr}
`; + html += `
Status${statusLabel}
`; + html += `
Uptime${uptimePct}%
`; + + // Region breakdown + let regions = []; + try { regions = JSON.parse(seg.dataset.regions || '[]'); } catch {} + if (regions.length > 0) { + html += '
'; + for (const r of regions) { + const flag = {'eu-central':'🇩🇪','us-west':'🇺🇸'}[r.region] || '🌐'; + const rLabel = r.region || 'unknown'; + const status = r.up ? 'Up' : 'Down'; + const lat = r.latency_ms != null ? `${r.latency_ms}ms` : ''; + html += `
${flag} ${rLabel}${lat} ${status}
`; + } + html += '
'; + } + + const rid = seg.dataset.run; + if (rid) html += `
${rid}
`; + + statusTooltip.innerHTML = html; + statusTooltip.classList.remove('hidden'); + + // Position horizontally centered on the segment + const barRect = statusBar.getBoundingClientRect(); + const segRect = seg.getBoundingClientRect(); + const segCenter = segRect.left + segRect.width / 2 - barRect.left; + const tw = statusTooltip.offsetWidth; + let left = segCenter - tw / 2; + if (left < 0) left = 0; + if (left + tw > barRect.width) left = barRect.width - tw; + statusTooltip.style.left = left + 'px'; + }); + + statusBar.addEventListener('mouseleave', () => { + statusTooltip.classList.add('hidden'); + }); + // Running totals for incremental stat updates let _total = <%= pings.length %>, _up = <%= upPings.length %>; let _latSum = <%= latencies.reduce((a,b)=>a+b,0) %>, _latCount = <%= latencies.length %>; @@ -500,22 +564,27 @@ const rid = ping.run_id || ping.checked_at; let existing = bar.querySelector(`[data-run="${rid}"]`); if (existing) { - // Update existing run segment const up = parseInt(existing.dataset.up || '0') + (ping.up ? 1 : 0); const down = parseInt(existing.dataset.down || '0') + (ping.up ? 0 : 1); existing.dataset.up = up; existing.dataset.down = down; + existing.dataset.total = up + down; + existing.dataset.time = ping.checked_at; + const regions = JSON.parse(existing.dataset.regions || '[]'); + regions.push({ region: ping.region || '', up: ping.up, latency_ms: ping.latency_ms }); + existing.dataset.regions = JSON.stringify(regions); const color = down === 0 ? 'bg-green-500/70' : (up === 0 ? 'bg-red-500/70' : 'bg-orange-400/70'); - const label = down === 0 ? 'Up' : (up === 0 ? 'Down' : 'Partial'); - existing.className = `flex-1 rounded-sm ${color}`; - existing.title = `${new Date(ping.checked_at).toLocaleString()} — ${label}`; + existing.className = `flex-1 rounded-sm ${color} cursor-pointer`; } else { const seg = document.createElement('div'); - seg.className = `flex-1 rounded-sm ${ping.up ? 'bg-green-500/70' : 'bg-red-500/70'}`; + const color = ping.up ? 'bg-green-500/70' : 'bg-red-500/70'; + seg.className = `flex-1 rounded-sm ${color} cursor-pointer`; seg.dataset.run = rid; seg.dataset.up = ping.up ? '1' : '0'; seg.dataset.down = ping.up ? '0' : '1'; - seg.title = `${new Date(ping.checked_at).toLocaleString()} — ${ping.up ? 'Up' : 'Down'}`; + seg.dataset.total = '1'; + seg.dataset.time = ping.checked_at; + seg.dataset.regions = JSON.stringify([{ region: ping.region || '', up: ping.up, latency_ms: ping.latency_ms }]); bar.appendChild(seg); while (bar.children.length > 60) bar.removeChild(bar.firstChild); }