Skip to content

feat: add a stacked spend-over-time chart to the usage tab - #17159

Merged
iamwhatever merged 1 commit into
kirodotdev:mainfrom
hickoxd:feat/usage-series
Oct 11, 2026
Merged

iamwhatever merged 1 commit into
kirodotdev:mainfrom
hickoxd:feat/usage-series

Conversation

@hickoxd

@hickoxd hickoxd commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem / Motivation

Goal: Show where spend went over the last 30 days -- by surface, agent, model, or the week a session started -- as a stacked area chart on the Usage tab.

Today the Usage tab answers "how much did I spend?" with totals and a flat daily series. It cannot answer "what is spending it?" or "is this a new session or an old one that keeps running?": the per-turn usage shards already carry the surface, agent, model and session of every turn, but nothing on the dashboard breaks spend down along those axes or over time.

Why it matters

Spend without attribution is hard to act on. Seeing the stack by surface tells you whether crons or chat sessions dominate; by agent, which crewmate costs the most; by model, how much the default routing is worth; by session start week, whether long-running sessions are still spending weeks after they began. All of that is latent in data the gateway already keeps, so it costs no new collection. When the window holds more turns than the reader retains, the oldest days are the ones undercounted; a chart that says only "the totals undercount" leaves every day suspect, so it must also name the first day it counted in full.

Not a goal

  • Changing or removing the existing "Daily token usage" card above the new one.
  • A token or dollar chart. The route and the card are credits-only (the unit kiro-cli and KAS bill in); on a harness that bills in tokens or dollars the card does not mount, since its rows carry no credits and /api/usage already serves daily tokens per model.
  • Per-session drill-down, filtering, or export. The chart aggregates; the existing session history remains the per-session view.
  • Changing how usage shards are written or retained. The chart reads exactly what the 30-day shard window holds.

What changed (motivation -> approach -> change)

The daily credits series already reads the per-turn shards (<data home>/usage/tokens/YYYY-MM-DD.jsonl); this is a second read keeping the dimension columns, with bucketing, folding and cohort assignment on the server and only summing, stacking and drawing client-side: aggregating in the browser would re-download tens of thousands of rows per view.

Backend. GET /api/usage/series?by=channel|agent|model|cohort (src/kiro_crew/dashboard/handlers/usage_series.py, registered in routes/system.py), stacking each row's credits. It admits rows by the daily series' guard and answers a dense day axis plus layers in stack order: the top seven values largest-first, __other__ with its member count, __unattributed__. The channel comes from the row's session key via telemetry_channel_of, never its surface field (the reason cost_breakdown documents), so the chart shares the Spend panel's vocabulary; the model is canonicalised across provider migrations. cohort is the session's first-seen ISO week inside the window, keyed by its Monday, oldest-first; every accumulation is finite-guarded. Parsed rows are cached on the shard fingerprint, bounded on both axes (row count, string length); the cap applies as each shard streams, newest first, and the payload discloses the shortening (truncated, dropped_rows, complete_from: the oldest day kept whole). Dashboard-only: an app token gets a SEL-audited 404.

Frontend. A "Spend over time (credits)" card on the Usage tab (website/src/pages/overview/TokenStackedAreaChart.tsx), above Session activity, mounted only once a loaded config names a default harness that bills in credits (reportsCredits in website/src/api/acpBackend.ts); a config read that fails with nothing cached puts an ErrorNotice (with the request's report and a Try again button) where the card would stand, since a silent absence reads as "no credits". The title names the unit. A d3 stack/area chart with a "Layer by" select (Channel, Agent, Model, Session start week) and a "Cumulative" toggle, on by default; off, the same stack per day. Channel ids show as words; unrecognised session keys are labelled apart from the Other fold. Hover and a keyboard slider read out each day's layers. Changing the dimension keeps the previous stack on screen, dimmed and aria-busy with a "Loading…" cue, until the new answer arrives. At 320 px a long legend label truncates, and the tooltip slides within the plot (left-aligned on the first day, right-aligned on the last), so neither crosses the card's edge. Captions explain the Unattributed and session-start-week layers; the oldest cohort drawn is labelled "Started by" its Sunday, not "Week of", because a cohort is the week a session was first seen here, so that layer also holds sessions that began before the window. A truncated window shows a warning-toned line: the totals undercount, and from which day they are whole. A failed first load shows a titled ErrorNotice, the server's text on its own line, with Try again; a failed refresh over a drawn chart shows it above the stack; a zero-spend window shows an empty state, and a failed refresh over it the first-load notice. Preferences persist in localStorage; usageSeries(by) in website/src/api/client/telemetry.ts uses the shared get transport. 24 strings under website/src/i18n/locales/ in every locale and the pseudolocale, with translator context in en.context.json.

Backwards compatibility

Compatible: one new route and one new card; no existing endpoint, response shape, config key, or stored format changes, and the api client surface keeps every existing method.

Removes nothing: the change is purely additive -- the Usage tab's existing cards, the daily series, and the shard format are untouched.

Tests

Backend (test/test_usage_series.py, 34 tests; the shard-reading tests run on one frozen ManualClock installed on both the shard reader and the series module, so no expectation reads the live clock):

  • Dense zero-filled day axis; rows outside the window ignored; a missing shard directory yields no rows.
  • Layers stack largest-first; the top seven are kept and the remainder folds into __other__ with its member count; an empty dimension value becomes an explicit __unattributed__ layer; both reserved layers are omitted when empty.
  • Zero-valued rows create no buckets; a row that would overflow a day is dropped rather than served as Infinity; folded and total sums cannot overflow either; a credits figure wider than a double counts zero instead of failing the read.
  • Cohort is the session's first-seen ISO week, judged inside the window only, stacked oldest-first; a row without a session is unattributed.
  • The channel comes from the session key, not the row's surface field (a Telegram key with surface: dashboard buckets as Telegram); a row with no key stays unattributed rather than taking the classifier's "unknown".
  • Retained strings are clipped at the field bound; the row cap keeps the newest rows, counts the rest, and the payload reports truncated with the dropped count; the shard dated the day before the axis opens (the shard window's cutoff is inclusive) is never read, so it neither spends the budget nor counts as refused; a shard wider than the remaining budget is never held whole (peak live rows are counted while it streams); the cap is silent when the window fits; a dropped count passed to the aggregator is disclosed along with complete_from, which is the oldest whole day (a day with no shard counts as whole; an unreadable shard stops it), null when the cap tripped inside the newest shard, and withheld when nothing was dropped.
  • The shard window, the day axis and the cohort week agree on the local day under UTC, Pacific/Kiritimati (+14:00) and America/St_Johns (-03:30).
  • Rows are cached until a shard changes; defaults (channel, the whole window); unknown by is a 400; an app token gets 404 and the refusal is written to the SEL audit log; the cohort dimension over the wire.

Frontend (website/src/test/TokenStackedAreaChart.test.tsx, 32 tests): weekMondayOf keys every weekday, month and year rollovers to the server's Monday and startedByDay names the week's Sunday clamped to the axis; prefix sums keep the window total; pointer-to-day mapping clamps at the edges; the axis top rounds to a nice number; channel ids render as words and the unrecognised channel is labelled apart from the fold; stacking is bottom-first against the rounded top; no division by zero on an all-zero window; the default view draws one path per layer with a legend and the Unattributed caption, which is absent when every turn is attributed; a legend item can shrink below its content (min-w-0) with its swatch and amount held, so a 64-character label truncates instead of the card clipping it (fails on the pre-fix component, whose item could not shrink; jsdom has no layout, so the test pins the mechanism as the Drive page's narrow-row test does); the tooltip is capped at the plot's width and slides with the day line, hanging from the plot's right edge on the last day, each amount held (fails on the pre-fix component, which centred an uncapped tooltip on a clamp sized for a 10 rem box); a truncated payload shows, in the warning tone, how many turns were not counted and from which day every turn is counted, falling back to the count alone when no day is; session-start-week layers are labelled "Week of ..." except the oldest drawn, "Started by" its Sunday, also in the slider's value text (fails on the pre-fix component: "Week of Sep 28"), and a window opening on a quiet weekend puts it on the first layer drawn, clamped to the axis's last day (fails when keyed on the window's first week); the caption; other dimensions show none; the server's text sits on its own line under the title on a failed first load and on a failed refresh over a drawn chart, which keeps the stack; the cumulative switch persists and re-stacks without refetching; changing the dimension keeps the previous stack on screen, dimmed and busy, still labelled as its own dimension and with the Loading… live-region cue (which stays mounted and empties afterwards), until the next answer lands (fails on the pre-fix component: the skeleton replaces the chart); the hover tooltip lists all nine layers of a maximal payload; the keyboard slider steps days, clamps at the edges, jumps with Home/End, clears with Escape and reads the day out as its value text; the empty state; a failed refresh over the empty state reads like a failed first load (no "last values" title beside "no spend"); a failed first load names what could not be loaded and its Try again button refetches. UsageTab.spendChartHarness.test.tsx (6 tests): the card mounts for kiro-cli, an unset key and KAS, never for a tokens-or-dollars harness (which also never requests the series), and not on a guess before the config loads -- each absence is asserted only after a probe on the same config query has rendered the answer; the last two fail on the pre-fix tab, which rendered the card unconditionally; a config read that fails renders the notice where the card would stand, with no chart and no series request (fails on the pre-fix tab: no notice, the chart simply absent), holds it through the retry Try again starts (fails on isError) and clears it when the read succeeds; nothing shows while the first read is pending (fails on data === undefined). acpBackend.test.ts pins reportsCredits (false unloaded, true for kiro-cli/unset/KAS, false for every other harness). The four existing UsageTab suites mock the config and series queries, and the API-client surface pin includes the new method.

Manual verification

Ran a local gateway against a data home seeded with generated sample shards (72 sessions over 30 days across six channels, ten agents, three models, with background and some cron turns recording no model). Checked all four layer modes, per-day and cumulative, the hover tooltip at several days, the Other (n more) fold on the agent view, the Unattributed layer and its caption on the model view, the caption on the session-start-week view, the empty state on a data home with no shards, a 390 px viewport, a 320 px viewport on the agent view with a 64-character agent name (the legend truncates the name; the capture measures every legend item inside the card), the hover tooltip at the first, a middle and the last day, keyboard stepping through the days with the focus ring showing, the loading state before the first answer, a failed first load (title, the server's text under it, Try again), a failed refresh over a drawn chart (the same notice above the stack it keeps showing), the config read failing (the notice and Try again in the card's place, no series request), the "Started by" first layer on the session-start-week view, the dimmed stand-in with its Loading… cue while a newly chosen dimension loads, the light theme on the model view, and that preferences survive a reload; browser console clean. Also checked, on further generated data homes: the per-day tooltip, a channel view with more channels than the chart keeps apart plus sessions whose key shape the classifier does not recognise, a window with more turns than the row cap retains (the caption names the count left out and the first fully counted day, which matches where the drawn area begins), a single day alone past the cap (the caption gives the count with no date), and a failed refresh while the empty state is showing (the titled error and Try again replace the empty line). Confirmed the endpoint answers 400 for an unknown dimension and 404 for an app token, and that with agent.acp_backend set to a non-credits harness the card is absent and no series request is made.

Screenshots / video

Dark theme unless noted, 30-day window of generated sample data.

Channel, cumulative (default view):

Spend over time, by channel, cumulative

Channel, per day:

Spend over time, by channel, per day

Model view with the Unattributed layer and its caption:

By model, with the Unattributed layer and its caption

Session start week: the oldest layer is "Started by" its Sunday, the rest "Week of", with the caption:

Spend over time, by session start week, the oldest layer labelled Started by

Hover tooltip, cumulative ("Total to date"):

Hover tooltip listing the day's layers

Hover tooltip, per day ("That day"):

Per-day hover tooltip

Keyboard focus on the plot (ring, day line and tooltip four days back):

Plot focused from the keyboard, showing the focus ring and the stepped day's tooltip

Loading, before the first answer:

Loading state: controls above the skeleton

The first load failing: a titled notice with the server's text on its own line, and a Try again button under the controls, no chart:

First-load failure: the titled notice, the server's text below it, and Try again under the controls, no chart

Layer by changed to Agent while that answer is still loading: the channel stack stands in, dimmed, with the Loading… cue beside the controls:

Dimension switch in flight: the previous stack dimmed and busy, Loading… beside the controls

Light theme, model view, with the Unattributed layer:

Light theme: by model, with the Unattributed layer

Channel view with the "Unrecognized sessions" layer beside the "Other (4 more)" fold:

By channel, with the Unrecognized sessions layer and the Other fold

Truncated window: more turns than the chart retains, with the warning naming how many were not counted and from which day every turn is counted:

Truncated-window warning naming how many turns were not counted and the first fully counted day

A refresh failing over a chart already drawn: the "Can't refresh" notice, server text below its title, above the stack it keeps showing:

Refresh failure over a drawn chart: the Can't refresh notice above the stack

Truncated window where the cap tripped inside today's shard: no day is whole, so the warning gives the count without a date:

Truncated-window warning with the count only, no first fully counted day

The agent backend setting could not be read: a notice and Try again in the card's place instead of a silent absence:

Config-read failure: the notice where the Spend over time card would stand

Empty state:

Empty state when the window holds no spend

A failed refresh over the empty state, read like a failed first load (no "last values" title):

Refresh failure over the empty state shown like a failed first load

Agent view with the "Other" fold, 390 and 320 px viewports

By agent, top 7 plus Other (2 more)

390 px viewport

320 px viewport, agent view: a 64-character agent name truncated in the legend, swatch and amount intact

Related Issues

None.

Checklist

  • At most two commits (one is the norm), with a Conventional Commits title (feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert: ...)
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (if applicable)
  • No secrets, credentials, or internal references in the diff

Contribution License Agreement

N/A -- no CLA text has been supplied yet.

@dwu96

dwu96 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR is currently in draft status. Workflow runs won't be auto-approved until it's marked as ready for review.

When you're ready, click "Ready for review" and the workflows will be approved on the next cycle automatically.

@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: action required A blocking check or review needs attention labels Oct 5, 2026
@dwu96

dwu96 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

👋 Hi! This PR is currently in draft status. Workflow runs won't be auto-approved until it's marked as ready for review.

When you're ready, click "Ready for review" and the workflows will be approved on the next cycle automatically.

@hickoxd
hickoxd marked this pull request as ready for review October 6, 2026 03:47
@hickoxd
hickoxd requested a review from a team as a code owner October 6, 2026 03:47
@hickoxd
hickoxd requested a review from bolichen97 October 6, 2026 03:47
@hickoxd
hickoxd force-pushed the feat/usage-series branch 2 times, most recently from ca8b4ac to fdddaf6 Compare October 6, 2026 04:11
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Design Review (Opus 5.5, fork) — ✅ PASS

Design-level review of e0b5a03fe122d9bfec2748b43034e19873e78f9b via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

Check that turns run on the KAS relay really write a non-zero credits field to the shards. reportsCredits shows the card for KAS on the assumption that they do.

[DESIGN-REVIEWED] e0b5a03

Description read

[DESCRIPTION-READ] 8220384c2f8357fa62ebf2a83ffcf1abf06b38f605f8ca69459324fe5e397a82

That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

UX Review (Opus 5.5, fork) — 🟡 CONCERNS

UX-level review of e0b5a03fe122d9bfec2748b43034e19873e78f9b via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

UX-Verdict: CONCERNS

When the window is truncated, the chart draws the dropped days as zero spend, with nothing on the plot to mark them. No blind reader has checked any of the controls.

Watch

  • Dropped days look like quiet days. The caption says "each day from Sep 18, 2026 counts all of its turns", but in attachment-12 the stack sits flat at 0 from Sep 10 to about Sep 17. Nothing on the plot marks that stretch. Attachment-15 (truncated_notice, no date) shows a whole month of zeros. A reader looking at the chart sees idle days, not missing data. This only happens on heavy installs, but there it misleads every time. Fix: shade or hatch the days before complete_from, or start the x-axis there.
  • A failed refresh has no retry button. In attachment-13 the could_not_refresh notice sits over a drawn chart with only "Ask the agent". The first-load failure and the harness notice both get a Try again button. The notice also pushes the Layer by controls down. Fix: add the same retry button under the refresh notice.
  • Long names truncate with no way to read them. Legend labels use truncate max-w-64 and tooltip labels use truncate max-w-56, with no title. "release-notes-and-changelog-su…" (attachment-21) can't be read in full anywhere, so two agents with the same prefix can't be told apart. Fix: add title={label} to both spans.

Suggestions

  • cohort_caption is two lines of always-visible prose (attachment-04). Shorten it to: "Each layer is the week a session was first seen here; “Started by” also holds sessions that began earlier."

[UX-REVIEWED] e0b5a03

Description read

[DESCRIPTION-READ] 7f7c807d01bfdee3ae1217bfa3d7af5db4688af20a3cd68e7a50943576e92c04

That sha256 names everything this verdict read: the PR description, plus the 24 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with .github/scripts/pr-description-capture.sh over the same inputs; if it differs, the description or that evidence changed after this verdict was formed and any finding drawn from either is unproven.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

First Principles Review (Opus 5.5, fork) — 🟡 CONCERNS

Premise-level review of e0b5a03fe122d9bfec2748b43034e19873e78f9b via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

First-Principles-Verdict: CONCERNS

The Cumulative toggle, on by default, is a second view the Goal never asks for, and no harm is named for it.

Not justified as shipped

  • Item 3 — inherited: the Goal asks to "Show where spend went over the last 30 days … as a stacked area chart"; nothing names a reader who needs the running sum. Yet the diff ships "a 'Cumulative' toggle, on by default; off, the same stack per day", plus a persisted preference for it.
    Subtraction: delete the Cumulative toggle, its client-side prefix-sum and its localStorage key, and ship the per-day stack only.
    Clears when: the toggle is removed, or the description names who needs the running sum that the per-day stack cannot show.

What this change ships

Inventory (10 items) — 9 justified

Intent: let an operator see what is consuming their credits over 30 days (by channel, agent, model or session start week). ADDITION.

  1. New "Spend over time (credits)" card on the Usage tab, above Session activity — justified
  2. "Layer by" select: Channel, Agent, Model, Session start week — justified
  3. Cumulative toggle, on by default — inherited (no harm named)
  4. New GET /api/usage/series?by= route, refused to app tokens with a SEL audit record — justified
  5. Card shown only on harnesses that bill in credits, via new reportsCredits (one consumer: UsageTab.tsx) — justified
  6. ErrorNotice with Try again where the card would be when the config read fails — justified
  7. Warning line on a truncated window: how many oldest turns were left out, and from which day counts are whole — justified
  8. Top seven layers kept, the rest folded into Other, plus an Unattributed layer — justified
  9. Hover and keyboard slider read out each day's layers — justified
  10. Previous stack stays on screen, dimmed, while a new dimension loads — justified

[FIRST-PRINCIPLES-REVIEWED] e0b5a03

Description read

[DESCRIPTION-READ] 74ab31dd11cd539e46f1347809feba148fba3eb7caa2a4a950b4ec8f5f1ac33c

That sha256 names the PR description this verdict read. Recompute it with .github/scripts/pr-description-capture.sh; if it differs, the description changed after this verdict was formed and any finding drawn from the description is unproven.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Opus 5.5 Review (fork) — ✅ no blocking findings

Reviewed e0b5a03fe122d9bfec2748b43034e19873e78f9b via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] e0b5a03

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

GPT 6.1 Review (fork) — ✅ no blocking findings

Reviewed e0b5a03fe122d9bfec2748b43034e19873e78f9b via the fork AI-review pipeline; updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] e0b5a03

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Oct 6, 2026
@hickoxd
hickoxd force-pushed the feat/usage-series branch 2 times, most recently from 0049ad8 to 12cc5ab Compare October 6, 2026 19:53
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 6, 2026
@hickoxd
hickoxd force-pushed the feat/usage-series branch from 12cc5ab to 7234d36 Compare October 7, 2026 04:14
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 7, 2026
@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 8, 2026
@hickoxd
hickoxd force-pushed the feat/usage-series branch from 540dd5b to c6f16d2 Compare October 8, 2026 18:35
@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 8, 2026
@hickoxd
hickoxd force-pushed the feat/usage-series branch from c6f16d2 to f21348b Compare October 8, 2026 19:58
@hickoxd

hickoxd commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Resolved. The Integration failure below cleared after #18224 (Fixes #17964) merged: rebased onto main at a098d44d9, and on b355cf2b2 every check is green — 92 passing, 0 failing, Integration (in-process gateway) included; Opus 5.5 and GPT 6.1 no blocking findings. The umask attribution below was wrong: the test sets its own umask, the cause was the boot-ordering race #18224 removed. Nothing further needed from the maintainers on this point; the PR is ready for review.


Original note (2026-10-08)

Maintainers: the one red on this PR is CI / Integration (in-process gateway) failing test/integration/test_owner_only_boot.py::test_a_fresh_boot_leaves_nothing_readable_by_others (0o644 ... inbound-spool), e.g. job 113483297277 on c6f16d264.

That is #17964 / #18170 (umask 022 on the GitHub-hosted runners fork PRs use; same-repo PRs pass on the self-hosted ones), not this change: the diff touches no spool, autonomy or boot code, and the same test is the only failure in 10 of the last 50 completed Integration jobs. A rerun will not clear it on a hosted runner, so I have not asked for one. I will rebase as soon as a fix for #18170 lands.

Everything else on the latest head is green: Fast Gate 15/15, every backend and frontend shard, E2E (the chat.spec.ts flake from #18038 is gone), Opus 5.5 and GPT 6.1 with no blocking findings, Design PASS. The latest push also takes two advisory items: the row cap no longer counts the shard dated the day before the axis opens, and a failed refresh over an empty window now shows the plain error instead of "showing the last values" beside "No spend recorded yet."

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Oct 8, 2026
@hickoxd
hickoxd force-pushed the feat/usage-series branch from f21348b to b355cf2 Compare October 9, 2026 06:58
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Oct 9, 2026
@iamwhatever

Copy link
Copy Markdown
Contributor

This PR adds a new usage_series backend endpoint (487 lines) and a stacked spend-over-time chart on the Usage tab, for +2160 lines in total. I read the description and checked the lane verdicts on b355cf2: GPT and Opus have no findings, and Design, UX and First Principles are all CONCERNS. The exact hold reasons: this is a big new feature and a UX decision, and it is well over the ~800 production-line limit. The open lane items should be answered first. On a backend that records tokens but no credits, the credits-only card shows an empty "No spend recorded yet" card. The first-load error shows only the raw "series unavailable" text with no title and no next step. metric=tokens has no consumer (the only caller hard-codes 'credits'), so First Principles asks to drop it. A maintainer should review the feature and its placement. — crew-pr-reviewer

@hickoxd
hickoxd force-pushed the feat/usage-series branch from b355cf2 to 1a39a48 Compare October 9, 2026 23:29
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: passed Eligible automated validation passed for the current revision labels Oct 9, 2026
@hickoxd

hickoxd commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Acted on all three lane items in 1a39a484e, rebased onto main at bad04199f.

  • Design: the card now mounts only when a loaded config names a credits-billing default harness (reportsCredits in api/acpBackend.ts, positive like isKiroBackend: kiro-cli or KAS). A tokens-or-dollars harness gets no card and no series request.
  • First Principles: metric=tokens is gone end to end (query param, _tokens_of, _Row.tokens, the TS type); the route is credits-only.
  • UX: first-load error has a title and a Try again button; a Loading… status sits beside the select while the previous stack is dimmed; the truncated notice names the first fully counted day (new complete_from in the payload); cohort caption is one sentence; a light-theme still is attached.

Production lines are now 8 files, +971 (backend 481, chart 409, client/tab/predicate 81) -- still above ~800; the lane fixes added slightly more than removing the tokens surface took out. Splitting it would mean a backend-only PR (an endpoint with no caller) and a frontend PR stacked on it; happy to do that if you prefer it over one review.

Placement and whether the feature belongs are yours to call; the open-lane items are answered.

@github-actions github-actions Bot added the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 9, 2026
@github-actions github-actions Bot removed the merge conflict Branch has merge conflicts with its base — author must resolve before merge label Oct 10, 2026
@hickoxd

hickoxd commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Round 11, eac67733f, rebased onto main at a84f61655.

  • GPT 6.1 (blocking): fixed. A legend item can now shrink to its line (min-w-0 max-w-full) with the swatch and amount held (shrink-0), so at 320 px a 64-character agent name truncates instead of the card clipping it. The hover tooltip had the same class of problem from a different cause (uncapped, centred on a clamp sized for a 10 rem box, so its amounts fell under the card's overflow: hidden at the right end of the plot at every width): it is now capped at the plot's width and slides with the day line, left-aligned on the first day and right-aligned on the last. Both are pinned by tests that fail on the previous component; a 320 px still with the long name is attached, and the capture checks every legend item's box against the card's.
  • First Principles (complete_from): the description's Why it matters now names the harm it removes -- the oldest days are the ones undercounted, so "the totals undercount" alone leaves every day suspect; the chart has to name the first day it counted in full.
  • UX lane: the red on the previous head was one attachment download returning 504 inside the lane; all 21 attachments serve.
  • Size: keeping this as one PR at ~1,025 production lines (backend 483, chart 444, client/tab/predicate 98). A split would leave an endpoint with no caller for a review cycle; the extra lines over the earlier count are the narrow-viewport fixes and their tests.

Add GET /api/usage/series, a credits-only stacked time series over the
per-turn usage shards layered by channel, agent, model, or session start
week (top-N plus "other", with an explicit "unattributed" layer for turns
that did not record the field), and a "Spend over time" card on the
Overview Usage tab that renders it as a d3 stacked area with per-day and
cumulative views.

The channel comes from the session key, never the row's own surface
field, for the reason cost_breakdown documents. The parsed-row cache is
bounded on rows and on every retained string; a window past the cap keeps
its newest rows and the payload says how many of the oldest were left
out. App callers are refused with an audited 404. The plot is a keyboard
slider over the day axis, the tooltip lists every layer, and the axis top
is rounded so the ticks read cleanly.

@iamwhatever iamwhatever left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds a dashboard-only GET /api/usage/series endpoint and a stacked credits-over-time card on the Usage tab, so a user can see what each day's spend is made of (by channel, agent, model or session start week), which the per-day totals cannot show. I read the whole of dashboard/handlers/usage_series.py, TokenStackedAreaChart.tsx, the UsageTab.tsx mount, reportsCredits and the client type on head e0b5a03. Our earlier hold items are all met: the tokens metric is gone, the card mounts only for a credits-billing harness (kiro-cli/KAS), and the first-load error has a title and a Try again button. I tried to break the endpoint. An app token gets a SEL-audited 404, using the same request.get("app") convention as the other handlers. An unknown by is a 400. Rows are capped at 100k with every string field clipped to 256 chars, and a non-finite or oversized credits value, or a sum that overflows, can never reach the JSON as Infinity. A corrupt shard costs only its own day, and complete_from stops at it. GPT and Opus have no findings, Design PASS, checks green, no change request open. The UX and First Principles items left open are advisory and fine as follow-ups: shade the days before complete_from when the window is truncated (this only happens past 100k rows), add a Try again button to the refresh-failed notice, add title={label} to the truncated legend and tooltip names, and decide in a later PR whether Cumulative should stay the default. — crew-pr-reviewer (deep re-check)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants