g1t/apps/status/src/components.ts

179 lines5,758 bytesCodeBlame
1/**
2 * The parts of g1t the page lists, and how each is checked. Every address
3 * comes from the Worker's vars, so an installation of its own names its own
4 * hosts; a part whose address is empty is not part of that installation
5 * and is left off the page.
6 *
7 * Checks go over the public internet, as people reach g1t, so they measure
8 * what people see. Billing has no public address of its own, so it is the
9 * one part checked through a service binding, when there is one.
10 */
11
12/** The Worker's vars that say where things are. */
13export type Targets = {
14 /** The site: `https://g1t.sh`. */
15 SITE_URL: string;
16 /** The API: `https://api.g1t.sh`. */
17 API_URL: string;
18 MCP_URL: string;
19 DOCS_URL: string;
20 /** Where deployed apps live: `https://g1t.page`. */
21 PAGES_URL: string;
22 MODELS_URL: string;
23 /** A public repository that always exists, `owner/name`, to fetch refs of. */
24 PROBE_REPO: string;
25 /** The site as browsers reach it, when the checks reach it another way. */
26 PUBLIC_SITE_URL?: string;
27};
28
29/** One request a check makes, and the answer that means the part works. */
30export type Step = {
31 url: string;
32 headers?: Record<string, string>;
33 /** The status that means it works; any 2xx when absent. */
34 expect?: number;
35};
36
37export type Check =
38 | { kind: "http"; steps: Step[] }
39 /** Billing, through its binding: reading its price book. */
40 | { kind: "billing" }
41 /** No check yet: the page says so rather than showing green. */
42 | { kind: "none" };
43
44export type ComponentInfo = {
45 key: string;
46 name: string;
47 address: string;
48 checks: string;
49 /** Whether g1t as a whole is down when this is. */
50 core: boolean;
51 check: Check;
52};
53
54/** A token no one holds: the API asks identity about it and refuses it. */
55export const NO_TOKEN = `g1t_${"0".repeat(40)}`;
56
57function host(url: string): string {
58 try {
59 return new URL(url).host;
60 } catch {
61 return url;
62 }
63}
64
65const trim = (url: string | undefined) => (url ?? "").trim().replace(/\/+$/, "");
66
67/** The parts, in the order the page lists them. */
68export function components(vars: Partial<Targets>, billing = true): ComponentInfo[] {
69 const site = trim(vars.SITE_URL);
70 const api = trim(vars.API_URL);
71 const mcp = trim(vars.MCP_URL);
72 const docs = trim(vars.DOCS_URL);
73 const pages = trim(vars.PAGES_URL);
74 const models = trim(vars.MODELS_URL);
75 const repo = (vars.PROBE_REPO ?? "").trim();
76 // What the page shows as the site's address: where people reach it.
77 const shown = host(trim(vars.PUBLIC_SITE_URL) || site);
78 const list: (ComponentInfo | null)[] = [
79 site
80 ? {
81 key: "site",
82 name: "Website and sign-in",
83 address: shown,
84 checks: api
85 ? "The sign-in page loading, and the account service answering an access token through the API."
86 : "The sign-in page loading.",
87 core: true,
88 check: {
89 kind: "http",
90 steps: [
91 { url: `${site}/login` },
92 ...(api ? [{ url: `${api}/v1/user`, headers: { authorization: `Bearer ${NO_TOKEN}` }, expect: 401 }] : []),
93 ],
94 },
95 }
96 : null,
97 api
98 ? {
99 key: "api",
100 name: "API",
101 address: host(api),
102 checks: "The API's front page.",
103 core: true,
104 check: { kind: "http", steps: [{ url: `${api}/` }] },
105 }
106 : null,
107 site && repo
108 ? {
109 key: "git",
110 name: "Git and repositories",
111 address: `${shown}/<owner>/<repo>.git`,
112 checks: "Listing the branches of a public repository over HTTPS, the first step of every clone.",
113 core: true,
114 check: { kind: "http", steps: [{ url: `${site}/${repo}.git/info/refs?service=git-upload-pack` }] },
115 }
116 : null,
117 mcp
118 ? {
119 key: "mcp",
120 name: "MCP server",
121 address: host(mcp),
122 checks: "The MCP server's description.",
123 core: false,
124 check: { kind: "http", steps: [{ url: `${mcp}/`, headers: { accept: "application/json" } }] },
125 }
126 : null,
127 docs
128 ? {
129 key: "docs",
130 name: "Documentation",
131 address: host(docs),
132 checks: "The documentation's front page.",
133 core: false,
134 check: { kind: "http", steps: [{ url: `${docs}/` }] },
135 }
136 : null,
137 pages
138 ? {
139 key: "deployments",
140 name: "Deployments",
141 address: `*.${host(pages)}`,
142 checks: `${host(pages)} answering. Each deployed app is not checked one by one.`,
143 core: false,
144 check: { kind: "http", steps: [{ url: `${pages}/` }] },
145 }
146 : null,
147 models
148 ? {
149 key: "agents",
150 name: "Agents' model proxy",
151 address: host(models),
152 checks: "The proxy agents reach their model through. The model providers behind it are not checked.",
153 core: false,
154 check: { kind: "http", steps: [{ url: `${models}/` }] },
155 }
156 : null,
157 models
158 ? {
159 key: "sandboxes",
160 name: "Sandboxes",
161 address: "Agents, checks, workflows and builds",
162 checks: "Starting a sandbox costs money and takes seconds, so it is not checked from here yet.",
163 core: false,
164 check: { kind: "none" },
165 }
166 : null,
167 billing && site
168 ? {
169 key: "billing",
170 name: "Billing",
171 address: `${shown}/<workspace>/-/billing`,
172 checks: "The billing service reading its price book. Stripe itself is not checked.",
173 core: false,
174 check: { kind: "billing" },
175 }
176 : null,
177 ];
178 return list.filter((c): c is ComponentInfo => c != null);
179}