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

Merged opened this

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

Merged

`/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

opened this

/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.

  • Chart: src/boards/chart.ts draws the line chart as SVG with no dependencies. Axis labels are escaped and the chart has a fixed size, so nothing shifts.
  • Page: src/web/pages/boards.ts lists 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.
  • Access: only an admin sees it, and everyone else gets a 404. Admins get a Boards link in the header.
  • Name reserved: boards is now a reserved top-level name, since /boards/data owns it.
  • Tests: test/boards-page.test.ts is in GATE. Seeded runs render a path with the right point count, and a non-admin gets 404.
  • One failure outside this change: test/web-phone.test.ts failed 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.
  • Why the earlier push was rejected: my fork already held this branch built on an older main. I merged that in instead of force-pushing, so the branch carries a merge commit.

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

Changes requested — before the latest push

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) or null (for example by=issue_id on 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/fvalue with by = group. With fcolumn=state&fvalue=failed&by=kind, clicking the agent point 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.by in the table head is a ternary whose two branches are the same. Write p.by.
  • The rows view lists count grouped by the source's first column. For pulls that column is issue_id and for deploys it is id. 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.

commented

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 through toQuery and is bound as a number, because toQuery converts by the column's type. A point whose group is null now 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). Drawing by=kind filtered on state=failed and following each point's link lists only m1 for deploy and only m3 for check. A second test checks that by=turns narrows by number (gvalue=5 lists only m4) and that by=issue_id renders 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 --noEmit is clean.
  • test/boards-page.test.ts and test/boards-query.test.ts pass (39 tests).

Not verified

  • I haven't confirmed that /boards/data renders 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 fcolumn filter besides the point's group.

🤖 Generated with Claude Code

Approved

Approved. Every item in #1560's done list is met, and I found no security problem.

  • Identity and access: the page reads ctx.auth.user and 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 the SOURCES registry and binds every value. A bad query becomes an on-page refusal, not a 500.
  • Output: every param goes back out through the html template, attributes included, and there is a test with a hostile from. In chart.ts, labels are stripped of XML-illegal characters, clamped, then escaped with escapeXml, and hrefs are escaped. The SVG has a fixed viewBox and the box has an aspect-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, boards reserved, 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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs