Dialogue.  · Usage dashboard

Usage dashboard — the money, by step

A rebuild of the console's Usage & cost tab into a real dashboard: daily / weekly / monthly views, cost per function (persona msg · Floor Producer · Prop Master · Act · memory writer · dreamer · Vibes · persona updater · …), cost by model tier, daily token volume, and a double-click drill-down for any period.

Status: MOCKUP — awaiting owner review. Everything below runs on deterministic fake data. Nothing is shipped; the ledger change in §2 is the build prerequisite and is not written yet.

1 · The mockup

All controls work: switch the range, switch the lens, hover a bar for every value, click to select, double-click (double-tap on a phone) to open the full detail drawer for that day / week / month.

Admin console · Usage & cost MOCK · GENERATED DATA
hover = every value · click = select · double-click = details (double-tap on touch)
Spend by model tier
By function

Three design notes on what you just used:

2 · Why this needs one new table (the ledger gap)

Today the per-function split never reaches the database. Room._tally() already sees every provider call with its kind — but only accumulates it in memory, per room, all-time. The DB ledger turn_events books one row per whole turn (speech + FP + prop + act + dispatch summed), with no kind and no cache-token columns. And two functions bill nowhere at all today: the dreamer and the growth sweep only print their cost to stderr.

EVERY provider call persona msg · FP (staging) prop master · act · memory dispatch · translate · title voice · interpreter · ruling authoring · promise · empty + Studio · Seen · Convene · Vibes · STT · dream · growth Room._tally(...) kind · model · usage · $ the ONE seam — smoketest counts its call sites TODAY · session by_kind (memory) per room, all-time, never dated — no daily / weekly / monthly view possible TODAY · db turn_events 1 row per TURN — whole turn's spend, no kind, no cache tokens · keeps caps alive PROPOSED — add one write db call_events — 1 row per CALL ts · room_id · user_id · kind · model in_tok · out_tok · cw_tok · cr_tok · cost the dashboard reads only this table turn_events stays untouched — spend caps, the leaderboard and per-room totals keep working; the dream and growth sweeps (stderr-only today) start booking rows here too, so nothing bills invisibly again.
the fix is one INSERT at a seam that already exists — no call site changes, the smoketest invariant keeps guarding it
CREATE TABLE call_events ( id INTEGER PRIMARY KEY, ts TEXT NOT NULL, -- ISO UTC, second precision room_id TEXT, -- real room, or a function pseudo-room user_id INTEGER, -- who pays (NULL for system sweeps) kind TEXT NOT NULL, -- speech · staging · prop · act · memory · dream · growth · … model TEXT, in_tok INTEGER DEFAULT 0, -- fresh input out_tok INTEGER DEFAULT 0, -- output cw_tok INTEGER DEFAULT 0, -- cache write cr_tok INTEGER DEFAULT 0, -- cache read cost REAL DEFAULT 0 ); CREATE INDEX ce_ts ON call_events(ts); CREATE INDEX ce_kind ON call_events(kind, ts);

Volume: a busy day is a few thousand calls — SQLite yawns. A year is ~1M rows; if it ever matters, a monthly rollup table is a follow-up, not a prerequisite. The dashboard's aggregates are three GROUP BY substr(ts,1,10) queries.

3 · The function taxonomy

What each dashboard slot means, in today's code. "by_kind only" = the split exists in memory but is never dated or persisted; those all start writing call_events rows through _tally on day one.

Dashboard slotToday's sourceBooked in DB today?
Persona msgkind="speech" — the panel reply itselfmerged into the turn row
Floor Producerkind="staging" — f's staging callmerged into the turn row
Act callkind="act"merged into the turn row
Prop Masterkind="prop"merged into the turn row
Memory writerkind="memory" — the A/B sweep callsmerged into the turn row
Dreamermemory_dream_all() — nightly consolidationNO — stderr only
Persona updaterthe growth "since the record" weekly sweepNO — stderr only
Vibesvibes_brew — the daily brewyes — pseudo-room "vibes"
Other (unfolds in the drawer) dispatch · translate · autotitle · voice · interpreter · ruling · authoring · promise · empty retries (all _tally kinds) + Persona Studio · Seen · Convene · Dictation (pseudo-rooms) kinds merged into the turn row; pseudo-rooms yes
Before the ledgerevery turn_events row older than the ship — one unsplit sum per turnyes, unsplit

4 · What ships (when you approve the mock)

  1. The ledger. call_events table + one INSERT inside _tally (best-effort, never breaks a turn) + the six direct sites (Studio · Seen · Convene · Vibes · STT already book turn_events; they add a kind) + first-time booking for the dreamer and the growth sweep. turn_events untouched.
  2. The endpoint. GET /api/admin/usage2?gran=day|week|month — periods with per-kind, per-model, per-token-class sums; plus a ?detail=<period> drill-down (adds rooms + users for that period). Old days served from turn_events as "unsplit".
  3. The tab. The mock above becomes the top of the Usage & cost tab; today's by-model / by-user / by-room / backfill cards stay below it unchanged.
  4. Verification. python lib/smoketest.py (it already counts _tally call sites); a probe that one fake turn produces N call_events rows whose costs sum to the turn row.
  5. The doc. This page flips from MOCKUP to LIVE and tracks the shipped tab (room2-docs-sync picks it up).

5 · Open questions for you

Dialogue · design docs — Usage dashboard · mockup 2026-08-21 · chart palette validated (dataviz six checks) against both app surfaces