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 `
+
+
+
${escapeHtml(hourDateLabel(hist.hourKeys[i]))}
+
${escapeHtml(stats)}
+ ${inc ? `
Incident: ${escapeHtml(inc)}
` : ''}
+
+
`;
})
.join('');
body.push(`