Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 1 | /** |
| 2 | * What a check's result means, how the parts add up to one line for the | |
| 3 | * whole, and the 90 days of history behind each uptime bar. No Workers | |
| 4 | * imports, so it is tested under Node. | |
| 5 | */ | |
| 6 | import type { | |
| 7 | ComponentImpact, | |
| 8 | IncidentImpact, | |
| 9 | IncidentStatus, | |
| 10 | MaintenanceState, | |
| 11 | StatusComponent, | |
| 12 | StatusComponentState, | |
| 13 | StatusIncident, | |
| 14 | StatusMaintenance, | |
| 15 | StatusOverall, | |
| 16 | StatusOverallState, | |
| 17 | StatusReport, | |
| 18 | } from "@g1t/contracts"; | |
| 19 | ||
| 20 | import type { ComponentInfo } from "./components.ts"; | |
| 21 | import type { ProbeResult } from "./probe.ts"; | |
| 22 | ||
| 23 | /** Slower than this, a part that answered counts as degraded. */ | |
| 24 | export const SLOW_MS = 1500; | |
| 25 | /** How many days of history the page shows and keeps. */ | |
| 26 | export const HISTORY_DAYS = 90; | |
| 27 | /** Checks older than this mean the checker has stopped: the page says so. */ | |
| 28 | export const STALE_MS = 10 * 60 * 1000; | |
| 29 | ||
| 30 | /** What a check's result means for its part. `null` is a part with no check. */ | |
| 31 | export function classify(result: ProbeResult | null, slowMs = SLOW_MS): { state: StatusComponentState; detail: string } { | |
| 32 | if (!result) return { state: "unmonitored", detail: "Not monitored yet" }; | |
| 33 | if (!result.ok) return { state: "down", detail: `Failed: ${result.error ?? "no answer"}` }; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 34 | if (result.degraded) return { state: "degraded", detail: result.degraded }; |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 35 | const ms = Math.round(result.ms); |
| 36 | if (ms > slowMs) return { state: "degraded", detail: `Slow: answered in ${ms} ms` }; | |
| 37 | return { state: "up", detail: `Answered in ${ms} ms` }; | |
| 38 | } | |
| 39 | ||
| 40 | /** A part's state at its last check, as stored. */ | |
| 41 | export type Current = { state: StatusComponentState; detail: string; latency_ms: number | null }; | |
| 42 | ||
| 43 | /** One part's checks on one UTC day, as stored. */ | |
| 44 | export type DayRow = { | |
| 45 | component: string; | |
| 46 | /** `YYYY-MM-DD`, UTC. */ | |
| 47 | day: string; | |
| 48 | checks: number; | |
| 49 | up: number; | |
| 50 | degraded: number; | |
| 51 | down: number; | |
| 52 | latency_total: number; | |
| 53 | latency_count: number; | |
| 54 | }; | |
| 55 | ||
| 56 | /** One day's square on a bar. */ | |
| 57 | export type DayBar = { | |
| 58 | day: string; | |
| 59 | state: "up" | "degraded" | "down" | "none"; | |
| 60 | /** Share of checks that answered, 0 to 100; null with none. */ | |
| 61 | uptime: number | null; | |
| 62 | checks: number; | |
| 63 | failed: number; | |
| 64 | slow: number; | |
| 65 | avg_ms: number | null; | |
| 66 | /** Incidents on this part that were open some time that day. */ | |
| 67 | incidents: string[]; | |
| 68 | }; | |
| 69 | ||
| 70 | const DAY_MS = 24 * 60 * 60 * 1000; | |
| 71 | ||
| 72 | /** `YYYY-MM-DD` of a time, in UTC. */ | |
| 73 | export function dayOf(at: Date | number): string { | |
| 74 | return new Date(at).toISOString().slice(0, 10); | |
| 75 | } | |
| 76 | ||
| 77 | /** The last `count` days, oldest first, ending today. */ | |
| 78 | export function lastDays(now: Date, count = HISTORY_DAYS): string[] { | |
| 79 | const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); | |
| 80 | return Array.from({ length: count }, (_, i) => dayOf(today - (count - 1 - i) * DAY_MS)); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Share of checks answered, rounded down to hundredths so a single failure | |
| 85 | * never shows as 100%. | |
| 86 | */ | |
| 87 | export function uptimeOf(answered: number, checks: number): number | null { | |
| 88 | if (checks <= 0) return null; | |
| 89 | return Math.floor((answered / checks) * 10000) / 100; | |
| 90 | } | |
| 91 | ||
| 92 | /** Words for an uptime: "100%", "99.93%". */ | |
| 93 | export function percent(uptime: number | null): string { | |
| 94 | if (uptime == null) return "No data"; | |
| 95 | return uptime >= 100 ? "100%" : `${uptime.toFixed(2)}%`; | |
| 96 | } | |
| 97 | ||
| 98 | const LEVEL = { none: 0, up: 1, degraded: 2, down: 3 } as const; | |
| 99 | ||
| 100 | /** | |
| 101 | * A day's colour: answered at least 99.9% of the time is up (one blip in a | |
| 102 | * day of minutely checks), unless over a quarter of answers were slow; at | |
| 103 | * least 95% is degraded; less is down. An incident on the part that day | |
| 104 | * colours it at least as badly as its impact. | |
| 105 | */ | |
| 106 | export function dayState(row: Pick<DayRow, "checks" | "up" | "degraded" | "down"> | null, incidentImpact: IncidentImpact | null = null): DayBar["state"] { | |
| 107 | let state: DayBar["state"] = "none"; | |
| 108 | if (row && row.checks > 0) { | |
| 109 | const uptime = ((row.up + row.degraded) / row.checks) * 100; | |
| 110 | if (uptime >= 99.9) state = row.degraded / row.checks > 0.25 ? "degraded" : "up"; | |
| 111 | else if (uptime >= 95) state = "degraded"; | |
| 112 | else state = "down"; | |
| 113 | } | |
| 114 | if (incidentImpact && LEVEL[incidentImpact] > LEVEL[state]) state = incidentImpact; | |
| 115 | return state; | |
| 116 | } | |
| 117 | ||
| 118 | /** Whether an incident was open at some time during `day`. */ | |
| 119 | export function openOn(incident: Pick<StatusIncident, "started_at" | "resolved_at">, day: string): boolean { | |
| 120 | const start = Date.parse(`${day}T00:00:00Z`); | |
| 121 | const end = start + DAY_MS; | |
| 122 | const began = Date.parse(incident.started_at); | |
| 123 | const ended = incident.resolved_at ? Date.parse(incident.resolved_at) : Number.POSITIVE_INFINITY; | |
| 124 | return began < end && ended >= start; | |
| 125 | } | |
| 126 | ||
| 127 | /** A part's bar: one square per day, oldest first. */ | |
| 128 | export function barFor( | |
| 129 | key: string, | |
| 130 | rows: DayRow[], | |
| 131 | incidents: Pick<StatusIncident, "title" | "started_at" | "resolved_at" | "component_impacts">[], | |
| 132 | days: string[], | |
| 133 | ): DayBar[] { | |
| 134 | const byDay = new Map(rows.filter((r) => r.component === key).map((r) => [r.day, r])); | |
| 135 | const mine = incidents | |
| 136 | .map((i) => ({ incident: i, impact: i.component_impacts.find((c) => c.key === key)?.impact ?? null })) | |
| 137 | .filter((m) => m.impact != null && m.impact !== "operational"); | |
| 138 | return days.map((day) => { | |
| 139 | const row = byDay.get(day) ?? null; | |
| 140 | const during = mine.filter((m) => openOn(m.incident, day)); | |
| 141 | const impact = during.some((m) => m.impact === "major_outage") ? "down" : during.length ? "degraded" : null; | |
| 142 | return { | |
| 143 | day, | |
| 144 | state: dayState(row, impact), | |
| 145 | uptime: row ? uptimeOf(row.up + row.degraded, row.checks) : null, | |
| 146 | checks: row?.checks ?? 0, | |
| 147 | failed: row?.down ?? 0, | |
| 148 | slow: row?.degraded ?? 0, | |
| 149 | avg_ms: row && row.latency_count > 0 ? Math.round(row.latency_total / row.latency_count) : null, | |
| 150 | incidents: during.map((m) => m.incident.title), | |
| 151 | }; | |
| 152 | }); | |
| 153 | } | |
| 154 | ||
| 155 | /** A part's uptime over the days given. */ | |
| 156 | export function uptimeOver(key: string, rows: DayRow[]): number | null { | |
| 157 | let checks = 0; | |
| 158 | let answered = 0; | |
| 159 | for (const row of rows) { | |
| 160 | if (row.component !== key) continue; | |
| 161 | checks += row.checks; | |
| 162 | answered += row.up + row.degraded; | |
| 163 | } | |
| 164 | return uptimeOf(answered, checks); | |
| 165 | } | |
| 166 | ||
| 167 | ||
| 168 | const RANK: Record<StatusOverallState, number> = { unknown: 0, up: 1, maintenance: 1.5, degraded: 2, down: 3 }; | |
| 169 | ||
| 170 | function worse(a: StatusOverallState, b: StatusOverallState): StatusOverallState { | |
| 171 | return RANK[b] > RANK[a] ? b : a; | |
| 172 | } | |
| 173 | ||
| 174 | /** Words for an incident's status. */ | |
| 175 | export const INCIDENT_STATUS: Record<IncidentStatus, string> = { | |
| 176 | investigating: "Investigating", | |
| 177 | identified: "Identified", | |
| 178 | monitoring: "Monitoring", | |
| 179 | resolved: "Resolved", | |
| 180 | }; | |
| 181 | ||
| 182 | /** Words for what an incident does to a part. */ | |
| 183 | export const IMPACT_WORD: Record<ComponentImpact, string> = { | |
| 184 | operational: "Operational", | |
| 185 | degraded: "Degraded performance", | |
| 186 | partial_outage: "Partial outage", | |
| 187 | major_outage: "Major outage", | |
| 188 | }; | |
| 189 | ||
| 190 | /** A part's state for an impact on it. */ | |
| 191 | export const IMPACT_STATE: Record<ComponentImpact, StatusComponentState> = { | |
| 192 | operational: "up", | |
| 193 | degraded: "degraded", | |
| 194 | partial_outage: "partial", | |
| 195 | major_outage: "down", | |
| 196 | }; | |
| 197 | ||
| 198 | /** How bad each state is: the worse of a check and an incident wins. */ | |
| 199 | const STATE_RANK: Record<StatusComponentState, number> = { unmonitored: 0, up: 1, maintenance: 1, degraded: 2, partial: 3, down: 4 }; | |
| 200 | ||
| 201 | /** An incident's impact as a whole: `down` when any part has a major outage. */ | |
| 202 | export function overallImpact(impacts: { impact: ComponentImpact }[]): IncidentImpact { | |
| 203 | return impacts.some((i) => i.impact === "major_outage") ? "down" : "degraded"; | |
| 204 | } | |
| 205 | ||
| 206 | /** | |
| 207 | * What the page shows for a part: the worse of its last check and what | |
| 208 | * open incidents say about it. During maintenance it is "Under | |
| 209 | * maintenance", unless an incident says worse, since checks failing in a | |
| 210 | * window are expected. | |
| 211 | */ | |
| 212 | export function effectiveState(check: StatusComponentState, impacts: ComponentImpact[], underMaintenance: boolean): StatusComponentState { | |
| 213 | let reported: StatusComponentState | null = null; | |
| 214 | for (const impact of impacts) { | |
| 215 | const state = IMPACT_STATE[impact]; | |
| 216 | if (reported == null || STATE_RANK[state] > STATE_RANK[reported]) reported = state; | |
| 217 | } | |
| 218 | if (underMaintenance && (reported == null || reported === "up")) return "maintenance"; | |
| 219 | if (reported == null) return check; | |
| 220 | if (check === "unmonitored" || check === "maintenance") return reported; | |
| 221 | return STATE_RANK[reported] > STATE_RANK[check] ? reported : check; | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Where maintenance stands at `now`. Cancelled and completed are final; | |
| 226 | * otherwise the window decides, so the page is right even if the | |
| 227 | * minutely job that moves it along is late. | |
| 228 | */ | |
| 229 | export function maintenanceState(m: Pick<StatusMaintenance, "state" | "starts_at" | "ends_at">, now: Date): MaintenanceState { | |
| 230 | if (m.state === "cancelled" || m.state === "completed") return m.state; | |
| 231 | const t = now.getTime(); | |
| 232 | if (t >= Date.parse(m.ends_at)) return "completed"; | |
| 233 | if (t >= Date.parse(m.starts_at) || m.state === "in_progress") return "in_progress"; | |
| 234 | return "scheduled"; | |
| 235 | } | |
| 236 | ||
| 237 | /** The parts under maintenance at `now`. */ | |
| 238 | export function underMaintenance( | |
| 239 | list: Pick<StatusMaintenance, "state" | "starts_at" | "ends_at" | "components">[], | |
| 240 | now: Date, | |
| 241 | ): Set<string> { | |
| 242 | const keys = new Set<string>(); | |
| 243 | for (const m of list) if (maintenanceState(m, now) === "in_progress") for (const k of m.components) keys.add(k); | |
| 244 | return keys; | |
| 245 | } | |
| 246 | ||
| 247 | /** | |
| 248 | * The whole, from its parts and its open incidents: a major outage when a | |
| 249 | * core part is down or an incident says so; a partial outage when any part | |
| 250 | * is down or partly down, or an incident is open; degraded performance | |
| 251 | * when parts are only slow; under maintenance when work is under way and | |
| 252 | * nothing else is wrong. Parts with no check never make it look better or | |
| 253 | * worse. | |
| 254 | */ | |
| 255 | export function summarize( | |
| 256 | parts: Pick<StatusComponent, "key" | "name" | "state">[], | |
| 257 | core: Set<string>, | |
| 258 | open: Pick<StatusIncident, "title" | "impact" | "status">[] = [], | |
| 259 | maintenance: Pick<StatusMaintenance, "title">[] = [], | |
| 260 | ): StatusOverall { | |
| 261 | const monitored = parts.filter((c) => c.state !== "unmonitored"); | |
| 262 | if (monitored.length === 0 && open.length === 0) { | |
| 263 | return { state: "unknown", title: "Status unavailable", line: "No checks have run yet." }; | |
| 264 | } | |
| 265 | let state: StatusOverallState = monitored.length > 0 ? "up" : "unknown"; | |
| 266 | for (const c of monitored) { | |
| 267 | if (c.state === "down") state = worse(state, core.has(c.key) ? "down" : "degraded"); | |
| 268 | else if (c.state === "partial" || c.state === "degraded") state = worse(state, "degraded"); | |
| 269 | else if (c.state === "maintenance") state = worse(state, "maintenance"); | |
| 270 | } | |
| 271 | for (const incident of open) state = worse(state, incident.impact); | |
| 272 | ||
| 273 | const down = monitored.filter((c) => c.state === "down" || c.state === "partial").map((c) => c.name); | |
| 274 | const slow = monitored.filter((c) => c.state === "degraded").map((c) => c.name); | |
| 275 | const lead = open[0]; | |
| 276 | const incidentLine = lead ? `${INCIDENT_STATUS[lead.status]}: ${lead.title}.` : null; | |
| 277 | const named = (names: string[]) => names.join(", "); | |
| 278 | ||
| 279 | if (state === "up") return { state, title: "All systems normal", line: "Every part of g1t answered its last check." }; | |
| 280 | if (state === "maintenance") { | |
| 281 | const work = maintenance[0]; | |
| 282 | return { state, title: "Under maintenance", line: work ? `Planned work: ${work.title}.` : "Planned work is under way." }; | |
| 283 | } | |
| 284 | if (state === "down") { | |
| 285 | return { | |
| 286 | state, | |
| 287 | title: "Major outage", | |
| 288 | line: incidentLine ?? (down.length ? `Not answering: ${named(down)}.` : "g1t is not working right now."), | |
| 289 | }; | |
| 290 | } | |
| 291 | if (state === "degraded") { | |
| 292 | if (down.length || open.length) { | |
| 293 | return { | |
| 294 | state, | |
| 295 | title: "Partial outage", | |
| 296 | line: incidentLine ?? `Not answering: ${named(down)}${slow.length ? `. Slow: ${named(slow)}` : ""}.`, | |
| 297 | }; | |
| 298 | } | |
| 299 | return { state, title: "Degraded performance", line: `Slow to answer: ${named(slow)}.` }; | |
| 300 | } | |
| 301 | return { state, title: "Status unavailable", line: "No checks have run yet." }; | |
| 302 | } | |
| 303 | ||
| 304 | /** Open incidents, and those resolved within `days` of `now`, newest first. */ | |
| 305 | export function recentIncidents<T extends Pick<StatusIncident, "started_at" | "resolved_at">>( | |
| 306 | incidents: T[], | |
| 307 | now: Date, | |
| 308 | days = HISTORY_DAYS, | |
| 309 | ): T[] { | |
| 310 | const since = now.getTime() - days * DAY_MS; | |
| 311 | return incidents | |
| 312 | .filter((i) => i.resolved_at == null || Date.parse(i.resolved_at) >= since) | |
| 313 | .sort((a, b) => Date.parse(b.started_at) - Date.parse(a.started_at)); | |
| 314 | } | |
| 315 | ||
| 316 | /** Everything the page and the JSON show. */ | |
| 317 | export type PageModel = { | |
| 318 | report: StatusReport; | |
| 319 | bars: Record<string, DayBar[]>; | |
| 320 | /** The checks have not run for a while: the checker itself is in trouble. */ | |
| 321 | stale: boolean; | |
| 322 | }; | |
| 323 | ||
| 324 | /** The report, from what is stored. */ | |
| 325 | export function buildPage(input: { | |
| 326 | parts: ComponentInfo[]; | |
| 327 | current: Map<string, Current>; | |
| 328 | checkedAt: string | null; | |
| 329 | days: DayRow[]; | |
| 330 | incidents: StatusIncident[]; | |
| 331 | maintenance?: StatusMaintenance[]; | |
| 332 | now: Date; | |
| 333 | }): PageModel { | |
| 334 | const { parts, current, checkedAt, days, now } = input; | |
| 335 | const window = lastDays(now); | |
| 336 | const since = window[0]!; | |
| 337 | const rows = days.filter((r) => r.day >= since); | |
| 338 | const incidents = recentIncidents(input.incidents, now); | |
| 339 | const open = incidents.filter((i) => i.resolved_at == null); | |
| 340 | // Upcoming and under way, soonest first, each with its state as of now. | |
| 341 | const maintenance = (input.maintenance ?? []) | |
| 342 | .map((m) => ({ ...m, state: maintenanceState(m, now) })) | |
| 343 | .filter((m) => m.state === "scheduled" || m.state === "in_progress") | |
| 344 | .sort((a, b) => a.starts_at.localeCompare(b.starts_at)); | |
| 345 | const working = underMaintenance(maintenance, now); | |
| 346 | const components: StatusComponent[] = parts.map((info) => { | |
| 347 | const seen = info.check.kind === "none" ? null : current.get(info.key); | |
| 348 | const check: StatusComponentState = seen?.state ?? "unmonitored"; | |
| 349 | const impacts = open.flatMap((i) => i.component_impacts.filter((c) => c.key === info.key).map((c) => c.impact)); | |
| 350 | return { | |
| 351 | key: info.key, | |
| 352 | name: info.name, | |
| 353 | address: info.address, | |
| 354 | checks: info.checks, | |
| 355 | state: effectiveState(check, impacts, working.has(info.key)), | |
| 356 | check_state: check, | |
| 357 | detail: seen?.detail ?? (info.check.kind === "none" ? "Not monitored yet" : "Not checked yet"), | |
| 358 | latency_ms: seen?.latency_ms ?? null, | |
| 359 | uptime_90d: info.check.kind === "none" ? null : uptimeOver(info.key, rows), | |
| 360 | }; | |
| 361 | }); | |
| 362 | const core = new Set(parts.filter((p) => p.core).map((p) => p.key)); | |
| 363 | const bars = Object.fromEntries(parts.map((p) => [p.key, barFor(p.key, rows, incidents, window)])); | |
| 364 | const inProgress = maintenance.filter((m) => m.state === "in_progress"); | |
| 365 | return { | |
| 366 | report: { checked_at: checkedAt, overall: summarize(components, core, open, inProgress), components, incidents, maintenance }, | |
| 367 | bars, | |
| 368 | stale: checkedAt == null || now.getTime() - Date.parse(checkedAt) > STALE_MS, | |
| 369 | }; | |
| 370 | } |