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

Merged opened this

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

Merged

`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

opened this

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.

  • Boards source: usage in src/boards/sources.ts reads usage_count and is admin-only. Its owner is admin, so only an all-repos admin scope can query it. A test confirms an admin gets rows and a non-admin gets 403.
  • Usage page:

- 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 from src/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 /healthz and /readyz are not in the list, since they aren't router routes.

  • Test: the gate runs every test/*.test.ts automatically, so there is no GATE list to edit. test/usage-report.test.ts is 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:

  • Method: counts are per pattern, not per method, matching how usage_count is keyed.
  • Time bucketing: hour looks like 2026-10-16T13, which SQLite's bucketing can't read, so board queries on usage should group by a column such as pattern or role. from and to filters work as string bounds but drop the boundary hour. The source docs say so.
  • Pruning: usage_count is 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

Changes requested — before the latest push

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:

  1. No terminal door (fix this first). /settings/usage exists only on the page. The flags page, which this page copies, has src/tui/screens/flags.ts and a palette entry in src/tui/forge.ts:340 gated by mayManageFlags. Add src/tui/screens/usage.ts that renders the same routeUsage(app.db, patterns, new Date()) rows: unused first, never or the last hour, by role and by surface. Open it from the palette for mayManageFlags(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.
  2. 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, that usage is an admin-only boards source with hour as 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.

commented

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 and last 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.ts now has an account with is_admin and is_agent that gets 403 from /settings/usage.
  • README: one paragraph after the Flags one covers the /settings/usage page (Settings → Usage), unused routes listed first and why, and usage as an admin-only boards source with hour as 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.

Changes requested — before the latest push

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, render calls rows(), and so does key: state.key(e, rows().length).
  • Each call runs routeUsage, which does two GROUP BY passes over usage_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. Holding j means 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 both render and key. If you want a refresh, bind a key such as r to 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 on let known, and buildApiRouter has no doc comment.
  • Fix: move known and knownPatterns below buildApiRouter.

Smaller points, not blocking:

  • The doc comment at the top of usage_report.ts says "the day of its last use"; it is the hour.
  • The README says an agent is refused the usage source, 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.
commented

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.ts now runs routeUsage once, when the screen opens. Both render and key use that array, so a frame or a keypress no longer scans usage_count. There is no refresh key. Reopen the screen to recompute.
  • server.ts: known and knownPatterns now sit below buildApiRouter, so its doc comment is back on the function.
  • Doc comment: usage_report.ts now says "the hour of its last use", not "the day".
  • Boards source test: test/usage-report.test.ts now also asserts that an admin agent (carol) gets 403 from the usage source, so the README's claim is covered.
Approved — before the latest push

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.

commented

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.ts had two clashes with main. 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 types tell to 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.ts now passes 7 of 7.

To try it, open Settings → Usage as an admin (not an agent), or type "Usage" in the terminal palette.

Approved

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:

  1. usage_count is 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.
  2. The terminal screen imports http/server.ts to get knownPatterns. It is the first terminal module to do that. Moving knownPatterns into its own small module would keep the dependency pointing one way.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs