Repository navigation
Feature: feat/panel to dev - #494
Open
github-actions[bot] wants to merge 27 commits into
Open
github-actions[bot] wants to merge 27 commits into
github-actions[bot] wants to merge 27 commits into
Conversation
points.me.checkIn writes attendance and awards points, so it now sits behind notHackathonBanned like the other participation writes. The summary, leaderboard and store reads stay open. Adds a test that a banned hacker's check-in is refused before any participant lookup or insert.
listAnnouncements counted every stamped sentAt as sent, but an in-app post is written with sentAt already set. Only sendBatch claims rows, so sent now counts rows that were claimed and sent, pending is counted directly in SQL, and a new inAppOnly flag marks posts that went to event pages without email.
The id can come from the URL or the picker, so it is passed through encodeURIComponent once and every link on the page uses that path.
The application draft in localStorage no longer holds gender, race or the emergency contact. The portal is used on shared and borrowed laptops; the fields still work in the form and are still submitted.
Organisers' rules for bad actors: nobody is checked into two events that run at the same time, or into two less than 30 minutes apart. Both live in recordEventAttendance, the one writer of attendance rows, so pass scans, event codes and manual check-in all follow them. - Meals are exempt on both sides; back-to-back events do not overlap. - A repeat of an event already attended still reads "already checked in". - The participant row is locked (FOR NO KEY UPDATE) before the read, so two check-ins for one person at the same moment cannot both pass. - The rule and the row use one app clock rather than the database's now(), which follows the session time zone. - Refusals are FORBIDDEN with a sentence for the hacker or for staff; staff are told to remove the earlier check-in if it was a mistake.
Contributor
Author
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Contributor
Author
|
Visit the preview URL for this PR (updated for commit 8f1e13c): https://hacklytics2027--pr-494-p2herrsg.web.app (expires Fri, 23 Oct 2026 04:07:38 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c48ba34db61581e25fe2978355160b5eefe0e83f |
A check-in the rules refuse (two events at once, or two under 30 minutes apart) rolls back, so the attempt is now written afterwards on the pool to a new hackathon_check_in_flag table. Repeats of the same pair add to an attempt count. Writing the flag is best effort and never changes the refusal the person sees. All three check-in paths (event codes, pass scans, manual check-in) are wrapped. Judging entries gain nullable prize_ineligible_at and prize_ineligible_by_id columns. Schema changes are additive only.
listEntries returns each entry's flags (who, which events, how, how many attempts) and a Flagged filter; overview counts open flags for the page header. New dismissCheckInFlag and setPrizeEligibility mutations let organisers clear a flag or pull a team from prizes, both audited.
Computing placements skips prize-ineligible entries, and the public results drop them at once. The organiser results list shows each row's open flag count and a prize toggle, with a warning before publishing while flags are open. No flag detail reaches the public payload.
Judges see a bare Flagged marker and a note that organisers decide prize eligibility. Who was flagged and why stays with organisers.
Once a scheduled presentation's judging deadline passes, a judge can no longer submit a first evaluation or edit one already submitted. A draft still open at the deadline is submitted for the judge as before. Remote and unscheduled challenges stay editable until organisers close judging. The desk reports the lock as ended, and the judge sheet says time is up instead of asking the judge to join a room.
A submit or save sent in the last seconds of a presentation is no longer refused for the time it spent on the network: writes land up to DEADLINE_GRACE_MS past the deadline, while reads still show the deadline as passed. Auto-submitting open drafts waits out the same grace, so it never races the judge's own submit. Submitted evaluations now count down the time left to edit, the time-up notice shows what was submitted, and copy that promised edits after the deadline is corrected.
Worktrees created under .claude/worktrees sit inside the repo, and the path filters in pnpm test matched their copies of the test files too.
Each project now gets one evaluation per root challenge, written by one judge. saveDraft and submit take a transaction-level advisory lock on the project and challenge after the judge's own draft and evaluation rows, then refuse with CONFLICT when another judge has already submitted an evaluation or holds a live draft (saved in the last ten minutes, or a presentation draft before its deadline). A judge editing their own evaluation skips the check, and duplicate evaluations from before this rule are left alone. The deadline auto-submit takes the same locks in key order and keeps a draft rather than submitting it once any judge has graded the project. desk.projects and desk.project report where each project stands for the judge (mine, graded, in progress or open) without naming other judges, and the sheet is read-only on a project another judge has. A new desk.nextProject query walks each judge through the open projects in an order of their own, hashed from judge, project and challenge, so judges at one table each draw a random sample. The desk shows Graded and Being graded chips and a Grade next project button, and the hacker tab says each challenge grades a project once.
With every project graded once, one strict, lenient, bunched or drifting judge decides each project's score. calibration.ts fits an offset, a spread and a drift for each judge within each root challenge, so sponsor judges are only compared with their own challenge's judges, and judges with few grades are shrunk toward no correction. computePlacements ranks on the adjusted scores of live, prize-eligible entries. The model assumes the desk queue hands each judge a random sample of projects. judging_placement gains a nullable raw_score column holding the plain mean (additive; drizzle-kit push applies it). Only the organisers' judging.results.list returns it, with a per-judge calibration summary; public results and the judge desk are unchanged. The results tab captions adjusted lists, shows the raw mean beside each score, and describes each judge's habits in plain words.
A table judge now grades a sponsor's challenge only while seated in a room that serves it. evaluatedChallenge refuses with FORBIDDEN on project, projects, nextProject, saveDraft, submit and startGrading, and desk.context lists only the challenges a member may pick. The schedule's task selection reads the same scope.isSponsorChallenge flag. At an untimed challenge a judge's first claim of a project has to be one of the first QUEUE_WINDOW (3) open projects in their own queue, so each judge's sample stays random for calibration. loadQueue in queue-store.ts is shared by nextProject and the claim check, so the two never disagree, and a project the judge has only opened stays on offer to them. nextProject takes a skip list for a team that is away from its table and reports allSkipped; the desk keeps the skips and shows a Skip link next to Grade next project. A scheduled challenge under a saved schedule is unchanged. The new desk.startGrading mutation holds a project from the moment it is opened with an empty revision 0 draft, so another judge is not sent to the same table, and the editor's first save carries on from it. The deadline auto-submit keeps an empty draft rather than submitting it, and desk.projects no longer reports one as a saved draft. For untimed challenges the project list shows the judge's own projects first, then open ones in queue order.
An evaluation now counts toward placings and calibration once it holds a rating for every rubric rating item, so a draft submitted at the deadline with a written response missing still places its project. scoring.hasEveryRating decides it. results.compute, appointmentResults (completeCount becomes countedCount) and evaluationAudit use the same rule, and the results tab says whether an incomplete evaluation counts. judging.admin.reopenGrade deletes one evaluation and every draft for its project and challenge so another judge can grade it. It is admin only, audited as critical, takes the edition lock, then the drafts, then the grading lock, and is refused while results are published. The results tab offers it on each evaluation behind a confirmation. The drift correction was mostly noise: chance alone gave judges with no drift a fitted one of about 0.3 of a point. K_D rises from 10 to 150 and no drift is fitted below MIN_DRIFT_GRADES (20), which keeps that under 0.1 of a point in seeded simulations; a real drift is now only partly removed. The organiser view flags drift from 0.2. judging.results.list caches the per-judge calibration for 60 seconds, keyed on the edition's evaluation count and latest save and on its ranked entries; compute always calibrates fresh. docs/packages/api.md describes these changes and the desk's queue window, hold on open and sponsor room rule.
Add a second scheduling stage. Once every presentation has a time and a room, each one gets exactly one judge, so every project has a time, a place and a grader. Schema: judging_appointment.assigned_judge_id (nullable, FK to judging_judge, on delete set null) and an index on it. Model (services/judging/schedule/judges.ts): a judge grades only the challenges they may grade, a guest only in their own room, one presentation per slot, the schedule's breaks to walk to another room or building (staying in a room needs none), and at most MAX_CONSECUTIVE slots in a row. Goals in order: every presentation has a judge, sponsor presentations go to the sponsor's own judges rather than table judges, even load (busiest judge, then spread inside each pool), then mix (few repeats of a room, as much of the day as possible). validateJudgeAssignment and scoreJudgeAssignment check and measure an assignment; assignJudges is the deterministic fallback for servers without OR-Tools: three greedy starts, then a bounded local search. cpsat.py: with a judges array it assigns judges as a second CP-SAT model after the placement, writes assignments into the solution, and keeps a challenge's presentations in a slot within its judges as the first placement goal. A problem with placements only assigns judges, with locked assignments kept and an assignmentHint to start from. Version-1 files without judges are unchanged. cpsat-bridge and cpsat-runner carry the judges, validate what comes back with validateJudgeAssignment and fall back to assignJudges, and allow the extra time limit. Router: saving a schedule stores each presentation's judge; the board read returns each appointment's judge, per-judge loads and warnings for slots with too few judges; assignJudges re-assigns what has not started and has no evaluation or draft, keeping the rest (CP-SAT when the server has it); setAssignedJudge and judgeChoices change one appointment's judge by hand; moving an appointment clears a judge who can no longer grade it. Organiser UI: judge on each appointment, an Assign judges button with warnings, a per-judge load list, and a dialog to change one judge.
…hort The judge assignment model forbade clashes pair by pair. It now states them as at-most-one sets per judge: one per slot, and a room ruling out the rooms too far to reach in each of the next few slots. Same rules, a fraction of the constraints: a 336-presentation event builds in 1.3 s instead of 4.9 s, and an 896-presentation one finishes inside the runner's kill timer. A stage cut short by its time share added no bound, so the stages after it could trade its goal away (leave presentations out to put fewer table judges on sponsor ones). The hint now bounds the first goal, and a stage that runs out of time holds its goal where the assignment in hand has it. An appointment under way whose judge has left the roster is no longer kept: it tied up nobody who could be planned around, and made the server refuse every CP-SAT answer for not keeping it. The board's canAssignJudge and judgeChanges no longer search the list once per appointment.
A presentation with an assigned judge is now theirs from its start until TAKEOVER_AFTER_MS (5 minutes) later. Other judges see it as reserved, with the time it opens, and are refused until then. After that any judge who may grade it there can take it over, unless the assigned judge has started: their draft holds the project. Every claim checks the assignment inside the transaction that holds the grading lock, so two judges at the takeover instant serialise. The desk never writes assigned_judge_id and never tells a judge which other judge is assigned to or holds a project. nextProject offers the judge's own running assignments first and skips presentations still reserved for someone else. The project list marks assignments as the judge's own. A new desk.itinerary query lists the judge's assigned presentations that have not ended, with the room and whether they are seated there, and the desk shows it above the tabs. A new desk.releaseGrading gives back a project the judge opened and never saved to, so skipping or backing out does not hold it. The desk now counts an evaluation the way placings do: every rating in, written response or not (scoring.hasEveryRating, and the new SQL form holdsEveryRating). Scores, the judged count, hiding judged projects and the shortlist all follow it.
The live board now shows, next to each started presentation's assigned judge, whether they are grading it, have graded it, were taken over by another judge, or have not started once the takeover time has passed. It never says who took over. The rule is in schedule/judge-progress.ts. It uses the desk's takeover time and the board's existing rule for matching evaluations and drafts to appointments, and adds no new query to judging.schedule.get. reopenGrade now deletes the edition's unpublished placings in the same transaction, under the config lock that compute and publish take. Publishing then needs a fresh compute, and the organiser is warned before confirming. Organiser counts, the board and hacker itinerary status, and hacker feedback now count an evaluation as placings do: every rating in, whether or not a written response was left empty. The judging API docs describe assigned judges, the new desk procedures and this counting rule.
A select from one table with no joins drops the table name from every column in a sql expression, so the correlated subqueries in the judging overview and the setup checklist compared the subquery's own columns: "schedule_id" = "id" and "room_id" = "id". The overview always reported 0 presentations on a saved schedule and 0 linked rooms, and the checklist never saw a room's QR link. outerColumn names the outer row's column with its table, and the three subqueries use it. The test renders both forms through drizzle.
Both event foreign keys on hackathon_check_in_flag cascaded, so deleting either event deleted the flag. A refused attempt on a session that was later cancelled vanished, and prize review lost the only record of the rule break. The organiser view's "A deleted event" fallback could never run. event_id and conflict_event_id are now nullable, and their foreign keys are ON DELETE SET NULL. The participant foreign key still cascades. New flags always carry both ids, so the attempt upsert is unchanged. checkInFlagsForEntries skips null ids when it looks up events and shows them as "A deleted event". For drizzle-kit push this is only DROP NOT NULL and a foreign key action change. Tests cover the foreign key actions and flags whose event, conflicting event, or both were deleted.
Pulling or restoring prize eligibility, removing a project and restoring one from the Projects list only invalidated judging queries. The same eligibility change on the Results tab also invalidates hackathon.getResults. Portal results stay fresh for minutes and do not refetch on focus, so the dashboard, the edition page and /hacklytics could keep showing a team that had just been pulled. These three mutations now also invalidate hackathon.getResults, the same way the Results tab does. Changing challenges and dismissing a flag still refresh judging only. A component test checks both groups.
evaluationTiming returned a presentation's deadline only while it was still ahead. A first saveDraft or startGrading that landed in the fifteen-second write grace window was allowed, but it stored a draft with no deadline. reconcileDueDrafts only submits drafts that have a deadline, so those answers never became an evaluation. Until a presentation has ended, which for a write includes the grace window, evaluationTiming now returns its deadline. saveDraft and startGrading stamp it on the draft, and reconcile submits the draft as of the deadline. Reads have no grace, so the desk sheet never receives a deadline that has already passed. Edits to a submitted evaluation are still untimed. Tests cover the timing in the grace window, a first save and an open in it, and the reconcile submit that follows.
Dropping evaluations, resetting projects and resetting everything evict the public results on the server, but the danger zone only refreshed the judging screens. The portal keeps hackathon.getResults fresh for minutes, so its copy is now dropped after those three resets as well, the same way the Projects list and Results tab do.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3fe3d4f. Configure here.
Once both events of every flag have been deleted, the organiser flag read had no event ids left but still queried hackathon_event with an empty IN list. It now skips that read, like the other empty-list guards in the file, and the flags list with the "A deleted event" name.
The skipped lookup's empty array needs the row type the query returns, or the event name map does not typecheck.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Automated PR tracking changes from
feat/panelintodev.Note
High Risk
Touches hackathon check-in enforcement, prize eligibility, public results caching, and core judging desk/schedule/placement logic—areas that affect fairness, auth visibility, and live event operations.
Overview
This merge brings the judging panel work into
dev: check-in rule enforcement tied to judging, richer schedule/judge assignment, desk behavior for timed presentations, and calibrated placings—with heavy documentation and router-level test coverage.Check-in flags document and test a new
hackathon_check_in_flagflow: overlapping or too-close event check-ins are refused and recorded; organisers see full flag detail on judging entries/results and can dismiss flags or mark teams prize-ineligible (prize_ineligible_at), while judges only get a bareflaggedboolean on desk project lists. Hackers and public/cached results never see flag data; pulled teams drop fromhackathon.getResultsand fromjudging.results.computerankings.Judging schedule extends
cpsat.pywith a second CP-SAT stage that assigns one judge per presentation (eligibility, travel gaps, consecutive limits, locked/hint assignments, optionalplacements-only runs). Placement solving also soft-limits concurrent presentations per challenge vs judge capacity (judgeShortfall).JUDGING_CPSAT_PYTHONnow coversassignJudgeswhen set.Judge desk tests cover assigned presentation rules: reserved until
TAKEOVER_AFTER_MS, takeover blocked by assigned judge drafts/grades,nextProject/itinerary/releaseGrading, deadline grace on drafts, and scoring that counts evaluations with every rating item (notisCompletealone).Results add judge-bias calibration for placings (raw vs adjusted scores, 60s calibration cache keyed in
CacheKeys.judgingCalibration), shared “holds every rating” rules across overview, feedback, and itinerary status. Announcements fix listing in-app-only sends as published (use SQLpending, not derived).Supporting changes: DB tx mock
innerJoinfor selects, announcements test forclaimedAt/ in-app listing.Reviewed by Cursor Bugbot for commit 8f1e13c. Bugbot is set up for automated code reviews on this repo. Configure here.