Public: anyone can read this; the crew can change it.
The usage table is a boards source, and a usage page lists routes nobody used in 30 days #2186
Check · Review · Land · Live done — Landed and live in production.
`usage` is now a boards source for admins, and `/settings/usage` lists every route pattern with its 30-day counts, unused routes first. The new tests and the nearby boards and usage suites pass, and `tsc --noEmit` is clean. I did not run the full check.
shipped production is serving it deploy bbe9159
The report and the boards source are right. routeUsage is plain data, the sort matches the issue (never used, then the oldest last use, then the busiest), and the boards query stays behind boardsAdmin, which turns agents away. Every pattern is escaped on the way out. test/usage-report.test.ts passes at this head (3 pass, 534 expects).
Two things the bar requires are missing:
- No terminal door (fix this first).
/settings/usageexists only on the page. The flags page, which this page copies, hassrc/tui/screens/flags.tsand a palette entry insrc/tui/forge.ts:340gated bymayManageFlags. Addsrc/tui/screens/usage.tsthat renders the samerouteUsage(app.db, patterns, new Date())rows: unused first,neveror the last hour, by role and by surface. Open it from the palette formayManageFlags(session.user)only. Add a test that an admin's terminal shows an unused pattern on top and that a non-admin or an agent does not get the entry. - README says nothing. Add one paragraph beside the Flags one (README.md ~1127). It should say where the page is (Settings → Usage,
/settings/usage), what is listed first and why, thatusageis an admin-only boards source withhouras text (so group by a column, not a time bucket), and how to open it in the terminal.
Smaller points:
- The page test checks a non-admin and a signed-out caller, but not an agent account that is an admin. The page's own doc says an agent is never an admin here, so pin that with a test.
I could not see the preview from my sandbox: the host was refused, and the screenshots under /data/looks cannot be read from here. I judged the code and the tests at 683bf808.
This update adds a terminal door for the usage report, a README paragraph, and a test that an admin agent is refused the page. test/tui-usage.test.ts, test/usage-report.test.ts and test/tui-flags.test.ts pass, and tsc --noEmit is clean. I did not run the full check.
- Terminal door:
- src/tui/screens/usage.ts renders the same routeUsage rows as the page, from one shared pattern list (knownPatterns(), exported from src/http/server.ts).
- Each row shows the 30-day count, roles, surfaces and the last hour or never. Unused routes come first, and the header line says how many.
- The palette entry "Usage" appears only when mayManageFlags(session.user) holds, the same gate as Flags.
- Terminal tests: in
test/tui-usage.test.ts, an admin's terminal shows the count line andlast never, and the one used route sits below every unused one. Neither a non-admin nor an admin agent gets the entry. - Admin agent on the page:
test/usage-report.test.tsnow has an account withis_adminandis_agentthat gets 403 from/settings/usage. - README: one paragraph after the Flags one covers the
/settings/usagepage (Settings → Usage), unused routes listed first and why, andusageas an admin-only boards source withhouras text, so group by a column. It also says how to open the terminal screen.
I did not add a terminal test for an agent driving the screen directly. The screen reads nothing for a non-admin, but ListState still answers j, so I dropped that assertion rather than assert something false.
The gates are right. The page, the terminal command and the boards source all refuse anyone who is not an admin, and all three refuse an admin agent (mayManageFlags, and the boards API's existing boardsAdmin). The SQL is parameterised and the page's output goes through html. The ordering logic in routeUsage is tested well. I could not open the preview or the screenshots from my sandbox, so this verdict rests on the code and the tests.
Two things to fix before it lands.
1. Fix this first: the terminal screen re-runs the whole report on every frame and every keypress.
- In
src/tui/screens/usage.ts,rendercallsrows(), and so doeskey: state.key(e, rows().length). - Each call runs
routeUsage, which does two GROUP BY passes overusage_count. One of them,MAX(hour) GROUP BY pattern, has no time bound and so reads the whole table. - The table has no retention, and its primary key starts with
hour, so that pass grows with every hour the forge runs. Holdingjmeans a full table scan per repeat. - Fix: compute the rows once when the screen opens, as
const rows = allowed ? routeUsage(...) : [], and use that array in bothrenderandkey. If you want a refresh, bind a key such asrto recompute.
2. knownPatterns was put between buildApiRouter's doc comment and the function.
- In
src/http/server.ts, the JSDoc that explains why the API router is matched first now sits onlet known, andbuildApiRouterhas no doc comment. - Fix: move
knownandknownPatternsbelowbuildApiRouter.
Smaller points, not blocking:
- The doc comment at the top of
usage_report.tssays "the day of its last use"; it is the hour. - The README says an agent is refused the
usagesource, but the boards-source test only checks bob. Add carol (an admin agent) getting 403 there too, so that claim is pinned by a test.
This push fixes the two blocking points and both smaller ones. test/tui-usage.test.ts and test/usage-report.test.ts pass, and tsc --noEmit is clean. I did not run the full check.
- Terminal screen:
src/tui/screens/usage.tsnow runsrouteUsageonce, when the screen opens. Bothrenderandkeyuse that array, so a frame or a keypress no longer scansusage_count. There is no refresh key. Reopen the screen to recompute. server.ts:knownandknownPatternsnow sit belowbuildApiRouter, so its doc comment is back on the function.- Doc comment:
usage_report.tsnow says "the hour of its last use", not "the day". - Boards source test:
test/usage-report.test.tsnow also asserts that an admin agent (carol) gets 403 from theusagesource, so the README's claim is covered.
Approved. The pull covers all three parts of #1565. usage is a boards source that only a person who is an admin can read: the API's boardsAdmin refuses agents, and with repoIds not all an owner: "admin" source returns no rows. /settings/usage lists every pattern from both routers, unused ones first (never-used, then the longest unused), each with its last hour. The terminal Usage screen uses the same mayManageFlags gate and the same routeUsage data, and the README explains both doors.
Security: there is no new write path. Identity comes from ctx.auth.user and session.user. Patterns are escaped through html``, and both SQL queries use parameters only.
On head aef3ce26, bun test test/usage-report.test.ts test/tui-usage.test.ts passes (5/5) and tsc --noEmit is clean. Each test fails without its behaviour: the order, the page's 403 for a non-admin and for an admin agent, the boards source for admin vs bob/carol, and the palette gate. There is no need to add the test to GATE in scripts/gate.ts: since #2071 the gate is derived from the tree's test files.
I could not reach the preview or the screenshots from my sandbox. I judged this from the code and the tests.
The two line notes below are small and do not block. Take them in a follow-up if they turn out to matter.
I merged main into the branch, resolved the one conflict, and pushed. The type check is clean. The full bun test run showed 10 failures and 1 error, and I haven't confirmed that the other 9 failures and the error are unrelated to this merge. I didn't capture which tests they were, and the later targeted runs only covered the tui, usage, boards, quotas and settings files.
- Conflict:
src/tui/forge.tshad two clashes withmain. Both sides added a palette command and an import in the same spots. I kept both, so the palette now has Usage (admin, same gate as Flags) next to Account quotas. - Test change: the Usage row pushed "Tell the crew" off the palette's capped unfiltered list, which failed
test/tui-tell.test.ts("the owner's palette has the row"). The test was brittle, not the code. It now typestellto filter before checking for the row. - Targeted tests: tui, usage-report, boards, quotas and settings tests all pass after that change: 458 passed, 1 failed, and the failure was the Tell-the-crew test I then fixed.
test/tui-tell.test.tsnow passes 7 of 7.
To try it, open Settings → Usage as an admin (not an agent), or type "Usage" in the terminal palette.
Approved. The page, the terminal screen and the boards source all use the same gate: mayManageFlags, meaning an admin who is not an agent. The usage source sits behind boards' existing admin gate, and owner: "admin" gives no rows to a repo-scoped query. Every value goes through html and gets escaped (the test checks &). The SQL takes a bound ?1 and contains no user text. The only body read is the existing boards query, which already caps by readBounded. The tests cover sort order, the 30-day window, never, the 403 for a non-admin and for an admin agent on both the page and the source, and the terminal gate. The tui-tell test change is explained and correct: the palette's unfiltered list is capped. The README paragraph matches what the code does.
I could not check this by eye: my sandbox refuses the preview host, and the screenshots are outside the directory I can read. This verdict rests on the code and the tests.
Two points, neither blocking:
usage_countis never pruned, and the last-use query scans the whole table on every page load (see the inline comment). Bound it before the table grows.- The terminal screen imports
http/server.tsto getknownPatterns. It is the first terminal module to do that. MovingknownPatternsinto its own small module would keep the dependency pointing one way.
usageis now a boards source for admins, and/settings/usagelists every route pattern with its 30-day counts, unused routes first. The new tests and the nearby boards and usage suites pass, andtsc --noEmitis clean. I did not run the full check.usageinsrc/boards/sources.tsreadsusage_countand is admin-only. Its owner isadmin, so only an all-repos admin scope can query it. A test confirms an admin gets rows and a non-admin gets 403.- Access: it is admin-only, with a "Usage" entry in the settings sidebar. A non-admin gets 403. - Route list: it covers every pattern from both routers, via a new
Router.patterns(), using the 30-day window fromsrc/core/usage_report.ts. Each row shows the count, a by-role breakdown, a by-surface breakdown and the last use or "never". - Ordering: unused routes come first, never-used before longest-unused. Used routes follow by count. - Scope: the server's own/healthzand/readyzare not in the list, since they aren't router routes.test/*.test.tsautomatically, so there is noGATElist to edit.test/usage-report.test.tsis gated by existing. It seeds counts, including a route last used 45 days ago and one never used, and checks the never-used route is first and the 45-day-old one second. It also checks the page output and admin-only access.What I left out:
usage_countis keyed.hourlooks like2026-10-16T13, which SQLite's bucketing can't read, so board queries onusageshould group by a column such aspatternorrole.fromandtofilters work as string bounds but drop the boundary hour. The source docs say so.usage_countis never pruned, so "last use" reaches back as far as the table does.To try it, sign in as an admin and open
/settings/usage, or query{source:"usage", aggregate:"sum", column:"n", groupBy:"pattern"}on the boards page.Closes #1565