Public: anyone can read this; the crew can change it.
The fleet and forge-size reads name their Server-Timing phases: graph, ops, one op, sizes #4992
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4982-the-fleet-and-forge-size-reads-name-thei → main 1 commit fork
2 files
changed +79
−14
src/http/api.ts +18 −14
| @@ -4982,23 +4982,22 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4982 | 4982 | }); |
| 4983 | 4983 | |
| 4984 | 4984 | router.add("GET", "/api/v1/fleet/ops", (ctx) => { |
| 4985 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 4985 | 4986 | if (!ctx.auth.user) return unauthorized(); |
| 4986 | 4987 | const q = ctx.url.searchParams; |
| 4987 | 4988 | const limit = pageLimit(q.has("limit") ? Number(q.get("limit")) : undefined); |
| 4988 | const ops = listFleetOps( | |
| 4989 | ctx.app, | |
| 4990 | ctx.auth.user, | |
| 4991 | limit, | |
| 4992 | q.get("cursor") ?? undefined, | |
| 4989 | const ops = timePhase(ctx.request, "ops", () => | |
| 4990 | listFleetOps(ctx.app, ctx.auth.user!, limit, q.get("cursor") ?? undefined), | |
| 4993 | 4991 | ); |
| 4994 | 4992 | if (!ops.ok) return respondFleet(ops.error); |
| 4995 | return json(paged(ops.value, limit, (op) => op.id)); | |
| 4993 | return timePhase(ctx.request, "render", () => json(paged(ops.value, limit, (op) => op.id))); | |
| 4996 | 4994 | }); |
| 4997 | 4995 | |
| 4998 | 4996 | router.add("GET", "/api/v1/fleet/ops/:id", (ctx) => { |
| 4999 | const op = getFleetOp(ctx.app, ctx.auth.user, ctx.params.id!); | |
| 4997 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 4998 | const op = timePhase(ctx.request, "op", () => getFleetOp(ctx.app, ctx.auth.user, ctx.params.id!)); | |
| 5000 | 4999 | if (!op.ok) return respondFleet(op.error); |
| 5001 | return json(op.value); | |
| 5000 | return timePhase(ctx.request, "render", () => json(op.value)); | |
| 5002 | 5001 | }); |
| 5003 | 5002 | |
| 5004 | 5003 | router.add("POST", "/api/v1/fleet/ops/:id/undo", async (ctx) => { |
| @@ -5198,11 +5197,14 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 5198 | 5197 | |
| 5199 | 5198 | // What small, medium and large mean: public, and cacheable by ETag. |
| 5200 | 5199 | router.add("GET", "/api/v1/forges/sizes", (ctx) => { |
| 5201 | const text = JSON.stringify(sizesView()); | |
| 5202 | const etag = weakEtag(text); | |
| 5203 | const headers = { etag, "cache-control": "public, max-age=300" }; | |
| 5204 | if (holds(ctx.request, etag)) return new Response(null, { status: 304, headers }); | |
| 5205 | return new Response(text, { headers: { "content-type": "application/json", ...headers } }); | |
| 5200 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 5201 | const text = timePhase(ctx.request, "sizes", () => JSON.stringify(sizesView())); | |
| 5202 | return timePhase(ctx.request, "render", () => { | |
| 5203 | const etag = weakEtag(text); | |
| 5204 | const headers = { etag, "cache-control": "public, max-age=300" }; | |
| 5205 | if (holds(ctx.request, etag)) return new Response(null, { status: 304, headers }); | |
| 5206 | return new Response(text, { headers: { "content-type": "application/json", ...headers } }); | |
| 5207 | }); | |
| 5206 | 5208 | }); |
| 5207 | 5209 | |
| 5208 | 5210 | // A forge's children (docs/SPAWN.md): an admin's, planned before made. |
| @@ -5861,10 +5863,12 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 5861 | 5863 | // per repository by UTC hour (24 h) and day (14 d), for `?repo=owner/name` |
| 5862 | 5864 | // repeated, or every repository the caller may write that spent lately. |
| 5863 | 5865 | router.add("GET", "/api/v1/fleet/graph", (ctx) => { |
| 5866 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 5864 | 5867 | if (!ctx.auth.user) return unauthorized(); |
| 5865 | 5868 | const asked = fleetSelection(ctx.url.searchParams.getAll("repo")); |
| 5866 | 5869 | if (!asked.ok) return apiError("invalid_selection", 422, asked.error); |
| 5867 | return json(readFleetGraph(ctx.app, ctx.auth.user, asked.value)); | |
| 5870 | const graph = timePhase(ctx.request, "graph", () => readFleetGraph(ctx.app, ctx.auth.user!, asked.value)); | |
| 5871 | return timePhase(ctx.request, "render", () => json(graph)); | |
| 5868 | 5872 | }); |
| 5869 | 5873 | |
| 5870 | 5874 | // The address an agent tries first. |
test/fleet-timing.test.ts added +61
| @@ -0,0 +1,61 @@ | ||
| 1 | /** The fleet and forge-size reads name their phases in Server-Timing. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 8 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 9 | ||
| 10 | let dataDir = ""; | |
| 11 | let app: App, servers: Servers, bearer = ""; | |
| 12 | ||
| 13 | beforeAll(async () => { | |
| 14 | dataDir = await mkdtemp(join(tmpdir(), "forge-ftime-")); | |
| 15 | app = createApp({ dataDir, port: 0 }); | |
| 16 | servers = await serve(app); | |
| 17 | await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" }); | |
| 18 | const minted = await fetch(`${servers.url}/api/v1/tokens`, { | |
| 19 | method: "POST", | |
| 20 | headers: { "content-type": "application/json" }, | |
| 21 | body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }), | |
| 22 | }); | |
| 23 | bearer = ((await minted.json()) as { token: string }).token; | |
| 24 | }); | |
| 25 | ||
| 26 | afterAll(async () => { | |
| 27 | servers.stop(); | |
| 28 | app.close(); | |
| 29 | await rm(dataDir, { recursive: true, force: true }); | |
| 30 | }); | |
| 31 | ||
| 32 | async function phases(path: string): Promise<{ status: number; names: string[] }> { | |
| 33 | const res = await fetch(`${servers.url}${path}`, { headers: { authorization: `Bearer ${bearer}` } }); | |
| 34 | await res.arrayBuffer(); | |
| 35 | const names = (res.headers.get("server-timing") ?? "").split(", ").filter(Boolean).filter((p) => !p.startsWith("app;")).map((p) => p.split(";")[0]!); | |
| 36 | return { status: res.status, names }; | |
| 37 | } | |
| 38 | ||
| 39 | test("the fleet graph names session, graph, render", async () => { | |
| 40 | const r = await phases("/api/v1/fleet/graph"); | |
| 41 | expect(r.status).toBe(200); | |
| 42 | expect(r.names).toEqual(["session", "graph", "render"]); | |
| 43 | }); | |
| 44 | ||
| 45 | test("the fleet ops list names session, ops, render", async () => { | |
| 46 | const r = await phases("/api/v1/fleet/ops"); | |
| 47 | expect(r.status).toBe(200); | |
| 48 | expect(r.names).toEqual(["session", "ops", "render"]); | |
| 49 | }); | |
| 50 | ||
| 51 | test("one fleet op names session, op, render; a missing one still names session", async () => { | |
| 52 | const r = await phases("/api/v1/fleet/ops/nope"); | |
| 53 | expect(r.status).toBeGreaterThanOrEqual(400); | |
| 54 | expect(r.names.slice(0, 2)).toEqual(["session", "op"]); | |
| 55 | }); | |
| 56 | ||
| 57 | test("the forge sizes name session, sizes, render", async () => { | |
| 58 | const r = await phases("/api/v1/forges/sizes"); | |
| 59 | expect(r.status).toBe(200); | |
| 60 | expect(r.names).toEqual(["session", "sizes", "render"]); | |
| 61 | }); |
new above
offline