Skip to content

feat(web): usage dashboard — put the token/cost ledger on a screen (AGT-4289) - #613

Merged
unohee merged 1 commit into
mainfrom
feat/usage-dashboard
Sep 10, 2026
Merged

feat(web): usage dashboard — put the token/cost ledger on a screen (AGT-4289)#613
unohee merged 1 commit into
mainfrom
feat/usage-dashboard

Conversation

@unohee

@unohee unohee commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Why

/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:

Issue The number Axis
AGT-4286 draft cache hit rate 30.5% while every other stage sat at 83-88% by=stage
AGT-4288 13,287 draft calls across 239 tasks — 55.6 per task by=stage + by=task

What 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-235b shows 0.0% cache beside deepseek-v4-flash at 82.7%.

Numbers it refuses to state rather than state wrongly

  • A rate with no prompt tokens behind it, and a share from a row that reported no price, render as . Printing 0.0% would put an unmetered row beside rows that genuinely cost nothing relative to their peers — the exact comparison this page exists to support.
  • The unattributed bucket is not a task, so it is out of the task ranking and out of calls-per-task, and the note above that table says how many calls and how much cost were excluded.
  • Rows past the cap are counted and priced, because the project axis returns 67 rows over 30 days and the panel draws 25.

Also in this PR

  • Collapses six duplicated page-shell route blocks in webAppRoutes.ts into a PAGE_SHELLS table + tryServePageShellthe first test coverage any of those six routes has had.
  • Widens lint from oxlint src/ to include 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. This surfaces 15 pre-existing warnings in other files (webToken.js ×7, threadBoard.mjs, themeBoot.js, sessionPanel.mjs, diffPanel.mjs); usage.mjs itself is clean and oxlint exits 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-fit answers 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:

1920 | panel 617  table 774 | clipped | 4 of 7 columns

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.ts is a tripwire on the two declarations that were wrong before, because jsdom computes neither layout nor colour.

Verification

Gate Result
npx tsc --noEmit exit 0
npm run build exit 0
npm run lint exit 0 (15 pre-existing warnings, above)
npx vitest run (full) exit 0 — 5955 passed / 10 skipped
Chromium, live ledger, 6 viewports 0 console errors, 0 document overflow

Review

Four rounds of layer-2 independent subagent review (openswarm review is 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. truncationNote summed the aggregate's cost-ordered tail while the task panel re-sorted by calls inside renderTable, so it printed $0.05 for $50.00 of hidden spend — worse than the silence it replaced. renderTable now owns the ordering and reports what it hid; truncationNote only formats.

Closes AGT-4289.

🤖 Generated with Claude Code

…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>
@unohee
unohee merged commit 7fa3d69 into main Sep 10, 2026
8 checks passed
@unohee
unohee deleted the feat/usage-dashboard branch September 10, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant