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
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 }> = {};
for (const r of rollupRows) {
if (!bucketsByMonitor[r.monitor_id]) bucketsByMonitor[r.monitor_id] = [];
bucketsByMonitor[r.monitor_id]!.push({
start: r.bucket_start instanceof Date ? r.bucket_start.toISOString() : String(r.bucket_start),
total: r.total,
up: r.up_count,
});
const startIso = r.bucket_start instanceof Date ? r.bucket_start.toISOString() : String(r.bucket_start);
if (!indexed[r.monitor_id]) indexed[r.monitor_id] = {};
indexed[r.monitor_id]![startIso] = { total: r.total, up: r.up_count, avg_latency: r.avg_latency ?? null };
if (r.avg_latency != null) {
const acc = latencyByMonitor[r.monitor_id] ?? { sum: 0, n: 0 };
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).
const latRows = await sql<any[]>`
SELECT monitor_id, region, bucket_start, avg_latency

View File

@ -24,15 +24,15 @@
return 'Unknown';
}
function statusColor(s) {
if (s === 'up') return '#10b981';
if (s === 'down') return '#ef4444';
return '#9ca3af';
if (s === 'up') return 'var(--bar-up)';
if (s === 'down') return 'var(--bar-down)';
return 'var(--muted)';
}
function bucketColor(b) {
if (b.total === 0) return '#374151';
if (b.up === b.total) return '#10b981';
if (b.up === 0) return '#ef4444';
return '#f59e0b';
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)';
}
// 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'
: overall === 'degraded' ? 'Some systems degraded'
: 'Major outage in progress';
const overallColor = overall === 'up' ? '#10b981'
: overall === 'degraded' ? '#f59e0b'
: '#ef4444';
const overallColor = overall === 'up' ? 'var(--bar-up)'
: overall === 'degraded' ? 'var(--bar-partial)'
: 'var(--bar-down)';
%><!DOCTYPE html>
<html lang="en" class="<%= themeClass %>">
<head>
@ -63,27 +63,36 @@
<link rel="manifest" href="/<%= page.slug %>/manifest.json">
<style>
:root {
--bg: #ffffff; --fg: #0f172a; --muted: #64748b; --card: #f8fafc;
--border: #e2e8f0; --accent: #0ea5e9; --green: #10b981; --red: #ef4444; --amber: #f59e0b;
--bg: #ffffff; --fg: #1e293b; --muted: #64748b; --card: #f8fafc;
--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; }
@media (prefers-color-scheme: dark) {
html:not(.light) {
--bg: #0a0a0a; --fg: #f1f5f9; --muted: #94a3b8; --card: #111827;
--border: #1f2937; --accent: #38bdf8;
--bg: #0a0a0a; --fg: #c2c8d0; --muted: #6b7280; --card: #131418;
--border: #1f2026; --accent: #3b9bd1;
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026;
--overall-fg: #f1f5f9;
}
}
html.dark {
--bg: #0a0a0a; --fg: #f1f5f9; --muted: #94a3b8; --card: #111827;
--border: #1f2937; --accent: #38bdf8;
--bg: #0a0a0a; --fg: #c2c8d0; --muted: #6b7280; --card: #131418;
--border: #1f2026; --accent: #3b9bd1;
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026;
--overall-fg: #f1f5f9;
}
* { 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; }
main { max-width: 880px; margin: 0 auto; padding: 3rem 1.5rem; }
h1 { font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem; }
.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 .dot { width: 12px; height: 12px; border-radius: 50%; background: white; }
.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: 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; }
.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; }
@ -93,8 +102,10 @@
.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); }
.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; }
.bar { flex: 1; min-width: 0; border-radius: 2px; }
.bars { display: flex; gap: 2px; height: 32px; margin-top: 0.75rem; align-items: stretch; }
.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; }
.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); }
@ -160,13 +171,23 @@
<span class="uptime-pct"><%= fmtPct(m.uptime_pct) %></span>
</div>
</div>
<% if (m.buckets && m.buckets.length > 0) { %>
<%
const buckets = m.buckets || [];
const windowLabel = page.default_window === '24h' ? 'Last 24 hours'
: page.default_window === '7d' ? 'Last 7 days'
: page.default_window === '30d' ? 'Last 30 days'
: 'Last 90 days';
const hasData = buckets.some(b => b.total > 0);
%>
<div class="bars" title="<%= statusLabel(m.current_state) %>">
<% m.buckets.forEach(function(b) { %>
<div class="bar" style="background: <%= bucketColor(b) %>;"></div>
<% 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) { %>
<div class="regions">
<% m.region_states.forEach(function(r) { %>