fix
This commit is contained in:
parent
102f78b523
commit
a7fef0a2b7
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) { %>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue