fix uptime %
This commit is contained in:
parent
91ca996e74
commit
2db6c3402c
|
|
@ -44,7 +44,10 @@ export interface MonitorRow {
|
|||
group_id: string | null;
|
||||
position: number;
|
||||
display_mode: "compact" | "expanded"; // resolved (per-monitor override → page default → 'expanded')
|
||||
current_state: "up" | "down" | "unknown";
|
||||
// 'paused' means the monitor was disabled in the dashboard — the runner has
|
||||
// stopped checking it, and the public page should treat it as planned
|
||||
// maintenance rather than an outage.
|
||||
current_state: "up" | "down" | "unknown" | "paused";
|
||||
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
|
||||
|
|
@ -103,12 +106,17 @@ export async function loadMonitors(
|
|||
barFrequency: BucketType = "daily",
|
||||
barCount: number = 90,
|
||||
): Promise<MonitorRow[]> {
|
||||
// Step 1: page → monitors with display overrides + group + position.
|
||||
// Step 1: page → monitors with display overrides + group + position. Pull
|
||||
// m.enabled too so we can render disabled monitors as "Maintenance" on the
|
||||
// public page (the runner stops checking them when disabled, so their
|
||||
// region_states would otherwise drift to a stale "up" — visitors should
|
||||
// see this as planned downtime, not phantom uptime).
|
||||
const monitorRows = await sql<any[]>`
|
||||
SELECT
|
||||
spm.monitor_id AS id,
|
||||
COALESCE(spm.display_name, m.name) AS display_name,
|
||||
m.url,
|
||||
m.enabled AS enabled,
|
||||
spm.group_id,
|
||||
spm.position,
|
||||
spm.display_mode AS spm_display_mode
|
||||
|
|
@ -356,9 +364,13 @@ export async function loadMonitors(
|
|||
}
|
||||
|
||||
// Multi-window uptime is a straight read from the windowTotals accumulator.
|
||||
// We deliberately do NOT round to 2 decimals here — the formatter on the
|
||||
// public page truncates (not rounds) to 2 decimals so a value like 99.9999%
|
||||
// doesn't visually round up to "100.00%". Pre-rounding here would erase that
|
||||
// information before the formatter ever sees it.
|
||||
const multiWindow: Record<string, MultiWindowUptime> = {};
|
||||
const toPct = (up: number, total: number): number | null =>
|
||||
total > 0 ? +(100 * up / total).toFixed(2) : null;
|
||||
total > 0 ? (100 * up / total) : null;
|
||||
for (const id of ids) {
|
||||
const wt = windowTotals[id];
|
||||
multiWindow[id] = wt
|
||||
|
|
@ -376,12 +388,18 @@ export async function loadMonitors(
|
|||
const anyUp = region_states.some((s) => s.state === "up");
|
||||
current_state = anyDown ? "down" : anyUp ? "up" : "unknown";
|
||||
}
|
||||
// A disabled monitor is in operator-declared maintenance — runner has
|
||||
// stopped checking it. Override whatever the last region state was so the
|
||||
// public page reads "Maintenance" instead of a stale "Operational".
|
||||
if (m.enabled === false) current_state = "paused";
|
||||
const buckets = bucketsByMonitor[m.id] ?? [];
|
||||
let uptime_pct: number | null = null;
|
||||
if (buckets.length > 0) {
|
||||
const tot = buckets.reduce((a, b) => a + b.total, 0);
|
||||
const upT = buckets.reduce((a, b) => a + b.up, 0);
|
||||
uptime_pct = tot > 0 ? +(100 * upT / tot).toFixed(2) : null;
|
||||
// Full precision — the display layer truncates (not rounds) to 2 decimals
|
||||
// so any downtime, however small, never visually rounds up to 100%.
|
||||
uptime_pct = tot > 0 ? (100 * upT / tot) : null;
|
||||
}
|
||||
const avg_latency = fastestLatency[m.id] ?? null;
|
||||
// Per-monitor display mode override → page default → 'expanded'.
|
||||
|
|
|
|||
|
|
@ -50,8 +50,14 @@
|
|||
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) + "%");
|
||||
// Full precision pct so the formatter can decide. Anything below 100% gets
|
||||
// 2 truncated (not rounded) decimals — same rule as the page-level uptime
|
||||
// numbers, so a bucket with one failed check never displays as "100%".
|
||||
var pct = total > 0 ? (100 * up / total) : null;
|
||||
var pctText;
|
||||
if (pct == null) pctText = "—";
|
||||
else if (pct >= 100) pctText = "100%";
|
||||
else pctText = (Math.floor(pct * 100) / 100).toFixed(2) + "%";
|
||||
var html = '<div class="head">' + fmtBucketRange(start) + "</div>";
|
||||
if (total > 0) {
|
||||
html += '<div class="row"><span>Checks</span><span>' + total + "</span></div>";
|
||||
|
|
|
|||
|
|
@ -16,16 +16,23 @@
|
|||
|
||||
function fmtPct(p) {
|
||||
if (p == null) return '—';
|
||||
return p === 100 ? '100%' : p.toFixed(2) + '%';
|
||||
// Only show "100%" when the value is *exactly* 100. Anything below — even
|
||||
// 99.9999% — must show 2 decimals so visitors can see there was downtime.
|
||||
// Truncate (floor) rather than round, otherwise 99.9999 would render as
|
||||
// "100.00" and silently swallow the downtime.
|
||||
if (p >= 100) return '100%';
|
||||
return (Math.floor(p * 100) / 100).toFixed(2) + '%';
|
||||
}
|
||||
function statusLabel(s) {
|
||||
if (s === 'up') return 'Operational';
|
||||
if (s === 'down') return 'Down';
|
||||
if (s === 'paused') return 'Under maintenance';
|
||||
return 'Unknown';
|
||||
}
|
||||
function statusColor(s) {
|
||||
if (s === 'up') return 'var(--bar-up)';
|
||||
if (s === 'down') return 'var(--bar-down)';
|
||||
if (s === 'paused') return 'var(--bar-paused)';
|
||||
return 'var(--muted)';
|
||||
}
|
||||
function bucketColor(b) {
|
||||
|
|
@ -42,20 +49,30 @@
|
|||
}
|
||||
function fmtUptime(p) {
|
||||
if (p == null) return '—';
|
||||
if (p === 100) return '100%';
|
||||
return p.toFixed(2) + '%';
|
||||
// Only "100%" if the monitor was up for every single check in the window.
|
||||
// Truncate (not round) below that so 99.9999% never displays as "100.00".
|
||||
if (p >= 100) return '100%';
|
||||
return (Math.floor(p * 100) / 100).toFixed(2) + '%';
|
||||
}
|
||||
|
||||
// Overall status: down if any monitor is down, degraded if any partial, else up.
|
||||
// Paused monitors are operator-declared maintenance — they don't count
|
||||
// toward "down" or "degraded", but we surface a small note in the banner
|
||||
// when at least one is in maintenance so visitors aren't confused.
|
||||
let overall = 'up';
|
||||
let paused_count = 0;
|
||||
for (const m of monitors) {
|
||||
if (m.current_state === 'paused') { paused_count++; continue; }
|
||||
const partial = m.region_states.some(r => r.state === 'down') && m.region_states.some(r => r.state === 'up');
|
||||
if (m.current_state === 'down') { overall = 'down'; break; }
|
||||
if (partial && overall !== 'down') overall = 'degraded';
|
||||
}
|
||||
const overallText = overall === 'up' ? 'All systems operational'
|
||||
let overallText = overall === 'up' ? 'All systems operational'
|
||||
: overall === 'degraded' ? 'Some systems degraded'
|
||||
: 'Major outage in progress';
|
||||
if (paused_count > 0) {
|
||||
overallText += ' — ' + paused_count + (paused_count === 1 ? ' service' : ' services') + ' under maintenance';
|
||||
}
|
||||
const overallColor = overall === 'up' ? 'var(--bar-up)'
|
||||
: overall === 'degraded' ? 'var(--bar-partial)'
|
||||
: 'var(--bar-down)';
|
||||
|
|
@ -77,6 +94,7 @@
|
|||
--bg: #ffffff; --fg: #1e293b; --muted: #64748b; --card: #f8fafc;
|
||||
--border: #e2e8f0; --accent: #0284c7;
|
||||
--bar-up: #10b981; --bar-down: #ef4444; --bar-partial: #f59e0b; --bar-empty: #e5e7eb;
|
||||
--bar-paused: #6366f1;
|
||||
--overall-fg: #ffffff;
|
||||
}
|
||||
/* OLED-friendly dark mode: pure black background for power saving, but a
|
||||
|
|
@ -87,14 +105,14 @@
|
|||
html:not(.light) {
|
||||
--bg: #0a0a0a; --fg: #c2c8d0; --muted: #6b7280; --card: #131418;
|
||||
--border: #1f2026; --accent: #3b9bd1;
|
||||
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026;
|
||||
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026; --bar-paused: #5b6cb8;
|
||||
--overall-fg: #f1f5f9;
|
||||
}
|
||||
}
|
||||
html.dark {
|
||||
--bg: #0a0a0a; --fg: #c2c8d0; --muted: #6b7280; --card: #131418;
|
||||
--border: #1f2026; --accent: #3b9bd1;
|
||||
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026;
|
||||
--bar-up: #1f8f6e; --bar-down: #c84a4a; --bar-partial: #c98a2c; --bar-empty: #1f2026; --bar-paused: #5b6cb8;
|
||||
--overall-fg: #f1f5f9;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
|
|
@ -119,6 +137,13 @@
|
|||
.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); }
|
||||
.maintenance-pill {
|
||||
display: inline-flex; align-items: center; gap: 0.3rem;
|
||||
padding: 0.15rem 0.55rem; border-radius: 999px;
|
||||
font-size: 0.7rem; font-weight: 600;
|
||||
color: var(--bar-paused); border: 1px solid var(--bar-paused);
|
||||
background: transparent; text-transform: uppercase; letter-spacing: 0.04em;
|
||||
}
|
||||
.bars { display: flex; gap: 0.1rem; height: 32px; margin-top: 0.75rem; align-items: stretch; position: relative; }
|
||||
.bar { flex: 1; min-width: 0; border-radius: 2px; transition: opacity 0.15s; }
|
||||
.bar:hover { opacity: 0.8; }
|
||||
|
|
@ -287,8 +312,12 @@
|
|||
<span class="name"><%= m.display_name %></span>
|
||||
</div>
|
||||
<div class="monitor-meta">
|
||||
<% if (m.current_state === 'paused') { %>
|
||||
<span class="maintenance-pill" title="This service is paused for maintenance — checks are not running.">Maintenance</span>
|
||||
<% } else { %>
|
||||
<% if (page.show_response_time && m.avg_latency != null) { %><span><%= m.avg_latency %>ms</span><% } %>
|
||||
<span class="uptime-pct"><%= fmtUptime(u[page.default_window === '24h' ? 'd24' : page.default_window === '7d' ? 'd7' : page.default_window === '30d' ? 'd30' : 'd90']) %></span>
|
||||
<% } %>
|
||||
<svg class="chev" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue