fix: add pagination to user, project, and subtask lists - #493
Conversation
There was a problem hiding this comment.
Important
The background drain in AssignedTasksList shares the ["projects","all"] infinite-query cache with the home grid, so it loads every project page and effectively disables the grid's new "Load more projects" button. The same effect also re-fires without bound when a later page fetch fails.
Reviewed changes
- Workspace-wide must-change-password count —
CountUsersMustChangePasswordadded to the user repo/service interfaces plus the Postgres implementation, and exposed asPagedUsersResponse.MustChangePasswordCount; the admin stats bar no longer approximates this from the current page. Paginationprimitive — newcomponents/ui/pagination.tsx(collapsing page range, prev/next,aria-current, i18n) wired into the admin users route, which gains realpagestate,keepPreviousData, and a snap-back effect for a shrinking result set.- Project list infinite pagination —
projectsInfiniteQueryOptions(["projects","all"], 50/page) replaces the single-page project fetch across the home grid, the sidebar switcher, and the assigned-tasks widget, with a load-more button on the home grid. - Subtask cursor pagination —
listSubtasksnow returnsTaskListResultand pages vianext_cursorat 50/page behindsubtasksInfiniteQueryOptions; the unusedepicChildTasksQueryOptionswas removed and the subtasks section gained a load-more affordance. - i18n and tests — new pagination/load-more keys across all 9 locales;
pagination.test.tsx,subtasks-section.test.tsx, and API-test updates.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the single commit pushed since the prior pullfrog review (4c2fce38), which addresses both findings from that review.
- Added
projectsLookupInfiniteQueryOptions— the assigned-tasks widget now pages the project list under its own["projects","all","lookup"]cache key, so its eager background drain no longer exhausts the["projects","all"]cache shared with the home grid and sidebar switcher. - Gated the widget's drain effect on
!isFetchNextPageError— a failed next-page fetch no longer re-firesfetchNextPagein an unbounded loop, sincehasNextPagestaystrueafter a failure. - Extracted shared
fetchProjectsPage/getNextProjectsPageParamhelpers so both project infinite-query options page identically. - Added regression coverage: the widget test asserts the drain makes exactly one failed attempt, and
project-api.test.tsasserts the lookup key is distinct from the browsable key.
The Go must-change-password count path, the Pagination primitive, subtask cursor paging, and i18n parity were covered by the prior review and are unchanged here.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Summary
Fixes #492 — the admin Users page fetched a paginated list but never advanced past page 1 (the page setter was declared but never used), so accounts beyond the first 20 were invisible with no page controls to reach them, even though the backend already supported real pagination and the UI displayed the true total.
An audit for the same pattern elsewhere in the app found it in three more places, all fixed here.
What was broken
/admin/users):const [page] = useState(1)— the setter was unused, so the page was permanently stuck at 1.projectsQueryOptions(page=1, pageSize=50)was always called with defaults and never advanced — same shape of bug, capped at 50 projects, with the true total shown right next to the truncated grid.listSubtaskshardcodedpage: 1, page_size: 200and discarded the response's cursor, so a task with more than 200 direct children silently hid the rest.Also found and fixed an adjacent bug the Users page surfaced once paging actually worked: the "must change password" stat was computed by filtering the current page's users instead of a system-wide count, so it would have started fluctuating as an admin paged through users.
Fix
Paginationcomponent (page-number style) for the admin Users table — fits a structured data table with row actions better than infinite scroll, matching typical admin UX.projectsInfiniteQueryOptionsandsubtasksInfiniteQueryOptions(cursor-paginated), mirroring the app's existing picker/board conventions, and wired a "Load more" button / scroll handler into the home grid, sidebar switcher, and Subtasks panel.CountUsersMustChangePasswordthrough the repository → service → handler layers plus amust_change_password_countfield on the paginated response, so the stat stays accurate regardless of which page is showing.parsePage/parsePageSize/pagingParamscall site cross-referenced against every frontendinfiniteQueryOptionsconsumer) — confirmed notifications, agent activity, conversations, the actual task board/backlog view, and the epic/task pickers all already wirefetchNextPageto real UI.Test plan
tsc -bandbiome checkpassgo build ./...,go vet ./...,gofmt, andgo test ./...all passPagination,projectsInfiniteQueryOptions,subtasksInfiniteQueryOptions, and the must-change-password counttotalunchanged; created a throwaway task with 3 subtasks and confirmed cursor pagination returns the correct items across pages, ending innext_cursor: null; confirmedmust_change_password_countreflects a true workspace-wide count (18 of 22 real users) regardless of which page is displayed🤖 Generated with Claude Code