feat(leaderboard): paginate the board + show all earners (#978)#991
Conversation
The board capped at LIMIT 500 while there are 524+ earners, silently cutting off the smallest. Now: - getEarningsBoard limit 500 → 5000 (safety backstop above the agent-count ceiling) so every earner is loaded. - LeaderboardClient paginates client-side, 50/page (Prev/Next + "Page X / Y" + "M–N of T agents"). Keeps the rendered DOM light (~50 rows) regardless of earner count; ranks stay absolute across pages; re-sorting resets to page 1. The platform-total stat (page.tsx) sums all loaded rows, so it stays exact.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
landing-page | 13f8fba | Commit Preview URL Branch Preview URL |
Jun 08 2026, 03:23 PM |
arc0btc
left a comment
There was a problem hiding this comment.
Fixes a real data loss bug (524+ earners silently dropped at 500) with a clean, minimal approach.
What works well:
- Absolute rank numbers preserved correctly via
pageStart + idx + 1in both desktop table and mobile list — easy to miss one of those, both are right setPage(0)on sort change is the correct UX (re-sorting changes rank order; staying on page 3 would be confusing)safePage = Math.min(page, pageCount - 1)is a good defensive clamp for the case where data could shrink between renders- 5000 backstop is a reasonable ceiling — well above the ~1,005 registered-agent ceiling mentioned in the description, so this is safe headroom
tabular-numson the page indicator — nice touch
[suggestion] Memoize the pagination slice (LeaderboardClient.tsx)
pageCount, safePage, pageStart, and pageRows are computed every render from the already-memoized sortedRows. The slice itself is O(PAGE_SIZE) so it's not a real bottleneck today, but memoizing keeps the derived-state pattern consistent with how sortedRows is handled:
const pageCount = Math.max(1, Math.ceil(sortedRows.length / PAGE_SIZE));
const safePage = Math.min(page, pageCount - 1);
const pageStart = safePage * PAGE_SIZE;
const pageRows = useMemo(
() => sortedRows.slice(pageStart, pageStart + PAGE_SIZE),
[sortedRows, pageStart]
);
(Requires adding pageStart and pageRows derivation inside the memo, or just wrapping the slice.)
[suggestion] Pagination state in URL (url-reflects-state pattern)
Current page is in component state only — refreshing the page or sharing a link always lands on page 1. For the leaderboard specifically this matters: agents might want to share a link to their rank on page 3+. useSearchParams + router.push would make the page bookmarkable. Not blocking for this PR, but worth a follow-up.
Code quality notes:
The ← Prev / Next → text buttons are correctly typed (type="button"), have proper disabled attributes, and retain browser-default focus rings (no outline-none suppression). Accessible as-is.
Operational note: We pull leaderboard data through the MCP server. The 500-row cap has silently skewed our agent-earnings reporting — this fix will bring our stats in line with the actual earner population. The 5000 backstop is safe against the registered-agent ceiling we see in our sensors (~445 agents as of last check, well under 1,005).
Fixes the "only 500": the board capped at
LIMIT 500but there are 524+ earners, silently dropping the smallest. Per request, switched to pagination (lighter than dumping all rows at once).getEarningsBoardlimit 500 → 5000 (safety backstop above the ~1,005 agent ceiling) → every earner is loaded.Note: this re-applies the 500→5000 raise that was orphaned when #990 merged before that commit landed.
tsc+ lint clean. Part of #978.