Repository navigation
Feature: feat/panel to dev - #479
github-actions[bot] wants to merge 42 commits into
Conversation
Ranking and dispatch have to be extractable later, so they live under judging/ as pure functions with no host imports. Co-authored-by: Cursor <cursoragent@cursor.com>
One API and the club Postgres, with the judge desk, organizer console, and a handoff from the club site when an edition opts in. Co-authored-by: Cursor <cursoragent@cursor.com>
The root ignore rule was hiding judging/db/migrations, and a Playwright run result had been committed with the desk. Co-authored-by: Cursor <cursoragent@cursor.com>
Judging was a standalone product under judging/ with its own scope, licence, CI
workflow, compose file and an import boundary against @query/*. It now lives as
ordinary workspaces: packages/judging-{core,db,server,cli} and sites/judging,
scoped @query/judging-*. Lint, typecheck, test and build run in ci.yml; image
publishing on panel-v tags moved there too. The judging server joins the root
docker-compose under the "judging" profile on the shared Postgres.
packages/api now depends on @query/judging-core, so the ranking replay test
imports it by name and is back in the api tsconfig. Both pre-existing lint
failures (replay.test.ts and the web e2e spec outside tsconfig) are fixed.
Judging no longer has its own server or web app. mainweb mounts the judging API at /api/panel and resolves the caller from the portal session: staff organize, volunteers volunteer, and a judge is matched by the email on their portal judge row for a panel edition. The desk, organizer console, public board and feedback card are portal routes (/judge/panel, /admin/judging/panel, /judging), so the ticket handoff, magic-link sign-in and organizer token are gone. Club-side sync calls the judging procedures in process instead of over HTTP, and published results are read straight from the shared database. Live views poll, since the portal process has no WebSocket hub. Removed the standalone pieces this leaves unused: sites/judging, the server entrypoint and WebSocket hub, the two-server check, Dockerfiles, Terraform, and the PANEL_URL / PANEL_SERVICE_TOKEN / PANEL_WEB_URL settings.
Neon suspends after five idle minutes and the plan has 100 compute hours a month, so anything that polls keeps the database billing. The public board and the organizer floor now poll only during judging_live and only while the tab is visible; the board gets a refresh button otherwise. /api/panel/readyz (a Postgres round trip) and /metrics are no longer public. Judging now borrows the club's connection pool, which is tuned to let Neon scale to zero, instead of opening a second pool to the same database. On the 0.5 GB side, a drained outbox row is deleted rather than kept, since event_log already holds the history. The judge desk no longer holds a score the server refused (past the hard limit, a voided visit). It used to keep it on the phone and resend the same refusal every time the page opened; only outages are held now.
The desk lost these when the WebSocket hub went away. It now asks /v1/session/status every 15 seconds, but only while a visit is open and the screen is on. Judging is live whenever a visit is open, so Neon is awake anyway, and a phone in a pocket sends nothing. A recall does not void the visit, and dispatch can hand the same visit back, so the status carries the time of the latest recall and the desk acts once per recall. The lookup reads only log rows written since the hand-out, on the existing (event_id, created_at) index.
Judging is per hackathon. Staff switch an edition to panel on /admin/judging; that creates its judging event from the hackathon row (organization hacklytics, slug = hackathon id) with default timers and the club's five criteria, then copies in submitted projects and the portal's judges. The PANEL_EVENT_ID / PANEL_ORG_SLUG / PANEL_EVENT_SLUG settings are gone, and the portal routes are keyed by hackathon id. Synced judges were stored as invited, which the desk refuses, so no portal judge could ever score. Approval in the portal now approves them in judging and deactivating suspends them. Deploys run drizzle-kit push against the shared database. With the judging tables there, push stopped at a drop prompt with no TTY, exited 0, and skipped every club schema change; db:check would then fail this branch on the missing judging_backend column. The drizzle config now hides the judging tables (read from the judging migrations) and declares the judging enums. Publishing results drains the outbox, so it no longer needs a manual step, and panel doctor treats PANEL_JWT_SECRET as optional.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Rework judging around rooms and a saved schedule:
- judging_* tables: challenges and groups, buildings and rooms, rubric,
schedule and appointments, evaluations with drafts and revisions,
deliberation boards, room access links and guest judge sessions
- Devpost submissions CSV import alongside portal promotion
- schedule solver: aamoghCPSolver (counting bounds, room-class
symmetry, sweep, goal-ordered repair) plus a CP-SAT fallback script
(scripts/judging-schedule/cpsat.py) with problem export and checked
solution upload
- routers judging.{admin,schedule,desk,hacker,results}; judge.* trimmed
to the roster
- admin console, judge desk, hacker itinerary and results UI
- check-in by event code, staff lookup or static pass instead of live
QR codes
Add hackathon points: adjustments and a store with computed balances,
routers points.{admin,store,me}.
Delete the panel packages (judging-core, judging-db, judging-server,
judging-cli) and the pool dispatch judge routers. Panel enum types stay
declared in retired.ts and its tables are filtered in drizzle.config.ts
so drizzle-kit push does not stall on drop prompts.
|
Visit the preview URL for this PR (updated for commit 484414a): https://hacklytics2027--pr-479-zplkrecy.web.app (expires Thu, 22 Oct 2026 04:07:15 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: c48ba34db61581e25fe2978355160b5eefe0e83f |
Judges sit at numbered tables in a hall and teams rotate to them; sponsors keep their rooms. A table is a judging_room with kind "table" (new column, default "room"), so the scheduler treats it as any other station. - createTables adds a numbered run of tables to a hall, skipping numbers already in use and slotting each table in by number. - generateRoomLinks issues QR links for every station in a batch that has none, with one audit entry. - Rooms tab: "Add tables" form, tables shown as number chips per hall with QR and seating counts, bulk QR link button. - Judges, guests and hackers see "room" or "table" to match the station, including the desk, itinerary, QR sheets and judge emails. - QR sheet copy keeps table codes with the judges instead of on the table. - Reordering rooms writes only stations whose place changed.
…ign-in copy - Public site: a compact Notify me button in the phone header, and Hacker portal / Judge Hacklytics links in the footer. The CTA stays on the Typeform for now. - Sign-in: hacker framing and a school email placeholder when arriving from /hacklytics or /hackathons; in-app browsers (Instagram, TikTok, ...) get a notice and email-code as the primary option since Google refuses them. - Drop the fixed 1.5s wait before redirecting after sign-in. - Verify page: Resend code with a 60s cooldown, and "Use a different email" keeps the callbackUrl.
…n edition - Attendees: the row action is Accept + email; silent approve is labelled "(no email)". Rows show whether the acceptance email went out, and link an uploaded portal resume when the application has no URL. - Announcements: separate waitlisted and rejected audiences with their own templates, so a waitlisted hacker is not told no. - Registration controls: Open only from draft, announced or closed, with confirms; the deadline saves on blur; deadline and cap can be cleared. - Dashboard tab kept in the URL hash, Judging and Edit links in the header, status pill on cards, jump to the new edition after create, delete only for super admins, time-zone hint on date fields. - Judging results: "Mark edition completed" once placings are published. - Pass scans return the hacker's name as its own field.
- The desk scanner stays open after a good scan and shows the name, first arrival and points for 1.5s; only failures need a tap. A badge held in front of the camera is not read twice. - Check-in desk preselects the live edition and the event running now, and hides events that have ended. - Turning on self check-in issues the first code. - Scanning a poster QR from Club Check-In opens the hackathon check-in page. - Hackers turned away from code check-in get a "Show my pass" button. - Nav: "Event Check-In" is now "Club Check-In".
- The tab comes from the URL, so ?tab= links work on the same page; text pointers to the pass and points tabs are links. - Status or the Register button sits above About, Prizes and Rules. - Judging, Points and Results appear once the event starts (by status or start date); the tab strip fades at the edge to show it scrolls. - Registration answers are kept as a per-account draft across tab switches, seeded from the account name and the interest list; step changes scroll to the heading and each failing field shows its own error. - Resume upload in the form, linked MLH policies with MLH's wording, and the server now requires both MLH consents like the client does. - Accepted hackers' pages refresh every 30s during the event and say "You're checked in" when the scan lands; the pass no longer flashes "Waiting on review" while loading. - Waitlist copy no longer promises automatic promotion.
…teammates - Teams tab: search by team or member, hide full teams, member names on cards, join confirm, captains can disband or close the team to new members (team.setOpen), and an invite link that opens and highlights the team. - /submit: teammates who are not the captain see the project read-only with who can edit it; team management moves to the Teams tab; withdraw asks for confirmation; success links to the Judging tab. - Edit-window error no longer mentions Devpost.
| const complete = trpc.hackathon.update.useMutation({ | ||
| onSuccess: () => void utils.hackathon.getById.invalidate(), | ||
| onError, | ||
| }); |
There was a problem hiding this comment.
Completed status list stays stale
Medium Severity
Marking an edition completed only invalidates hackathon.getById. The admin list and cards read hackathon.listAll, so they keep showing the previous status after the save until that cache expires.
Additional Locations (1)
Triggered by learned rule: Invalidate the tRPC queries each mutation writes
Reviewed by Cursor Bugbot for commit 1c1c431. Configure here.
- Autosave never gives up on a flaky connection: capped back-off retries, answers mirrored on the device and restored when newer than the server copy (clock skew corrected), including answers typed offline before a deadline auto-submit. "Reload saved answers" asks first. - The screen stays awake while a presentation deadline is running. - "Next project" after submitting; Up next above the filters; an empty state that says everything here is judged. - The "join a room" notice opens the picker; the picker puts the judge's track first, groups by building and keeps Cancel within reach. - Hide judged hides only complete evaluations. - A named guest who scans another room's or table's code keeps their judge row (same browser and edition, within four hours of the session ending); the old session ends so room counts stay right. - Club judges are told to sign in before scanning, on the guest name gate and on every QR sheet.
- Judge sign-up prefills from the account, starts at the linked edition, shows an existing application, participant or organiser-added judge instead of the form, and drops the track-assignment promise. - Organisers adding a judge directly now send an "added as a judge" email. - Publishing the itinerary emails every scheduled team's members once (the flag flips with a conditional update, so a double click sends once). - First results publish emails accepted hackers and project members; a republish after a correction does not. - Approval emails link to this deploy's origin; the approval copy tells club judges to sign in before scanning. - Router tests stub every email send.
- Itinerary: "Walk to <building>, leave by <time>" between slots in different buildings and "Starts in N min" on the next one. - Judging tab links to the other tabs it mentions; a project already submitted on the portal says so instead of offering a claim. - Results poll while the event is in progress and tag the caller's team when its name is unambiguous.
| metadata: { placements: published }, | ||
| }); | ||
|
|
||
| if (announce) void notifyResultsPublished(db, input.hackathonId); |
There was a problem hiding this comment.
Mass judging emails drop after response
High Severity
First-publish results and itinerary-on notices are started with void after the mutation returns. Other mail loops in this repo await forEachWithConcurrency because Cloud Run only keeps CPU for the request and cuts it off at 300s. Once the response is sent, the fan-out can stall, so teams never get the mail. Results also writes the judging.results.publish audit row first, and later publishes refuse to announce, so a dropped send cannot be retried.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 515a4af. Configure here.
…port Building the stub from the real module loaded the mail transport first, which could push the first emailing test past its timeout on a busy runner.
- Accepted hackers can tap "I'm coming" (attendanceConfirmedAt, a new
nullable column); organisers can filter "Approved, not confirmed" and
export it to chase no-shows.
- Pending applications can be edited while registration is open, held to
the same rules as the first submission; a decision landing mid-edit wins.
- The check-in desk can admit an approved hacker who has no phone ("Admit
(ID checked)"), sharing the pass-scan transaction and writing an audit
entry.
- Any full staff member can add and remove volunteers; every other role
stays super-admin only, checked on both the role set and the role held.
The staff page has a volunteer-only mode, and the scanner links to it.
- Hackers see the latest announcements sent to them on the event page (listForMe reads their frozen recipient rows only). - "In app only" publishes an announcement without emailing it. - "Send test to me" sends the draft to the organiser's own address, rate limited, writing nothing; a live preview mirrors the email. - CTA links must be http(s), so a link never vanishes in app while still going out by email.
- One pure helper decides a hacker's next step (register, wait, show your pass, find a team, submit by the deadline, next judging slot, results), shown on the dashboard, the event page header and /hacklytics. - The dashboard opens on the hackathon view while an accepted hacker's event is running. - "My <edition>" heads the nav for an accepted hacker from two days before the event to its end. - /hacklytics shows a registered hacker their status instead of Register, and still resolves after the event, pointing at the results.
|
|
||
| {/* What organisers announced to this hacker, emailed or in app only. | ||
| The uuid, not the route param: that may be a slug. */} | ||
| {regStatus && <HackerAnnouncements hackathonId={hackathon.id} />} |
There was a problem hiding this comment.
In-app notices miss interest audience
Medium Severity
In-app announcements to the interested audience never appear. HackerAnnouncements mounts only when the viewer has a registration, while listForMe is keyed off compose-time recipient rows from the interest list. Those recipients get no email and no in-app surface, so the publish succeeds and the message is invisible.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 1eff092. Configure here.
- A complete, valid hint becomes the starting schedule, so running out of time can never discard it; every goal stage is bounded by the schedule in hand and skips presolve, so the result is never worse than the hint. - Without a hint, the first schedule may use the whole attempt before the goals share what is left. - The cross-building break is relaxed only when a strict schedule is proven impossible, not when the strict attempt merely timed out. - `-` reads the problem from stdin. - Python tests: brute-force agreement on small problems, the command line, exit codes, and hints that survive a timeout.
aamoghCPSolver runs 3.5 to 6.7x faster on the bench instances (forge-sized event 2.8s to 0.46s, 800-project event 8.9s to 2.6s) and finds full-break schedules on tight events where it used to fall back to relaxed breaks or fail: a seeded min-conflicts search runs only when the repair leaves presentations unplaced. Output stays deterministic and strict. Across 871 generated events it scores better on 368, the same on 454 and worse on 49.
JUDGING_CPSAT_PYTHON (and optionally JUDGING_CPSAT_SCRIPT) let the server run cpsat.py itself: judging.schedule.cpsatAvailability reports whether it can, and solveCpsat sends the problem with the greedy schedule as its hint, then reads the answer through the same validation as an upload. Nothing starts Python when the variables are unset, and the TypeScript solver stays the default. The process is killed well past its own time limit, and the problem file it reads is removed after every run.
Under the schedule preview, organisers can run CP-SAT on the server when it is configured, or download the problem, solve it on a laptop and upload the solution. Either way the result is shown against the in-app preview before it can be saved.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 466474f. Configure here.
| saved.copyFrom(board); | ||
| stats.nodes += polish(m, board, current[FINISH]!, polishBudget); | ||
| settle(); | ||
| keepIfNoWorse(); |
There was a problem hiding this comment.
Settle undoes a better finish
Medium Severity
polish can empty the last slot and finish earlier, but settle then calls reassign with the pre-polish current finish. That window still includes the old last slot, so a cheaper assignment of changes or breaks can occupy it again. The post-polish board is never scored into best, so a shorter finish is thrown away for a later goal.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 466474f. Configure here.
#66706E measured 4.46:1 on the paper ground, just under AA, and it is the colour of every 13px label and hint. #5F6967 clears 4.5:1 on every light ground and stays lighter than the muted text above it.
From a rendered-phone review against an anti-slop design rubric: the team list leads the Teams tab, cards are quieter (one tag for Full, overlapping initials, names under the pitch), the invite link uses the phone's share sheet where there is one, read-only teammates see a plain record instead of dead controls, and the submit notices sit next to the button that caused them. Tap targets are at least 44px.
Group heads and room lines no longer leave a separator dangling at a line break, table groups lead with what needs attention (no QR link, nobody seated), and counts read as one line per hall.
The email form is always open inside in-app browsers, errors are announced to screen readers, every action is at least 44px tall, and the verify page uses one-time-code autofill and keeps the masthead of the sign-in page.
Pink marks only the action and the blooms (the hero date line is ink grey now), the pixel display face is used at its real weight, section titles follow one type scale, the closed mobile menu is fully hidden, track titles and descriptions keep their spacing in the subgrid, and the moon and the healthcare flower sprites read as what they are.
…n Hacklytics On touch screens every button gets a 44px min-width, which replaced the flex default and squeezed a strip of tabs into each other; tabs no longer shrink. The Hacklytics page is always dark, so its success, warning and danger states take the night edition's colours (3:1 before, 7:1 or more now).
The next-step strip is one shared component with a readable title and detail, the status is said once, the registration form's checkboxes are 20px boxes inside 44px rows instead of 44px blocks, a failing field's error shows once beside it, past events are no longer faded below contrast, and a selected tab past the edge scrolls into view.
The pass leads the Schedule tab with the check-in line beside it, the day's schedule marks what is on now, and the check-in and points screens send a hacker who is not checked in straight to their pass.
One-line station title, larger rating targets with a weak-to-outstanding legend, 44px links and checkboxes that keep 20px boxes, the station picker marks where the judge is instead of offering it, long titles clamp to two lines, and urgent announcements stay visible while picking a station.
The scanner offers "Scan next badge" after a failure, the manual search keeps focus where the volunteer is typing, /scan hides the edition picker when only one is live, and the staff page's volunteer mode reads plainly.
The itinerary says how long the walk between buildings leaves, results read as a ledger with the caller's team marked, and /hacklytics shows a registered hacker's status and next step without the register pitch.
The 2027 artwork (the globe with the blossom ribbon) cropped square on its dark ground: favicon.ico at 16 to 64px and a 180px Apple touch icon, linked from the layout metadata.
- Closing or reloading the page now stops a server-side CP-SAT run: the request's abort signal reaches runCpsat, which kills the process and removes its problem file (about 20 ms instead of running to the limit). - The panel states the real cost: up to the time limit of solving per attempt, plus up to a minute of setup on large events. - Schedule router and planner copy say room or table, and previews count tables apart from rooms. - Runner tests that start a real process get a 30s limit; 5s was flaky on busy machines.
- Pressed bloom buttons keep white text: --ink on --bloom-deep was 2.0:1 in the light theme; white is 6.0:1 (dark) and 9.2:1 (light). - The browser bar follows a saved theme choice, not only the device: the pre-paint script and ThemeToggle set every theme-color meta to the page's ground, including metas re-rendered by hydration or a client navigation, and a cleared choice returns each meta to its own scheme. - A theme chosen in another tab applies to open tabs (storage event). - The service worker falls through to the network when navigation preload rejects or comes back empty, instead of serving a cached page while online.


Automated PR tracking changes from
feat/panelintodev.Note
High Risk
Large architectural swap of judging (schema, API, auth, results) plus new points and check-in behavior; mistakes affect live event operations and published placings.
Overview
Replaces the retired pool-and-queue hackathon judging flow with room-based judging: new
judgingtRPC namespace (admin, schedule, desk, hacker, results),judging_*schema, sharedservices/judging/, and optional server-side CP-SAT scheduling viacpsat.py/JUDGING_CPSAT_*env vars. Thejudgerouter is narrowed to roster (apply, approve, add/remove);isJudgeis dropped from the documented procedure gates in favor of judging-specific access (judging-accessexport).Adds a hackathon points surface (
pointsrouter +points.tstables) tied to event check-ins and a point store. Public results merge legacyhackathon_resultsnapshots with newjudging_placementrows in one shape; project withdrawal soft-deletesjudging_entriesand clears results cache.Hackathon operations gain attendance confirmation, manual
admitcheck-in (audited, volunteer-friendly), unconfirmed-attendee filters, MLH registration fields, and looser volunteer staffing rules for plain admins. Announcements add in-app-only delivery, per-hackerlistForMe, organiser test send (rate-limited), split waitlisted/rejected audiences, and stricter CTA URLs.Hacklytics 2027 marketing CTAs point at Typeform for now, with footer links to hacker/judge portals. Docs, glossary, and routes are updated throughout; tests drop old queue/vote coverage and add judging-service and announcement coverage; root
pnpm testincludes mainweb components.Reviewed by Cursor Bugbot for commit 484414a. Bugbot is set up for automated code reviews on this repo. Configure here.