feat(dashboard): browsable activity log and rebuilt usage analytics - #474
Conversation
Activity answers "is traffic flowing and is anything failing", but it does not answer "find me this request". Three of the cheap, UI-side reasons why, from issue #427: Rows are near-identical because the Tokens column shows one total, which on a cached agent workload is ~98% cache read. The column now draws that total's composition as a thin stacked bar (fresh input, cache read, cache write, output), so a scan compares shapes instead of similar large numbers. The split comes from `billing_meters` when the row has it, because providers disagree on whether cache tokens sit inside `prompt_tokens` and the row does not record which convention it followed; the raw columns are the clamped fallback. Imported agent traffic could not be separated from gateway traffic while browsing, even though `/v1/usage` already takes a `source` filter and the page already had friendly labels for known sources. There is now a Source select, its options drawn from the provenance breakdown of the log itself, with the source filter omitted from that query so switching sources needs no clearing first. Refresh threw away your position. It re-anchored the rolling window, which recomputed "now", which changed the filter set, which reset the page: pressing refresh on page 12 landed you on page 1. Refresh now means "same view, newer rows" and refetches without moving the window; re-picking the active preset is the explicit re-anchor gesture. The same mechanism fired on mount, so a shared or bookmarked `?page=3` URL always opened on page 1; both re-anchor effects now skip their first run, since the state initializers have already snapshotted the window. Left for follow-ups: sessions as the entry point, sortable columns, keyset pagination, searchable `source_event_id`, and error grouping all need server work. Fixes #427 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThe PR adds billed-token usage aggregation, grouped usage-series APIs, interactive dashboard charts, source filtering, token-composition views, integration coverage, documentation updates, and regenerated dashboard assets. ChangesUsage analytics
Estimated code review effort: 5 (Critical) | ~90+ minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review of #474 caught that the palette put the palest fill on the segment that dominates the workload this bar exists for. `--otari-line` is the hairline/track token; used as a data fill it sits at 1.21:1 against the bar's own track, so the canonical 98%-cache-read row rendered as a near-empty pill: the exact opposite of the point. Shading is now assigned for legibility rather than by price, so the weakest fill is 1.98:1 (cache write, the rarest bucket) and every adjacent pair is at least 1.88:1. A cache-heavy row now reads as a filled mid-tone bar and a fresh-input row as a dark one, which is the distinction being drawn. The bar also grows from 4px to 6px, and `--otari-brand-soft` darkens to suit its one use. The source-suggestion query is no longer unconditional. With no source picked, the model-suggestion summary is already computed without one, so its provenance breakdown is that same full list; only a picked source needs a query of its own. That drops the third summary request that a model filter used to add, and stops the page relying on two filter sets hashing alike to collapse. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Tokens column shows the total its bar splits, which counts the cache buckets. A provider that reports those outside the prompt (the Anthropic shape, and every imported Claude Code row) leaves the stored `total_tokens` well below it, so the same request could read 100,700 in the column and 1,200 as "Total tokens" one click away, with nothing on screen to reconcile them. The detail panel now carries both, so the column's number is explained where the raw provider fields already live, rather than the two silently disagreeing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a standard drag-to-zoom histogram The Usage page now follows the model the OpenAI usage dashboard and the Anthropic console converged on: one main time-series chart with metric tabs (Cost, Tokens, Requests) and a group-by dimension (model, user, API key, source) that splits it into stacked bars, on a fixed categorical palette validated for CVD separation and surface contrast. Ungrouped views use the richest encoding each metric has: tokens stack their billed composition (fresh input, cache read, cache write, output; the same encoding as the Activity token bar), requests split succeeded/failed. Tiles trade the three raw cache counters for a cache hit rate, and the per-request table moves wholly to the Activity page (an "Open activity log" action carries the current view across). The timeline histogram drops its edge-handle slider and pan strip for the standard interaction: drag across the plot to zoom (via recharts' own event coordinates, so no more pixel math against axis margins), a minimap rail to pan when zoomed, and failed requests as a red segment. Backend: summary series points carry per-bucket error counts and billed token composition, totals carry billed input tokens, breakdown tokens are billed (input incl. cache plus output) everywhere, and a new GET /v1/usage/series returns a top-8-plus-other grouped series for the stacked charts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three follow-ups from trying the page: the date presets, the Add filter toggle, and the window caption now share one row (FilterChips grew optional start/end slots); the redundant "Open activity log" header button is gone (breakdown rows already drill there); and provenance UI (the Source filter on Activity, the source group-by and breakdown tab on Usage) only appears once the window actually holds more than one source, since a single-option dimension is noise on a plain gateway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grouping the analytics chart against a gateway that lacks /v1/usage/series (not yet restarted onto this build, or vite dev facing an older one) used to retry, spin, and then surface a bare "Not Found" banner. A 404 there is version skew, so the query no longer retries on it, the chart falls back to the ungrouped view, and an inline notice says what to do instead of an error that reads like a broken page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
web/src/components/ActivityTimeline.test.tsx (1)
70-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBoth test files assert brush selectability through the
.cursor-crosshairTailwind class. The shared root cause is thatTrendChartexposes selectability only as a styling class, so tests must reach into the DOM for a utility class instead of querying something a user perceives. A renaming or relocation of that class during a styling pass breaks both tests without any behavior change.
web/src/components/ActivityTimeline.test.tsx#L70-L77: replace thedocument.querySelector(".cursor-crosshair")assertion with a check on a stable selectability signal, for example anaria-describedbyhint or adata-selectableattribute set byTrendChart.web/src/components/charts.test.tsx#L50-L69: use the same signal in all three renders of this test so the selectable and non-selectable cases assert on behavior rather than on the cursor class.Adding the attribute in
web/src/components/charts.tsxnext to the existingselectablecomputation is a two-line change and gives both files something durable to query.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/ActivityTimeline.test.tsx` around lines 70 - 77, Replace the `.cursor-crosshair` DOM assertion in web/src/components/ActivityTimeline.test.tsx lines 70-77 with an assertion against a stable selectability signal exposed by TrendChart. Update all three renders in web/src/components/charts.test.tsx lines 50-69 to use that same signal for selectable and non-selectable cases, and add the corresponding attribute in TrendChart near the existing selectable computation in web/src/components/charts.tsx.Source: Coding guidelines
web/src/components/charts.test.tsx (1)
50-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe test name promises two conditions and checks one.
The title says brush selection needs a handler and two or more buckets. The body varies only the bucket count;
onSelectRangestays provided in both renders. A third render withoutonSelectRangewould cover the other half and is one line, sinceselectableinTrendChartisBoolean(onSelectRange) && data.length > 1.💚 Proposed addition
rerender( <TrendChart data={[{ x: "a", cost: 1 }]} series={COST_SERIES} formatValue={String} ariaLabel="c" onSelectRange={onSelect} />, ); expect(container.querySelector(".cursor-crosshair")).toBeNull(); + rerender( + <TrendChart + data={[ + { x: "a", cost: 1 }, + { x: "b", cost: 2 }, + ]} + series={COST_SERIES} + formatValue={String} + ariaLabel="c" + />, + ); + expect(container.querySelector(".cursor-crosshair")).toBeNull(); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/components/charts.test.tsx` around lines 50 - 69, Extend the test around TrendChart to also render a multi-bucket dataset without onSelectRange and assert that .cursor-crosshair is absent, while preserving the existing assertions for handler presence and bucket-count behavior.web/src/api/hooks.ts (1)
781-795: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer
undefinedfor the absent dimension, which also removes the cast.The hook shape and caching mirror
useUsageSummarynicely, and skipping the request entirely while ungrouped is the right call.Two small things travel together. The guidelines ask for
undefinedrather thannullfor absent values in our own TypeScript types. Switching the parameter also lets you narrowgroupBybefore building the params, so thegroupBy as stringcast on Line 792 disappears. That cast is currently load-bearing only becauseenabledgates the call, which is a slightly fragile invariant if anyone ever prefetches this key.♻️ Proposed change
export function useUsageGroupedSeries( filters: UsageFilters, bucket: UsageBucket, - groupBy: UsageGroupBy | null, + groupBy: UsageGroupBy | undefined, enabled = true, ) { return useQuery({ queryKey: [USAGE, "series", filters, bucket, groupBy], queryFn: () => { + if (!groupBy) throw new Error("group_by is required"); const params = usageParams(filters); params.set("bucket", bucket); - params.set("group_by", groupBy as string); + params.set("group_by", groupBy); return apiFetch<UsageGroupedSeries>(`/v1/usage/series?${params.toString()}`); }, - enabled: enabled && groupBy !== null, + enabled: enabled && groupBy !== undefined,The caller in
web/src/pages/UsagePage.tsxthen passesgroupBy || undefined.Based on coding guidelines: "Use
undefinedrather thannullfor absent values in own TypeScript types."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/api/hooks.ts` around lines 781 - 795, Update useUsageGroupedSeries to accept groupBy: UsageGroupBy | undefined instead of null, and adjust its disabled condition to check for undefined. Narrow groupBy before setting the request parameter so params.set("group_by", ...) uses the value directly without a cast; update the UsagePage caller to pass groupBy || undefined.Source: Coding guidelines
tests/integration/test_usage_summary.py (2)
342-399: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider one row where the output meter differs from the column.
This test is a good one. It pins meter precedence on the input side and the meterless fallback, and it checks that
tokenskeeps the raw provider total.The one uncovered shape is a row whose
completion_tokensmeter differs from the storedcompletion_tokenscolumn. Both rows here set them equal (50/50 and the meterless 30), so_billed_output_sum()andSUM(completion_tokens)return the same number. That equality is what hides the fold-row reconciliation issue I raised insrc/gateway/api/routes/usage.pyaround Line 618. A single divergent row plus a top-N fold would turn that into a failing test.💚 Sketch of the extra row
_make_log( db_session, user_id="comp", timestamp=ts, prompt_tokens=200, completion_tokens=30, total_tokens=230, cache_read_tokens=120, status="error", ) + # Output meter diverges from the stored column (billed output exceeds the + # provider-reported completion count), which is where a fold row that + # reconciles against the raw column drifts. + _make_log( + db_session, + user_id="comp", + timestamp=ts, + prompt_tokens=10, + completion_tokens=5, + total_tokens=15, + billing_meters={"total_input_tokens": 10, "completion_tokens": 25}, + ) db_session.commit()The assertions above then move to the new expected sums.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/test_usage_summary.py` around lines 342 - 399, Extend test_series_composition_prefers_meters_and_falls_back with a row whose billing_meters["completion_tokens"] differs from its stored completion_tokens value, and include it in the existing top-N fold scenario. Update the expected output_tokens and by_model tokens assertions to use the meter-derived billed output while keeping the raw provider total assertion based on the stored total_tokens column.
401-408: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSmall naming mismatch between the two tests.
test_grouped_series_requires_master_key_and_group_byasserts only the 401. Thegroup_byvalidation lives intest_grouped_series_rejects_unknown_group_bybelow, which also covers the missing-parameter case. Trimming the first name totest_grouped_series_requires_master_keywould make each name match what it checks. Purely cosmetic, and the coverage itself is correct.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/test_usage_summary.py` around lines 401 - 408, Rename test_grouped_series_requires_master_key_and_group_by to test_grouped_series_requires_master_key so its name matches the sole 401 assertion; leave the validation test and coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/api/routes/usage.py`:
- Around line 618-623: Update the usage aggregation flow so the fold row in
_breakdown reconciles against billed output totals rather than raw
totals.completion_tokens. Add and populate a billed-output total in UsageTotals
and _totals using the same meter-preferred, column-fallback logic as per-group
rows, then use it when calculating billed_total while preserving the existing
residual calculation.
In `@web/src/components/charts.tsx`:
- Around line 236-256: Clamp the start and end dimming indexes to valid bounds
before dereferencing entries in the two ReferenceArea blocks near the chart
window logic. Use the clamped values for data[...] access so stale out-of-range
window props cannot produce undefined.x, and gate both dimming areas with
showDimming plus their existing side conditions.
In `@web/src/pages/UsagePage.tsx`:
- Line 578: Update the ErrorBanner error expression in the UsagePage render to
check groupBy !== "" before using grouped.error, while retaining the existing
groupingUnsupported guard and summary.error precedence. Ensure the fallback
evaluates to null whenever no grouping is active.
---
Nitpick comments:
In `@tests/integration/test_usage_summary.py`:
- Around line 342-399: Extend
test_series_composition_prefers_meters_and_falls_back with a row whose
billing_meters["completion_tokens"] differs from its stored completion_tokens
value, and include it in the existing top-N fold scenario. Update the expected
output_tokens and by_model tokens assertions to use the meter-derived billed
output while keeping the raw provider total assertion based on the stored
total_tokens column.
- Around line 401-408: Rename
test_grouped_series_requires_master_key_and_group_by to
test_grouped_series_requires_master_key so its name matches the sole 401
assertion; leave the validation test and coverage unchanged.
In `@web/src/api/hooks.ts`:
- Around line 781-795: Update useUsageGroupedSeries to accept groupBy:
UsageGroupBy | undefined instead of null, and adjust its disabled condition to
check for undefined. Narrow groupBy before setting the request parameter so
params.set("group_by", ...) uses the value directly without a cast; update the
UsagePage caller to pass groupBy || undefined.
In `@web/src/components/ActivityTimeline.test.tsx`:
- Around line 70-77: Replace the `.cursor-crosshair` DOM assertion in
web/src/components/ActivityTimeline.test.tsx lines 70-77 with an assertion
against a stable selectability signal exposed by TrendChart. Update all three
renders in web/src/components/charts.test.tsx lines 50-69 to use that same
signal for selectable and non-selectable cases, and add the corresponding
attribute in TrendChart near the existing selectable computation in
web/src/components/charts.tsx.
In `@web/src/components/charts.test.tsx`:
- Around line 50-69: Extend the test around TrendChart to also render a
multi-bucket dataset without onSelectRange and assert that .cursor-crosshair is
absent, while preserving the existing assertions for handler presence and
bucket-count behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 190f276c-658a-4860-b52b-d2a486e52087
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (50)
docs/public/otari.postman_collection.jsonsrc/gateway/api/routes/usage.pysrc/gateway/static/dashboard/assets/ActivityPage-C5gSmqNJ.jssrc/gateway/static/dashboard/assets/ActivityPage-DafcDxtA.jssrc/gateway/static/dashboard/assets/AliasesPage-BCKU8r2J.jssrc/gateway/static/dashboard/assets/BudgetsPage-DjOFqWpt.jssrc/gateway/static/dashboard/assets/ConfirmDialog-BqPbzxvo.jssrc/gateway/static/dashboard/assets/DataTable-CrQq0sYf.jssrc/gateway/static/dashboard/assets/DocsPage-BnNJ3j9_.jssrc/gateway/static/dashboard/assets/Field-DEJ6Wjg9.jssrc/gateway/static/dashboard/assets/FilterChips-BLdhUEZK.jssrc/gateway/static/dashboard/assets/FilterChips-Dqy1SR3R.jssrc/gateway/static/dashboard/assets/KeysPage-B3V9UXb4.jssrc/gateway/static/dashboard/assets/ModelScopeControl-Cn3h4zHZ.jssrc/gateway/static/dashboard/assets/ModelsPage-Cwq_m0h_.jssrc/gateway/static/dashboard/assets/OverviewPage-BCJLvjTB.jssrc/gateway/static/dashboard/assets/OverviewPage-WmIAewmo.jssrc/gateway/static/dashboard/assets/ProvidersPage-C-pvQzFZ.jssrc/gateway/static/dashboard/assets/SettingsPage-BJ2aOAko.jssrc/gateway/static/dashboard/assets/TablePagination-CdOL4h84.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-bsSr7FHF.jssrc/gateway/static/dashboard/assets/UsagePage-CGU7s9AT.jssrc/gateway/static/dashboard/assets/UsagePage-DgkxRXKV.jssrc/gateway/static/dashboard/assets/UserComboBox-nt7pHfqn.jssrc/gateway/static/dashboard/assets/UsersPage-5DYj5V6Q.jssrc/gateway/static/dashboard/assets/charts-BI8Uczg4.jssrc/gateway/static/dashboard/assets/charts-DIsd9m--.jssrc/gateway/static/dashboard/assets/heroui-BI50yK5B.jssrc/gateway/static/dashboard/assets/heroui-DZj66Arc.jssrc/gateway/static/dashboard/assets/index-BTjXME2B.csssrc/gateway/static/dashboard/assets/index-Bi2hajrg.jssrc/gateway/static/dashboard/assets/index-vOUWOCSB.jssrc/gateway/static/dashboard/assets/recharts-BwTTRGtj.jssrc/gateway/static/dashboard/assets/recharts-C4kRDmvS.jssrc/gateway/static/dashboard/assets/tableSelection-CojkFPwd.jssrc/gateway/static/dashboard/index.htmltests/integration/test_usage_summary.pyweb/src/api/hooks.tsweb/src/api/types.tsweb/src/components/ActivityTimeline.test.tsxweb/src/components/ActivityTimeline.tsxweb/src/components/FilterChips.test.tsxweb/src/components/FilterChips.tsxweb/src/components/charts.test.tsxweb/src/components/charts.tsxweb/src/pages/ActivityPage.test.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsxweb/src/styles/globals.css
💤 Files with no reviewable changes (6)
- src/gateway/static/dashboard/assets/charts-DIsd9m--.js
- src/gateway/static/dashboard/assets/ActivityPage-C5gSmqNJ.js
- src/gateway/static/dashboard/assets/UsagePage-CGU7s9AT.js
- src/gateway/static/dashboard/assets/FilterChips-Dqy1SR3R.js
- src/gateway/static/dashboard/assets/OverviewPage-WmIAewmo.js
- src/gateway/static/dashboard/assets/index-Bi2hajrg.js
Fold main's parallel work on both sides into the rebuilt dashboard: - main's session/endpoint/provider breakdown dimensions and dimension counts are kept as new tabs in the PR's tabbed breakdown table. - main's unpriced-served-request counting (success rows only) wins over the PR's cost-null count. - Both _SERIES_TOP_N and _SESSION_BREAKDOWN_TOP_N constants are kept. - Web test suite reconciled to the merged layout (one tabbed table, default Model tab) and 400/400 pass; dashboard bundle rebuilt; OpenAPI spec and Postman collection regenerated. Co-Authored-By: kimi <noreply@moonshot.cn>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/pages/ActivityPage.tsx (1)
514-525: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPass the
sourcebreakdown array intouseUsageSummary.
SourcePage.tsx:525callsuseUsageSummary(sourceSuggestFilters, "day", Boolean(sourceFilter)), butuseUsageSummaryexpectsdimensions?: SummaryDimension[]as the third argument andenabledas the fourth.Boolean(sourceFilter)is currently used asdimensions, so the query is not skipped when no source is picked and it may requestdimensions=undefined. Add aSOURCE_BREAKDOWNconstant and call with that 3rd argument, then passBoolean(sourceFilter)as the 4th if the conditional fetch is intended.🐛 Proposed fix
+const SOURCE_BREAKDOWN: SummaryDimension[] = ["source"]; + const sourceSummary = useUsageSummary(sourceSuggestFilters, "day", SOURCE_BREAKDOWN, Boolean(sourceFilter));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/src/pages/ActivityPage.tsx` around lines 514 - 525, Add a SOURCE_BREAKDOWN summary-dimensions constant and pass it as the third argument to useUsageSummary in the sourceSummary call. Pass Boolean(sourceFilter) as the fourth enabled argument so the summary query remains disabled when no source is selected.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@web/src/pages/ActivityPage.tsx`:
- Around line 514-525: Add a SOURCE_BREAKDOWN summary-dimensions constant and
pass it as the third argument to useUsageSummary in the sourceSummary call. Pass
Boolean(sourceFilter) as the fourth enabled argument so the summary query
remains disabled when no source is selected.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c886e8d-d06a-4152-a0cc-d7aee6d50833
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (26)
docs/public/otari.postman_collection.jsonsrc/gateway/api/routes/usage.pysrc/gateway/static/dashboard/assets/ActivityPage-jh-c4sFD.jssrc/gateway/static/dashboard/assets/AliasesPage-YM65r14v.jssrc/gateway/static/dashboard/assets/BudgetsPage-Cs-653hs.jssrc/gateway/static/dashboard/assets/ConfirmDialog-BwYxBGWV.jssrc/gateway/static/dashboard/assets/DocsPage-BBXgkTvH.jssrc/gateway/static/dashboard/assets/KeysPage-DcoGz01B.jssrc/gateway/static/dashboard/assets/ModelScopeControl-DzdXCGhl.jssrc/gateway/static/dashboard/assets/ModelsPage-CWJm2U51.jssrc/gateway/static/dashboard/assets/OverviewPage-CJSulLXs.jssrc/gateway/static/dashboard/assets/ProvidersPage-BWNRS2H4.jssrc/gateway/static/dashboard/assets/SettingsPage-CXw8bILw.jssrc/gateway/static/dashboard/assets/TablePagination-BNEmGJK5.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-PL5hX8Mz.jssrc/gateway/static/dashboard/assets/UsagePage-BXPxDNhK.jssrc/gateway/static/dashboard/assets/UsersPage-DP8L8Ura.jssrc/gateway/static/dashboard/assets/index-3dfJPlkb.jssrc/gateway/static/dashboard/index.htmltests/integration/test_usage_summary.pyweb/src/api/hooks.tsweb/src/api/types.tsweb/src/pages/ActivityPage.test.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
- src/gateway/static/dashboard/index.html
- web/src/api/hooks.ts
- docs/public/otari.postman_collection.json
- web/src/api/types.ts
- tests/integration/test_usage_summary.py
- web/src/pages/ActivityPage.test.tsx
- web/src/pages/UsagePage.test.tsx
- web/src/pages/UsagePage.tsx
- src/gateway/api/routes/usage.py
…downs Main gained selectable summary dimensions, session/endpoint/provider breakdowns, and status codes on usage logs (#469, #470) while this branch rebuilt the same surface around billed tokens and grouped series. Reconciled by keeping both: the summary serves every dimension with billed token counts, the Usage page grows a secondary breakdown card (Session default, then Endpoint / Provider / Source, source still gated on multiple sources) beside the Model/User card, and the source picker options ride the model-suggestion query's source breakdown again. Also addresses the CodeRabbit review: the breakdown fold row reconciles against billed output (new billed_output_tokens total, plus a divergent-meter regression test) instead of the raw completion column, TrendChart clamps stale window indexes before dereferencing the series, and the grouped-error banner guard uses an explicit comparison. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both this session and a sibling one merged main into the branch. The remote side predates the CodeRabbit fixes and the secondary-breakdown restructure, so where the two merges resolved the same regions this keeps the newer resolution: billed output in the totals, the two-card breakdown layout (Model/User beside Session/Endpoint/Provider/Source), and no API-key spend table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/static/dashboard/assets/ActivityPage-B_xlQB4w.js`:
- Line 1: Include the usage-series query error from P in the ActivityPage error
presentation alongside M.error and L.error, so a failed P request does not
produce an empty-series “No activity in this range.” state. Update the kt error
prop usage in xs, preserving the existing loading and successful-series
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: eb0165c1-3e26-4170-a11f-8b7a3ff65cab
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (26)
src/gateway/api/routes/usage.pysrc/gateway/static/dashboard/assets/ActivityPage-B_xlQB4w.jssrc/gateway/static/dashboard/assets/AliasesPage-CoIJBCqd.jssrc/gateway/static/dashboard/assets/BudgetsPage-C3pAx2YI.jssrc/gateway/static/dashboard/assets/ConfirmDialog-D9trL3a0.jssrc/gateway/static/dashboard/assets/DocsPage-Dcxbdpf9.jssrc/gateway/static/dashboard/assets/KeysPage-CbATIDtN.jssrc/gateway/static/dashboard/assets/ModelScopeControl-xLQ9nHtN.jssrc/gateway/static/dashboard/assets/ModelsPage-DZ6zDjTj.jssrc/gateway/static/dashboard/assets/OverviewPage-gipwh9ys.jssrc/gateway/static/dashboard/assets/ProvidersPage-Bvzss9dV.jssrc/gateway/static/dashboard/assets/SettingsPage-DGmFxbDI.jssrc/gateway/static/dashboard/assets/TablePagination-C-pfgcm6.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-CSEh0CuT.jssrc/gateway/static/dashboard/assets/UsagePage-4kYrVw0Z.jssrc/gateway/static/dashboard/assets/UsersPage-Ch4B0UUJ.jssrc/gateway/static/dashboard/assets/charts-B_hm0rXH.jssrc/gateway/static/dashboard/assets/index-BJ3N7V3K.csssrc/gateway/static/dashboard/assets/index-gNSmom8p.jssrc/gateway/static/dashboard/index.htmltests/integration/test_usage_summary.pyweb/src/api/types.tsweb/src/components/charts.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
- src/gateway/static/dashboard/index.html
- web/src/pages/UsagePage.test.tsx
- web/src/pages/ActivityPage.tsx
- src/gateway/api/routes/usage.py
- web/src/api/types.ts
- web/src/components/charts.tsx
- tests/integration/test_usage_summary.py
- web/src/pages/UsagePage.tsx
A failed series request left the strip reading "No activity in this range", which misreads as a quiet gateway. The error banner now carries the summary error too. Flagged by CodeRabbit on the bundle output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR overhauls the otari admin dashboard’s Activity and Usage analytics experiences (token composition, grouped/stacked time series, and drag-to-zoom interaction) and adds the backend usage aggregation needed to support those views, including a new grouped-series endpoint and billed-token normalization.
Changes:
- Added billed-token composition and per-bucket error counts to
/v1/usage/summary, and introducedGET /v1/usage/series?group_by=...for top-N grouped time series. - Reworked dashboard chart primitives to support stacked series, legends/tooltips for multi-series, and a shared drag-to-select zoom interaction.
- Updated dashboard pages/tests/types/hooks plus regenerated OpenAPI-derived artifacts (Postman) and rebuilt/committed the bundled dashboard output.
Reviewed changes
Copilot reviewed 40 out of 51 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| web/src/styles/globals.css | Adds design tokens for brand-soft and fixed categorical palette slots for grouped charts. |
| web/src/pages/UsagePage.test.tsx | Updates Usage page tests for grouped series, billed-token semantics, and cache hit-rate tile behavior. |
| web/src/pages/ActivityPage.tsx | Adds source filtering, token-composition token bar, window re-anchoring behavior, and timeline error visualization. |
| web/src/pages/ActivityPage.test.tsx | Extends Activity page tests for source picker behavior, token composition rendering, and pagination/refresh anchoring. |
| web/src/components/FilterChips.tsx | Adds start/end slots to share the filter-toggle row with range/refresh controls. |
| web/src/components/FilterChips.test.tsx | Tests new start/end slot rendering. |
| web/src/components/charts.tsx | Introduces shared stacked-series trend chart with drag-to-select zoom, plus legend + richer tooltip. |
| web/src/components/charts.test.tsx | Adds coverage for stacked rendering, selectability gating, legend behavior, and stacked-tooltip behavior. |
| web/src/components/ActivityTimeline.test.tsx | Updates timeline interaction expectations (drag-to-zoom, error split legend, pan-rail behavior). |
| web/src/api/types.ts | Extends API types for billed token totals/composition, per-bucket errors, and grouped series responses. |
| web/src/api/hooks.ts | Adds useUsageGroupedSeries with query caching and 404-skew retry suppression. |
| tests/integration/test_usage_summary.py | Adds integration coverage for billed-token normalization (meters vs fallback) and /v1/usage/series grouping/folding. |
| src/gateway/static/dashboard/index.html | Updates bundled asset references after dashboard rebuild. |
| src/gateway/static/dashboard/assets/UserComboBox-nt7pHfqn.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/UsagePage-D-kXqHLU.js | Removed old bundled UsagePage asset. |
| src/gateway/static/dashboard/assets/UsagePage-CjaSd7XG.js | Added new bundled UsagePage asset reflecting grouped analytics overhaul. |
| src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DkLhKtcA.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/tableSelection-CojkFPwd.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/TablePagination-0H6sFfIt.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/OverviewPage-Tme0ej_W.js | Added new bundled OverviewPage asset (hash/contents updated). |
| src/gateway/static/dashboard/assets/OverviewPage-t4LUmdYC.js | Removed old bundled OverviewPage asset. |
| src/gateway/static/dashboard/assets/ModelScopeControl-gV_ax1BN.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/FilterChips-BLdhUEZK.js | Added new bundled FilterChips asset. |
| src/gateway/static/dashboard/assets/FilterChips-1TBsBOAK.js | Removed old bundled FilterChips asset. |
| src/gateway/static/dashboard/assets/Field-DEJ6Wjg9.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/DataTable-CrQq0sYf.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/ConfirmDialog-BBkZMSqE.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/charts-DIsd9m--.js | Removed old bundled charts asset. |
| src/gateway/static/dashboard/assets/charts-B_hm0rXH.js | Added new bundled charts asset (stacked/brush-select trend chart). |
| src/gateway/static/dashboard/assets/AliasesPage-CtStwRk-.js | Rebuilt dashboard bundle artifact (hash/contents updated). |
| src/gateway/static/dashboard/assets/ActivityPage-DbrHuFM5.js | Removed old bundled ActivityPage asset. |
| src/gateway/api/routes/usage.py | Implements billed-token aggregation helpers, extends summary series/totals, and adds grouped-series endpoint with top-N folding. |
| docs/public/otari.postman_collection.json | Regenerated Postman collection to include /v1/usage/series and updated summary/CSV descriptions. |
Files not reviewed (5)
- src/gateway/static/dashboard/assets/ActivityPage-BENT_1o4.js: Generated file
- src/gateway/static/dashboard/assets/FilterChips-BLdhUEZK.js: Generated file
- src/gateway/static/dashboard/assets/OverviewPage-Tme0ej_W.js: Generated file
- src/gateway/static/dashboard/assets/UsagePage-CjaSd7XG.js: Generated file
- src/gateway/static/dashboard/assets/charts-B_hm0rXH.js: Generated file
Suppressed comments (1)
web/src/components/charts.tsx:101
- Inline
style={{ backgroundColor: ... }}is also used in the legend markers. To stay consistent with the dashboard's no-inline-style convention, render the marker as an SVG/rectwith afillattribute.
khaledosman
left a comment
There was a problem hiding this comment.
Backend logic holds up: the other fold reconciles by construction, the key/is_other NULL encoding avoids sentinel collisions, and the keeps_null branch correctly separates a real NULL group from the past-top-N remainder. Test coverage on both sides is unusually good for a change this size.
Nothing blocking. Inline findings, roughly in priority order:
/seriessilently dropsprovider,source_label, andstatus_codewhile its docstring claims filter parity with/summary— latent divergence between the stacked chart and the tiles beside it.TrendChart's drag path dereferencesdata[index]unguarded, while thewindowprop is explicitly clamped against exactly that race.- The breakdown and CSV
tokenscolumns change meaning (raw -> billed); worth a compat note since git-cliff will file this underfeat. /serieshas no_MAX_SERIES_POINTSanalogue.- Per-row JSON extraction on a Postgres
json(notjsonb) column, multiplied across totals + series + 6 breakdown passes.
Plus a few small ones: a dead labelFor, a stale duplicated comment, role="img" on a now-interactive chart, and mouse-only drag on a PWA surface.
🤖 Review by Claude Code
…, and tile consistency From khaledosman's review: /v1/usage/series now accepts the same provider, source_label, and status_code filters as /summary (it claimed parity while silently dropping them, letting the stacked chart diverge from the tiles) and rejects an hourly grid wider than the summary's series cap instead of ballooning the payload. TrendChart clamps the drag indices like it already clamped the window prop, presents as a labelled group rather than an image while it owns drag selection, and wires the same drag to touch events so the PWA keeps a zoom affordance. The cache hit rate tile reads the meter-normalized series composition (the numbers its own sparkline uses) instead of mixing a raw numerator into a normalized denominator. Dead labelFor plumbing and a stale duplicated comment are gone, the source-suggestion asymmetry on the Activity page is documented as deliberate, and the JSON-extraction cost note points at the jsonb / persisted-columns escape hatches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/gateway/static/dashboard/assets/ActivityPage-CcRVRd0J.js`:
- Line 1: Update the source ActivityPage bulk mutation builder Pe() so its
by_filter payload always includes counts_toward_budget: false, including when
k.allMatching is true; preserve the existing selected-IDs path and rebuild the
generated ActivityPage asset afterward.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 70f8fa4c-5b27-4ca8-b141-3115900ccb57
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (25)
docs/public/otari.postman_collection.jsonsrc/gateway/api/routes/usage.pysrc/gateway/static/dashboard/assets/ActivityPage-CcRVRd0J.jssrc/gateway/static/dashboard/assets/AliasesPage-CXPQdDm6.jssrc/gateway/static/dashboard/assets/BudgetsPage-Cnrtd_az.jssrc/gateway/static/dashboard/assets/ConfirmDialog-D3P-vj-t.jssrc/gateway/static/dashboard/assets/DocsPage-BgBskDDe.jssrc/gateway/static/dashboard/assets/KeysPage-D9JSwodo.jssrc/gateway/static/dashboard/assets/ModelScopeControl-wPC6RjBJ.jssrc/gateway/static/dashboard/assets/ModelsPage-CH43id2j.jssrc/gateway/static/dashboard/assets/OverviewPage-BOO3r0BN.jssrc/gateway/static/dashboard/assets/ProvidersPage-enPv456e.jssrc/gateway/static/dashboard/assets/SettingsPage-VWHKo1Ho.jssrc/gateway/static/dashboard/assets/TablePagination-D_2uYqKa.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-CBa0KCZv.jssrc/gateway/static/dashboard/assets/UsagePage-5U02HhO3.jssrc/gateway/static/dashboard/assets/UsersPage-AJZgT8Wn.jssrc/gateway/static/dashboard/assets/charts-DNysd9jl.jssrc/gateway/static/dashboard/assets/index-DMrbj_xn.jssrc/gateway/static/dashboard/index.htmltests/integration/test_usage_summary.pyweb/src/components/charts.tsxweb/src/pages/ActivityPage.tsxweb/src/pages/UsagePage.test.tsxweb/src/pages/UsagePage.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
- src/gateway/static/dashboard/index.html
- web/src/pages/UsagePage.test.tsx
- docs/public/otari.postman_collection.json
- web/src/pages/ActivityPage.tsx
- web/src/pages/UsagePage.tsx
- src/gateway/api/routes/usage.py
- tests/integration/test_usage_summary.py
Copilot review nit: the tooltip and legend swatches used inline background-color styles, which the dashboard conventions rule out. An SVG rect's fill attribute carries the same var(--otari-*) tokens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
Two related dashboard overhauls, plus the backend they need. The design follows what the mainstream usage dashboards (OpenAI usage page, Anthropic console, Grafana-family tooling) converged on: one main time-series chart with a metric selector and a group-by dimension, token counts split into their billed composition, and drag-on-the-chart time selection.
Activity log: browsable, not just watchable
?page=URLs open on the right page; re-picking the active preset is the explicit re-anchor gesture.Usage page: metric by dimension analytics
Backend
/v1/usage/summaryseries points now carry per-bucket error counts and the billed token composition, normalized through each row'sbilling_meters(with the same raw-column fallback the dashboard uses); totals carrybilled_input_tokens.GET /v1/usage/series?group_by=model|user_id|api_key_id|source: a per-group time series for the stacked charts, folding past-top-8 groups in SQL so the payload stays bounded.Compatibility note: the
tokensfield in/v1/usage/summarybreakdowns and in the/v1/usage/summary.csvexport changes meaning from the raw provider-reported total to the billed total (fresh input, both cache buckets, and output). For additive-convention providers (Anthropic) the numbers step up accordingly. Both endpoints are recent, master-key-gated analytics surfaces, but downstream consumers of the CSV should be told.Tested on SQLite and PostgreSQL; OpenAPI spec and Postman collection regenerated; dashboard bundle rebuilt and committed.
PR Type
Relevant issues
N/A
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).AI Usage
AI Model/Tool used:
Claude Code (design discussion and drafting), kimi (PR template alignment)
Any additional AI details you'd like to share:
The implementation was driven by Claude through several rounds of discussion with @njbrake; the design decisions and final review are his.
NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)
🤖 Generated by kimi
Summary
/v1/usage/serieswith grouping, filtering, bucket limits, error counts, and billed-token metrics.Benefits
Users can analyze usage trends more clearly, compare sources and groups, and investigate failed or costly requests from one activity view. Billed usage now remains consistent across dashboard views and exports.