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

Board panels patch when a matching event arrives, not on a data-live-every timer #2875

Merged opened this

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

Merged

scout/forge:scout/2872-board-panels-patch-when-a-matching-event → main 2 commits fork 3 threads · 3 unresolved

  • Merged. scout/2872-board-panels-patch-when-a-matching-event was merged into main as fed1f58.

Review

Sign in to review this change.
3 files changed +19 −13
README.md +2 −2
@@ -1502,8 +1502,8 @@ work here.
15021502 by the admin who made it, and a file that does not validate is not shown. Make, rename,
15031503 duplicate, delete, add/edit/remove/reorder panels, or pin the query on `/boards/data`.
15041504 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.
1505 A board with a refresh redraws its grid on that beat through the live-region swap
1506 (`/boards/:slug/live`, etag'd). The same doors are in the API: `GET /api/v1/boards`, `GET|PUT|DELETE /api/v1/boards/:slug`
1505 A live board (any `refresh` but `off`; the form offers off or on, and `10s` and `1m` are still accepted and mean the same) redraws its grid when a run, pull, check, issue or usage event arrives, through the live-region swap
1506 (`/boards/:slug/live`, etag'd, one fetch per burst). The same doors are in the API: `GET /api/v1/boards`, `GET|PUT|DELETE /api/v1/boards/:slug`
15071507 (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.
15081508 - **Issues** — labels, threaded comments, close/reopen, markdown.
15091509 - **Pull requests** — open from a branch, a **beautiful diff viewer** (syntax
src/web/pages/boards-view.ts +11 −10
@@ -1,15 +1,16 @@
11 /**
22 * Boards, drawn: `/boards` lists them and makes one, `/boards/:slug` is the
33 * grid with each panel's chart and, under it, the panels' management (edit,
4 * move, remove) and the board's own (rename, range, refresh, duplicate,
4 * move, remove) and the board's own (rename, range, live, duplicate,
55 * delete). The grid is a live region with an address of its own
6 * (`/boards/:slug/live`): a board that refreshes is refetched on its beat by
7 * the same swap the wall uses, no client framework. Every form posts to
8 * `boards-edit.ts`. Anyone but a person who is an admin gets the not-found page.
6 * (`/boards/:slug/live`): a board that is live is refetched, by the same
outdated

This line is overlong, and the sentence should say the grid is refetched when a run, pull, check, issue or usage event arrives. Reflow it.

7 * swap the wall uses, when a run, pull, check, issue or usage event arrives.
8 * No client framework. Every form posts to `boards-edit.ts`. Anyone but a
9 * person who is an admin gets the not-found page.
910 */
1011 import { listAllBoards, showBoard } from "../../actions/boards.ts";
1112 import { CHART_NAMES } from "../../boards/chart.ts";
12 import { MAX_H, MAX_PANELS, MAX_TITLE, MAX_W, RANGES, REFRESHES, type Board, type Panel } from "../../boards/store.ts";
13 import { MAX_H, MAX_PANELS, MAX_TITLE, MAX_W, RANGES, type Board, type Panel } from "../../boards/store.ts";
1314 import type { App } from "../../app.ts";
1415 import type { Ctx } from "../../http/context.ts";
1516 import { liveRegionsJs } from "../client.ts";
@@ -21,7 +22,8 @@ import { flowPanel, flowPanelCss } from "./flow-panel.ts";
2122 import { cellsOf, chartBox, paramsOf, queryFields, readParams, runFor } from "./boards.ts";
2223
2324 const RANGE_MS = { "1h": 3_600_000, "24h": 86_400_000, "7d": 604_800_000, "30d": 2_592_000_000 } as const;
24 const EVERY = { "10s": 10, "1m": 60 } as const;
25 /** The events the panels' queries read (runs, pulls, checks, issues, usage): any of them may change a chart. */
26 const READS = "run.*,pull.*,check.*,issue.*,usage.*";
2527
2628 const option = (value: string, selected: boolean) =>
2729 html`<option value="${value}" ${selected ? raw("selected") : null}>${value}</option>`;
@@ -48,13 +50,12 @@ function drawPanel(app: App, board: Board, panel: Panel): Html {
4850
4951 /** The grid alone: what the page shows and what the live address answers with. */
5052 function grid(app: App, slug: string, board: Board): Html {
51 const every = board.refresh === "off" ? null : EVERY[board.refresh];
5253 return html`<section
outdated

refresh is now only on/off here, yet the settings form and README still present 10s/1m as an interval. Make the setting say what it does, and update README line 1505.

5354 class="bgrid"
5455 id="board-grid"
55 ${every === null
56 ${board.refresh === "off"
5657 ? null
57 : html`data-live="board.none" data-live-src="/boards/${slug}/live" data-live-every="${String(every)}"`}
58 : html`data-live="${READS}" data-live-src="/boards/${slug}/live"`}
5859 >
5960 ${board.panels.length === 0
6061 ? html`<p class="muted">No panels yet. Add one below.</p>`
@@ -145,7 +146,7 @@ function manage(slug: string, board: Board, adding: Html): Html {
145146 </form>
146147 <form method="post" action="/boards/${slug}/settings" class="bform">
147148 <label class="qf"><span>Range</span><select class="input" name="range">${RANGES.map((r) => option(r, r === board.range))}</select></label>
148 <label class="qf"><span>Refresh</span><select class="input" name="refresh">${REFRESHES.map((r) => option(r, r === board.refresh))}</select></label>
149 <label class="qf"><span>Live</span><select class="input" name="refresh"><option value="off" ${board.refresh === "off" ? raw("selected") : null}>off</option><option value="10s" ${board.refresh === "off" ? null : raw("selected")}>on: redraw when events arrive</option></select></label>

Nit: the form posts the legacy value 10s for on. It works, but see the follow-up: make the store speak off|on.

149150 ${btn("Save settings")}
150151 </form>
151152 <form method="post" action="/boards/${slug}/duplicate" class="bform">${btn("Duplicate")}</form>
test/boards-store.test.ts +6 −1
@@ -172,8 +172,13 @@ describe("/boards", () => {
172172 const saved = await post("/boards/fleet-health/settings", { range: "24h", refresh: "10s" });
173173 expect(saved.status).toBe(303);
174174 const body = await (await asAdmin("/boards/fleet-health")).text();
175 expect(body).toContain('data-live-every="10"');
175 const grid = body.match(/<section[^>]*id="board-grid"[^>]*>/)?.[0] ?? "";
176 expect(grid).not.toContain("data-live-every");
177 expect(grid).toMatch(/data-live="[^"]+"/);
178 expect(grid).toContain('data-live="run.*,pull.*,check.*,issue.*,usage.*"');
176179 expect(body).toContain('data-live-src="/boards/fleet-health/live"');
180 expect(body).toMatch(/<option value="10s" selected>on: redraw when events arrive<\/option>/);
181 expect(body).not.toContain(">1m</option>");
177182 const live = await asAdmin("/boards/fleet-health/live");
178183 expect(live.status).toBe(200);
179184 const text = await live.text();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs