feat(ui): provider/model filter bar — URL-persisted filters across all dashboard tabs#66
Merged
Merged
Conversation
…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
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>
Merged
3 tasks
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>
Merged
5 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
FilterBarbetween 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.GET /api/providersroute +?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
FiltersProvider/useFilters) is the single source of truth for filter state. Pure URL helpers live inservices/filterUrl.ts(no JSX) so they're unit-testable undernode --test.modelsmap, 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)./api/providersreturns claude:146, codex:22, gemini:17, cursor:7, cline:1, droid:1, qwen:1;/api/projects?provider=cursornarrows 187 → 7 projects.Files
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.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).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