diff --git a/apps/api/src/jobs/rollup.ts b/apps/api/src/jobs/rollup.ts index 96ca677..9df5794 100644 --- a/apps/api/src/jobs/rollup.ts +++ b/apps/api/src/jobs/rollup.ts @@ -2,17 +2,16 @@ import sql from "../db"; // Aggregates raw pings into monitor_uptime_rollup so status pages and dashboard // widgets can compute uptime % over arbitrary windows without ever scanning the -// pings table at read time. Three resolutions: hourly, daily, weekly. +// pings table at read time. Two resolutions: hourly and daily. // // Each pass aggregates the *current* bucket only. The query is bounded by the // bucket size, not the table size, so it's cheap regardless of history depth. -type BucketType = "hourly" | "daily" | "weekly"; +type BucketType = "hourly" | "daily"; const BUCKET_TRUNC: Record = { hourly: "hour", daily: "day", - weekly: "week", }; async function rollupCurrent(bucket: BucketType): Promise { @@ -83,12 +82,11 @@ export async function startRollupJob() { // we throw so a broken rollup query trips the api process and shows in the // service logs immediately, instead of leaving the table mysteriously empty. try { - const [h, d, w] = await Promise.all([ + const [h, d] = await Promise.all([ backfillRecent("hourly", 48), backfillRecent("daily", 90), - backfillRecent("weekly", 26), ]); - console.log(`[rollup] backfilled rows: hourly=${h} daily=${d} weekly=${w}`); + console.log(`[rollup] backfilled rows: hourly=${h} daily=${d}`); } catch (e) { console.error("[rollup] backfill FAILED — rollup table will be empty until fixed:", e); } @@ -97,7 +95,7 @@ export async function startRollupJob() { // run rollupCurrent immediately so we have at least one row per type. This // covers the "fresh deploy with very recent pings only" case. try { - for (const b of ["hourly", "daily", "weekly"] as BucketType[]) { + for (const b of ["hourly", "daily"] as BucketType[]) { if (await rollupIsEmpty(b)) { console.log(`[rollup] ${b} still empty — forcing current-bucket aggregation`); await rollupCurrent(b); @@ -110,5 +108,4 @@ export async function startRollupJob() { // Periodic refreshes for the *current* bucket of each resolution. setInterval(() => { rollupCurrent("hourly").catch((e) => console.warn("[rollup] hourly failed:", e)); }, 5 * 60 * 1000); setInterval(() => { rollupCurrent("daily").catch((e) => console.warn("[rollup] daily failed:", e)); }, 30 * 60 * 1000); - setInterval(() => { rollupCurrent("weekly").catch((e) => console.warn("[rollup] weekly failed:", e)); }, 6 * 60 * 60 * 1000); } diff --git a/apps/status/src/data.ts b/apps/status/src/data.ts index e7f9dfc..d816f9b 100644 --- a/apps/status/src/data.ts +++ b/apps/status/src/data.ts @@ -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 = { "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 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` 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 }; }); } diff --git a/apps/status/src/static/expand.js b/apps/status/src/static/expand.js index 0d3c5ed..31e5f5d 100644 --- a/apps/status/src/static/expand.js +++ b/apps/status/src/static/expand.js @@ -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 += '
Checks' + total + "
"; html += '
Successful' + up + "
"; html += '
Uptime' + pctText + "
"; + if (lat != null) { + html += '
Avg ping' + lat + "ms
"; + } } else { html += '
No data
'; } diff --git a/apps/status/src/views/page.ejs b/apps/status/src/views/page.ejs index e55032b..766078a 100644 --- a/apps/status/src/views/page.ejs +++ b/apps/status/src/views/page.ejs @@ -311,7 +311,7 @@
<% buckets.forEach(function(b) { %> -
+
data-latency="<%= b.avg_latency %>"<% } %>>
<% }); %>