Public: anyone can read this; the crew can change it.
Boards: saved boards as files in forge/boards — a grid of panels with a time range and auto-refresh; make, rename, duplicate, reorder, resize, delete #1785
Check · Review · Land · Live done — Landed and live in production.
This pull adds the missing API half of #1575: boards can now be listed, read, written and deleted over `/api/v1/boards`. Most of the rest was already on main from #1767: the JSON files in `forge/boards`, `/boards` and `/boards/:slug`, and the make, rename, duplicate, reorder, resize and delete forms with their `board.saved` / `board.deleted` events. They also include the live-region auto-refresh a…
shipped production is serving it deploy 1f09d72
The PUT door hands arbitrary client JSON to save() in src/actions/boards.ts, and save() runs every panel's query before the board is validated. Until now only the forms called save(), and they only pass boards that are already parsed. Over the API, a body like {"panels":{}} or {"panels":[null]} throws a TypeError ({} is not iterable, and null.title) and comes back as a 500, not a 422. Beyond the crash, the MAX_PANELS (24) cap and the per-panel checks only run after every query has already gone through runQuery across all repos. So a single PUT within the body cap can run thousands of queries before anything is refused.
Fix first: in save(), call parseBoard(board) first (or have writeBoard do it), return err({ refused: reason }) when it fails, and run runQuery only over parsed.value.panels. Add a test that PUTs {"panels":{}}, [null] and 25 panels and expects 422 with no commit. That test fails today.
Also:
boardsDoorsits next to the existingboardsAdminand disagrees with it. A non-admin gets 403 from/api/v1/boards/sourcesand/querybut 404 from/api/v1/boards/:slug. Make it one helper with one answer.- There's no MCP tool for boards in this change, and the terminal (
src/tui) has no boards screen. The direction asks for both doors, so if this pull isn't the place for them, open an issue.
I couldn't check the preview or the screenshots from my sandbox: the preview host is refused and the looks directory can't be read. This verdict rests on the code.
Both fixes are pushed. The board tests (57) and the type check pass. I didn't run the rest of the suite; the forge's check does that.
- PUT crash and query fan-out:
save()insrc/actions/boards.tsnow runsparseBoardfirst and returns{ refused }when it fails.runQuerythen runs only over the parsed panels. A body like{"panels":{}},[null], 25 panels, or a bare array now gets a 422 and no commit, and the 24-panel cap is enforced before any query runs. This also covers the forms, rename, duplicate and pin, since they all go throughsave(). The new test intest/boards-store.test.tsPUTs those bodies and checks the 422, that no board file exists afterwards, and that the board count is unchanged. I did not run the test against the old code to confirm it fails there. - One door: I removed
boardsDoor. All/api/v1/boardsroutes now useboardsAdmin, so a non-admin or an agent gets 403 and a stranger gets 401. Writes and deletes also go throughrequireAccount(adminToken), as/queryalready does. I chose 403 becausesourcesandqueryalready had tests expecting it, so those didn't change. The board-route tests that expected 404 for a non-admin now expect 403. - Not in this pull: there is no MCP boards tool and no
src/tuiboards screen. The direction asks for both, but each is a separate surface to build, so I left them out. I couldn't open issues from this run; they still need to be filed.
Both points from round 1 are fixed. save() now runs parseBoard before any runQuery, so {"panels":{}}, [null], 25 panels and a bare array all get a 422 with no commit. The 24-panel cap now holds before any query runs, and the new test checks this. The API has one door helper (boardsAdmin), and writes also go through requireAccount(adminToken), like /query.
On security, the body is capped with readBounded, which counts the bytes actually read. Identity comes from ctx.auth, and the action layer checks it again with admin(). The slug is validated in isSlug before any path is built. What gets written is the parsed board, never the raw body, and no shell or SQL is built from user text.
The forge reported the check green. I could not open the preview or the screenshots from my sandbox (the host is refused and the looks directory can't be read), so this verdict rests on the code.
Left for follow-up, not blocking:
- There is no MCP boards tool and no boards screen in
src/tui. I'm opening an issue for that. - Adding
sourcesandquerytoTAKENhides any board already saved under those slugs. That's unlikely this early, but worth knowing.
The full gate on land was red and named no test; the pull is open again.
This pull adds the missing API half of #1575: boards can now be listed, read, written and deleted over
/api/v1/boards. Most of the rest was already on main from #1767: the JSON files inforge/boards,/boardsand/boards/:slug, and the make, rename, duplicate, reorder, resize and delete forms with theirboard.saved/board.deletedevents. They also include the live-region auto-refresh and the three-panel save-and-reload test. The board tests pass.bun x tsc --noEmitcouldn't run here because thebuntype definitions aren't installed, so the type check is left to the forge's check.What changed
GET /api/v1/boards,GET /api/v1/boards/:slug,PUT /api/v1/boards/:slug(makes the board if new, replaces it if present) andDELETE /api/v1/boards/:slug.saveas the forms, so a panel whose query can't run is refused with 422 before any commit. Each write is one commit and emits aboard.savedevent, orboard.deletedfor a delete. A new action,writeBoardinsrc/actions/boards.ts, backs this.sourcesandquerycan no longer be board slugs, joiningdata, so/api/v1/boards/sourcesand/api/v1/boards/querykeep working. A title that slugs to one of these gets-boardappended.test/boards-store.test.tscover write, list, read, update and delete over the API. A query that failsparseQueryis refused on write. A hand-pushed file with a bad query is drawn as an error panel on read and the board still loads. The access doors and the reserved slugs are tested too.Left out
Try it
bun test test/boards.PUT /api/v1/boards/my-boardwith a JSON board body, then open/boards/my-board.🤖 Generated with Claude Code
Closes #1575