diff --git a/apps/status/src/static/expand.js b/apps/status/src/static/expand.js
index 31e5f5d..3c5c43f 100644
--- a/apps/status/src/static/expand.js
+++ b/apps/status/src/static/expand.js
@@ -1,218 +1,87 @@
// 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.
+// 1. Click any monitor row to collapse/expand its detail panel.
+// 2. Hover any heartbeat bar to see the bucket time range and uptime breakdown.
+// All monitor detail HTML is server-rendered; this script only toggles a CSS
+// class on click. Reads its config from window.PINGQL_PAGE.
(function () {
var cfg = window.PINGQL_PAGE || {};
- var slug = cfg.slug;
var defaultWindow = cfg.default_window || "24h";
- var showResponseTime = !!cfg.show_response_time;
- if (!slug) return;
+
+ // ── Click to expand/collapse ──────────────────────────────────────────
+ var rows = document.querySelectorAll(".monitor .monitor-row");
+ for (var r = 0; r < rows.length; r++) {
+ (function (row) {
+ var card = row.parentElement;
+ row.addEventListener("click", function () {
+ var isOpen = card.classList.toggle("expanded-state");
+ row.setAttribute("aria-expanded", isOpen ? "true" : "false");
+ });
+ })(rows[r]);
+ }
// ── Bar tooltips ──────────────────────────────────────────────────────
var tooltip = document.getElementById("bar-tooltip");
- if (tooltip) {
- var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000 : 86400 * 1000;
- function fmtBucketRange(startIso) {
- var start = new Date(startIso);
- var end = new Date(start.getTime() + bucketSpanMs);
- 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);
- }
- // 7d / 30d / 90d → daily buckets
- return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
- }
- function uptimeBand(p) {
- if (p == null) return "";
- if (p >= 99.9) return "good";
- if (p >= 99.0) return "warn";
- return "bad";
- }
+ if (!tooltip) return;
- 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");
- var latRaw = bar.getAttribute("data-latency");
- var lat = latRaw == null ? null : parseInt(latRaw, 10);
- 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 + "
";
- if (lat != null) {
- html += '
Avg ping' + lat + "ms
";
- }
- } 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";
+ var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000 : 86400 * 1000;
+ function fmtBucketRange(startIso) {
+ var start = new Date(startIso);
+ var end = new Date(start.getTime() + bucketSpanMs);
+ 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);
}
- 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, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
- }
- function bucketColor(b) {
- if (!b || b.total === 0) return "var(--bar-empty)";
- if (b.up === b.total) return "var(--bar-up)";
- if (b.up === 0) return "var(--bar-down)";
- return "var(--bar-partial)";
+ return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
}
function uptimeBand(p) {
- if (p == null) return "empty";
+ if (p == null) return "";
if (p >= 99.9) return "good";
if (p >= 99.0) return "warn";
return "bad";
}
- function fmtUptime(p) {
- if (p == null) return "—";
- if (p === 100) return "100" + "%";
- return p.toFixed(2) + "%";
- }
- function windowLabel(w) {
- if (w === "24h") return "Last 24 hours";
- if (w === "7d") return "Last 7 days";
- if (w === "30d") return "Last 30 days";
- return "Last 90 days";
- }
- function fmtTs(iso) {
- var d = new Date(iso);
- return d.toLocaleString(undefined, {
- month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
- });
- }
- function renderBars(buckets) {
- var html = "";
- for (var i = 0; i < buckets.length; i++) {
- var b = buckets[i];
- var pct = b.total > 0 ? Math.round(100 * b.up / b.total) + "% up" : "no data";
- html += '';
- }
- return html;
- }
-
- function renderIncident(i) {
- var klass = "incident " + i.severity + (i.resolved_at ? " resolved" : "");
- var html = '
';
- html += '
' + escapeHtml(i.title) + "
";
- html += '
' + i.status + "";
- html += "Started " + fmtTs(i.started_at);
- if (i.resolved_at) html += " · Resolved " + fmtTs(i.resolved_at);
- html += "
";
- if (i.updates && i.updates.length > 0) {
- html += '
';
- for (var k = 0; k < i.updates.length; k++) {
- var u = i.updates[k];
- html += '
';
- html += '
' + u.status + "" + fmtTs(u.created_at) + "
";
- html += '
' + u.body_html + "
";
- html += "
";
+ function showTooltipForBar(bar) {
+ var total = parseInt(bar.getAttribute("data-total") || "0", 10);
+ var up = parseInt(bar.getAttribute("data-up") || "0", 10);
+ var start = bar.getAttribute("data-start");
+ var latRaw = bar.getAttribute("data-latency");
+ var lat = latRaw == null ? null : parseInt(latRaw, 10);
+ 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 + "
";
+ if (lat != null) {
+ html += '
Avg ping' + lat + "ms
";
}
- html += "
";
+ } else {
+ html += '
No data—
';
}
- html += "
";
- return html;
+ tooltip.innerHTML = html;
+ tooltip.style.display = "block";
+ var rect = bar.getBoundingClientRect();
+ tooltip.style.left = (rect.left + rect.width / 2) + "px";
+ tooltip.style.top = rect.top + "px";
+ }
+ function hideTooltip() {
+ tooltip.style.display = "none";
}
- function renderDetail(payload) {
- var m = payload.monitor;
- var u = m.uptime || { d24: null, d7: null, d30: null, d90: null };
- var buckets = m.buckets || [];
- var hasData = false;
- for (var i = 0; i < buckets.length; i++) if (buckets[i].total > 0) { hasData = true; break; }
-
- var barsHtml = renderBars(buckets);
-
- var latencyHtml = "";
- if (showResponseTime && m.avg_latency != null) {
- latencyHtml = "" + m.avg_latency + "ms · ";
+ 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);
}
-
- var incidentsHtml = "";
- if (payload.incidents && payload.incidents.length > 0) {
- for (var x = 0; x < payload.incidents.length; x++) {
- incidentsHtml += renderIncident(payload.incidents[x]);
- }
- }
-
- var html = "";
- html += '