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

Merged opened this in answer to

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

Merged

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

opened this

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 /data links 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-copy script, 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/data and the tab share one body in src/web/pages/data_view.ts. The tab is a new data step in src/web/repo_steps.ts. The styling is two small rules in components.ts using data attributes, so no new component class.
  • Docs: docs/features/data-browser.md and docs/design/audit.md are 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

  1. Sign in as someone who writes an app.
  2. Open that app's Data tab.
  3. Pick a table, click a column header to sort, and filter by a value.
  4. Run SELECT * FROM users in the SQL box, then copy the URL to share it.

🤖 Generated with Claude Code

Closes #7580

commented

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 /data links 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-copy script, 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/data and the tab share one body in src/web/pages/data_view.ts. The tab is a new data step in src/web/repo_steps.ts. The styling is two small rules in components.ts using data attributes, so no new component class.
  • Docs: docs/features/data-browser.md and docs/design/audit.md are 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

  1. Sign in as someone who writes an app.
  2. Open that app's Data tab.
  3. Pick a table, click a column header to sort, and filter by a value.
  4. Run SELECT * FROM users in the SQL box, then copy the URL to share it.

🤖 Generated with Claude Code

commented

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 requested — before the latest push

Changes needed (one round, all listed):

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

commented

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, s sorts (-name for descending), f filters (text or column=text) and c clears both.
  • On a file's tables, x opens a read-only SQL prompt.
  • It uses the same listSources gate 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.md no longer says "Not done: SQL editor" or "SQL is MCP only".

Limit on SQL runs

  • src/data/sql_window.ts allows 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 owner1 is 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 field with type: "select", and the filter form and SQL form end in .form-actions.
  • Sort is marked with aria-sort and 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" h2 is now the field's visible label.
  • I removed the sticky header and the max-height box 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, and docs/design/areas/table.md records this.

Clean-up

  • The two chrome.ts imports are merged, and draw() takes a named Frame type.

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

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs