pr_01m47d15m3e54sn21z27rpy5n9/apps/web/app/lib/status.ts

237 lines8,305 bytesCodeBlame

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 look1/**
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
10export type ComponentState = "up" | "degraded" | "down" | "unmonitored";
11export type OverallState = "up" | "degraded" | "down" | "unknown";
12
13export type ComponentKey =
14 | "site"
15 | "api"
16 | "git"
17 | "mcp"
18 | "docs"
19 | "deployments"
20 | "agents"
21 | "sandboxes"
22 | "billing";
23
24export 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. */
36export 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. */
85export 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. */
94export const SLOW_MS = 1500;
95
96export 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. */
108export 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. */
120export 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
133export type IncidentUpdate = { at: string; text: string };
134
135export 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 */
153export const INCIDENTS: Incident[] = [];
154
155/** How far back the page lists incidents. */
156export const INCIDENT_DAYS = 90;
157
158export type Overall = { state: OverallState; line: string };
159
160const RANK: Record<OverallState, number> = { unknown: 0, up: 1, degraded: 2, down: 3 };
161
162function 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 */
171export 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
197export 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`. */
205export 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. */
211export 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. */
218export 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. */
226export 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}