Skip to content

feat(dashboard): filter usage views on several models, users, or keys - #521

Merged
njbrake merged 7 commits into
mainfrom
fix/dashboard-analytics-multiselect
Aug 6, 2026
Merged

feat(dashboard): filter usage views on several models, users, or keys#521
njbrake merged 7 commits into
mainfrom
fix/dashboard-analytics-multiselect

Conversation

@njbrake

@njbrake njbrake commented Aug 6, 2026

Copy link
Copy Markdown
Member

Description

The User, Model, and API key pickers committed one value each, so a spend question that is really a comparison ("these two models", "this team's three keys") could only be asked one entity at a time. Both usage views now take sets.

API. model, user_id, and api_key_id are repeatable on every usage endpoint (/v1/usage, /v1/usage/count, /summary, /series, /summary.csv): several values match any of them, capped at 50 per call so a caller cannot post an unbounded IN list. A single value stays an equality test, so the existing wire form and every existing caller are unaffected.

Bulk mutations. This is what gated extending the multiselect to the request log. "Select all N matching" counts rows over the filters on screen and re-derives the target set server-side from the mutation body, so a body that could not express a multi-value filter would delete or reprice rows the operator was never shown. UsageSelection now takes the same repeatable form, and the one-value-or-many helper lives in core/sql.py so the route and the service build the condition from one place instead of two that could drift.

Dashboard. A new FilterMultiComboBox accumulates picks, keeps the list open on what remains, and stops at 50 with the remaining options inert (a 51st pick would 422 every query on the page). Each value is its own chip whose ✕ clears just it. Activity carries a filter as repeated URL params, and its Model box still accepts a name outside its suggestions (Enter commits it), since those suggestions only cover models with traffic in the window. The analytics drill-down now carries a whole selection into the log rather than dropping it.

Generated artifacts refreshed: dashboard bundle, OpenAPI spec, Postman collection.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Fixes #489

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

Test notes: npm --prefix web test 469 passed; uv run pytest 2284 passed, 10 skipped. The load-bearing new tests are the destructive ones: a two-model bulk delete removes exactly those two models' imported rows and leaves the third, and the count that sized "all matching" was taken over the same two values. Two pre-existing failures (test_provider_error_does_not_leak_details, test_streaming_creation_error_returns_http_error) expect a 502 from a real provider call and return 404 in a sandbox with no network egress; they fail the same way without this change.

Also smoke-tested against a live gateway seeded with 1,140 imported rows over 50 days across four models, four users, and three keys: two-model and two-key unions reconcile exactly with their singles, the grouped series and CSV export honor a multi-value filter, and 50 values pass while 51 return 422.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used: Claude Code (Opus 5)

Any additional AI details you'd like to share:

Implemented and tested by Claude Code through back and forth with @njbrake; the scoping decisions are his, including the call to cover the request log in the same PR once the bulk-mutation hazard had a clean fix.

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

  • I am an AI Agent filling out this form (check box if true)

Summary

  • Added multi-select filters for models, users, and API keys in Usage and Activity views.
  • Added removable filter chips, custom model values, and a 50-value selection limit.
  • Preserved selected filters during analytics drill-downs.
  • Extended bulk delete and price updates to use the same filters.
  • Updated API documentation, generated dashboard assets, Postman collections, and tests.

Technical notes

  • Usage endpoints support repeated model, user_id, and api_key_id parameters.
  • Single-value filters retain their existing behavior.
  • Added shared match_any filtering and URL-state support for repeated values.

… or keys

The Usage page's User, Model, and API key pickers committed one value each, so
a spend question that is really a comparison ("these two models", "this team's
three keys") could only be asked one entity at a time, and the chart could never
show them side by side.

The three entity filters are now repeatable on the analytics endpoints
(/v1/usage/summary, /v1/usage/series, /v1/usage/summary.csv): several values
match any of them, capped at 50 per call so a caller cannot post an unbounded IN
list. A single value stays an equality test, so every existing caller and the
existing wire form are unaffected.

The pickers accumulate values, each pick becomes its own removable chip, and the
request log's own filters stay single-value: its bulk delete / set-price
selection is expressed one value per dimension, and widening it there would let
a bulk op reach past the rows the operator was shown. A drill-down therefore
carries an entity filter only while it holds a single value.

Fixes #489

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake
njbrake had a problem deploying to integration-tests August 6, 2026 15:53 — with GitHub Actions Failure
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@njbrake, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 62264cc9-7d31-471c-8311-ce86243884bc

📥 Commits

Reviewing files that changed from the base of the PR and between 064d0b5 and 373a419.

📒 Files selected for processing (24)
  • .github/skills/frontend-standards/components.md
  • AGENTS.md
  • docs/api-reference.md
  • docs/dashboard.md
  • src/gateway/AGENTS.md
  • src/gateway/static/dashboard/assets/ActivityPage-m0y8T4IF.js
  • src/gateway/static/dashboard/assets/BudgetsPage-CCjnKhY4.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-BqztIUQ8.js
  • src/gateway/static/dashboard/assets/DocsPage-BLX9Ugok.js
  • src/gateway/static/dashboard/assets/FilterChips-DTdIceb1.js
  • src/gateway/static/dashboard/assets/KeysPage-eN3TspGP.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-BTAbFoU-.js
  • src/gateway/static/dashboard/assets/ModelsPage-BHFs32Be.js
  • src/gateway/static/dashboard/assets/OverviewPage-CJaIAUu7.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BYCOF7U0.js
  • src/gateway/static/dashboard/assets/RoutingPage-CuN4PH28.js
  • src/gateway/static/dashboard/assets/SettingsPage-DAz4lpRs.js
  • src/gateway/static/dashboard/assets/TablePagination-CodBBmt3.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-Bh69IbXF.js
  • src/gateway/static/dashboard/assets/UsagePage-BbuXicXf.js
  • src/gateway/static/dashboard/assets/UsersPage-DgtI9L1U.js
  • src/gateway/static/dashboard/assets/index-DnXjp6g_.js
  • src/gateway/static/dashboard/index.html
  • tests/integration/test_usage_summary.py

Walkthrough

Changes

Usage filter multiselect

Layer / File(s) Summary
Backend repeatable filters
src/gateway/core/sql.py, src/gateway/services/usage_admin_service.py, src/gateway/api/routes/usage.py, docs/api-reference.md, docs/public/otari.postman_collection.json
Usage endpoints and bulk actions accept up to 50 repeated user, model, and API-key filters. Single values use equality; multiple values use IN matching.
Dashboard multiselect and drill-downs
web/src/api/*, web/src/lib/urlState.*, web/src/components/*, web/src/pages/ActivityPage.tsx, web/src/pages/UsagePage.tsx
Dashboard filters use arrays, multi-select controls, removable chips, repeated query parameters, and complete drill-down selections.
Filter behavior validation
tests/integration/*, web/src/components/ui.test.tsx, web/src/lib/urlState.test.tsx, web/src/pages/*test.tsx
Tests cover selection, URL state, endpoint limits, grouped results, CSV output, bulk actions, chip removal, and drill-down forwarding.
Dashboard bundle and documentation
src/gateway/static/dashboard/*, docs/dashboard.md
Generated dashboard assets include the updated runtime, pages, components, imports, and multi-value filter documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: khaledosman

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses a valid Conventional Commit feature prefix, clearly describes multi-value usage filtering, uses imperative wording, and is under 70 characters.
Description check ✅ Passed The description follows the template, explains the API and dashboard changes, identifies issue #489, records tests, documentation, and AI usage.
Linked Issues check ✅ Passed The implementation satisfies issue #489 by adding multiselect support for models, users, and API keys in the analytics dashboard.
Out of Scope Changes check ✅ Passed The additional activity, bulk-mutation, documentation, generated-artifact, and test changes support the stated multi-value filtering objectives.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/dashboard-analytics-multiselect
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-analytics-multiselect
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/dashboard-analytics-multiselect

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from khaledosman August 6, 2026 15:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
web/src/api/types.ts (1)

560-568: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Keep raw activity filters scalar in the type system.

UsageFilters now permits arrays for all consumers. useUsageLogs and useUsageCount accept this type, but /v1/usage and /v1/usage/count remain single-value endpoints. A future caller can therefore send repeated entity parameters to a scalar endpoint.

Define an analytics-specific multi-value filter type. Keep the raw list and count hook inputs scalar. This preserves the bulk-operation scope contract at compile time.

🤖 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/types.ts` around lines 560 - 568, Split the shared UsageFilters
definition into scalar raw activity filters and an analytics-specific type that
permits string arrays for model, user_id, and api_key_id. Update useUsageLogs
and useUsageCount to accept the scalar type, while analytics consumers use the
multi-value type; preserve endpoint and provider as scalar fields.
🤖 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/index-Dit1BUBh.js`:
- Line 2: Cap multi-select usage filters for user_id, model, and api_key_id at
the backend limit of 50, matching _MAX_FILTER_VALUES. Update the relevant
usage-filter combobox selection handler to prevent selections beyond the cap or
display clear client-side feedback, and ensure query construction never includes
more than 50 values.

In `@tests/integration/test_usage_summary.py`:
- Line 20: Update the usage summary tests to use a test-local limit value of 50
instead of importing the private _MAX_FILTER_VALUES constant. Extend the
coverage around the existing usage summary limit tests to assert that the CSV
endpoint enforces the same 50-value limit contract.

In `@web/src/components/ui.test.tsx`:
- Around line 233-244: Update the test case around the Harness combobox to also
assert that the matching option “claude-sonnet-5” remains visible after typing
“claude”, while preserving the existing assertion that “gpt-5.6” is hidden and
onChange is not called.

In `@web/src/components/ui.tsx`:
- Around line 554-615: Update FilterMultiComboBox to accept a maxSelections prop
defaulting to 50, prevent onSelectionChange from adding values once
values.length reaches that limit, and display a concise limit message when the
limit is reached. Keep maxVisible limited to suggestion rendering only and
preserve existing selection behavior below the limit.

---

Nitpick comments:
In `@web/src/api/types.ts`:
- Around line 560-568: Split the shared UsageFilters definition into scalar raw
activity filters and an analytics-specific type that permits string arrays for
model, user_id, and api_key_id. Update useUsageLogs and useUsageCount to accept
the scalar type, while analytics consumers use the multi-value type; preserve
endpoint and provider as scalar fields.
🪄 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: 98c0e4fc-4766-4638-9fbd-4223ce9248d9

📥 Commits

Reviewing files that changed from the base of the PR and between b6cf83f and 42c29e1.

⛔ Files ignored due to path filters (1)
  • docs/public/openapi.json is excluded by !docs/public/openapi.json
📒 Files selected for processing (33)
  • docs/dashboard.md
  • docs/public/otari.postman_collection.json
  • src/gateway/api/routes/usage.py
  • src/gateway/static/dashboard/assets/ActivityPage-BbEENQgu.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DGkl3NSe.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-Dt_8xaSM.js
  • src/gateway/static/dashboard/assets/DocsPage-AglHrVWY.js
  • src/gateway/static/dashboard/assets/FilterChips-C0emi5Kg.js
  • src/gateway/static/dashboard/assets/FilterChips-CTE3I1G3.js
  • src/gateway/static/dashboard/assets/KeysPage-CEc7g4XL.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-BhMRwgM-.js
  • src/gateway/static/dashboard/assets/ModelsPage-299cCHBM.js
  • src/gateway/static/dashboard/assets/OverviewPage-CHysnnsw.js
  • src/gateway/static/dashboard/assets/ProvidersPage-BPyKQR5x.js
  • src/gateway/static/dashboard/assets/RoutingPage-2qgzgln4.js
  • src/gateway/static/dashboard/assets/SettingsPage-CLw9HtK0.js
  • src/gateway/static/dashboard/assets/TablePagination-BynkRKqB.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-CSbQtPkh.js
  • src/gateway/static/dashboard/assets/UsagePage-BTnJt3lF.js
  • src/gateway/static/dashboard/assets/UsagePage-tyubYvXE.js
  • src/gateway/static/dashboard/assets/UsersPage-C_yR1ElB.js
  • src/gateway/static/dashboard/assets/index-D-R1nuKP.js
  • src/gateway/static/dashboard/assets/index-Dit1BUBh.js
  • src/gateway/static/dashboard/index.html
  • tests/integration/test_usage_summary.py
  • web/src/api/hooks.ts
  • web/src/api/types.ts
  • web/src/components/FilterChips.tsx
  • web/src/components/ui.test.tsx
  • web/src/components/ui.tsx
  • web/src/pages/ActivityPage.tsx
  • web/src/pages/UsagePage.test.tsx
  • web/src/pages/UsagePage.tsx
💤 Files with no reviewable changes (3)
  • src/gateway/static/dashboard/assets/FilterChips-CTE3I1G3.js
  • src/gateway/static/dashboard/assets/index-D-R1nuKP.js
  • src/gateway/static/dashboard/assets/UsagePage-tyubYvXE.js

Comment thread src/gateway/static/dashboard/assets/index-Dit1BUBh.js Outdated
Comment thread tests/integration/test_usage_summary.py Outdated
Comment thread web/src/components/ui.test.tsx
Comment thread web/src/components/ui.tsx
…s accept

Review follow-ups on the analytics multiselect.

The picker had no ceiling while the endpoints cap a repeatable filter at 50
values: picked options drop out of the list and it refills, so a 51st pick was
reachable and failed every query on the page with a 422 the operator cannot act
on. At the ceiling the remaining options are now offered but inert and the input
reads "50 selected (max)", rather than an empty popover that explains nothing.

Also drops a no-op: the Usage page prepended its picked models to the option
list, which the picker then filters right back out, and the comment claimed
behavior the component contradicts. The repeated one-value-or-many logic in the
request-group filter now goes through the same helper as the entity filters, and
the API reference names the three repeatable filters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake njbrake changed the title fix(dashboard): let the analytics filters take several models, users, or keys feat(dashboard): filter the analytics charts on several models, users, or keys Aug 6, 2026
…keys

Extends the multiselect to the Activity page, so both usage views ask the same
kind of question and a drill-down from the analytics charts arrives intact.

What gated this was the bulk path, not the UI. "Select all N matching" counts
rows over the filters on screen and re-derives the set server-side from the
mutation body, so a body that could not express a multi-value filter would
delete or reprice rows the operator was never shown. UsageSelection now takes
the same repeatable form as the read filters, and the one-value-or-many helper
moved to core so the route and the service build the condition from one place
rather than two that could drift.

The request log's own read endpoints (/v1/usage, /v1/usage/count) take the three
entity filters repeatably too, the URL carries a filter as repeated params, and
each value is its own chip. The Model box keeps accepting a name outside its
suggestions (Enter commits it), since the suggestion list only covers models
with traffic in the window.

With the log able to express a set, the analytics drill-down no longer drops a
multi-value filter: it carries the whole selection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njbrake njbrake changed the title feat(dashboard): filter the analytics charts on several models, users, or keys feat(dashboard): filter usage views on several models, users, or keys Aug 6, 2026
Review follow-up on the multi-value filters.

Widening the bulk delete / set-price selection to accept several values per
dimension left it with no ceiling while every read endpoint caps at 50. A count
of 51 values is refused (422), yet the delete over those same 51 ran, on an
unbounded IN list. That contradicts the agreement the selection body exists to
hold: the "N matching" an operator confirms is taken over the read filters, so a
set the count cannot express must not be deletable either. Unreachable from the
dashboard, whose picker stops at 50, and reachable by any master-key caller.

The ceiling is annotated on the list arm alone, not on the str | list[str] field:
on the union it would also bound a single value's character length and reject a
long provider-qualified model name, which a test now pins. The constant moved
beside match_any in core, since a service may not import the API layer and both
sides need the same number.

Also drops FilterComboBox, whose last caller went away when the request log
picked up the multi-value picker; trims URL filter values so a hand-edited
whitespace param reads as cleared rather than as a filter on a space; and
applies a key's default only when the param is absent, matching how the
single-value reader treats present-but-blank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 `@docs/api-reference.md`:
- Around line 277-280: Add a `/v1/usage/series` row to the API reference table,
documenting its series response and repeatable `user_id`, `model`, and
`api_key_id` filters with a maximum of 50 values each and any-match behavior.
Keep the authorization requirement consistent with the related usage endpoints.
🪄 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: 5ac4a5e8-f04d-4aae-bafc-44cac5220c6f

📥 Commits

Reviewing files that changed from the base of the PR and between 42c29e1 and 064d0b5.

⛔ Files ignored due to path filters (1)
  • docs/public/openapi.json is excluded by !docs/public/openapi.json
📒 Files selected for processing (34)
  • docs/api-reference.md
  • docs/dashboard.md
  • docs/public/otari.postman_collection.json
  • src/gateway/api/routes/usage.py
  • src/gateway/core/sql.py
  • src/gateway/services/usage_admin_service.py
  • src/gateway/static/dashboard/assets/ActivityPage-BPU2LGjK.js
  • src/gateway/static/dashboard/assets/BudgetsPage-DJlQbSqO.js
  • src/gateway/static/dashboard/assets/ConfirmDialog-SjNq9mb0.js
  • src/gateway/static/dashboard/assets/DocsPage-y0JhnHoz.js
  • src/gateway/static/dashboard/assets/KeysPage-Bpm4Jwdc.js
  • src/gateway/static/dashboard/assets/ModelScopeControl-C95TdfTG.js
  • src/gateway/static/dashboard/assets/ModelsPage-SIJ8rtyI.js
  • src/gateway/static/dashboard/assets/OverviewPage-DuVfvos1.js
  • src/gateway/static/dashboard/assets/ProvidersPage-CooXsqcL.js
  • src/gateway/static/dashboard/assets/RoutingPage-C1MiygDT.js
  • src/gateway/static/dashboard/assets/SettingsPage-C-d0gEqT.js
  • src/gateway/static/dashboard/assets/TablePagination-C0s9RgHF.js
  • src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BOqIdkNT.js
  • src/gateway/static/dashboard/assets/UsagePage-DKSiL958.js
  • src/gateway/static/dashboard/assets/UsersPage-ClMRPPza.js
  • src/gateway/static/dashboard/assets/index-hq5nyILv.js
  • src/gateway/static/dashboard/index.html
  • tests/integration/test_usage_admin.py
  • tests/integration/test_usage_summary.py
  • web/src/api/types.ts
  • web/src/components/ui.test.tsx
  • web/src/components/ui.tsx
  • web/src/lib/urlState.test.tsx
  • web/src/lib/urlState.ts
  • web/src/pages/ActivityPage.test.tsx
  • web/src/pages/ActivityPage.tsx
  • web/src/pages/UsagePage.test.tsx
  • web/src/pages/UsagePage.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/gateway/static/dashboard/index.html
  • docs/dashboard.md
  • web/src/components/ui.test.tsx
  • tests/integration/test_usage_summary.py

Comment thread docs/api-reference.md
njbrake and others added 3 commits August 6, 2026 17:50
/v1/usage/series was missing from the endpoint table while its siblings were all
listed, so the two public references disagreed: the Postman collection carries it
(generated from the spec) and the reference did not. This PR changes its filters,
which is what surfaced the gap.

Documents the required group_by, the repeatable entity filters it shares with
/summary, the eight-group fold that keeps the stack reconciling with the summary
totals, and the 422 an hourly bucket over a too-wide window returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vironment

Three notes this PR earned the hard way, plus one stale pointer found beside them.

src/gateway/AGENTS.md gains the rule that a bulk usage mutation re-derives its
target set from the filter fields in its body, so the read filters and
UsageSelection have to agree on which filters exist, what value space each
accepts, and what bounds it. Each of those three was violated at least once
while building this change, the last one only on the destructive path, so the
section says what goes wrong rather than only what the rule is.

The root Test Notes now say PostgreSQL is required rather than preferred, and
why SQLite is not a fallback despite the URL helper accepting one (the fixtures
tear down with DROP TABLE ... CASCADE). It also names the two tests that assert
error sanitization through a real outbound provider call, which report a status
mismatch in a sandbox with no egress and are not a regression signal.

The frontend components table lists the filter-bar primitives, and its "Tabular
data" row pointed at components/Table.tsx, which does not exist; the component
is DataTable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cs-multiselect

# Conflicts:
#	src/gateway/static/dashboard/assets/ActivityPage-CHnjo06U.js
#	src/gateway/static/dashboard/assets/BudgetsPage-B9iEC7ec.js
#	src/gateway/static/dashboard/assets/BudgetsPage-DJlQbSqO.js
#	src/gateway/static/dashboard/assets/BudgetsPage-H5EC9TjF.js
#	src/gateway/static/dashboard/assets/ConfirmDialog-DzIVWoi5.js
#	src/gateway/static/dashboard/assets/ConfirmDialog-SjNq9mb0.js
#	src/gateway/static/dashboard/assets/ConfirmDialog-mbnZRETP.js
#	src/gateway/static/dashboard/assets/DocsPage-D53o1bCm.js
#	src/gateway/static/dashboard/assets/DocsPage-DT-vQq9F.js
#	src/gateway/static/dashboard/assets/DocsPage-y0JhnHoz.js
#	src/gateway/static/dashboard/assets/FilterChips-o0JYRTLi.js
#	src/gateway/static/dashboard/assets/KeysPage-Bpm4Jwdc.js
#	src/gateway/static/dashboard/assets/KeysPage-CtY-9BqY.js
#	src/gateway/static/dashboard/assets/KeysPage-fg3Rz_lV.js
#	src/gateway/static/dashboard/assets/ModelScopeControl-BBYX_HiM.js
#	src/gateway/static/dashboard/assets/ModelScopeControl-C95TdfTG.js
#	src/gateway/static/dashboard/assets/ModelScopeControl-dT2wvSYp.js
#	src/gateway/static/dashboard/assets/ModelsPage-SIJ8rtyI.js
#	src/gateway/static/dashboard/assets/OverviewPage-0PkW5qfi.js
#	src/gateway/static/dashboard/assets/OverviewPage-B5D-L-_p.js
#	src/gateway/static/dashboard/assets/OverviewPage-DuVfvos1.js
#	src/gateway/static/dashboard/assets/ProvidersPage-CooXsqcL.js
#	src/gateway/static/dashboard/assets/RoutingPage-C1MiygDT.js
#	src/gateway/static/dashboard/assets/RoutingPage-CD9-r0aL.js
#	src/gateway/static/dashboard/assets/RoutingPage-D1os8M2m.js
#	src/gateway/static/dashboard/assets/SettingsPage-BtoAJ9Y8.js
#	src/gateway/static/dashboard/assets/SettingsPage-C-d0gEqT.js
#	src/gateway/static/dashboard/assets/SettingsPage-C2Hp1OPt.js
#	src/gateway/static/dashboard/assets/TablePagination-BEmYAlSB.js
#	src/gateway/static/dashboard/assets/TablePagination-Bnlr8FbD.js
#	src/gateway/static/dashboard/assets/TablePagination-C0s9RgHF.js
#	src/gateway/static/dashboard/assets/ToolsGuardrailsPage-BOqIdkNT.js
#	src/gateway/static/dashboard/assets/ToolsGuardrailsPage-C-E4XKsV.js
#	src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DNC3Wa-x.js
#	src/gateway/static/dashboard/assets/UsagePage-pCJIBBUc.js
#	src/gateway/static/dashboard/assets/UsersPage-Be1Tcz9b.js
#	src/gateway/static/dashboard/assets/UsersPage-CRNgQ9x1.js
#	src/gateway/static/dashboard/assets/UsersPage-ClMRPPza.js
#	src/gateway/static/dashboard/assets/index-BE7E0N2z.js
#	src/gateway/static/dashboard/index.html
@njbrake
njbrake temporarily deployed to integration-tests August 6, 2026 18:13 — with GitHub Actions Inactive
@njbrake
njbrake merged commit 9ef21b1 into main Aug 6, 2026
8 of 11 checks passed
@njbrake
njbrake deleted the fix/dashboard-analytics-multiselect branch August 6, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard: analytics page should allow multiselect of models/users/api keys for populating chargs

1 participant