Judging works end to end without an organiser improvising - #320
Conversation
**Approving a judge builds their queue.** `setActive` flipped a flag and created nothing; `judge.register` always writes an assignment row and `assignToHackathon` refuses anyone who already has one — so the documented recruitment path produced an active judge whose portal said "All Done" having judged nothing. The only remedy was `assignJudgesToProjects`, which deletes and rebuilds every queue in the event and is unsafe once judging is live. Approval now builds the queue itself, and only when the queue is empty: a judge suspended mid-event and reinstated comes back to the queue they were part-way through. **Late submissions reach a judge.** `promoteSubmissions` returned `queuesNeedRebuild` and nothing consumed it beyond a banner, so a project promoted after assignment sat in nobody's queue, received zero votes, and was then dropped from the standings entirely by the zero-vote rule — the team simply absent. Promotion now appends those projects to the queues that already exist, at the coverage the existing queues give a project, without reordering a single row anyone is part-way through. The banner now means only "these projects reached no judge", which is a track mismatch a human has to resolve. **A judge's track has to exist.** Free text went straight into the routing column, and any string not on the edition classified them as sponsor/special and filtered their pool to zero — invisible to them and to the organiser, and uncorrectable because assignToHackathon refuses a second assignment. Both entry points now validate against the edition's own tracks and challenges, both pickers offer them, and `updateAssignmentTrack` can fix a wrong one (rebuilding the queue unless the judge has already scored). **Recruitment stays open when registration closes.** `closed` — the natural step before recruiting judges — emptied /judge/register with no explanation. Also: judges are told they were approved (the success screen promised it), applicants get a pending state where the apply button used to sit and throw, and results can be announced from the same screen that publishes them. Verified: typecheck, 405 tests, lint --max-warnings 0, build.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_27f38569-61e8-4296-b54a-874cca57469c) |
|
Visit the preview URL for this PR (updated for commit 0c3f0da): https://hacklytics2027--pr-320-752eelde.web.app (expires Sun, 16 Aug 2026 01:15:50 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c48ba34db61581e25fe2978355160b5eefe0e83f |
|
| Filename | Overview |
|---|---|
| packages/api/src/routers/judge/admin.ts | Adds queue lifecycle, track correction, promotion, and approval behavior, but the two previously reported atomicity failures remain reachable. |
| packages/api/src/routers/judge/portal.ts | Adds QR-based visit start behavior and server-side arrival timing without a blocking issue identified in the eligible follow-up scope. |
| packages/api/src/.internal-tests/judge-edge.test.ts | Adds broad edge-case coverage, though the mocked tests do not exercise rollback after queue-rebuild failures. |
| sites/mainweb/components/admin/hackathons/JudgesTab.tsx | Exposes judge approval and track-management workflow changes without an eligible blocking issue identified. |
| sites/mainweb/app/(portal)/judge/page.tsx | Gates scoring behind judging activation and QR arrival without an eligible blocking issue identified. |
Reviews (2): Last reviewed commit: "fix(judging): serialise queue creation, ..." | Re-trigger Greptile
…s scores Both points from Greptile on #320. **Two approvals could build the queue twice.** "Is the queue empty? then build it" is a read followed by a write, and judge_queue has no unique on (judge, project) to fall back on — so a double-click, or two organisers working the same list, both saw an empty queue and both built one, handing the judge every project twice. The check and the build now happen in one transaction behind a lock on the judge row: the second request sees what the first wrote and does nothing. **Changing a track after scoring left the queue disagreeing with it.** The track was written and the rebuild skipped, so the judge carried on scoring the pool they were routed to before with nothing on screen saying so. It now refuses, names how many projects they have already scored, and offers an explicit override — the pattern assignJudgesToProjects already uses. Forcing keeps every completed slot (skipProject marks one done without writing a vote, so they cannot be rebuilt from the votes table) and rebuilds only the remainder, numbering the new slots after the kept ones. Both guards are mutation-tested: removing the lock fails the lock test, and removing the refusal fails two. Verified: typecheck, 410 tests, lint --max-warnings 0, build.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_f3c9fc3b-2187-4cee-a9f6-6b4ae57a991b) |
|
Both were real. Fixed in 0c3f0da. Approval-time queue creation is not atomic — correct. "Is the queue empty? then build it" is a read followed by a write, and Track change with completed scores — also correct, and worse than it looked: the track was written and the rebuild skipped, so the assignment and the queue actively disagreed and the judge carried on scoring the old pool with nothing on screen saying so. It now refuses with a CONFLICT naming how many projects they have already scored, and the UI offers "Change track anyway" only after that message has been read — the same refuse-then-override pattern On the coverage note — you were right that neither case was tested. Added four for the track change (refusal, the message naming the cost, completed slots surviving a force, and a clean change when nothing is scored) and one asserting the row lock. Both guards are mutation-tested: removing the lock fails the lock test, removing the refusal fails two. Gate: typecheck · 410 tests · lint |
Third in the stack, on top of #318.
Approving a judge builds their queue
setActiveflipped a flag and created nothing.judge.registeralways writes an assignment row andassignToHackathonrefuses anyone who already has one — so the documented recruitment path produced an active judge whose portal said "All Done" having judged nothing. The only remedy wasassignJudgesToProjects, which deletes and rebuilds every queue in the event and is unsafe once judging is live.Approval now builds the queue itself, and only when the queue is empty: a judge suspended mid-event and reinstated comes back to the queue they were part-way through rather than a fresh one that forgets what they already scored.
Late submissions reach a judge
promoteSubmissionsreturnedqueuesNeedRebuildand nothing consumed it beyond a banner. A project promoted after assignment sat in nobody's queue, received zero votes, and was then filtered out of the results entirely by the zero-vote rule — the team simply absent from the standings unless an organiser remembered to re-run assignment.Promotion now appends those projects to the queues that already exist, at the coverage the existing queues give a project, without reordering a single row anyone is part-way through. Appending is the only thing that is safe while judging is live. The amber banner now means only "these projects reached no judge" — a track no active judge covers, which is the one case a human has to resolve.
A judge's track has to exist
Free text went straight into the routing column. Any string not on the edition classified the judge as sponsor/special and filtered their pool to zero — invisible to them and to the organiser, and uncorrectable, because
assignToHackathonrefuses a second assignment. Both entry points now validate against the edition's own tracks and challenges (case-insensitively, storing the edition's spelling — routing compares exactly), both pickers offer them instead of a text box, andupdateAssignmentTrackcan fix a wrong one, rebuilding the queue unless the judge has already scored.Smaller
closededitions appear at/judge/register. Closing participant registration is the natural step before recruiting judges, and it emptied that page with no explanation.judge_assignment.statusis kept in sync with approval. It was written once at registration and never touched, so anyone reading the table saw every approved judge as pending.judges.isActiveremains the gate.Verification
typecheck · 405 tests · lint
--max-warnings 0· build — green on this commit. The queue-building and track-validation guards were mutation-tested.Note
Medium Risk
Changes core judging queue assignment, concurrent approval, and results snapshot logic during live events; mitigated by extensive edge-case tests but still touches vote timing and ranking inputs.
Overview
This PR closes several gaps where organisers had to re-run bulk assignment or SQL to get judging working: approving a judge now builds their queue when it is empty (with a row lock to avoid duplicate queues), while reinstatement leaves an in-progress queue untouched.
Late
promoteSubmissionsappends new projects into existing judge queues at current coverage instead of only warning to rebuild; a banner remains only when no active judge covers the new track.Track routing is validated against the edition’s tracks/challenges (normalized casing), with pickers in admin/register UI and
updateAssignmentTrackto fix mistakes (refuse or force-rebuild while keeping completed slots). Withdrawn projects are excluded from queue building and listing.Scan-to-start: judges must scan a table QR (
startByQrCode,arrivedAt) before the scoring form opens; duration uses server arrival time. Table cards and admin UI support printing codes.Smaller fixes: hackathon-scoped judge list, sync assignment status on approve, approval email + portal cache invalidation,
myApplications/ pending UI, judging-not-open gate on the judge flow, real event dates on setup, results compute upsert track"overall"and skip unjudged projects, announce results from Judges tab.Reviewed by Cursor Bugbot for commit 0c3f0da. Bugbot is set up for automated code reviews on this repo. Configure here.