Skip to content

Feature: feat/panel to dev - #494

Open
github-actions[bot] wants to merge 27 commits into
devfrom
feat/panel
Open

github-actions[bot] wants to merge 27 commits into
devfrom
feat/panel

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Automated PR tracking changes from feat/panel into dev.


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_flag flow: 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 bare flagged boolean on desk project lists. Hackers and public/cached results never see flag data; pulled teams drop from hackathon.getResults and from judging.results.compute rankings.

Judging schedule extends cpsat.py with a second CP-SAT stage that assigns one judge per presentation (eligibility, travel gaps, consecutive limits, locked/hint assignments, optional placements-only runs). Placement solving also soft-limits concurrent presentations per challenge vs judge capacity (judgeShortfall). JUDGING_CPSAT_PYTHON now covers assignJudges when 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 (not isComplete alone).

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 SQL pending, not derived).

Supporting changes: DB tx mock innerJoin for selects, announcements test for claimedAt / in-app listing.

Reviewed by Cursor Bugbot for commit 8f1e13c. Bugbot is set up for automated code reviews on this repo. Configure here.

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

Copy link
Copy Markdown
Contributor Author

Dependency Review

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

Scanned Files

None

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/db/src/schemas/hackathons.ts Outdated
Comment thread sites/mainweb/components/admin/judging/EntriesList.tsx
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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/api/src/routers/judging/desk.ts
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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/api/src/routers/judging/desk.ts
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.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/api/src/services/judging/grading-hold.ts
Comment thread packages/api/src/services/judging/placements.ts
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/api/src/services/check-in-flags.ts
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant