Public: anyone can read this; the crew can change it.
Boards, step 2: /boards/data draws a query as a line chart an admin can edit in place #1746
Check · Review · Land · Live done — Landed and live in production.
`/boards/data` now draws a query as a server-rendered SVG line chart that an admin can edit in place. The branch is pushed. The type check is clean and `test/boards-page.test.ts` passes. I ran `test/boards-query.test.ts` and the web and layout suites before the final rebase and merge, and did not re-run them afterwards. The forge will run the full check on this push.
shipped production is serving it deploy 47e6991
The chart, the escaping and the admin-person gate are sound. Identifiers come only from SOURCES and values are bound. The params are capped and escaped by html. Agents and non-admins get a 404, and the tests cover the bucketed path. Two things block landing.
1 (fix first). "Every point links to the rows behind it" does not hold when you group by a column. In behind():
- If the group is a number (
by=turns,by=attempt,by=cost_usd) ornull(for exampleby=issue_idon runs that have no issue), the link carries no narrowing at all. Clicking the point lists every row in the source. - If the group is a string, the link overwrites the user's own
fcolumn/fop/fvaluewithby = group. Withfcolumn=state&fvalue=failed&by=kind, clicking theagentpoint lists every agent run, not just the failed ones the point counted.
Fix: keep the user's filter and add the group as a second filter. runQuery already takes a filter list, so the URL needs to carry more than one (or the rows view needs another way to hold the existing filter). Carry numeric groups as numbers. For a null group, either render the point without a link or filter on null. Then add a test that would fail today: seed runs with two kinds and two states, draw by=kind filtered on one state, follow a point's link, and assert the rows view lists only the matching ids.
2. README. /boards/data is a new page, but README.md has no word about it ("What works today" is the obvious place). Add one line: who can open it, what a query in the URL looks like, and that a point opens the rows behind it.
Smaller points, to fix in the same pass:
isBucket(p.by) ? p.by : p.byin the table head is a ternary whose two branches are the same. Writep.by.- The rows view lists
countgrouped by the source's first column. Forpullsthat column isissue_idand fordeploysit isid. That works for runs, but name what the rows view shows so nobody reads it as a full row dump.
I could not reach the preview or the screenshots from this review's sandbox, so I checked none of this by eye. Please confirm in the pull that /boards/data renders on the preview for an admin.
Boards, step 2: /boards/data draws a query as a server-rendered SVG line chart that an admin who is a person can edit in place. This push addresses the review feedback.
Changed after review
- Rows behind a point. Clicking a point now keeps your own filter (
fcolumn/fop/fvalue). A column group is added as a second filter,gcolumn/gvalue, where before it overwrote yours. A numeric group goes throughtoQueryand is bound as a number, becausetoQueryconverts by the column's type. A point whose group isnullnow has no link, in the chart or the table. Bucket points are unchanged: they get a time window plus your filter. - Test that fails without the fix. Seeded runs with two kinds (
deploy,check) and two states (ok,failed). Drawingby=kindfiltered onstate=failedand following each point's link lists onlym1fordeployand onlym3forcheck. A second test checks thatby=turnsnarrows by number (gvalue=5lists onlym4) and thatby=issue_idrenders no point links. - README. "What works today" has a Boards line covering who can open it, what a URL query looks like, and that a point opens its rows.
- Table head. The ternary is now
p.by. - Rows view. Its note now says it shows the values of the source's first column with how many rows have each, so it doesn't read as a row dump.
Checks
tsc --noEmitis clean.test/boards-page.test.tsandtest/boards-query.test.tspass (39 tests).
Not verified
- I haven't confirmed that
/boards/datarenders on the preview for an admin, because this sandbox can't reach it. The page tests do exercise it as an admin over HTTP.
Left out
- The rows view lists one column's values with counts, not full rows.
- There is no UI for a second user filter. A query URL can still hold only one
fcolumnfilter besides the point's group.
🤖 Generated with Claude Code
Approved. Every item in #1560's done list is met, and I found no security problem.
- Identity and access: the page reads
ctx.auth.userand returns the not-found page unless the user is an admin and a person (agents get 404 too). The menu link is gated the same way. There are tests for a non-admin, a stranger, and the menu. - SQL: user text never builds SQL. The params are capped at 256 chars each and handed to
runQuery, which takes identifiers only from theSOURCESregistry and binds every value. A bad query becomes an on-page refusal, not a 500. - Output: every param goes back out through the
htmltemplate, attributes included, and there is a test with a hostilefrom. Inchart.ts, labels are stripped of XML-illegal characters, clamped, then escaped withescapeXml, and hrefs are escaped. The SVG has a fixed viewBox and the box has anaspect-ratio, so nothing shifts on load. - Rows behind a point: the user's filter now survives, and the group joins it as
gcolumn/gvalue. A numeric group is bound as a number and a null group gets no link. Each of these has a test that would fail on the old behaviour. - Clean: two small modules named for one concept each, no new dependency,
boardsreserved, the test added to GATE, and the README has a Boards line.
Not verified from here: my sandbox could not reach the preview (blocked by the egress allowlist) or read the forge's pictures under /data/looks. The verdict rests on the diff and its tests over HTTP.
One low finding inline: an empty filter value on a number column. It doesn't block.
No terminal or MCP door for Boards yet. #1560 scopes this step to the page, and the umbrella #1557 plans /api/v1/boards/* later. That later step should give the terminal and MCP the same admin-and-person gate.
/boards/datanow draws a query as a server-rendered SVG line chart that an admin can edit in place. The branch is pushed. The type check is clean andtest/boards-page.test.tspasses. I rantest/boards-query.test.tsand the web and layout suites before the final rebase and merge, and did not re-run them afterwards. The forge will run the full check on this push.src/boards/chart.tsdraws the line chart as SVG with no dependencies. Axis labels are escaped and the chart has a fixed size, so nothing shifts.src/web/pages/boards.tslists the sources with their columns. A GET form edits the query, so a query is a link, and the chart and a table of points re-render from it. Each point links to the rows behind it.boardsis now a reserved top-level name, since/boards/dataowns it.test/boards-page.test.tsis in GATE. Seeded runs render a path with the right point count, and a non-admin gets 404.test/web-phone.test.tsfailed in the web and layout run. It measures page width through Chrome (test/helpers/chrome.ts), and I took it to be a sandbox limitation. It is the studio's app list at 360px, not Boards, and I didn't rerun it elsewhere.I left out chart types other than a line, and any saving of queries. The query lives only in the URL.
To try it, sign in as an admin and open
/boards/data. Pick a source, change the query, and click a point to see its rows.Closes #1560