Skip to content

fix(findings): server-side pagination — show all findings, not just first 100 - #324

Merged
0xmanhnv merged 1 commit into
developfrom
fix/findings-server-pagination
Jul 24, 2026
Merged

fix(findings): server-side pagination — show all findings, not just first 100#324
0xmanhnv merged 1 commit into
developfrom
fix/findings-server-pagination

Conversation

@0xmanhnv

Copy link
Copy Markdown
Contributor

The bug

On the Findings list, the severity tabs showed the true total (e.g. All (6330)) but the table was stuck at "Page 10 of 10 · of 100 results" — a tenant with thousands of findings could only ever see the first 100.

Cause: findings/page.tsx fetched a single page of per_page: 100 (no page param) and handed those 100 rows to the shared DataTable, which paginated them client-side (10/page → exactly 10 pages). The tab count comes from the separate stats endpoint (COUNT(*)), so it was correct — which made the capped table look broken.

The fix — real server-side pagination

The API + types already supported page/per_page/total_pages; only the UI wiring was missing.

DataTable (shared, ~100 callers): add opt-in manual pagination —

  • manualPagination, pageCount/rowCount, controlled pagination + onPaginationChange
  • getRowId for stable row selection when the page's data swaps
  • when manual, getPaginationRowModel is omitted (the parent already fetched one page) and the "Showing X of Y" total uses the server row count

The default (client-side) path is unchanged — every existing caller is byte-compatible.

findings/page.tsx: drive page/per_page from pagination state (default 20/page), feed the response total to the pager, and reset to page 1 on any filter change.

Verify

  • tsc --noEmit = 0 errors; eslint clean on both files.
  • Live: /findings compiles + returns 200; the pager now walks the full result set, page size selector works, and filters reset to page 1.

…irst 100

The findings list fetched a single page of per_page=100 and let the shared
DataTable paginate those rows client-side (10/page -> 10 pages). A tenant with
thousands of findings could therefore only ever reach the first 100; the tab
count (from the stats endpoint) correctly showed the true total, making the
capped table look broken.

- DataTable: add opt-in manual (server) pagination — manualPagination, pageCount/
  rowCount, controlled pagination + onPaginationChange, and getRowId for stable
  selection across page swaps. Default path (client-side) is byte-compatible, so
  the ~100 other callers are unaffected.
- findings page: drive page/per_page from pagination state, use the response
  total for the pager, reset to page 1 on any filter change.
@0xmanhnv
0xmanhnv force-pushed the fix/findings-server-pagination branch from 08232a9 to f147802 Compare July 24, 2026 07:37
@0xmanhnv
0xmanhnv merged commit ec9ada7 into develop Jul 24, 2026
13 checks passed
0xmanhnv added a commit that referenced this pull request Jul 27, 2026
…ved (#334)

Found while studying OASM's console against ours; none of them are UX gaps.

1. Silent data loss. The Add buttons on attack-surface external/internal/cloud
   built an object, pushed it onto local state and toasted 'added successfully'
   with NO API call anywhere in the file — while the page READ from useAssets.
   The asset was gone on the next reload. Each handler now calls createAsset and
   refetches; cloud creates 'cloud_account' because that is the type its own
   list queries, so the new row appears in the list that just confirmed it.

2. Copying an API key failed silently over plain HTTP. The page called
   navigator.clipboard.writeText directly, which is undefined outside a secure
   context — so on a LAN deployment over http:// the key was never copied and
   nothing said so. We already ship lib/clipboard.ts with an execCommand
   fallback for exactly this; use it.

3. Sorting a server-paginated table silently sorted only the visible page.
   #324 added manualPagination without manualSorting/manualFiltering, so
   tanstack kept sorting and filtering client-side over the one page it had.
   Clicking a column header appeared to sort 6330 findings and actually
   reordered 20. My regression; the findings page had been working around it
   with showSearch={false}.

Also: vitest's exclude used bare directory names, so a git worktree under
.claude/ dragged its own node_modules — and a second copy of the app — into
every local run: ~16,000 tests, 270s, and 274 phantom failures from Playwright
specs vitest cannot execute. Globbed the patterns. The suite is now 768 tests
in 11s, which is the difference between a signal and noise.

Verified: tsc 0 errors, eslint clean, 768/768 pass, and all four affected routes
compile and serve 200 on the live dev server.

Co-authored-by: Nguyen Manh <0xmanhnv@gmail.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