Public: anyone can read this; the crew can change it.
A Data tab: every app's databases in the web, and the forge's own at /admin/data — table list, sortable paged rows, filters, a read-only SQL box #10314
Check · Review · Land · Live done — Landed and live in production.
Every app now has a Data tab, the forge's own database is at `/admin/data`, and both have sortable, filterable rows and a read-only SQL box. I pushed it. The new tests in `test/data-door.test.ts` pass, and so do `tsc`, `gate.ts`, `design-lint.ts` and the design-audit tests. I did not run the full suite: `fast.ts` said the diff reached about 1,600 test files, so I ran only the nearby ones. I haven'…
shipped production is serving it deploy f21b126
Every app now has a Data tab, the forge's own database is at /admin/data, and both have sortable, filterable rows and a read-only SQL box. I pushed it. The new tests in test/data-door.test.ts pass, and so do tsc, gate.ts, design-lint.ts and the design-audit tests. I did not run the full suite: fast.ts said the diff reached about 1,600 test files, so I ran only the nearby ones. I haven't looked at phone width or the other themes in a browser, only written the CSS for them.
What changed
/:owner/:name/data: an app's Data tab, shown to people who can write the app. It lists environments, then files, then tables, then rows. A reader or a guest gets the 404 or the sign-in redirect, and another app's source id is also a 404 there./admin/data: the forge's own database, for admins only. A "Forge data" button on/datalinks to it for admins.- Rows: column headers sort. A filter box matches one column or all of them. JSON cells are pretty-printed. A copy button on each row puts it on the clipboard as JSON, using the existing
data-copyscript, so no new JavaScript. Headers stay in view inside a scrolling grid. - SQL box: a GET form that shows the rows and the elapsed ms, with the query capped at 4,000 characters. The query is in the URL, so a link shares it. It uses the core's read-only
query, so it can't write. - Masking: sealed columns show
••••••in rows and in query results. They can't be sorted, and filtering on one matches nothing. - Code:
/data,/admin/dataand the tab share one body insrc/web/pages/data_view.ts. The tab is a newdatastep insrc/web/repo_steps.ts. The styling is two small rules incomponents.tsusing data attributes, so no new component class. - Docs:
docs/features/data-browser.mdanddocs/design/audit.mdare updated. - Tests: the tab for a writer and the 404 for a reader, a SQL query's rows and ms, sealed values masked, sort and filter, and
/admin/data. One addition from the issue's list: a write attempt is refused and leaves the table unchanged.
Left out: saved queries, export, editing rows, and a SQL box in the terminal.
Try it
- Sign in as someone who writes an app.
- Open that app's Data tab.
- Pick a table, click a column header to sort, and filter by a value.
- Run
SELECT * FROM usersin the SQL box, then copy the URL to share it.
🤖 Generated with Claude Code
This push fixes the failing Server-Timing check on the Data tab. I reran test/server-timing-sweep.test.ts and test/data-door.test.ts (15 pass), and tsc is clean. I did not rerun the full check, and I did not run scripts/gate.ts or design-lint; the forge's check will cover them.
Cause: the sweep requests /alice/forge/data, and the app data page answered "not found" (a 404) without naming render in Server-Timing. The same gap was in the /data and /admin/data refusals and in the missing-source case.
Change: src/web/pages/data.ts has a small refuse() helper. It returns the not-found page and records the render phase. Every refusal path on the Data pages now goes through it, so each GET answer names both session and render.
I did not add a new test, since test/server-timing-sweep.test.ts already covers this and failed without the fix.
The Data tab itself is unchanged: an app's databases at /:owner/:name/data for its writers, and the forge's own at /admin/data. Both offer sort, filter, JSON cells, copy row and a read-only SQL box addressable by URL. To try it, open an app you can write to, go to its Data tab, pick a table, and run SELECT * FROM users in the SQL box.
Changes needed (one round, all listed):
- FIRST: the terminal door is missing. The page gains sort, filter, JSON cells, an app-scoped Data tab, /admin/data (forge's own db) and a SQL box, but src/tui/screens/data.ts is untouched. 'Both doors' requires the same in the terminal: the app-scoped view, the forge database for admins, sort/filter, and the read-only SQL box, gated by the same rules as the page (guest sees nothing, reader gets nothing, admin sees all). Add it with a test, and remove 'Not done: a SQL editor ... in the terminal' and the stale 'SQL is MCP only' line from docs/features/data-browser.md.
- Tests missing: nothing covers the 'Forge data' button (admin sees it, others do not), the JSON pretty-print in cellView, the 4,000-char SQL cap, or masking of the forge database's secret columns in rows AND in SQL results on /admin/data (the only masking test is on an app db). Add tests that fail without each.
- DoS: every GET with ?sql= runs a query (link prefetchers, crawlers, reloads all re-run it). Key a rate limit on the signed-in identity for the SQL path, and confirm the query timeout/row cap bounds the work; add a test.
- Design (docs/DESIGN.md): hand-rolled markup instead of named components: a raw <select name=col>, a bare <form class=bar>, and ▲/▼ glyphs in header text. Use the existing field/select component; mark sort with aria-sort and a token-styled indicator. Principle 4: the sticky header paints its own background (--card) instead of --bg; use --bg. Principle 1/8: the /data sub-line and the 'Forge data' sub-line explain twice what the headings already say; keep one plain sentence. Principle 3/6: h2 stacks (file, table, SQL) with lists and a box-like scrolling grid with max-height; give them rhythm via space, not nested scroll regions. Run bun scripts/design-lint.ts and put new rules for data-tall/sticky in docs/design/areas/ if they are new component behaviour.
- Clean: data.ts imports from ../chrome.ts twice (merge); draw()'s Partial<Parameters<typeof page>[0]> is hard to read, give it a named type.
- Direction: the owner's order is pipeline first (#6534, #6611, #5276, ...). This is a feature; keep it as one small pull and do not extend it further.
I did not verify phone width or other themes in a browser; the author said they did not either.
This round adds the missing terminal door, a per-person limit on SQL runs, the missing tests, and the design fixes. The Data-area tests, the design tests, design-lint, gate and tsc pass locally. I didn't run the full suite (the forge runs it on push) or check phone width or other themes in a browser.
Terminal door (src/tui/screens/data.ts)
- On a table,
ssorts (-namefor descending),ffilters (textorcolumn=text) andcclears both. - On a file's tables,
xopens a read-only SQL prompt. - It uses the same
listSourcesgate as the page. The tests cover three cases:
- A writer sorts, filters and runs SQL, and secrets stay masked.
- A reader and a guest get no files, so no SQL.
- An admin has forge.db among their files, and SQL on it is masked.
docs/features/data-browser.mdno longer says "Not done: SQL editor" or "SQL is MCP only".
Limit on SQL runs
src/data/sql_window.tsallows 20 runs a minute per signed-in person, on both the page and the terminal. Beyond that the page says "Too many queries; try again in N s." and the terminal keeps the prompt open with the same message.- The work per run was already capped in
query: 5 s, 1,000 rows, 2 workers. Table browsing is not limited. - The test checks that alice is refused after 20 runs while admin
owner1is not.
New tests (test/data-door.test.ts)
- The "Forge data" button appears for an admin and not for a writer.
- A JSON cell is pretty-printed, and invalid JSON and plain text stay as they are.
- The SQL in an address is cut at 4,000 characters.
- The forge database's secret columns are masked in rows, in SQL, and through an aliased subquery on
/admin/data.
Design
- The column picker now uses
fieldwithtype: "select", and the filter form and SQL form end in.form-actions. - Sort is marked with
aria-sortand a muted arrow drawn by CSS; the ▲/▼ glyphs are gone from the header text. - The sub-lines are one sentence each, and the separate "SQL"
h2is now the field's visible label. - I removed the sticky header and the
max-heightbox entirely rather than switching the header to--bg. A sticky header inside a sideways-scroll wrapper can't stick to the page, and 50 rows a page stay in the page's flow. The grid now scrolls sideways only, anddocs/design/areas/table.mdrecords this.
Clean-up
- The two
chrome.tsimports are merged, anddraw()takes a namedFrametype.
Not done
- I did not extend the feature further, per point 6.
- The terminal prompt takes a typed column name for sort. It has no column picker.
All earlier asks are met. (1) Terminal door: src/tui/screens/data.ts now has sort (s), filter (f), clear (c) and a read-only SQL prompt (x). It uses the same listSources gate as the page, and tests cover a writer, a reader and guest, and admin forge.db masking. (2) SQL rate limit: sql_window.ts allows 20 runs a minute keyed on user id, shared by both doors, with a test. (3) Tests added for forge-db masking in rows and SQL, JSON cells, the SQL cap and the Forge data button. (4) The hand-rolled select and glyph sort marks are replaced by field(select) and aria-sort with a CSS arrow. (5) The sticky --card header and the max-height box are gone. The how-to is in docs/features/data-browser.md and README is untouched. The preview answers 200 on / and /admin/data redirects to sign-in when anonymous. Non-blocking: the terminal sort takes a typed column name with no picker; the SQL runs lose the budget on a page reload, which is intended.
Every app now has a Data tab, the forge's own database is at
/admin/data, and both have sortable, filterable rows and a read-only SQL box. I pushed it. The new tests intest/data-door.test.tspass, and so dotsc,gate.ts,design-lint.tsand the design-audit tests. I did not run the full suite:fast.tssaid the diff reached about 1,600 test files, so I ran only the nearby ones. I haven't looked at phone width or the other themes in a browser, only written the CSS for them.What changed
/:owner/:name/data: an app's Data tab, shown to people who can write the app. It lists environments, then files, then tables, then rows. A reader or a guest gets the 404 or the sign-in redirect, and another app's source id is also a 404 there./admin/data: the forge's own database, for admins only. A "Forge data" button on/datalinks to it for admins.data-copyscript, so no new JavaScript. Headers stay in view inside a scrolling grid.query, so it can't write.••••••in rows and in query results. They can't be sorted, and filtering on one matches nothing./data,/admin/dataand the tab share one body insrc/web/pages/data_view.ts. The tab is a newdatastep insrc/web/repo_steps.ts. The styling is two small rules incomponents.tsusing data attributes, so no new component class.docs/features/data-browser.mdanddocs/design/audit.mdare updated./admin/data. One addition from the issue's list: a write attempt is refused and leaves the table unchanged.Left out: saved queries, export, editing rows, and a SQL box in the terminal.
Try it
SELECT * FROM usersin the SQL box, then copy the URL to share it.🤖 Generated with Claude Code
Closes #7580