update rollup

This commit is contained in:
2026-04-09 01:34:41 +04:00
parent ae5a8f4597
commit 1732a9d055
4 changed files with 30 additions and 39 deletions
+12 -15
View File
@@ -5,13 +5,13 @@
import sql from "./db";
export type Window = "24h" | "7d" | "30d" | "90d";
export type BucketType = "hourly" | "daily" | "weekly";
export type BucketType = "hourly" | "daily";
const WINDOW_TO_BUCKET: Record<Window, { bucket: BucketType; count: number }> = {
"24h": { bucket: "hourly", count: 24 },
"7d": { bucket: "daily", count: 7 },
"30d": { bucket: "daily", count: 30 },
"90d": { bucket: "weekly", count: 13 },
"90d": { bucket: "daily", count: 90 }, // 90 daily bars; 90 rows per monitor, cached.
};
export interface StatusPageRow {
@@ -53,7 +53,7 @@ export interface MonitorRow {
region_states: Array<{ region: string; state: "up" | "down" | "unknown"; updated_at: string | null }>;
uptime_pct: number | null; // for the page's default_window
uptime: MultiWindowUptime; // 24h / 7d / 30d / 90d row
buckets: Array<{ start: string; total: number; up: number }>; // bar chart input
buckets: Array<{ start: string; total: number; up: number; avg_latency: number | null }>; // bar chart input
avg_latency: number | null;
latency_history: Array<{ region: string; latency_ms: number | null; ts: string }>;
}
@@ -126,8 +126,8 @@ export async function loadMultiWindowUptime(monitorIds: string[]): Promise<Recor
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '7 days'), 0) AS pct_7d,
(sum(up_count) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '30 days'))::float
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '30 days'), 0) AS pct_30d,
(sum(up_count) FILTER (WHERE bucket_type='weekly' AND bucket_start > now() - interval '90 days'))::float
/ NULLIF(sum(total) FILTER (WHERE bucket_type='weekly' AND bucket_start > now() - interval '90 days'), 0) AS pct_90d
(sum(up_count) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '90 days'))::float
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '90 days'), 0) AS pct_90d
FROM monitor_uptime_rollup
WHERE monitor_id = ANY(${ids}::text[])
GROUP BY 1
@@ -240,7 +240,7 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
// Step 3: uptime rollup buckets covering the requested window.
const { bucket, count } = WINDOW_TO_BUCKET[window];
const truncUnit = bucket === "hourly" ? "hour" : bucket === "daily" ? "day" : "week";
const truncUnit = bucket === "hourly" ? "hour" : "day";
const intervalLiteral = `${count} ${truncUnit}s`;
let rollupRows = await sql<any[]>`
SELECT monitor_id, bucket_start, sum(total)::int AS total, sum(up_count)::int AS up_count, avg(avg_latency)::real AS avg_latency
@@ -288,16 +288,11 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
// Generate the full sequence of expected bucket timestamps so empty bars
// render as "no data" instead of disappearing entirely. Truncate `now()` to
// the unit so the slot boundaries line up with what the rollup writes.
const bucketMs = bucket === "hourly" ? 3600_000 : bucket === "daily" ? 86_400_000 : 604_800_000;
const bucketMs = bucket === "hourly" ? 3600_000 : 86_400_000;
const truncate = (d: Date): Date => {
const t = new Date(d);
if (bucket === "hourly") { t.setUTCMinutes(0, 0, 0); }
else { t.setUTCHours(0, 0, 0, 0); }
if (bucket === "weekly") {
// ISO week starts Monday.
const day = (t.getUTCDay() + 6) % 7;
t.setUTCDate(t.getUTCDate() - day);
}
if (bucket === "hourly") t.setUTCMinutes(0, 0, 0);
else t.setUTCHours(0, 0, 0, 0);
return t;
};
const nowTrunc = truncate(new Date()).getTime();
@@ -310,7 +305,9 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
const slotMap = indexed[id] ?? {};
bucketsByMonitor[id] = slotIsos.map((iso) => {
const hit = slotMap[iso];
return hit ? { start: iso, total: hit.total, up: hit.up } : { start: iso, total: 0, up: 0 };
return hit
? { start: iso, total: hit.total, up: hit.up, avg_latency: hit.avg_latency != null ? Math.round(hit.avg_latency) : null }
: { start: iso, total: 0, up: 0, avg_latency: null };
});
}
+12 -15
View File
@@ -14,27 +14,19 @@
// ── 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;
var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000 : 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" };
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;
// 7d / 30d / 90d → daily buckets
return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
}
function uptimeBand(p) {
if (p == null) return "";
@@ -44,9 +36,11 @@
}
function showTooltipForBar(bar, evt) {
var total = parseInt(bar.getAttribute("data-total") || "0", 10);
var up = parseInt(bar.getAttribute("data-up") || "0", 10);
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) + "%");
@@ -55,6 +49,9 @@
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>';
}
+1 -1
View File
@@ -311,7 +311,7 @@
</div>
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
<% buckets.forEach(function(b) { %>
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"></div>
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"<% if (b.avg_latency != null) { %> data-latency="<%= b.avg_latency %>"<% } %>></div>
<% }); %>
</div>
<div class="bars-meta">