Skip to content

Feature: feat/panel to dev - #479

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

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

Conversation

@github-actions

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

Copy link
Copy Markdown
Contributor

Automated PR tracking changes from feat/panel into dev.


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 judging tRPC namespace (admin, schedule, desk, hacker, results), judging_* schema, shared services/judging/, and optional server-side CP-SAT scheduling via cpsat.py / JUDGING_CPSAT_* env vars. The judge router is narrowed to roster (apply, approve, add/remove); isJudge is dropped from the documented procedure gates in favor of judging-specific access (judging-access export).

Adds a hackathon points surface (points router + points.ts tables) tied to event check-ins and a point store. Public results merge legacy hackathon_result snapshots with new judging_placement rows in one shape; project withdrawal soft-deletes judging_entries and clears results cache.

Hackathon operations gain attendance confirmation, manual admit check-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-hacker listForMe, 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 test includes mainweb components.

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

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>

@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/judging-core/src/aggregate/rank.ts Outdated
aamoghS and others added 3 commits October 8, 2026 13:14
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.
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file feature labels Oct 8, 2026

@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/panel-sync.ts Outdated
Comment thread packages/api/src/routers/judge/portal.ts Outdated
Comment thread packages/judging-server/src/app.ts Outdated
Comment thread packages/judging-server/src/app.ts Outdated
Comment thread packages/judging-server/src/app.ts Outdated
Comment thread packages/judging-cli/src/transfer.ts Outdated
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.

@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 sites/mainweb/app/(portal)/judge/panel/[hackathonId]/desk.tsx Outdated
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.

@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/drizzle.config.ts Outdated
Comment thread packages/judging-server/src/router.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor Author

Dependency Review

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

Scanned Files

None

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

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

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

const complete = trpc.hackathon.update.useMutation({
onSuccess: () => void utils.hackathon.getById.invalidate(),
onError,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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.

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

metadata: { placements: published },
});

if (announce) void notifyResultsPublished(db, input.hackathonId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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.

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


{/* 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} />}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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.

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

There are 4 total unresolved issues (including 3 from previous reviews).

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 466474f. Configure here.

saved.copyFrom(board);
stats.nodes += polish(m, board, current[FINISH]!, polishBudget);
settle();
keepIfNoWorse();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⤵️ pull dependencies Pull requests that update a dependency file feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant