Public: anyone can read this; the crew can change it.

/settings/crew's Server-Timing names its phases: session, scorecards, spending, render #3235

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3231-settings-crew-s-server-timing-names-its → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/3231-settings-crew-s-server-timing-names-its was merged into main as b7b7ec0.

Review

Sign in to review this change.
2 files changed +110 −18
src/web/pages/crew.ts +67 −18
@@ -35,6 +35,7 @@ import {
3535 type Scorecard,
3636 type SplitScorecard,
3737 } from "../../core/scorecard.ts";
38 import { notePhase, takeSessionTime } from "../../core/route_timing.ts";
3839 import { listTeams, type Team } from "../../core/teams.ts";
3940 import { crewFor } from "../../crew/dispatch.ts";
4041 import { MODELS } from "../../crew/models.ts";
@@ -409,24 +410,58 @@ function spendingCard(
409410 );
410411 }
411412
412 function crewView(ctx: Ctx, values: AgentValues, error?: string): Html {
413 const listed = listAllAgents(ctx.app, ctx.auth.user);
414 const agents = listed.ok ? listed.value : [];
415 const totals = agentTotals(ctx.app.db);
416 const teams = listTeams(ctx.app.db);
417 const teamCards = teamScorecards(ctx.app.db);
418 const recentTeamCards = teamScorecards(ctx.app.db, recentWindow());
419 const scorecards = crewScorecards(ctx.app.db);
420 const recentScorecards = crewScorecards(ctx.app.db, recentWindow());
421 const splitScorecards = crewScorecardSplits(ctx.app.db);
422 const recentSplitScorecards = crewScorecardSplits(ctx.app.db, recentWindow());
423 // Where the money goes today: every app running on its own, its spend
424 // against its ceiling. The ceilings are the one control on the drain, and
425 // this is the page where the crew is wielded, so they are read here.
426 const spending = listAutonomousRepos(ctx.app.db).map((repo) => ({
413 /** What the scorecard and split cards read. */
414 function loadScorecards(ctx: Ctx) {
415 return {
416 totals: agentTotals(ctx.app.db),
417 teams: listTeams(ctx.app.db),
418 teamCards: teamScorecards(ctx.app.db),
419 recentTeamCards: teamScorecards(ctx.app.db, recentWindow()),
420 scorecards: crewScorecards(ctx.app.db),
421 recentScorecards: crewScorecards(ctx.app.db, recentWindow()),
422 splitScorecards: crewScorecardSplits(ctx.app.db),
423 recentSplitScorecards: crewScorecardSplits(ctx.app.db, recentWindow()),
outdated

The data default means two ways to load. If the POST path stays untimed, a one-line comment saying so would keep the next reader from wondering.

424 byModel: agentSplits(ctx.app.db, "model"),
425 byPrompt: agentSplits(ctx.app.db, "prompt_version"),
426 };
427 }
428
429 /**
430 * Where the money goes today: every app running on its own, its spend
431 * against its ceiling. The ceilings are the one control on the drain, and
432 * this is the page where the crew is wielded, so they are read here.
433 */
434 function loadSpending(ctx: Ctx) {
435 return listAutonomousRepos(ctx.app.db).map((repo) => ({
427436 repo,
428437 status: autonomyStatus(ctx.app, repo),
429438 }));
439 }
440
441 interface CrewData {
442 scorecards: ReturnType<typeof loadScorecards>;
443 spending: ReturnType<typeof loadSpending>;
444 }
445
446 function crewView(
447 ctx: Ctx,
448 values: AgentValues,
449 error?: string,
450 data: CrewData = { scorecards: loadScorecards(ctx), spending: loadSpending(ctx) },
451 ): Html {
452 const listed = listAllAgents(ctx.app, ctx.auth.user);
453 const agents = listed.ok ? listed.value : [];
454 const {
455 totals,
456 teams,
457 teamCards,
458 recentTeamCards,
459 recentScorecards,
460 splitScorecards,
461 recentSplitScorecards,
462 } = data.scorecards;
463 const { scorecards, byModel, byPrompt } = data.scorecards;
464 const { spending } = data;

Destructure data.scorecards once instead of twice; recentScorecards and scorecards can sit in the same statement.

430465 const usernameError =
431466 error !== undefined && /username/i.test(error) ? error : undefined;
432467 return html`${crewReadiness(ctx)}
@@ -532,13 +567,13 @@ function crewView(ctx: Ctx, values: AgentValues, error?: string): Html {
532567 ${splitsCard(
533568 "Per model",
534569 "Model",
535 agentSplits(ctx.app.db, "model"),
570 byModel,
536571 () => html`<span class="muted">default</span>`,
537572 )}
538573 ${splitsCard(
539574 "Per prompt version",
540575 "Prompt version",
541 agentSplits(ctx.app.db, "prompt_version"),
576 byPrompt,
542577 () => html`<span class="muted">not recorded</span>`,
543578 )}
544579 ${card(
@@ -622,7 +657,21 @@ const EMPTY: AgentValues = {
622657 export function crewPage(ctx: Ctx): Response {
623658 const denied = requireAdmin(ctx);
624659 if (denied) return denied;
625 return shell(ctx, "Crew", crewView(ctx, EMPTY));
660 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
661 let at = performance.now();
662 const scorecards = loadScorecards(ctx);
663 notePhase(ctx.request, "scorecards", performance.now() - at);
664 at = performance.now();
665 const spending = loadSpending(ctx);
666 notePhase(ctx.request, "spending", performance.now() - at);
667 at = performance.now();
668 const res = shell(
669 ctx,
670 "Crew",
671 crewView(ctx, EMPTY, undefined, { scorecards, spending }),
672 );
673 notePhase(ctx.request, "render", performance.now() - at);
674 return res;
626675 }
627676
628677 // POST /settings/crew
test/crew-timing.test.ts added +43
@@ -0,0 +1,43 @@
1 /** /settings/crew names its 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-ctime-"));
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 test("the crew page names session, scorecards, spending and render once each", async () => {
33 const res = await fetch(`${servers.url}/settings/crew`, { headers: { authorization: `Bearer ${bearer}` } });
34 await res.arrayBuffer();
35 expect(res.status).toBe(200);
36 const parts = (res.headers.get("server-timing") ?? "").split(", ");
37 const names = parts.map((p) => p.split(";")[0]!);
38 for (const n of ["session", "scorecards", "spending", "render"]) {
39 expect(names.filter((x) => x === n).length).toBe(1);
40 const part = parts.find((p) => p.startsWith(`${n};`))!;
41 expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN();
42 }
43 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs