diff --git a/status/src/index.ts b/status/src/index.ts index 80b3db4e..44d6353d 100644 --- a/status/src/index.ts +++ b/status/src/index.ts @@ -283,8 +283,8 @@ ${twCss} ${autoRefresh ? '\n' : ''} - -
+ +
${body}
${FOOTER_HTML} @@ -348,16 +348,43 @@ function formatUptime(p: number): string { return rounded >= 100 ? '100%' : rounded.toFixed(3) + '%'; } -function barColorClass(b: ServiceBucket | null): string { - if (b === null) return 'bg-gray-200 dark:bg-gray-700'; - if (b.up === 0) return 'bg-red-500'; - if (b.up < b.total) return 'bg-yellow-500'; - return 'bg-green-500'; +function barStyle( + b: ServiceBucket | null, + incident: boolean, +): { cls: string; style: string } { + // Discrete states: gray = no checks, green = all up, yellow = mixed, + // red = all failed or an incident covered the hour. + if (incident) return { cls: '', style: 'background-color:#ef4444' }; + if (b === null) return { cls: 'bg-gray-200 dark:bg-gray-700', style: '' }; + if (b.up === 0) return { cls: '', style: 'background-color:#ef4444' }; + if (b.up < b.total) return { cls: '', style: 'background-color:#eab308' }; + return { cls: '', style: 'background-color:#22c55e' }; } -function barTitle(hourKey: string, b: ServiceBucket | null): string { - const label = hourKey.slice(5, 10) + ' ' + hourKey.slice(11, 13) + ':00 UTC'; - return b ? `${label} · ${b.up}/${b.total} up` : `${label} · no checks`; +const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + +function hourDateLabel(hourKey: string): string { + const mo = MONTH_NAMES[Number(hourKey.slice(5, 7)) - 1]; + return `${mo} ${hourKey.slice(8, 10)}, ${hourKey.slice(11, 13)}:00 UTC`; +} + +interface IncidentRange { + title: string; + created_at: string; + resolved_at: string | null; +} + +// Returns the title of the newest incident covering this hour bucket (ISO +// strings compare lexicographically), or null. +function incidentTitleForHour(hourKey: string, incidents: IncidentRange[]): string | null { + const start = hourKey; + const end = hourKey.slice(0, 13) + ':59:59Z'; + for (const inc of incidents) { + if (inc.created_at <= end && (inc.resolved_at === null || inc.resolved_at >= start)) { + return inc.title; + } + } + return null; } type ServiceStatusKey = 'operational' | 'degraded' | 'outage' | 'no_data' | 'disabled'; @@ -432,6 +459,7 @@ interface Snapshot { activeIncidents: Incident[]; activeUpdates: Map; resolved: Incident[]; + chartIncidents: IncidentRange[]; } async function loadSnapshot(env: Env): Promise { @@ -504,7 +532,18 @@ async function loadSnapshot(env: Env): Promise { ).all() ).results; - return { services, latest, statuses, histories, enabled, overall, activeIncidents, activeUpdates, resolved }; + // Incidents whose time range overlaps the 24h chart window (no LIMIT — an + // older but still-active/long-running incident must not be dropped). + const chartIncidents = ( + await env.DB.prepare( + `SELECT title, created_at, resolved_at FROM incidents + WHERE created_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + AND (resolved_at IS NULL OR resolved_at >= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-1 day')) + ORDER BY created_at DESC`, + ).all() + ).results; + + return { services, latest, statuses, histories, enabled, overall, activeIncidents, activeUpdates, resolved, chartIncidents }; } // --------------------------------------------------------------------------- @@ -641,7 +680,17 @@ async function dashboard(env: Env): Promise { const status = SERVICE_STATUS_UI[snap.statuses.get(s.id) ?? 'no_data']; const bars = hist.buckets .map((b, i) => { - return `
`; + const inc = incidentTitleForHour(hist.hourKeys[i], snap.chartIncidents); + const { cls, style } = barStyle(b, inc !== null); + const stats = b ? `${b.up}/${b.total} probes up` : 'no checks'; + return `
+
+ +
`; }) .join(''); body.push(`