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

Boards, step 6b: the Releases and Apps starting boards #2473

Merged opened this

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

Merged

scout/forge:scout/2459-boards-step-6b-the-releases-and-apps-sta → main 4 commits fork 5 threads · 5 unresolved

  • Merged. scout/2459-boards-step-6b-the-releases-and-apps-sta was merged into main as a7e80a9.

Review

Sign in to review this change.
5 files changed +77 −6
README.md +1 −1
@@ -1247,7 +1247,7 @@ work here.
12471247 `forge/boards`, a repo the forge owns and makes on the first boot with an admin; every save is a commit
12481248 by the admin who made it, and a file that does not validate is not shown. Make, rename,
12491249 duplicate, delete, add/edit/remove/reorder panels, or pin the query on `/boards/data`.
1250 The first boot also writes four starting boards, **Usage**, **Crew**, **Security** (sign-ins, failed sign-ins, tokens by scope, admin actions, from the admin-only `signins`, `tokens` and `adminActions` sources and `usage`; no token text, hash, payload or IP) and **Throughput** (items entering each stage from issue to live, per hour, from the `stages` source), once each: `.offered` in that repo records them, so an edit stays and a deletion sticks.
1250 The first boot also writes six starting boards, **Usage**, **Crew**, **Releases**, **Apps**, **Security** and **Throughput**. Apps' per-app panels show repo ids until a source exposes a safe name; Security (sign-ins, failed sign-ins, tokens by scope, admin actions, from the admin-only `signins`, `tokens` and `adminActions` sources and `usage`) shows no token text, hash, payload or IP; Throughput is items entering each stage from issue to live, per hour, from the `stages` source. Each is written once: `.offered` in that repo records them, so an edit stays and a deletion sticks.
12511251 A board with a refresh redraws its grid on that beat through the live-region swap
12521252 (`/boards/:slug/live`, etag'd). The same doors are in the API: `GET /api/v1/boards`, `GET|PUT|DELETE /api/v1/boards/:slug`
12531253 (an agent or anyone but an admin is 403; a panel whose query cannot run is refused with 422). The same four doors are MCP tools (`boards`, `board_write`, `board_delete`; `boards` with a `slug` reads one) and, for the same person, a **Boards** screen in the terminal that lists boards and shows a board's panels as text. No dependency was added.
src/boards/sources.ts +16
@@ -82,6 +82,22 @@ export const SOURCES: Readonly<Record<string, Source>> = {
8282 preview_data: "text", created_at: "time",
8383 },
8484 },
85 // One row per awake interval of an app environment (`core/meter.ts`): it
86 // wakes when the row begins and sleeps when it ends, so the two are two
87 // sources over one table, each timed by its own edge.
88 wakes: {
outdated

env is a free-text environment slug, which the header of this file says is never listed. Remove it here and in sleeps below.

89 table: "meter",
90 owner: "repo_id",
91 time: "started_at",
92 columns: { repo_id: "text", placement: "text", started_at: "time", ended_at: "time" },
93 },
94 sleeps: {
95 table: "meter",
96 owner: "repo_id",
97 where: "ended_at IS NOT NULL",
98 time: "ended_at",
99 columns: { repo_id: "text", placement: "text", started_at: "time", ended_at: "time" },
100 },
85101 decisions: {
86102 table: "decision",
87103 owner: "repo_id",
src/boards/starting.ts +28 −1
@@ -1,5 +1,5 @@
11 /**
2 * The boards the forge offers a new install: Usage, Crew, Security and Throughput. They are plain
2 * The boards the forge offers a new install: Usage, Crew, Releases, Apps, Security and Throughput. They are plain
33 * boards in `forge/boards` once written, so a person edits or deletes them
44 * like any other; `.offered` (a JSON list of slugs, committed with the
55 * boards) records what was offered so a deletion sticks and an edit is
@@ -43,6 +43,33 @@ export const STARTING_BOARDS: Readonly<Record<string, Board>> = {
4343 panel("Requests per hour", "bar", { source: "usage", aggregate: "sum", column: "n", groupBy: "hour", series: "status" }, 2),
4444 ],
4545 },
46 releases: {
47 title: "Releases",
48 range: "30d",
49 refresh: "1m",
50 panels: [
51 panel("Deploys per day", "bar", { source: "deploys", aggregate: "count", groupBy: { bucket: "day" }, series: "state" }, 2),
52 panel("Deploys by state", "bar", { source: "deploys", aggregate: "count", groupBy: "state" }),
53 panel("Deploys gone live per day", "line", {
54 source: "deploys", aggregate: "count", groupBy: { bucket: "day" }, filter: [{ column: "serving_at", op: "gt", value: "" }],
55 }),
56 panel("Slowest decision per day (ms)", "line", { source: "decisions", aggregate: "max", column: "ms", groupBy: { bucket: "day" } }, 2),
57 panel("Decider time per hour (ms)", "line", { source: "decisions", aggregate: "avg", column: "ms", groupBy: HOUR, series: "kind" }, 2),
58 ],
59 },
outdated

Series by env reads a free-text slug back out. Drop the series, or use placement.

60 apps: {
61 title: "Apps",
62 range: "7d",
63 refresh: "1m",
64 panels: [
65 panel("Apps made per day", "bar", { source: "apps", aggregate: "count", groupBy: { bucket: "day" }, series: "access" }),
66 panel("Apps by access", "bar", { source: "apps", aggregate: "count", groupBy: "access" }),
outdated

Repo ids as group labels are opaque on a phone. This is fine for now because the title says so, but a name column would be the first improvement.

67 panel("Wakes per app (by repo id)", "bar", { source: "wakes", aggregate: "count", groupBy: "repo_id", series: "placement" }, 2),
68 panel("Sleeps per app (by repo id)", "bar", { source: "sleeps", aggregate: "count", groupBy: "repo_id", series: "placement" }, 2),
69 panel("Wakes per hour", "line", { source: "wakes", aggregate: "count", groupBy: HOUR }, 2),

Repo ids on the axis are opaque to a person. Once a source exposes a safe app name, group by that and drop "(by repo id)" from the titles.

70 panel("Requests per hour by surface", "bar", { source: "usage", aggregate: "sum", column: "n", groupBy: "hour", series: "surface" }, 2),
71 ],
72 },
4673 crew: {
4774 title: "Crew",
4875 range: "7d",
test/boards-query.test.ts +28
@@ -354,3 +354,31 @@ describe("boards API", () => {
354354 expect(((await res.json()) as { error: string }).error).toBe("too_large");
355355 });
356356 });
357
358 describe("wakes and sleeps", () => {
359 const wakes = { source: "wakes", aggregate: "count", groupBy: "repo_id" };
360 const sleeps = { source: "sleeps", aggregate: "count", groupBy: "repo_id" };
361 beforeAll(() => {
362 db.run(
363 `INSERT INTO meter (repo_id, env, placement, started_at, beat_at, ended_at) VALUES
364 ('r1','production','owned','2026-10-05T10:00:00.000Z','2026-10-05T11:00:00.000Z','2026-10-05T11:00:00.000Z'),
365 ('r1','pull-1','owned','2026-10-06T10:00:00.000Z','2026-10-06T10:30:00.000Z',NULL),
366 ('r2','production','cloud','2026-10-05T12:00:00.000Z','2026-10-05T13:00:00.000Z','2026-10-05T13:00:00.000Z')`,
367 );
368 });
369 test("a wake counts every interval, open or ended, by when it began", () => {
370 expect(runQuery(db, wakes, ALL)).toEqual({ ok: true, rows: [{ group: "r1", value: 2 }, { group: "r2", value: 1 }] });
371 expect(runQuery(db, { ...wakes, from: "2026-10-06T00:00:00Z" }, ALL)).toEqual({ ok: true, rows: [{ group: "r1", value: 1 }] });
372 });
373 test("a sleep counts only the ended interval, by when it ended", () => {
374 expect(runQuery(db, sleeps, ALL)).toEqual({ ok: true, rows: [{ group: "r1", value: 1 }, { group: "r2", value: 1 }] });
375 expect(runQuery(db, { ...sleeps, from: "2026-10-05T12:00:00Z" }, ALL)).toEqual({ ok: true, rows: [{ group: "r2", value: 1 }] });
376 });
377 test("the repo scope narrows both", () => {
378 expect(runQuery(db, wakes, R1)).toEqual({ ok: true, rows: [{ group: "r1", value: 2 }] });
379 expect(runQuery(db, sleeps, R1)).toEqual({ ok: true, rows: [{ group: "r1", value: 1 }] });
380 });
381 test("the environment slug is not a column", () => {
382 expect(runQuery(db, { ...wakes, series: "env" }, ALL).ok).toBe(false);
383 });
384 });
test/boards-starting.test.ts +4 −4
@@ -1,5 +1,5 @@
11 /**
2 * The forge offers the Usage, Crew, Security and Throughput boards on boot: every panel runs, an
2 * The forge offers the six starting boards on boot: every panel runs, an
outdated

Typo: 'the the four starting boards boards'. Also add a test of the wakes and sleeps sources against seeded meter rows.

33 * edit survives the next boot, and a deletion is not undone.
44 */
55 import { afterAll, beforeAll, expect, test } from "bun:test";
@@ -36,10 +36,10 @@ test("a forge with no person to found the repo writes nothing", async () => {
3636 expect(await offerStartingBoards(bare)).toEqual([]);
3737 });
3838
39 test("a fresh boot writes Usage, Crew, Security and Throughput, and every panel runs", async () => {
40 expect((await offerStartingBoards(app)).sort()).toEqual(["crew", "security", "throughput", "usage"]);
39 test("a fresh boot writes the six starting boards, and every panel runs", async () => {
40 expect((await offerStartingBoards(app)).sort()).toEqual(["apps", "crew", "releases", "security", "throughput", "usage"]);
4141 const boards = await listBoards(app.reposRoot);
42 expect(boards.map((b) => b.slug)).toEqual(["crew", "security", "throughput", "usage"]);
42 expect(boards.map((b) => b.slug)).toEqual(["apps", "crew", "releases", "security", "throughput", "usage"]);
4343 let panels = 0;
4444 for (const { board } of boards) {
4545 for (const p of board.panels) {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs