Skip to content

feat(ui): provider/model filter bar — URL-persisted filters across all dashboard tabs#66

Merged
0bserver07 merged 1 commit into
mainfrom
feat/provider-model-filters
May 2, 2026
Merged

feat(ui): provider/model filter bar — URL-persisted filters across all dashboard tabs#66
0bserver07 merged 1 commit into
mainfrom
feat/provider-model-filters

Conversation

@0bserver07
Copy link
Copy Markdown
Owner

Summary

  • Global FilterBar between the tab strip and content on every dashboard tab. URL-persisted (?provider=cursor&provider=cline&model=opus-4-7), so filtered views survive refresh and can be shared.
  • Click-to-filter: Compare row → pin (provider, model); CostByProviderCard slice → filter to that provider; project picker gains optional group-by-provider mode (toggle persists in localStorage).
  • New GET /api/providers route + ?provider= / ?model= query params on /api/projects, /api/cost-data/by-provider, /api/jsonl-files, /api/messages, /api/dashboard-data. Empty filter = "all" (preserves existing API contracts). Case-insensitive on read, lowercased on emit.

Architecture

  • React Context (FiltersProvider / useFilters) is the single source of truth for filter state. Pure URL helpers live in services/filterUrl.ts (no JSX) so they're unit-testable under node --test.
  • React Query keys include the active filter set, so changes trigger refetches automatically — no risk of serving stale unfiltered data.
  • Backend filters are applied at the SQL boundary where cheap (cost-by-provider, providers catalogue), or post-query for routes whose aggregator runs project-wide (dashboard-data's models map, messages list).

Test plan

  • pytest tests/ -q — 1341 passed, 2 skipped (was 1333 + 8 new).
  • cd stackunderflow-ui && npm run typecheck — clean.
  • cd stackunderflow-ui && npm run build — clean.
  • node --test tests/services/filters.test.ts — 12 tests pass (URL parse, normalize, query-string build, round-trip, SSR safety, no-op write).
  • Smoke test against real store (188 projects, 7 providers): /api/providers returns claude:146, codex:22, gemini:17, cursor:7, cline:1, droid:1, qwen:1; /api/projects?provider=cursor narrows 187 → 7 projects.
  • Manual smoke against running v0.6.1 dashboard: select Cursor in filter bar → all tabs scope to Cursor data; click Compare row → models filter pushed; clear filter → returns to all.

Files

  • New: stackunderflow-ui/src/services/filters.tsx, services/filterUrl.ts, components/common/FilterBar.tsx, tests/services/filters.test.ts, tests/stackunderflow/routes/test_providers_filter.py.
  • Modified backend: routes/projects.py (+ /api/providers, ?provider= on /api/projects), routes/cost.py (?provider= on by-provider), routes/data.py (?provider=, ?model= on dashboard-data + messages), routes/sessions.py (?provider= on jsonl-files).
  • Modified frontend: App.tsx (<FiltersProvider>), pages/ProjectDashboard.tsx (<FilterBar> + filter-aware dashboard query), components/dashboard/{Compare,Sessions,Messages,Yield,OptimizeFindingsPanel}.tsx, components/cost/CostByProviderCard.tsx, components/layout/Header.tsx (group-by-provider picker), services/api.ts (filter-aware API helpers).

🤖 Generated with Claude Code

…l dashboard tabs

Adds a global FilterBar between the tab strip and content on every dashboard
tab. State lives in a React Context (`FiltersProvider`) and syncs to URL
search params (`?provider=cursor&provider=cline&model=opus-4-7`) via
`history.replaceState` so toggles never trap the back button and shared
links restore filter state.

Click-to-filter affordances:
- Compare row click → pin (provider, model) for the dashboard
- CostByProviderCard slice → filter to that provider
- Project picker per-group "filter" link → scope without leaving the picker

Project picker (Header dropdown) gains an optional group-by-provider mode
(toggle persists in localStorage) so users with multi-provider stores can
collapse the long flat list under provider headers.

Backend wiring:
- New `GET /api/providers` route returns the provider catalogue with
  project + session counts for the chip row's count column
- `/api/projects`, `/api/cost-data/by-provider`, `/api/jsonl-files`,
  `/api/messages`, `/api/dashboard-data` gain `?provider=` (and `?model=`
  on dashboard-data and messages) with empty=all back-compat semantics
- Filter values are case-insensitive on read (`?provider=Cursor` works)
  and lowercased on emit so canonical URLs stay stable

React Query keys include the active filter set so changes refetch
automatically — no risk of stale unfiltered data after a filter change.

Tests: 8 new backend tests (`test_providers_filter.py`) covering provider
catalogue shape, projects narrowing, case-insensitivity, repeated-param
union semantics, cost-by-provider rollup filtering, and empty-filter
back-compat. 12 new frontend tests (`filters.test.ts`) covering URL
parsing, normalize, build-query-string, round-trip, no-window SSR safety,
and no-op-write. 1341 backend tests pass (was 1333 + 8 new); frontend
typecheck + build clean.

Smoke-tested against the running v0.6.1 dashboard's real store (188
projects, 7 providers): `/api/providers` returns the expected
claude:146, codex:22, gemini:17, cursor:7, cline:1, droid:1, qwen:1
counts; `/api/projects?provider=cursor` narrows 187 → 7 projects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@0bserver07 0bserver07 merged commit 6672b26 into main May 2, 2026
9 checks passed
@0bserver07 0bserver07 deleted the feat/provider-model-filters branch May 2, 2026 14:11
0bserver07 added a commit that referenced this pull request May 2, 2026
PR #66 mounted FilterBar on every per-project tab via
ProjectDashboard.tsx but missed the top-level Projects Overview page
(Overview.tsx is a separate route, not a tab). On a 7-provider store
the user landed on the Overview page and saw no filter bar — the only
filter chrome was Overview's own per-model toggle, no provider filter.

This adds <FilterBar/> at the top of Overview.tsx so the provider/model
filter is visible from the moment the dashboard opens. State is shared
via FiltersProvider in App.tsx (already wraps both routes), so
filtering on Overview persists when the user navigates into a project
dashboard, and vice versa.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bserver07 added a commit that referenced this pull request May 5, 2026
Two issues blocked v0.6.1 multi-provider from working in real use:

1. Filter query params silently ignored on 5 routes.

   FastAPI requires ``Query()`` (or ``Annotated[..., Query()]``) to bind
   repeated list-typed query parameters. Without it the param stays
   ``None`` regardless of what the URL contains. The routes added in
   PR #66 used the bare ``list[str] | None = None`` shape, so every
   ``?provider=...`` / ``?model=...`` filter from FilterBar was being
   dropped on the server side. Verified live before fix:

       /api/projects?provider=cursor → 187 projects (all 7 providers)
       /api/projects?provider=claude → 187 projects (all 7 providers)

   After fix:

       /api/projects                            → 187 projects
       /api/projects?provider=cursor            →   7 projects
       /api/projects?provider=cursor+cline      →   8 projects

   Used ``Annotated[list[str] | None, Query()] = None`` so direct
   in-process calls (existing test pattern) keep getting ``None`` as
   the default while HTTP-bound calls go through FastAPI's normal
   parameter binding. 9 bindings touched across projects, sessions,
   cost, data (dashboard-data + messages), optimize.

2. Startup blocked HTTP for the entire reindex (~90s today, 30+min on
   cold 188-project store).

   The ``_lifespan`` handler ran ``run_ingest()`` synchronously before
   ``yield``, so the HTTP server didn't actually start serving until
   ingest finished — even though the CLI wrapper had already printed
   the misleading "live at..." line. Schema apply still runs sync
   (cheap, needed by every route), but ``run_ingest`` now runs in a
   daemon background thread so HTTP starts serving in <1s. The
   "Ingest complete" log line still fires when the thread finishes;
   any error in the thread degrades to a single ``logger.error`` call,
   matching the previous behaviour.

1341 backend tests pass. No version bump.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bserver07 added a commit that referenced this pull request May 20, 2026
…l dashboard tabs (#66)

Adds a global FilterBar between the tab strip and content on every dashboard
tab. State lives in a React Context (`FiltersProvider`) and syncs to URL
search params (`?provider=cursor&provider=cline&model=opus-4-7`) via
`history.replaceState` so toggles never trap the back button and shared
links restore filter state.

Click-to-filter affordances:
- Compare row click → pin (provider, model) for the dashboard
- CostByProviderCard slice → filter to that provider
- Project picker per-group "filter" link → scope without leaving the picker

Project picker (Header dropdown) gains an optional group-by-provider mode
(toggle persists in localStorage) so users with multi-provider stores can
collapse the long flat list under provider headers.

Backend wiring:
- New `GET /api/providers` route returns the provider catalogue with
  project + session counts for the chip row's count column
- `/api/projects`, `/api/cost-data/by-provider`, `/api/jsonl-files`,
  `/api/messages`, `/api/dashboard-data` gain `?provider=` (and `?model=`
  on dashboard-data and messages) with empty=all back-compat semantics
- Filter values are case-insensitive on read (`?provider=Cursor` works)
  and lowercased on emit so canonical URLs stay stable

React Query keys include the active filter set so changes refetch
automatically — no risk of stale unfiltered data after a filter change.

Tests: 8 new backend tests (`test_providers_filter.py`) covering provider
catalogue shape, projects narrowing, case-insensitivity, repeated-param
union semantics, cost-by-provider rollup filtering, and empty-filter
back-compat. 12 new frontend tests (`filters.test.ts`) covering URL
parsing, normalize, build-query-string, round-trip, no-window SSR safety,
and no-op-write. 1341 backend tests pass (was 1333 + 8 new); frontend
typecheck + build clean.

Smoke-tested against the running v0.6.1 dashboard's real store (188
projects, 7 providers): `/api/providers` returns the expected
claude:146, codex:22, gemini:17, cursor:7, cline:1, droid:1, qwen:1
counts; `/api/projects?provider=cursor` narrows 187 → 7 projects.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bserver07 added a commit that referenced this pull request May 20, 2026
PR #66 mounted FilterBar on every per-project tab via
ProjectDashboard.tsx but missed the top-level Projects Overview page
(Overview.tsx is a separate route, not a tab). On a 7-provider store
the user landed on the Overview page and saw no filter bar — the only
filter chrome was Overview's own per-model toggle, no provider filter.

This adds <FilterBar/> at the top of Overview.tsx so the provider/model
filter is visible from the moment the dashboard opens. State is shared
via FiltersProvider in App.tsx (already wraps both routes), so
filtering on Overview persists when the user navigates into a project
dashboard, and vice versa.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0bserver07 added a commit that referenced this pull request May 20, 2026
Two issues blocked v0.6.1 multi-provider from working in real use:

1. Filter query params silently ignored on 5 routes.

   FastAPI requires ``Query()`` (or ``Annotated[..., Query()]``) to bind
   repeated list-typed query parameters. Without it the param stays
   ``None`` regardless of what the URL contains. The routes added in
   PR #66 used the bare ``list[str] | None = None`` shape, so every
   ``?provider=...`` / ``?model=...`` filter from FilterBar was being
   dropped on the server side. Verified live before fix:

       /api/projects?provider=cursor → 187 projects (all 7 providers)
       /api/projects?provider=claude → 187 projects (all 7 providers)

   After fix:

       /api/projects                            → 187 projects
       /api/projects?provider=cursor            →   7 projects
       /api/projects?provider=cursor+cline      →   8 projects

   Used ``Annotated[list[str] | None, Query()] = None`` so direct
   in-process calls (existing test pattern) keep getting ``None`` as
   the default while HTTP-bound calls go through FastAPI's normal
   parameter binding. 9 bindings touched across projects, sessions,
   cost, data (dashboard-data + messages), optimize.

2. Startup blocked HTTP for the entire reindex (~90s today, 30+min on
   cold 188-project store).

   The ``_lifespan`` handler ran ``run_ingest()`` synchronously before
   ``yield``, so the HTTP server didn't actually start serving until
   ingest finished — even though the CLI wrapper had already printed
   the misleading "live at..." line. Schema apply still runs sync
   (cheap, needed by every route), but ``run_ingest`` now runs in a
   daemon background thread so HTTP starts serving in <1s. The
   "Ingest complete" log line still fires when the thread finishes;
   any error in the thread degrades to a single ``logger.error`` call,
   matching the previous behaviour.

1341 backend tests pass. No version bump.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant