pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/lib/status.ts
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.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | /** |
| 2 | * What /status shows and the footer's dot is coloured by: each public part | |
| 3 | * of g1t, whether it answered a quick check, and one line for the whole. | |
| 4 | * | |
| 5 | * The checks themselves run on the server (status.server.ts). This module | |
| 6 | * only says what a check's result means, so it can be tested on its own. | |
| 7 | * Fields are snake_case: /status.json is public, like the API. | |
| 8 | */ | |
| 9 | ||
| 10 | export type ComponentState = "up" | "degraded" | "down" | "unmonitored"; | |
| 11 | export type OverallState = "up" | "degraded" | "down" | "unknown"; | |
| 12 | ||
| 13 | export type ComponentKey = | |
| 14 | | "site" | |
| 15 | | "api" | |
| 16 | | "git" | |
| 17 | | "mcp" | |
| 18 | | "docs" | |
| 19 | | "deployments" | |
| 20 | | "agents" | |
| 21 | | "sandboxes" | |
| 22 | | "billing"; | |
| 23 | ||
| 24 | export type ComponentInfo = { | |
| 25 | key: ComponentKey; | |
| 26 | name: string; | |
| 27 | /** Where people meet it. */ | |
| 28 | address: string; | |
| 29 | /** What the check actually looks at, said plainly on the page. */ | |
| 30 | checks: string; | |
| 31 | /** Whether g1t as a whole is down when this is. */ | |
| 32 | core: boolean; | |
| 33 | }; | |
| 34 | ||
| 35 | /** The public parts of g1t, in the order the page lists them. */ | |
| 36 | export const COMPONENTS: ComponentInfo[] = [ | |
| 37 | { | |
| 38 | key: "site", | |
| 39 | name: "Website and sign-in", | |
| 40 | address: "g1t.sh", | |
| 41 | checks: "The account service every page asks who is signed in.", | |
| 42 | core: true, | |
| 43 | }, | |
| 44 | { key: "api", name: "API", address: "api.g1t.sh", checks: "The API's front page, over the public internet.", core: true }, | |
| 45 | { | |
| 46 | key: "git", | |
| 47 | name: "Git and repositories", | |
| 48 | address: "g1t.sh/<owner>/<repo>.git", | |
| 49 | checks: "The repository service looking up a public repository. Clones and pushes themselves are not exercised.", | |
| 50 | core: true, | |
| 51 | }, | |
| 52 | { key: "mcp", name: "MCP server", address: "mcp.g1t.sh", checks: "The MCP server's description, over the public internet.", core: false }, | |
| 53 | { key: "docs", name: "Documentation", address: "docs.g1t.sh", checks: "The documentation's front page.", core: false }, | |
| 54 | { | |
| 55 | key: "deployments", | |
| 56 | name: "Deployments", | |
| 57 | address: "*.g1t.page", | |
| 58 | checks: "g1t.page answering. Each deployed app is not checked one by one.", | |
| 59 | core: false, | |
| 60 | }, | |
| 61 | { | |
| 62 | key: "agents", | |
| 63 | name: "Agents' model proxy", | |
| 64 | address: "models.g1t.sh", | |
| 65 | checks: "The proxy g1t's agents reach their model through. The model providers behind it are not checked.", | |
| 66 | core: false, | |
| 67 | }, | |
| 68 | { | |
| 69 | key: "sandboxes", | |
| 70 | name: "Sandboxes", | |
| 71 | address: "Agents, checks, workflows and builds", | |
| 72 | checks: "Starting a sandbox costs money and takes seconds, so it is not checked from here yet.", | |
| 73 | core: false, | |
| 74 | }, | |
| 75 | { | |
| 76 | key: "billing", | |
| 77 | name: "Billing", | |
| 78 | address: "g1t.sh/<workspace>/-/billing", | |
| 79 | checks: "The billing service reading its price book. Stripe itself is not checked.", | |
| 80 | core: false, | |
| 81 | }, | |
| 82 | ]; | |
| 83 | ||
| 84 | /** A check's raw outcome. */ | |
| 85 | export type ProbeResult = { | |
| 86 | ok: boolean; | |
| 87 | /** How long it took, in milliseconds. */ | |
| 88 | ms: number; | |
| 89 | /** Why it failed, in a few words: "timed out", "HTTP 502". */ | |
| 90 | error?: string; | |
| 91 | }; | |
| 92 | ||
| 93 | /** Slower than this, a part that answered counts as degraded. */ | |
| 94 | export const SLOW_MS = 1500; | |
| 95 | ||
| 96 | export type ComponentStatus = { | |
| 97 | key: ComponentKey; | |
| 98 | name: string; | |
| 99 | address: string; | |
| 100 | checks: string; | |
| 101 | state: ComponentState; | |
| 102 | /** What was seen, for the page: "Answered in 84 ms", "Timed out". */ | |
| 103 | detail: string; | |
| 104 | latency_ms: number | null; | |
| 105 | }; | |
| 106 | ||
| 107 | /** What a check's result means for its part. `null` is a part with no check. */ | |
| 108 | export function classify(result: ProbeResult | null, slowMs = SLOW_MS): { state: ComponentState; detail: string } { | |
| 109 | if (!result) return { state: "unmonitored", detail: "Not monitored yet" }; | |
| 110 | if (!result.ok) { | |
| 111 | const why = result.error ?? "no answer"; | |
| 112 | return { state: "down", detail: `Failed: ${why}` }; | |
| 113 | } | |
| 114 | const ms = Math.round(result.ms); | |
| 115 | if (ms > slowMs) return { state: "degraded", detail: `Slow: answered in ${ms} ms` }; | |
| 116 | return { state: "up", detail: `Answered in ${ms} ms` }; | |
| 117 | } | |
| 118 | ||
| 119 | /** One component's row, from its check. */ | |
| 120 | export function componentStatus(info: ComponentInfo, result: ProbeResult | null, slowMs = SLOW_MS): ComponentStatus { | |
| 121 | const { state, detail } = classify(result, slowMs); | |
| 122 | return { | |
| 123 | key: info.key, | |
| 124 | name: info.name, | |
| 125 | address: info.address, | |
| 126 | checks: info.checks, | |
| 127 | state, | |
| 128 | detail, | |
| 129 | latency_ms: result ? Math.round(result.ms) : null, | |
| 130 | }; | |
| 131 | } | |
| 132 | ||
| 133 | export type IncidentUpdate = { at: string; text: string }; | |
| 134 | ||
| 135 | export type Incident = { | |
| 136 | id: string; | |
| 137 | title: string; | |
| 138 | /** How bad it is while open. */ | |
| 139 | impact: "degraded" | "down"; | |
| 140 | components: ComponentKey[]; | |
| 141 | started_at: string; | |
| 142 | /** Null while it is still going on. */ | |
| 143 | resolved_at: string | null; | |
| 144 | /** Newest first. */ | |
| 145 | updates: IncidentUpdate[]; | |
| 146 | }; | |
| 147 | ||
| 148 | /** | |
| 149 | * Incidents, newest first. Written by hand, in code, for now: add one when | |
| 150 | * something breaks, with updates as it is fixed, and set `resolved_at` once | |
| 151 | * it is. The page shows the last 90 days. | |
| 152 | */ | |
| 153 | export const INCIDENTS: Incident[] = []; | |
| 154 | ||
| 155 | /** How far back the page lists incidents. */ | |
| 156 | export const INCIDENT_DAYS = 90; | |
| 157 | ||
| 158 | export type Overall = { state: OverallState; line: string }; | |
| 159 | ||
| 160 | const RANK: Record<OverallState, number> = { unknown: 0, up: 1, degraded: 2, down: 3 }; | |
| 161 | ||
| 162 | function worse(a: OverallState, b: OverallState): OverallState { | |
| 163 | return RANK[b] > RANK[a] ? b : a; | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * One line for the whole: down when a core part is down, degraded when any | |
| 168 | * part is down or slow or an incident is open, up when every checked part | |
| 169 | * answered. Parts that are not checked never make it look better or worse. | |
| 170 | */ | |
| 171 | export function summarize(components: ComponentStatus[], incidents: Incident[] = []): Overall { | |
| 172 | const core = new Set(COMPONENTS.filter((c) => c.core).map((c) => c.key)); | |
| 173 | const monitored = components.filter((c) => c.state !== "unmonitored"); | |
| 174 | const open = incidents.filter((incident) => incident.resolved_at == null); | |
| 175 | if (monitored.length === 0 && open.length === 0) { | |
| 176 | return { state: "unknown", line: "Status is not available right now" }; | |
| 177 | } | |
| 178 | let state: OverallState = monitored.length > 0 ? "up" : "unknown"; | |
| 179 | for (const c of monitored) { | |
| 180 | if (c.state === "down") state = worse(state, core.has(c.key) ? "down" : "degraded"); | |
| 181 | else if (c.state === "degraded") state = worse(state, "degraded"); | |
| 182 | } | |
| 183 | for (const incident of open) state = worse(state, incident.impact); | |
| 184 | ||
| 185 | const failing = monitored.filter((c) => c.state === "down" || c.state === "degraded").map((c) => c.name); | |
| 186 | if (state === "up") return { state, line: "All monitored systems are working" }; | |
| 187 | if (state === "down") { | |
| 188 | return { state, line: failing.length ? `Major outage: ${failing.join(", ")}` : "Major outage" }; | |
| 189 | } | |
| 190 | if (state === "degraded") { | |
| 191 | if (failing.length) return { state, line: `Some systems are having trouble: ${failing.join(", ")}` }; | |
| 192 | return { state, line: open[0] ? `Investigating: ${open[0].title}` : "Some systems are having trouble" }; | |
| 193 | } | |
| 194 | return { state, line: "Status is not available right now" }; | |
| 195 | } | |
| 196 | ||
| 197 | export type StatusReport = { | |
| 198 | checked_at: string; | |
| 199 | overall: Overall; | |
| 200 | components: ComponentStatus[]; | |
| 201 | incidents: Incident[]; | |
| 202 | }; | |
| 203 | ||
| 204 | /** Incidents still open, or resolved within `days` of `now`. */ | |
| 205 | export function recentIncidents(incidents: Incident[], now: Date, days = INCIDENT_DAYS): Incident[] { | |
| 206 | const since = now.getTime() - days * 24 * 60 * 60 * 1000; | |
| 207 | return incidents.filter((incident) => incident.resolved_at == null || Date.parse(incident.resolved_at) >= since); | |
| 208 | } | |
| 209 | ||
| 210 | /** The whole report, from each part's check. */ | |
| 211 | export function report(results: Partial<Record<ComponentKey, ProbeResult | null>>, now: Date, incidents = INCIDENTS): StatusReport { | |
| 212 | const components = COMPONENTS.map((info) => componentStatus(info, results[info.key] ?? null)); | |
| 213 | const recent = recentIncidents(incidents, now); | |
| 214 | return { checked_at: now.toISOString(), overall: summarize(components, recent), components, incidents: recent }; | |
| 215 | } | |
| 216 | ||
| 217 | /** Words for a part's state. */ | |
| 218 | export const STATE_LABEL: Record<ComponentState, string> = { | |
| 219 | up: "Operational", | |
| 220 | degraded: "Degraded", | |
| 221 | down: "Down", | |
| 222 | unmonitored: "Not monitored yet", | |
| 223 | }; | |
| 224 | ||
| 225 | /** The dot's colour for a state, as a Tailwind background class. */ | |
| 226 | export function dotClass(state: ComponentState | OverallState | null | undefined): string { | |
| 227 | switch (state) { | |
| 228 | case "up": | |
| 229 | return "bg-accent"; | |
| 230 | case "degraded": | |
| 231 | return "bg-warn"; | |
| 232 | case "down": | |
| 233 | return "bg-danger"; | |
| 234 | default: | |
| 235 | return "bg-faint"; | |
| 236 | } | |
| 237 | } |