pingql/apps/status/src/static/expand.js

94 lines
4.2 KiB
JavaScript

// Public status page client JS:
// 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 barFrequency = cfg.bar_frequency || "daily";
// ── 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) return;
var bucketSpanMs = (barFrequency === "hourly") ? 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 (barFrequency === "hourly") {
return start.toLocaleDateString(undefined, dateOpts) + ", " +
start.toLocaleTimeString(undefined, timeOpts) + " - " +
end.toLocaleTimeString(undefined, timeOpts);
}
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";
}
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;
// Full precision pct so the formatter can decide. Anything below 100% gets
// 2 truncated (not rounded) decimals - same rule as the page-level uptime
// numbers, so a bucket with one failed check never displays as "100%".
var pct = total > 0 ? (100 * up / total) : null;
var pctText;
if (pct == null) pctText = "-";
else if (pct >= 100) pctText = "100%";
else pctText = (Math.floor(pct * 100) / 100).toFixed(2) + "%";
var html = '<div class="head">' + fmtBucketRange(start) + "</div>";
if (total > 0) {
html += '<div class="row"><span>Checks</span><span>' + total + "</span></div>";
html += '<div class="row"><span>Successful</span><span>' + up + "</span></div>";
html += '<div class="row"><span>Uptime</span><span class="pct ' + uptimeBand(pct) + '">' + pctText + "</span></div>";
if (lat != null) {
html += '<div class="row"><span>Avg ping</span><span>' + lat + "ms</span></div>";
}
} else {
html += '<div class="row"><span>No data</span><span>-</span></div>';
}
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";
}
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);
}
});
document.addEventListener("mouseout", function (e) {
var bar = e.target && e.target.closest && e.target.closest(".bar");
if (bar) hideTooltip();
});
window.addEventListener("scroll", hideTooltip, { passive: true });
})();