chore(proposals): server-side ?source= filter on GET /api/v1/proposals#83
Conversation
Per chore_proposals_source_filter_server_side: extend the list endpoint with ?source=study|manual so the backend filters before pagination + count. Drops the now-redundant client-side filter in the proposals page. Backend: ProposalSourceWire + ProposalSourceFilter Literals; source kwarg on list_proposals_paginated + count_proposals; ?source= on the endpoint. Frontend: ProposalsFilter gains source field; page.tsx maps 'all' chip → undefined param, drops matchesSourceFilter callback + the visibleRows useMemo; setSource now resets cursor. Test rewrites: page.test.tsx asserts server-side contract (?source=manual on the wire → backend returns only manual rows). Verified: 696 unit tests, mypy + enum gate clean, vitest 6/6 on proposals page tests. Archives chore_proposals_source_filter_server_side to implemented_features. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements server-side filtering for proposals based on their source (study-derived or manual). The changes involve updating the API endpoint to accept a source query parameter, modifying the database repository to apply the corresponding study_id filter, and adjusting the UI to send this filter to the backend, removing previous client-side filtering logic. Documentation for the MVP1 dashboard has also been updated to reflect the completion of this chore. Review comments suggest addressing an inconsistency where study_id is passed from the UI but not handled by the backend, improving the formatting of a multi-line function call in proposals.py, and refining type hints in _apply_source_filter for better type safety.
| const { status, cluster_id, study_id, source, cursor, limit } = filter; | ||
| return useQuery<ProposalsPage, ApiError>({ | ||
| queryKey: ['proposals', { status, cluster_id, study_id, cursor, limit }], | ||
| queryKey: ['proposals', { status, cluster_id, study_id, source, cursor, limit }], | ||
| queryFn: async () => { | ||
| const { data, headers } = await apiClient.get<ProposalsListResponse>('/api/v1/proposals', { | ||
| params: { status, cluster_id, study_id, cursor, limit }, | ||
| params: { status, cluster_id, study_id, source, cursor, limit }, | ||
| }); |
There was a problem hiding this comment.
The study_id parameter is being passed to the GET /api/v1/proposals endpoint, but the backend's list_proposals_endpoint function does not accept a study_id query parameter. This means the parameter is currently being ignored by the backend.
While this might be a pre-existing issue, since you're modifying this code block, it's a good opportunity to address this inconsistency. You should either remove study_id from the params object if it's not needed for this hook, or add support for it on the backend if it is intended to be used for filtering.
| total = await repo.count_proposals( | ||
| db, status=status_filter, cluster_id=cluster_id, source=source | ||
| ) |
There was a problem hiding this comment.
For consistency with the list_proposals_paginated call above, consider formatting this multi-line function call with each argument on its own line. This improves readability.
| total = await repo.count_proposals( | |
| db, status=status_filter, cluster_id=cluster_id, source=source | |
| ) | |
| total = await repo.count_proposals( | |
| db, | |
| status=status_filter, | |
| cluster_id=cluster_id, | |
| source=source, | |
| ) |
| return row | ||
|
|
||
|
|
||
| def _apply_source_filter(stmt: Any, source: ProposalSourceFilter | None) -> Any: |
There was a problem hiding this comment.
…tal) (#85) After Wave 2 (PRs #82-#84, 5 more items resolved), updates: - "Last updated" line reflects Wave 2 total - "Active feature" line: 14 items drained; remaining backlog breakdown is 4 inline-fix + 5 /bug-fix + 2 /pipeline + 4 keep-deferred - New "Most recent meaningful changes" entry for Wave 2 detailing PRs #82, #83, #84 + the 2 Gemini-accept fixes (time.monotonic on smoke timeout, (method, path)-pair orphan check on openapi-surface) Wave 2 was driven by an Explore-agent re-classification of the 7 items originally routed to /pipeline — turned out 5 of them were inline-fix / done-or-superseded on closer reading (validates the rubric's "lean inline" default). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…corecard alerts) (#431) * ci(security): add CodeQL SAST + write Dockerfile base-image digests literally Two follow-ups on the post-merge OSSF Scorecard surface (PR #430 closed the postcss vuln + ~50 pinning alerts; this closes the SAST finding + the 6 remaining containerImage alerts). CodeQL (#71): add .github/workflows/codeql.yml — GitHub first-party SAST on push/PR to main + weekly, scanning python + javascript-typescript with build-mode none. Closes the Scorecard "SAST tool is not run on all commits" finding and surfaces real code-level bugs on every PR. Action refs pinned to SHAs to match the repo's pinning posture. Dockerfile digests (#59/#60/#80-#83): revert the BASE_IMAGE ARG indirection back to literal `image@sha256:…` on each FROM. The pin was always real, but Scorecard's static parser only credits a digest it can see inline — an ARG-indirected digest reads as "unpinned", which is what kept all 6 containerImage alerts open (Dockerfile:24/46/79 + ui/Dockerfile:24/30/39, confirmed from the Scorecard SARIF locations). Writing tag+digest together also removes the override footgun Gemini flagged on PR #430; Dependabot bumps every FROM occurrence in lockstep, so the repeated node digest is not a sync hazard. Both Dockerfiles pass `docker buildx build --check`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> * ci(security): builder stage inherits from deps (Gemini #431) Accept Gemini's finding on PR #431: have the ui builder stage `FROM deps` instead of re-deriving from the base image. deps already has pnpm, WORKDIR, and node_modules, so this drops a redundant `npm install -g pnpm@9` + the node_modules COPY, removes one base-image digest occurrence, and eliminates one of the two npmCommand Scorecard findings. A stage ref to a digest-pinned stage is still credited as pinned by Scorecard. node_modules is .dockerignore'd so `COPY . .` doesn't clobber the installed deps. Verified with a full `docker buildx build` of the ui image (next build + runner copy succeed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> --------- Signed-off-by: SoundMindsAI <eric.starr@soundminds.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Summary
Wave 2 PR C of the pre-MVP2 backlog sweep:
chore_proposals_source_filter_server_side.Extends
GET /api/v1/proposalswith?source=study|manualso the backend filters BEFORE pagination + count. Drops the now-redundant client-side filter in the proposals page.Before
/proposalsapplied the source chip filter client-side after the cursor-paginated page landed:X-Total-Count(UI: "showing 2 of 50")After
Backend filters server-side → pagination,
X-Total-Count, and visible rows all agree.Files
backend/app/api/v1/schemas.pyProposalSourceWire = Literal["study", "manual"]backend/app/db/repo/proposal.pyProposalSourceFilterLiteral +_apply_source_filterhelper;list_proposals_paginated+count_proposalstakesourcekwargbackend/app/api/v1/proposals.pylist_proposals_endpointaccepts?source=, passes throughui/src/lib/api/proposals.tsProposalsFilter.sourcefield;useProposalspasses it as query paramui/src/app/proposals/page.tsxmatchesSourceFilter+visibleRows useMemo; mapssourceFilter !== 'all' → source;setSourceresets cursorui/src/__tests__/app/proposals/page.test.tsx?source=manualon the wire instead of client-side trimTest plan
scripts/ci/verify_enum_source_of_truth.sh— 21 allowlists clean (the newProposalSourceWireis grounded in the backend)app/proposals/page.test.tsx: 6/6 passArchives
chore_proposals_source_filter_server_side/idea.md→docs/00_overview/implemented_features/2026_05_13_chore_proposals_source_filter_server_side/idea.md. Dashboard regenerated.🤖 Generated with Claude Code