Public: anyone can read this; the crew can change it.
The API's board reads name their Server-Timing phases: session, boards, render #3949
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3936-the-api-s-board-reads-name-their-server → main 3 commits fork
2 files
changed +83
−8
src/http/api.ts +14 −8
| @@ -1505,21 +1505,25 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 1505 | 1505 | }; |
| 1506 | 1506 | |
| 1507 | 1507 | router.add("GET", "/api/v1/boards", async (ctx) => { |
| 1508 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 1508 | 1509 | const denied = boardsAdmin(ctx); |
| 1509 | 1510 | if (denied) return denied; |
| 1510 | const listed = await listAllBoards(ctx.app, ctx.auth.user); | |
| 1511 | return listed.ok | |
| 1512 | ? json({ boards: listed.value.map((b) => ({ slug: b.slug, ...b.board })) }) | |
| 1513 | : notFound(); | |
| 1511 | const listed = await timePhaseAsync(ctx.request, "boards", () => listAllBoards(ctx.app, ctx.auth.user)); | |
| 1512 | return timePhase(ctx.request, "render", () => | |
| 1513 | listed.ok | |
| 1514 | ? json({ boards: listed.value.map((b) => ({ slug: b.slug, ...b.board })) }) | |
| 1515 | : notFound()); | |
| 1514 | 1516 | }); |
| 1515 | 1517 | |
| 1516 | 1518 | // The signed-in admin's pins for home: GET lists them in order, POST |
| 1517 | 1519 | // {op: pin|unpin|move, slug, title, by?} changes them. Above `:slug`. |
| 1518 | 1520 | router.add("GET", "/api/v1/boards/pins", async (ctx) => { |
| 1521 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 1519 | 1522 | const denied = boardsAdmin(ctx); |
| 1520 | 1523 | if (denied) return denied; |
| 1521 | const listed = await listPinnedPanels(ctx.app, ctx.auth.user); | |
| 1522 | return listed.ok ? json({ pins: listed.value.map((p) => ({ slug: p.slug, title: p.title, panel: p.panel })) }) : notFound(); | |
| 1524 | const listed = await timePhaseAsync(ctx.request, "boards", () => listPinnedPanels(ctx.app, ctx.auth.user)); | |
| 1525 | return timePhase(ctx.request, "render", () => | |
| 1526 | listed.ok ? json({ pins: listed.value.map((p) => ({ slug: p.slug, title: p.title, panel: p.panel })) }) : notFound()); | |
| 1523 | 1527 | }); |
| 1524 | 1528 | |
| 1525 | 1529 | router.add("POST", "/api/v1/boards/pins", async (ctx) => { |
| @@ -1548,10 +1552,12 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 1548 | 1552 | }); |
| 1549 | 1553 | |
| 1550 | 1554 | router.add("GET", "/api/v1/boards/:slug", async (ctx) => { |
| 1555 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 1551 | 1556 | const denied = boardsAdmin(ctx); |
| 1552 | 1557 | if (denied) return denied; |
| 1553 | const shown = await showBoard(ctx.app, ctx.auth.user, ctx.params.slug!); | |
| 1554 | return shown.ok ? json({ slug: ctx.params.slug, ...shown.value }) : notFound(); | |
| 1558 | const shown = await timePhaseAsync(ctx.request, "boards", () => showBoard(ctx.app, ctx.auth.user, ctx.params.slug!)); | |
| 1559 | return timePhase(ctx.request, "render", () => | |
| 1560 | shown.ok ? json({ slug: ctx.params.slug, ...shown.value }) : notFound()); | |
| 1555 | 1561 | }); |
| 1556 | 1562 | |
| 1557 | 1563 | // One panel's data (`boards/panel_data.ts`): `:id` is its place on the |
test/api-boards-timing.test.ts added +69
| @@ -0,0 +1,69 @@ | ||
| 1 | /** The API's board reads name their Server-Timing phases (#3936). */ | |
| 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 { writeBoard } from "../src/actions/boards.ts"; | |
| 8 | import { createSession } from "../src/core/sessions.ts"; | |
| 9 | import { createUser } from "../src/core/users.ts"; | |
| 10 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 11 | import { quietLogs } from "./helpers/globals.ts"; | |
| 12 | ||
| 13 | quietLogs(); | |
| 14 | ||
| 15 | let dir: string; | |
| 16 | let app: App; | |
| 17 | let servers: Servers; | |
| 18 | let cookie = ""; | |
| 19 | ||
| 20 | beforeAll(async () => { | |
| 21 | dir = await mkdtemp(join(tmpdir(), "forge-api-boards-timing-")); | |
| 22 | app = createApp({ dataDir: dir, port: 0 }); | |
| 23 | servers = await serve(app); | |
| 24 | const made = await createUser(app.db, { | |
| 25 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", | |
| 26 | password: "a-good-passphrase", adminIfFirst: true, | |
| 27 | }); | |
| 28 | if (!made.ok) throw new Error(String(made.error)); | |
| 29 | cookie = `forge_session=${createSession(app.db, made.value.id).token}`; | |
| 30 | const w = await writeBoard(app, made.value, "ops", { | |
| 31 | title: "Ops", range: "all", refresh: "off", | |
| 32 | panels: [{ title: "Runs", chart: "line", query: { source: "runs", aggregate: "count", groupBy: "kind" }, w: 1, h: 1 }], | |
| 33 | }); | |
| 34 | if (!w.ok) throw new Error(JSON.stringify(w.error)); | |
| 35 | }); | |
| 36 | ||
| 37 | afterAll(async () => { | |
| 38 | await servers.stop(); | |
| 39 | app.close(); | |
| 40 | await rm(dir, { recursive: true, force: true }); | |
| 41 | }); | |
| 42 | ||
| 43 | async function phases(path: string, withCookie = true): Promise<{ status: number; names: string[]; body: string }> { | |
| 44 | const res = await fetch(`${servers.url}${path}`, { headers: withCookie ? { cookie } : {} }); | |
| 45 | const body = await res.text(); | |
| 46 | const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!); | |
| 47 | return { status: res.status, names: names.slice(names.indexOf("app") + 1), body }; | |
| 48 | } | |
| 49 | ||
| 50 | test("each board read names session, boards, render in order", async () => { | |
| 51 | for (const path of ["/api/v1/boards", "/api/v1/boards/pins", "/api/v1/boards/ops"]) { | |
| 52 | const got = await phases(path); | |
| 53 | expect(got.status).toBe(200); | |
| 54 | expect(got.names).toEqual(["session", "boards", "render"]); | |
| 55 | } | |
| 56 | expect(JSON.parse((await phases("/api/v1/boards")).body).boards[0].slug).toBe("ops"); | |
| 57 | }); | |
| 58 | ||
| 59 | test("a missing board still names its phases", async () => { | |
| 60 | const got = await phases("/api/v1/boards/nope"); | |
| 61 | expect(got.status).toBe(404); | |
| 62 | expect(got.names).toEqual(["session", "boards", "render"]); | |
| 63 | }); | |
| 64 | ||
| 65 | test("a refusal keeps its status and names session", async () => { | |
| 66 | const got = await phases("/api/v1/boards", false); | |
| 67 | expect(got.status).toBe(401); | |
| 68 | expect(got.names).toEqual(["session"]); | |
| 69 | }); |
new above
offline