Skip to content

Filter the approvals queue in place instead of navigating to it - #38

Merged
ralyodio merged 1 commit into
mainfrom
worktree-approvals-inpage-filter
Aug 17, 2026
Merged

ralyodio merged 1 commit into
mainfrom
worktree-approvals-inpage-filter

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The problem

The filter tabs on /approvals were <Link>s to /approvals?filter=…, so clicking one was a navigation, not a filter. The page blanked, the queue was fetched again, and the scroll position went back to the top — to show rows the browser already had. Four tabs meant four URLs for one screen.

What changed

The page asks for the whole pending queue once and the tabs pick from it in the browser. Switching tabs costs a re-render and no request at all.

  • bucket on every row. Each recommendation now carries ready / needs_draft / research, computed in the same SQL that produces the counts on the tabs, so a client-side tab can never disagree with the badge beside it. Recomputing that in the browser from action and draft_body would be a second implementation of a rule that already exists, free to drift from it.
  • The URL still tracks the tab, via replaceState rather than a navigation, so /approvals?filter=research still deep-links and a reload lands back on the tab you were reading. replaceState and not pushState: a filter is a view of one page, so Back should leave the queue rather than walk you through the tabs you clicked on the way in.
  • One page of the queue, not one per tab, so the limit is the API's own ceiling of 200 — production's queue is ~75 rows. Past that the counts still tell the truth and the page says it is showing a subset.

The bucket expression sits in the SELECT, ahead of the WHERE clause the filter narrows, so its placeholders bind first. That reordering is the classic way this breaks, and there is a test for it.

Verification

  • bun run check — format, typecheck, 847 tests pass.
  • next build for apps/web (the root typecheck does not cover it).
  • Driven in a real headless browser against a production build, with a seeded queue of 2 ready / 1 needs-draft / 3 research:
    • ?filter=research deep-links to the Research tab showing its 3 cards
    • clicking Ready swaps to its 2 cards and marks it pressed
    • a marker set on window survives the click — the page was never replaced
    • the URL becomes ?filter=ready
    • history.length does not grow, and no new document request is issued

🤖 Generated with Claude Code

The tabs on /approvals were links to /approvals?filter=…, so every click
was a fresh server round-trip: the page blanked, the queue was fetched
again and the scroll position went back to the top — to show rows the
browser already had. Four tabs meant four URLs for one screen.

The page now asks for the whole pending queue once and the tabs pick from
it in the browser. Switching costs a re-render and no request at all.

Each row carries the bucket it belongs to, computed in the same SQL that
produces the counts on the tabs, so a client-side tab can never disagree
with the badge beside it. Recomputing the classification in the browser
from `action` and `draft_body` would have been a second implementation of
that rule, free to drift.

The URL still tracks the tab, via replaceState rather than a navigation,
so /approvals?filter=research still deep-links and a reload lands back on
the tab you were reading. replaceState and not pushState: a filter is a
view of one page, so Back should leave the queue rather than walk you
through the tabs you clicked on the way in.

The fetch is one page of the queue rather than one page per tab, so the
limit is the API's own ceiling of 200 — production's queue is ~75 rows.
Past that the counts still tell the truth and the page says it is showing
a subset.

Verified in a real browser against a production build: clicking a tab
swaps the cards, updates the URL, adds no history entry and issues no
document request, and a marker set on `window` survives the click.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 4949bc0 into main Aug 17, 2026
4 checks passed
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