g1t/apps/status/src/components.ts
| 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. */ |
| 13 | export 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. */ |
| 30 | export 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 | |
| 37 | export 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 | |
| 44 | export 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 | /** Slower than this, it counts as degraded; `SLOW_MS` when absent. */ |
| 53 | slowMs?: number; |
| 54 | }; |
| 55 | |
| 56 | /** |
| 57 | * The page-speed budget the status page holds the site to: the slower of a |
| 58 | * public project page and Explore, to the first byte of the answer, from a |
| 59 | * Cloudflare data centre. docs/PERFORMANCE.md has the targets. |
| 60 | */ |
| 61 | export const SPEED_BUDGET_MS = 800; |
| 62 | |
| 63 | /** A token no one holds: the API asks identity about it and refuses it. */ |
| 64 | export const NO_TOKEN = `g1t_${"0".repeat(40)}`; |
| 65 | |
| 66 | function host(url: string): string { |
| 67 | try { |
| 68 | return new URL(url).host; |
| 69 | } catch { |
| 70 | return url; |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | const trim = (url: string | undefined) => (url ?? "").trim().replace(/\/+$/, ""); |
| 75 | |
| 76 | /** The parts, in the order the page lists them. */ |
| 77 | export function components(vars: Partial<Targets>, billing = true): ComponentInfo[] { |
| 78 | const site = trim(vars.SITE_URL); |
| 79 | const api = trim(vars.API_URL); |
| 80 | const mcp = trim(vars.MCP_URL); |
| 81 | const docs = trim(vars.DOCS_URL); |
| 82 | const pages = trim(vars.PAGES_URL); |
| 83 | const models = trim(vars.MODELS_URL); |
| 84 | const repo = (vars.PROBE_REPO ?? "").trim(); |
| 85 | // What the page shows as the site's address: where people reach it. |
| 86 | const shown = host(trim(vars.PUBLIC_SITE_URL) || site); |
| 87 | const list: (ComponentInfo | null)[] = [ |
| 88 | site |
| 89 | ? { |
| 90 | key: "site", |
| 91 | name: "Website and sign-in", |
| 92 | address: shown, |
| 93 | checks: api |
| 94 | ? "The sign-in page loading, and the account service answering an access token through the API." |
| 95 | : "The sign-in page loading.", |
| 96 | core: true, |
| 97 | check: { |
| 98 | kind: "http", |
| 99 | steps: [ |
| 100 | { url: `${site}/login` }, |
| 101 | ...(api ? [{ url: `${api}/v1/user`, headers: { authorization: `Bearer ${NO_TOKEN}` }, expect: 401 }] : []), |
| 102 | ], |
| 103 | }, |
| 104 | } |
| 105 | : null, |
| 106 | api |
| 107 | ? { |
| 108 | key: "api", |
| 109 | name: "API", |
| 110 | address: host(api), |
| 111 | checks: "The API's front page.", |
| 112 | core: true, |
| 113 | check: { kind: "http", steps: [{ url: `${api}/` }] }, |
| 114 | } |
| 115 | : null, |
| 116 | site && repo |
| 117 | ? { |
| 118 | key: "git", |
| 119 | name: "Git and repositories", |
| 120 | address: `${shown}/<owner>/<repo>.git`, |
| 121 | checks: "Listing the branches of a public repository over HTTPS, the first step of every clone.", |
| 122 | core: true, |
| 123 | check: { kind: "http", steps: [{ url: `${site}/${repo}.git/info/refs?service=git-upload-pack` }] }, |
| 124 | } |
| 125 | : null, |
| 126 | site && repo |
| 127 | ? { |
| 128 | key: "speed", |
| 129 | name: "Page speed", |
| 130 | address: `${shown}/${repo}`, |
| 131 | checks: `A public project page and Explore each answering within ${SPEED_BUDGET_MS} ms. Slower counts as degraded.`, |
| 132 | core: false, |
| 133 | check: { kind: "http", steps: [{ url: `${site}/${repo}` }, { url: `${site}/explore` }] }, |
| 134 | slowMs: SPEED_BUDGET_MS, |
| 135 | } |
| 136 | : null, |
| 137 | mcp |
| 138 | ? { |
| 139 | key: "mcp", |
| 140 | name: "MCP server", |
| 141 | address: host(mcp), |
| 142 | checks: "The MCP server's description.", |
| 143 | core: false, |
| 144 | check: { kind: "http", steps: [{ url: `${mcp}/`, headers: { accept: "application/json" } }] }, |
| 145 | } |
| 146 | : null, |
| 147 | docs |
| 148 | ? { |
| 149 | key: "docs", |
| 150 | name: "Documentation", |
| 151 | address: host(docs), |
| 152 | checks: "The documentation's front page.", |
| 153 | core: false, |
| 154 | check: { kind: "http", steps: [{ url: `${docs}/` }] }, |
| 155 | } |
| 156 | : null, |
| 157 | pages |
| 158 | ? { |
| 159 | key: "deployments", |
| 160 | name: "Deployments", |
| 161 | address: `*.${host(pages)}`, |
| 162 | checks: `${host(pages)} answering. Each deployed app is not checked one by one.`, |
| 163 | core: false, |
| 164 | check: { kind: "http", steps: [{ url: `${pages}/` }] }, |
| 165 | } |
| 166 | : null, |
| 167 | models |
| 168 | ? { |
| 169 | key: "agents", |
| 170 | name: "Agents' model proxy", |
| 171 | address: host(models), |
| 172 | checks: "The proxy agents reach their model through. The model providers behind it are not checked.", |
| 173 | core: false, |
| 174 | check: { kind: "http", steps: [{ url: `${models}/` }] }, |
| 175 | } |
| 176 | : null, |
| 177 | models |
| 178 | ? { |
| 179 | key: "sandboxes", |
| 180 | name: "Sandboxes", |
| 181 | address: "Agents, checks, workflows and builds", |
| 182 | checks: "Starting a sandbox costs money and takes seconds, so it is not checked from here yet.", |
| 183 | core: false, |
| 184 | check: { kind: "none" }, |
| 185 | } |
| 186 | : null, |
| 187 | billing && site |
| 188 | ? { |
| 189 | key: "billing", |
| 190 | name: "Billing", |
| 191 | address: `${shown}/<workspace>/-/billing`, |
| 192 | checks: "The billing service reading its price book. Stripe itself is not checked.", |
| 193 | core: false, |
| 194 | check: { kind: "billing" }, |
| 195 | } |
| 196 | : null, |
| 197 | ]; |
| 198 | return list.filter((c): c is ComponentInfo => c != null); |
| 199 | } |