feat(web): usage dashboard — put the token/cost ledger on a screen (AGT-4289) - #613
Merged
Conversation
…GT-4289)
`/api/usage` has served six axes (model · stage · task · project · adapter ·
day) since AGT-4178 and had no frontend reader; the only consumer was the
`openswarm cost` CLI. The dashboard's sole cost display was an SSE counter that
accumulated within a session and reset on refresh.
Two defects found while this was being written were each a single number on an
axis the endpoint already served, and both were found by hand-querying the
ledger over ssh rather than by looking at anything:
- AGT-4286: the draft stage at a 30.5% cache hit rate while every other stage
sat at 83-88%.
- AGT-4288: 13,287 draft calls across 239 tasks — 55.6 per task.
The endpoint groups by ONE axis per request, so the page fetches several in
parallel rather than changing the aggregate. No backend change.
The derived columns are the point. Cache rate, cost per call and calls per task
do not exist in the rows the API returns, and they are what carry the signal:
on the ledger this was written against, `qwen3-235b` shows a 0.0% cache rate
beside `deepseek-v4-flash` at 82.7%.
Numbers the page refuses to state rather than state wrongly:
- a rate with no prompt tokens behind it, and a share with no cost to divide,
render as `—`. Printing 0.0% would put an unmetered row beside the rows
that genuinely cost nothing relative to their peers — which is the exact
comparison this page exists to support.
- the unattributed bucket is not a task, so it is excluded from the task
ranking and from calls-per-task, and the note above that table says how
many calls and how much cost were left out.
- rows past the row cap are counted and priced in a note, because the project
axis returns 67 rows over 30 days and the panel draws 25.
Also collapses six duplicated page-shell route blocks in webAppRoutes.ts into a
PAGE_SHELLS table plus tryServePageShell, which is the first test coverage any
of those six routes has had, and widens `lint` to cover web/static/js — that
directory had never been linted by any gate in this repo, which is how a
412-line new file reached round three with dead code still in it. Widening it
surfaces 15 pre-existing warnings in other files; oxlint still exits 0.
Layout is measured, not reasoned: `auto-fit` answers a higher floor with more
tracks rather than wider ones, so a first attempt at the column-clipping fix
looked right and left 비용 off-screen at every desktop width. The floor is now
wider than the table it holds, and tests/web/usageLayout.test.ts is a tripwire
on the two declarations that were wrong before, since jsdom computes no layout
and no colour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
/api/usagehas served six axes (model · stage · task · project · adapter · day) since AGT-4178 and had no frontend reader — the only consumer was theopenswarm costCLI. The dashboard's sole cost display was an SSE counter that accumulated within a session and reset on refresh.Two defects found while this was being written were each a single number on an axis the endpoint already served, and both were found by hand-querying the ledger over ssh rather than by looking at anything:
by=stageby=stage+by=taskWhat it does
Fetches several axes in parallel; the endpoint groups by one axis per request. No backend change.
The derived columns are the point — cache rate, cost per call and calls per task do not exist in the rows the API returns. On the live ledger
qwen3-235bshows 0.0% cache besidedeepseek-v4-flashat 82.7%.Numbers it refuses to state rather than state wrongly
—. Printing0.0%would put an unmetered row beside rows that genuinely cost nothing relative to their peers — the exact comparison this page exists to support.Also in this PR
webAppRoutes.tsinto aPAGE_SHELLStable +tryServePageShell— the first test coverage any of those six routes has had.lintfromoxlint src/to includeweb/static/js/. That directory had never been linted by any gate in this repo, which is how a 412-line new file reached round three with dead code still in it. This surfaces 15 pre-existing warnings in other files (webToken.js×7,threadBoard.mjs,themeBoot.js,sessionPanel.mjs,diffPanel.mjs);usage.mjsitself is clean andoxlintexits 0, so CI is unaffected. Those 11 warnings are a follow-up, not a reason to keep the directory unlinted.Layout is measured, not reasoned
auto-fitanswers a higher floor with more tracks, not wider ones. The first attempt at the column-clipping fix raised the floor to 38rem, looked correct, and left 비용 off-screen at every desktop width:The floor is now wider than the table it holds — 7/7 at 1280/1440/1920/2560, no page-level horizontal scroll at any width including 390.
tests/web/usageLayout.test.tsis a tripwire on the two declarations that were wrong before, because jsdom computes neither layout nor colour.Verification
npx tsc --noEmitnpm run buildnpm run lintnpx vitest run(full)Review
Four rounds of layer-2 independent subagent review (
openswarm reviewis not run on my own changes, per the 2026-09-02 policy). Rounds 1-3 each returned REVISE and each found genuinely new defects; round 4 returned APPROVE with five findings, all filed as follow-ups, three of which are fixed here anyway.The one worth naming: round 3's R3-1 was introduced by my fix for round 2's finding of the same shape.
truncationNotesummed the aggregate's cost-ordered tail while the task panel re-sorted by calls insiderenderTable, so it printed$0.05for$50.00of hidden spend — worse than the silence it replaced.renderTablenow owns the ordering and reports what it hid;truncationNoteonly formats.Closes AGT-4289.
🤖 Generated with Claude Code