From 0e009dc6846077da5c8ef68ffa7cea43e29532a1 Mon Sep 17 00:00:00 2001 From: nate Date: Thu, 9 Apr 2026 01:02:15 +0400 Subject: [PATCH] feat: add tooltip to status page --- apps/status/src/static/expand.js | 82 +++++++++++++++++++++++++++++++- apps/status/src/views/page.ejs | 17 +++++-- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/apps/status/src/static/expand.js b/apps/status/src/static/expand.js index 2d98964..0d3c5ed 100644 --- a/apps/status/src/static/expand.js +++ b/apps/status/src/static/expand.js @@ -1,5 +1,8 @@ -// Click-to-expand for compact display mode on the public status page. -// Loaded by status pages that have at least one monitor in compact mode. +// Public status page client JS: +// 1. Bar tooltips (always active) — hover any heartbeat bar to see the +// bucket time range and uptime breakdown. +// 2. Click-to-expand for compact display mode (only fires when there are +// compact monitors on the page). // Reads its config from window.PINGQL_PAGE. (function () { var cfg = window.PINGQL_PAGE || {}; @@ -8,6 +11,81 @@ var showResponseTime = !!cfg.show_response_time; if (!slug) return; + // ── Bar tooltips ────────────────────────────────────────────────────── + var tooltip = document.getElementById("bar-tooltip"); + if (tooltip) { + var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000 + : (defaultWindow === "7d") ? 86400 * 1000 + : (defaultWindow === "30d") ? 86400 * 1000 + : 7 * 86400 * 1000; + function fmtBucketRange(startIso) { + var start = new Date(startIso); + var end = new Date(start.getTime() + bucketSpanMs); + var sameDay = start.toDateString() === new Date(end.getTime() - 1).toDateString(); + var dateOpts = { month: "short", day: "numeric" }; + var timeOpts = { hour: "2-digit", minute: "2-digit" }; + if (defaultWindow === "24h") { + return start.toLocaleDateString(undefined, dateOpts) + ", " + + start.toLocaleTimeString(undefined, timeOpts) + " — " + + end.toLocaleTimeString(undefined, timeOpts); + } + if (defaultWindow === "7d" || defaultWindow === "30d") { + return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }); + } + // 90d → weekly buckets + var endLabel = new Date(end.getTime() - 1).toLocaleDateString(undefined, dateOpts); + return "Week of " + start.toLocaleDateString(undefined, dateOpts) + " — " + endLabel; + } + function uptimeBand(p) { + if (p == null) return ""; + if (p >= 99.9) return "good"; + if (p >= 99.0) return "warn"; + return "bad"; + } + + function showTooltipForBar(bar, evt) { + var total = parseInt(bar.getAttribute("data-total") || "0", 10); + var up = parseInt(bar.getAttribute("data-up") || "0", 10); + var start = bar.getAttribute("data-start"); + if (!start) return; + var pct = total > 0 ? Math.round(1000 * up / total) / 10 : null; + var pctText = pct == null ? "—" : (pct === 100 ? "100%" : pct.toFixed(1) + "%"); + var html = '
' + fmtBucketRange(start) + "
"; + if (total > 0) { + html += '
Checks' + total + "
"; + html += '
Successful' + up + "
"; + html += '
Uptime' + pctText + "
"; + } else { + html += '
No data
'; + } + tooltip.innerHTML = html; + tooltip.style.display = "block"; + var rect = bar.getBoundingClientRect(); + // Position at top-center of the bar. + tooltip.style.left = (rect.left + rect.width / 2) + "px"; + tooltip.style.top = rect.top + "px"; + } + function hideTooltip() { + tooltip.style.display = "none"; + } + + // Delegate from each .bars container so dynamically-rendered bars + // (compact mode click-to-expand) work too. + document.addEventListener("mouseover", function (e) { + var bar = e.target && e.target.closest && e.target.closest(".bar"); + if (bar && bar.parentElement && bar.parentElement.classList.contains("bars")) { + showTooltipForBar(bar, e); + } + }); + document.addEventListener("mouseout", function (e) { + var bar = e.target && e.target.closest && e.target.closest(".bar"); + if (bar) hideTooltip(); + }); + // Hide on scroll so the tooltip doesn't lag behind the bar. + window.addEventListener("scroll", hideTooltip, { passive: true }); + } + // ── End bar tooltips ────────────────────────────────────────────────── + function escapeHtml(s) { return String(s) .replace(/&/g, "&") diff --git a/apps/status/src/views/page.ejs b/apps/status/src/views/page.ejs index 2f3840e..7fabaa9 100644 --- a/apps/status/src/views/page.ejs +++ b/apps/status/src/views/page.ejs @@ -117,10 +117,18 @@ .monitor-name .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .monitor-meta { display: flex; gap: 1rem; align-items: center; font-size: 0.85rem; color: var(--muted); } .uptime-pct { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--fg); } - .bars { display: flex; gap: 0.1rem; height: 32px; margin-top: 0.75rem; align-items: stretch; } + .bars { display: flex; gap: 0.1rem; height: 32px; margin-top: 0.75rem; align-items: stretch; position: relative; } .bar { flex: 1; min-width: 0; border-radius: 2px; transition: opacity 0.15s; } .bar:hover { opacity: 0.8; } .bars-meta { display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--muted); margin-top: 0.4rem; } + /* Tooltip shown when hovering a bar. Single floating element per page, positioned by JS. */ + #bar-tooltip { position: fixed; pointer-events: none; background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.75rem; font-size: 0.75rem; color: var(--fg); box-shadow: 0 8px 24px rgba(0,0,0,0.3); z-index: 100; min-width: 160px; transform: translate(-50%, -100%); margin-top: -8px; display: none; } + #bar-tooltip .head { color: var(--muted); margin-bottom: 0.25rem; font-size: 0.7rem; } + #bar-tooltip .row { display: flex; justify-content: space-between; gap: 1rem; } + #bar-tooltip .row span:last-child { color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; } + #bar-tooltip .pct.good { color: var(--bar-up); } + #bar-tooltip .pct.warn { color: var(--bar-partial); } + #bar-tooltip .pct.bad { color: var(--bar-down); } /* Multi-window uptime row: four labelled cells side by side. */ .uptime-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.5rem; margin-top: 0.75rem; } .uptime-cell { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem 0.65rem; } @@ -283,9 +291,9 @@ <%= fmtUptime(u[page.default_window === '24h' ? 'd24' : page.default_window === '7d' ? 'd7' : page.default_window === '30d' ? 'd30' : 'd90']) %> -
+
<% buckets.forEach(function(b) { %> -
+
<% }); %>
@@ -339,10 +347,9 @@ - <% if (anyCompact) { %> + - <% } %> <% if (page.auto_refresh_s > 0) { %>