Skip to content

Judging works end to end without an organiser improvising - #320

Merged
aamoghS merged 2 commits into
mainfrom
stack/3-judging
Aug 9, 2026
Merged

Judging works end to end without an organiser improvising#320
aamoghS merged 2 commits into
mainfrom
stack/3-judging

Conversation

@aamoghS

@aamoghS aamoghS commented Aug 9, 2026

Copy link
Copy Markdown
Member

Third in the stack, on top of #318.

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 rather than a fresh one that forgets what they already scored.

Late submissions reach a judge

promoteSubmissions returned queuesNeedRebuild and 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 assignToHackathon refuses 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, and updateAssignmentTrack can fix a wrong one, rebuilding the queue unless the judge has already scored.

Smaller

  • closed editions appear at /judge/register. Closing participant registration is the natural step before recruiting judges, and it emptied that page with no explanation.
  • Judges are emailed when approved — the success screen promised it and nothing sent it — and applicants get a pending state where the apply button used to sit and throw "You have already applied".
  • Results can be announced from the screen that publishes them, through the resumable machinery from Close the notification loop: interest list, resumable announcements, live funnel #318.
  • judge_assignment.status is 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.isActive remains 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 promoteSubmissions appends 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 updateAssignmentTrack to 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.

**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.
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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

@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown

Greptile Summary

The PR connects judge approval, assignment routing, late-project promotion, and scan-to-start judging into an end-to-end workflow.

  • Builds an empty judge queue during approval and synchronizes assignment status.
  • Validates and corrects judge tracks while preserving completed queue entries.
  • Appends late-promoted projects to existing queues without reordering active work.
  • Adds QR-based table arrival, judging-state UI gates, table cards, and related administration controls.

Confidence Score: 3/5

The PR is not yet safe to merge because approval and track-correction failures can still persist status or routing changes without the corresponding queue.

Judge activation and assignment approval commit before queue construction, while track correction similarly commits the new track before a non-transactional delete-and-reinsert rebuild; database failures can therefore leave approved judges with no queue or assignments inconsistent with their queues.

Files Needing Attention: packages/api/src/routers/judge/admin.ts

Important Files Changed

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

Comment thread packages/api/src/routers/judge/admin.ts Outdated
Comment thread packages/api/src/routers/judge/admin.ts Outdated
…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.
@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot 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)

@aamoghS

aamoghS commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

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 judge_queue has no unique on (judge_id, project_id) to catch the loser, so a double-click or two organisers working the same list both saw an empty queue and both built one — the judge gets every project twice. The check and the build now run in one transaction behind SELECT … FOR UPDATE on the judge row.

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 assignJudgesToProjects uses. Forcing keeps every completed slot (skipProject marks one done without writing a vote, so completed slots cannot be reconstructed from judge_vote) and rebuilds only the remainder, numbering new slots after the kept ones.

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 --max-warnings 0 · build.

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