This commit is contained in:
nate 2026-04-08 16:03:53 +04:00
parent 102f78b523
commit a7fef0a2b7
2 changed files with 83 additions and 34 deletions

View File

@ -125,15 +125,14 @@ export async function loadMonitors(pageId: string, window: Window): Promise<Moni
GROUP BY monitor_id, bucket_start GROUP BY monitor_id, bucket_start
ORDER BY monitor_id, bucket_start ASC ORDER BY monitor_id, bucket_start ASC
`; `;
const bucketsByMonitor: Record<string, MonitorRow["buckets"]> = {}; // Index actual rollup data by (monitor_id, isoBucketStart) so we can fill in
// the missing slots below.
const indexed: Record<string, Record<string, { total: number; up: number; avg_latency: number | null }>> = {};
const latencyByMonitor: Record<string, { sum: number; n: number }> = {}; const latencyByMonitor: Record<string, { sum: number; n: number }> = {};
for (const r of rollupRows) { for (const r of rollupRows) {
if (!bucketsByMonitor[r.monitor_id]) bucketsByMonitor[r.monitor_id] = []; const startIso = r.bucket_start instanceof Date ? r.bucket_start.toISOString() : String(r.bucket_start);
bucketsByMonitor[r.monitor_id]!.push({ if (!indexed[r.monitor_id]) indexed[r.monitor_id] = {};
start: r.bucket_start instanceof Date ? r.bucket_start.toISOString() : String(r.bucket_start), indexed[r.monitor_id]![startIso] = { total: r.total, up: r.up_count, avg_latency: r.avg_latency ?? null };
total: r.total,
up: r.up_count,
});
if (r.avg_latency != null) { if (r.avg_latency != null) {
const acc = latencyByMonitor[r.monitor_id] ?? { sum: 0, n: 0 }; const acc = latencyByMonitor[r.monitor_id] ?? { sum: 0, n: 0 };
acc.sum += r.avg_latency * r.total; acc.sum += r.avg_latency * r.total;
@ -142,6 +141,35 @@ export async function loadMonitors(pageId: string, window: Window): Promise<Moni
} }
} }
// 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 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);
}
return t;
};
const nowTrunc = truncate(new Date()).getTime();
const slotIsos: string[] = [];
for (let i = count - 1; i >= 0; i--) {
slotIsos.push(new Date(nowTrunc - i * bucketMs).toISOString());
}
const bucketsByMonitor: Record<string, MonitorRow["buckets"]> = {};
for (const id of ids) {
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 };
});
}
// Step 4: tiny recent latency history for the sparkline (last 30 hourly buckets). // Step 4: tiny recent latency history for the sparkline (last 30 hourly buckets).
const latRows = await sql<any[]>` const latRows = await sql<any[]>`
SELECT monitor_id, region, bucket_start, avg_latency SELECT monitor_id, region, bucket_start, avg_latency

View File

@ -24,15 +24,15 @@
return 'Unknown'; return 'Unknown';
} }
function statusColor(s) { function statusColor(s) {
if (s === 'up') return '#10b981'; if (s === 'up') return 'var(--bar-up)';
if (s === 'down') return '#ef4444'; if (s === 'down') return 'var(--bar-down)';
return '#9ca3af'; return 'var(--muted)';
} }
function bucketColor(b) { function bucketColor(b) {
if (b.total === 0) return '#374151'; if (!b || b.total === 0) return 'var(--bar-empty)';
if (b.up === b.total) return '#10b981'; if (b.up === b.total) return 'var(--bar-up)';
if (b.up === 0) return '#ef4444'; if (b.up === 0) return 'var(--bar-down)';
return '#f59e0b'; return 'var(--bar-partial)';
} }
// Overall status: down if any monitor is down, degraded if any partial, else up. // Overall status: down if any monitor is down, degraded if any partial, else up.
@ -45,9 +45,9 @@
const overallText = overall === 'up' ? 'All systems operational' const overallText = overall === 'up' ? 'All systems operational'
: overall === 'degraded' ? 'Some systems degraded' : overall === 'degraded' ? 'Some systems degraded'
: 'Major outage in progress'; : 'Major outage in progress';
const overallColor = overall === 'up' ? '#10b981' const overallColor = overall === 'up' ? 'var(--bar-up)'
: overall === 'degraded' ? '#f59e0b' : overall === 'degraded' ? 'var(--bar-partial)'
: '#ef4444'; : 'var(--bar-down)';
%><!DOCTYPE html> %><!DOCTYPE html>
<html lang="en" class="<%= themeClass %>"> <html lang="en" class="<%= themeClass %>">
<head> <head>
@ -63,27 +63,36 @@
<link rel="manifest" href="/<%= page.slug %>/manifest.json"> <link rel="manifest" href="/<%= page.slug %>/manifest.json">
<style> <style>
:root { :root {
--bg: #ffffff; --fg: #0f172a; --muted: #64748b; --card: #f8fafc; --bg: #ffffff; --fg: #1e293b; --muted: #64748b; --card: #f8fafc;
--border: #e2e8f0; --accent: #0ea5e9; --green: #10b981; --red: #ef4444; --amber: #f59e0b; --border: #e2e8f0; --accent: #0284c7;
--bar-up: #10b981; --bar-down: #ef4444; --bar-partial: #f59e0b; --bar-empty: #e5e7eb;
--overall-fg: #ffffff;
} }
/* OLED-friendly dark mode: pure black background for power saving, but a
muted slate-300 foreground instead of white so it doesn't blow out on
self-emissive displays. */
html.dark, html:not(.light):not(.dark) { color-scheme: dark; } html.dark, html:not(.light):not(.dark) { color-scheme: dark; }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html:not(.light) { html:not(.light) {
--bg: #0a0a0a; --fg: #f1f5f9; --muted: #94a3b8; --card: #111827; --bg: #0a0a0a; --fg: #c2c8d0; --muted: #6b7280; --card: #131418;
--border: #1f2937; --accent: #38bdf8; --border: #1f2026; --accent: #3b9bd1;
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026;
--overall-fg: #f1f5f9;
} }
} }
html.dark { html.dark {
--bg: #0a0a0a; --fg: #f1f5f9; --muted: #94a3b8; --card: #111827; --bg: #0a0a0a; --fg: #c2c8d0; --muted: #6b7280; --card: #131418;
--border: #1f2937; --accent: #38bdf8; --border: #1f2026; --accent: #3b9bd1;
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026;
--overall-fg: #f1f5f9;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; line-height: 1.5; } body { margin: 0; background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; line-height: 1.5; }
main { max-width: 880px; margin: 0 auto; padding: 3rem 1.5rem; } main { max-width: 880px; margin: 0 auto; padding: 3rem 1.5rem; }
h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; } h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; }
.muted { color: var(--muted); font-size: 0.875rem; } .muted { color: var(--muted); font-size: 0.875rem; }
.overall { padding: 1.25rem 1.5rem; border-radius: 12px; color: white; font-weight: 600; font-size: 1.05rem; margin: 1.5rem 0 2rem; display: flex; align-items: center; gap: 0.75rem; } .overall { padding: 1.25rem 1.5rem; border-radius: 12px; color: var(--overall-fg); font-weight: 600; font-size: 1.05rem; margin: 1.5rem 0 2rem; display: flex; align-items: center; gap: 0.75rem; }
.overall .dot { width: 12px; height: 12px; border-radius: 50%; background: white; } .overall .dot { width: 12px; height: 12px; border-radius: 50%; background: var(--overall-fg); opacity: 0.9; }
.group-title { font-size: 0.85rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin: 2rem 0 0.75rem; } .group-title { font-size: 0.85rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; margin: 2rem 0 0.75rem; }
.monitors { display: flex; flex-direction: column; gap: 0.5rem; } .monitors { display: flex; flex-direction: column; gap: 0.5rem; }
.monitor { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem; } .monitor { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem; }
@ -93,8 +102,10 @@
.monitor-name .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .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); } .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); } .uptime-pct { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--fg); }
.bars { display: flex; gap: 2px; height: 32px; margin-top: 0.5rem; align-items: stretch; } .bars { display: flex; gap: 2px; height: 32px; margin-top: 0.75rem; align-items: stretch; }
.bar { flex: 1; min-width: 0; border-radius: 2px; } .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; }
.regions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; font-size: 0.75rem; } .regions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem; font-size: 0.75rem; }
.region { padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--border); } .region { padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--border); }
.region.up { color: var(--green); border-color: rgba(16,185,129,0.3); } .region.up { color: var(--green); border-color: rgba(16,185,129,0.3); }
@ -160,13 +171,23 @@
<span class="uptime-pct"><%= fmtPct(m.uptime_pct) %></span> <span class="uptime-pct"><%= fmtPct(m.uptime_pct) %></span>
</div> </div>
</div> </div>
<% if (m.buckets && m.buckets.length > 0) { %> <%
<div class="bars" title="<%= statusLabel(m.current_state) %>"> const buckets = m.buckets || [];
<% m.buckets.forEach(function(b) { %> const windowLabel = page.default_window === '24h' ? 'Last 24 hours'
<div class="bar" style="background: <%= bucketColor(b) %>;"></div> : page.default_window === '7d' ? 'Last 7 days'
<% }); %> : page.default_window === '30d' ? 'Last 30 days'
</div> : 'Last 90 days';
<% } %> const hasData = buckets.some(b => b.total > 0);
%>
<div class="bars" title="<%= statusLabel(m.current_state) %>">
<% buckets.forEach(function(b) { %>
<div class="bar" style="background: <%= bucketColor(b) %>;" title="<%= b.total > 0 ? (Math.round(100 * b.up / b.total) + '% up') : 'no data' %>"></div>
<% }); %>
</div>
<div class="bars-meta">
<span><%= windowLabel %></span>
<span><%= hasData ? fmtPct(m.uptime_pct) + ' uptime' : 'awaiting data' %></span>
</div>
<% if (m.region_states && m.region_states.length > 1) { %> <% if (m.region_states && m.region_states.length > 1) { %>
<div class="regions"> <div class="regions">
<% m.region_states.forEach(function(r) { %> <% m.region_states.forEach(function(r) { %>