Tester Leaderboard
Status: in progress — first build slice implemented locally
Branch: adi/tester-leaderboard-plan
Goal: add a tester-focused leaderboard under the existing Leaderboard
section so the team can rank testers, filter by cycle/product/date/payout
state, and click into a tester for earnings, credit allocation, and activity
details.
Local environment check
This worktree is now on a real branch instead of detached HEAD.
Dependencies were installed with /opt/homebrew/bin/npm ci.
Verification results:
| Check | Result | Notes |
|---|---|---|
tsc --noEmit | Passes | Ran with local dummy Clerk + database env values. |
npm run build | Blocked locally | Next cannot load the native @next/swc-darwin-arm64 binary from this Codex process because of macOS code-signing/library-validation. WASM fallback is not installed. |
npm run lint | Blocked by same SWC issue | The script uses deprecated next lint, which also loads SWC. |
eslint . | Fails on existing lint debt | 14 unused-import warnings and 2 react-hooks/rules-of-hooks false positives in tests/e2e/fixtures/auth.ts where Playwright fixture arg is named use. |
Local setup gaps to clean up before/alongside implementation:
- Add a tracked local dev database recipe.
README.mdsaysdocker-compose up postgres -d, but onlydocker-compose.prod.ymlexists in the repo. - Replace
next lintwith the ESLint CLI script so lint works on Next 15 and is ready for Next 16. - Either run Next build/dev from a non-hardened local shell or add a documented WASM SWC fallback for Codex/macOS local verification.
What already exists
Do not rebuild the payout/balance math from scratch. These surfaces already contain most of the data needed for a tester leaderboard:
| Existing piece | File / route | Useful for leaderboard |
|---|---|---|
| Tester membership | testers table in src/db/schema.ts | Per-cycle role, join date, assigned testers with no activity. |
| Test sessions | test_runs table | Activity counts, last run, completed/abandoned runs, session notes. |
| Issue reporting | issues table | Filed count, severity mix, accepted/rejected rates, payout status. |
| Payout ledger | issue_payouts, payout_batches | Paid totals, paid date, method/reference, batch audit trail. |
| Tester self balance | GET /api/me/balance | Current-user earnings buckets and issue history. |
| Cycle payout by tester | GET /api/admin/test-cycles/:id/payouts/by-tester | Cycle-scoped per-tester payout buckets. |
| Admin payouts | GET /api/admin/payouts | Global paid/approved queue and top tester calculation. |
| Admin user detail | /app/admin/users/:id + UserCyclesPanel | Candidate destination for tester detail expansion. |
| Existing leaderboard nav | src/components/app-sidebar.tsx | Add a third child under Leaderboard. |
Important model note: there is no separate "tester credit allocation" table today. For v1, "credits" should be derived from issue payout buckets:
awaitingDecision: estimated credit from severity rate whilepayout_status='pending'.pendingVerification: approved credit blocked by verification gate.availableForPayout: approved credit ready for batch payout.paid: settled credit from current issue state and, for audit detail,issue_payouts.
If Hackorda later needs manual non-issue credits, bonuses, penalties, or
adjustments, add a real ledger table instead of overloading issues.
Privacy boundary
Recommended default: make exact money/credit amounts admin-only first.
Tester-facing leaderboard can show rank, activity, accepted bugs, accepted rate, and badges. Admin-facing leaderboard can show exact earnings, available credit, paid totals, payout references, and allocation detail. If the product decision is that testers may see everyone else's earnings, keep the same API but make the amount fields intentionally role-gated.
Proposed navigation
Add a third Leaderboard item:
Leaderboard
Quiz /app/leaderboard/quiz
Events /app/leaderboard/events
Testers /app/leaderboard/testersThe screenshot already matches this shape: src/components/app-sidebar.tsx
has leaderboardItems for Quiz and Events, so the first UI change is a small
nav extension plus the new page.
Leaderboard table
Page: /app/leaderboard/testers
Primary columns:
| Column | Meaning |
|---|---|
| Rank | Based on selected sort. Default sort: total approved+paid credit, then accepted issues, then recent activity. |
| Tester | Name, email, role chips when filtered to a cycle. |
| Activity | Runs, completed runs, issues filed, last active date. |
| Quality | Approved/paid issue count, rejected/no-payout count, acceptance rate. |
| Credits | Awaiting triage estimate, pending verification, available, paid. Admin-only exact amounts unless product approves public earnings. |
| Coverage | Cycles joined, organizations/products touched, severity mix. |
Filters:
- Date range: last 7 days, 30 days, 90 days, all time, custom.
- Cycle: all cycles or a selected cycle.
- Organization/product.
- Cycle status: active, review, closed.
- Tester role: tester, lead, observer.
- Payout bucket: awaiting triage, pending verification, available, paid.
- Activity: has runs, has issues, no activity, active this week.
- Search by tester name/email.
Sorts:
- Total committed credit (
approved + paid). - Paid credit.
- Available credit.
- Accepted issues.
- Issues filed.
- Acceptance rate, with a minimum decided-count guard.
- Last activity.
Tester detail
Click behavior can start as a detail drawer from the table and later become a deep-link page:
/app/leaderboard/testers/:userIdfor a shareable tester stats page, or- link admins to
/app/admin/users/:idand add a QA stats tab/panel there.
Recommended v1: detail drawer on the leaderboard plus admin profile link. This keeps the browsing loop fast and reuses the existing profile route for deeper admin actions.
Detail sections:
- Header: tester identity, global role, cycle role when scoped, last active.
- KPI strip: issues filed, approved/paid, acceptance rate, runs completed, total committed credit, paid credit.
- Credit allocation: the four existing buckets with separate currencies.
- Per-cycle breakdown: cycle, org/product, role, joined date, issues, runs, bucketed credits.
- Activity timeline: runs started/completed, issues filed, triage decisions,
payouts. V1 can derive this from
test_runs,issues, andissue_payouts; a first-class event log can come later. - Issue list: latest filed issues with severity, lifecycle bucket, payout amount/status, paid-at.
API shape
Prefer a shared aggregation module so admin payouts, tester balance, and the new leaderboard cannot drift:
src/lib/tester-stats.tsSuggested exported functions:
getTesterLeaderboard(filters, viewer)returns aggregate rows.getTesterStats(userId, filters, viewer)returns one tester detail.bucketIssueCredit(issue, cycle, product)centralizes the four-bucket credit math already mirrored in/api/me/balanceand cycle payouts.
Routes:
GET /api/testers/leaderboard
GET /api/testers/:userId/statsor, if exact amounts are admin-only from day one:
GET /api/admin/testers/leaderboard
GET /api/admin/testers/:userId/statsQuery params:
from, to, cycleId, organizationId, productId, cycleStatus,
role, payoutBucket, activity, search, sort, direction, limit, cursorResponse rules:
- Always split currencies. Never sum KZT and USD into one number.
- Include assigned testers with zero activity when scoped to a cycle.
- Include removed testers who still reported issues in the selected range, so historical payouts do not disappear.
- Gate sensitive fields at the API layer, not only in React.
Data and performance
V1 needs no schema change. Existing indexes cover the likely first version:
testers.user_id,testers.test_cycle_idissues.reported_by,issues.test_cycle_id,issues.payout_statustest_runs.tester_user_id,test_runs.test_cycle_idissue_payouts.issue_id,issue_payouts.batch_id
Add composite indexes only after the query shape is real and slow, likely:
issues(test_cycle_id, reported_by)issues(reported_by, created_at)test_runs(tester_user_id, started_at)
Implementation phases
Phase 0 — Local readiness
- Add or document a dev Postgres compose file.
- Migrate lint script from
next lintto ESLint CLI. - Fix the Playwright fixture lint false positives.
- Decide whether exact earnings are admin-only or visible to all testers.
Phase 1 — Aggregation foundation
- Extract shared credit-bucket logic into
src/lib/tester-stats.ts. - Reuse the existing
requiresVerification,suggestedAmountCentsFor, andDEFAULT_PAYOUT_CURRENCYhelpers. - Add API route for leaderboard rows with date/cycle/search/sort filters.
- Keep v1 fully derived from existing tables.
Phase 2 — Leaderboard page
- Add
TesterstoleaderboardItemsinsrc/components/app-sidebar.tsx. - Build
/app/leaderboard/testerswith filter bar, table, loading/empty states, and stable mobile layout. - Use TanStack Query hook, likely
src/hooks/test-cycles/tester-leaderboard.tsorsrc/hooks/leaderboard/useTesterLeaderboard.ts. - Show exact amount columns only when API marks
canViewFinancials=true.
Phase 3 — Tester detail
- Add row click to open a detail drawer.
- Fetch one-tester detail from the stats route.
- Include KPI strip, credit allocation, per-cycle breakdown, latest issues, and activity timeline.
- For admins, include a link to
/app/admin/users/:id.
Phase 4 — QA and docs
- Add flow doc
F-17 — Tester leaderboard. - Add Playwright coverage for filters, sorting, and detail drawer after the existing auth fixture lint issue is fixed.
- Manual smoke: seed onboarding cycle, assign testers, file mixed-state
issues, approve/pay some, verify leaderboard totals reconcile with
/app/test-cycles/balanceand/app/admin/payouts.
Open decisions
- Are tester earnings public to all authenticated testers, or admin-only?
- Should the row click open a drawer first, or go directly to a dedicated
/app/leaderboard/testers/:userIdpage? - Should leaderboard scoring prioritize money, accepted issue count, or a blended score?
- Do we need non-issue credits/bonuses in v1, or are derived issue payout credits enough?
Recommended first build slice
Start with an admin-visible /app/leaderboard/testers page backed by one
aggregate route, showing all assigned testers plus historical reporters. Ship
filters for date range, cycle, search, and sort first. Add the detail drawer
in the same PR if the route response is stable; otherwise make it the second
PR.
Implementation log
- Added admin-only tester leaderboard APIs:
GET /api/admin/testers/leaderboardGET /api/admin/testers/:userId/stats
- Added shared aggregation in
src/lib/tester-stats.tsso leaderboard rows, detail drawer, and future profile panels use the same credit bucket math. - Added
/app/leaderboard/testerswith filters, sortable ranking, summary strip, clickable rows, and tester detail drawer. - Added the admin-only
Testersitem under the existing Leaderboard sidebar. - Updated
npm run lintto use ESLint CLI and fixed existing lint blockers.