Hackorda Docs
Plans

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:

CheckResultNotes
tsc --noEmitPassesRan with local dummy Clerk + database env values.
npm run buildBlocked locallyNext 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 lintBlocked by same SWC issueThe script uses deprecated next lint, which also loads SWC.
eslint .Fails on existing lint debt14 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.md says docker-compose up postgres -d, but only docker-compose.prod.yml exists in the repo.
  • Replace next lint with 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 pieceFile / routeUseful for leaderboard
Tester membershiptesters table in src/db/schema.tsPer-cycle role, join date, assigned testers with no activity.
Test sessionstest_runs tableActivity counts, last run, completed/abandoned runs, session notes.
Issue reportingissues tableFiled count, severity mix, accepted/rejected rates, payout status.
Payout ledgerissue_payouts, payout_batchesPaid totals, paid date, method/reference, batch audit trail.
Tester self balanceGET /api/me/balanceCurrent-user earnings buckets and issue history.
Cycle payout by testerGET /api/admin/test-cycles/:id/payouts/by-testerCycle-scoped per-tester payout buckets.
Admin payoutsGET /api/admin/payoutsGlobal paid/approved queue and top tester calculation.
Admin user detail/app/admin/users/:id + UserCyclesPanelCandidate destination for tester detail expansion.
Existing leaderboard navsrc/components/app-sidebar.tsxAdd 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 while payout_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/testers

The 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:

ColumnMeaning
RankBased on selected sort. Default sort: total approved+paid credit, then accepted issues, then recent activity.
TesterName, email, role chips when filtered to a cycle.
ActivityRuns, completed runs, issues filed, last active date.
QualityApproved/paid issue count, rejected/no-payout count, acceptance rate.
CreditsAwaiting triage estimate, pending verification, available, paid. Admin-only exact amounts unless product approves public earnings.
CoverageCycles 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/:userId for a shareable tester stats page, or
  • link admins to /app/admin/users/:id and 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, and issue_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.ts

Suggested 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/balance and cycle payouts.

Routes:

GET /api/testers/leaderboard
GET /api/testers/:userId/stats

or, if exact amounts are admin-only from day one:

GET /api/admin/testers/leaderboard
GET /api/admin/testers/:userId/stats

Query params:

from, to, cycleId, organizationId, productId, cycleStatus,
role, payoutBucket, activity, search, sort, direction, limit, cursor

Response 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_id
  • issues.reported_by, issues.test_cycle_id, issues.payout_status
  • test_runs.tester_user_id, test_runs.test_cycle_id
  • issue_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 lint to 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, and DEFAULT_PAYOUT_CURRENCY helpers.
  • Add API route for leaderboard rows with date/cycle/search/sort filters.
  • Keep v1 fully derived from existing tables.

Phase 2 — Leaderboard page

  • Add Testers to leaderboardItems in src/components/app-sidebar.tsx.
  • Build /app/leaderboard/testers with filter bar, table, loading/empty states, and stable mobile layout.
  • Use TanStack Query hook, likely src/hooks/test-cycles/tester-leaderboard.ts or src/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/balance and /app/admin/payouts.

Open decisions

  1. Are tester earnings public to all authenticated testers, or admin-only?
  2. Should the row click open a drawer first, or go directly to a dedicated /app/leaderboard/testers/:userId page?
  3. Should leaderboard scoring prioritize money, accepted issue count, or a blended score?
  4. Do we need non-issue credits/bonuses in v1, or are derived issue payout credits enough?

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/leaderboard
    • GET /api/admin/testers/:userId/stats
  • Added shared aggregation in src/lib/tester-stats.ts so leaderboard rows, detail drawer, and future profile panels use the same credit bucket math.
  • Added /app/leaderboard/testers with filters, sortable ranking, summary strip, clickable rows, and tester detail drawer.
  • Added the admin-only Testers item under the existing Leaderboard sidebar.
  • Updated npm run lint to use ESLint CLI and fixed existing lint blockers.

On this page