evidence/StatsPanel.md
Rama master · réplica de lectura del repositorio original.
# StatsPanel — request-log rebuild for `#stats`
Worktree: `w80-stats` (from `19ff92e`, release/3.10.0). Lane: StatsPanel.
Scope: `#stats` section only in `src/dashboard/index.html` (markup 3854–3980 region, stats JS block, stats-only CSS), plus `src/dashboard/stats.js`, `src/dashboard/api.js`, `src/server.js`, i18n, and one test file. No other panel touched.
## What was built
### Backend — `GET /dashboard/api/stats/log`
New read-only endpoint (auth inherited from the dashboard API gate) over the persisted
per-request ring (`stats.recentRequests`, cap 500). Contract:
- Query: `from`, `to` (epoch ms), `model`, `account` (exact), `status` (numeric),
`limit` (1–200, default 50), `offset` (>= 0).
- Response: newest-first `items[]` with
`{ ts, model, account, status, success, ms, tokensIn, tokensOut, cacheRead, cacheWrite, credit }`,
`total`, `limit`, `offset`, `hasMore`, `retention {stored, capacity}`,
`filters {models, accounts, statuses}`, `summary`, `byModel`, `byAccount`, `byDay`.
- Every aggregate is computed from the **same filtered rows** as the table (one
denominator). `tokensIn` = fresh input + cache read + cache write, so
`tokensIn + tokensOut` reconciles with the lifetime token card.
- Existing `GET /stats` is untouched except additive per-row fields inside
`recentRequests`; `getStats()` still returns all previous keys (`modelCounts`,
`accountCounts`, `hourlyBuckets`, `tokenTotals`, `credits*`, `modelBilling`, …).
### Attribution (the non-obvious part)
`recordRequest` (attempt row) and `recordTokenUsage` (usage block) are called at
different depths and in **either order** depending on the route (Cascade stream,
connect non-stream, cache replay). A shared "last row" pointer would misattribute
tokens between concurrent requests, so `stats.js` now exposes an
`AsyncLocalStorage` scope:
- `withStatsScope(fn)` / `finalizeStatsScope(scope)` (stats.js).
- `startServer` wraps `route(req,res)` in one scope per HTTP request and stamps the
real `res.statusCode` on the last attempt row.
- `recordRequest` consumes usage recorded earlier in the same scope;
`recordTokenUsage` attaches to the current row or holds it as `pendingUsage`.
- Outside a scope (unit tests, direct handler calls) behaviour is unchanged and the
row carries the success-derived 200/500.
### Frontend — `#stats`
- Existing KPI cards and time-series chart kept (chart range control now drives the
log window too).
- New **Request log**: filters (model / account / status, populated from retained
rows), table (time, model, account, status badge, latency, tokens in/out, cost),
pagination (25/50/100/200, prev/next, `from-to / total`), retention label.
- New **Aggregates** tabs: by model / by account / by day, each with requests,
success rate, errors, avg, p95, tokens in/out, cost. The model tab carries the
existing FREE-promo badge from `/stats` `modelBilling`.
- Cost unit is Devin credits (no invented USD conversion). Log is labelled as
"one row per upstream attempt" and "retaining latest N of 500".
- All new copy is in both `en.json` and `zh-CN.json`; `check-i18n.js` passes.
## Verification run
| Check | Command | Result |
|---|---|---|
| New backend/API tests | `node --test test/dashboard-stats-log.test.js` | 15/15 pass |
| Dashboard suite | `node --import ./test/setup-env.mjs --test test/dashboard-*.test.js` | 532/532 pass |
| Stats import/export | `node --test test/stats-import.test.js test/sec4-stats-import-prototype.test.js` | pass (included above run) |
| Syntax + a11y | `node --test test/dashboard-syntax.test.js test/dashboard-a11y.test.js` | 21/21 pass |
| i18n | `node src/dashboard/check-i18n.js` | all checks pass |
| startServer routes | `node --import ./test/setup-env.mjs --test test/o10-ring-class.test.js test/precontent-status-stream.test.js test/startserver-failover.test.js test/startserver-cancel.test.js test/startserver-harness-shapes.test.js test/request-id-body.test.js test/http-cache-control.test.js` | 58/58 pass |
| HMC real requests | scratch on `:19120`, `/v1/chat/completions` ×2 | both HTTP 200; log rows show `tokensIn 474/419`, `tokensOut 16/4`, `credit 0/6`, status 200 |
### Screenshots
- `StatsPanel-hmc-real.png` — **real data** on the HMC scratch instance after two live
`swe-2-medium` / `claude-sonnet-4.6` calls: log rows with tokens and credits,
by-model aggregate, FREE promo badge.
- `StatsPanel-local-fixture.png` — local UI fixture (seeded rows, clearly not production
numbers) used to exercise 25-row pagination, filters, 3-day aggregates and the
chart at a realistic density.
### Not run / caveats
- Full repo gate (`npm test`, 4.8k tests) not run in this lane; only the targeted
suites above. `[UNVERIFIED]` for unrelated suites.
- Windows local instance could not reach upstream (no local accounts); real-request
evidence comes from the HMC scratch instance.
- Log retention is the existing 500-row ring; older-than-ring requests are not in
the table. The panel states this explicitly.
- `test/dashboard-escape-callsites.test.js` exemption line numbers were re-anchored
to the new `index.html` line numbers (the guard is line-keyed by design).