Repository navigation
Feature: feat/panel to dev - #479
github-actions[bot] wants to merge 8 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.
| await options.db | ||
| .update(loginCode) | ||
| .set({ consumedAt: new Date() }) | ||
| .where(eq(loginCode.id, row.id)); |
There was a problem hiding this comment.
Sign-in codes lack guess limits
Medium Severity
POST /v1/auth/magic-link inserts a new 6-digit login_code with no per-email send lock. Verify does not increment or delete a row after failed guesses, so a code stays brute-forceable for ten minutes and repeats are not throttled.
Triggered by learned rule: Email sign-in DB-backed guess and send caps
Reviewed by Cursor Bugbot for commit bfd6d02. Configure here.
| const [run] = await db | ||
| .select({ id: resultRun.id }) | ||
| .from(resultRun) | ||
| .where(eq(resultRun.eventId, eventId)); |
There was a problem hiding this comment.
Export picks an arbitrary result run
Medium Severity
exportResults loads a result_run by eventId with no published filter and no ordering. A later compute or an unpublished run can be written to the CSV/JSON instead of the published placements.
Reviewed by Cursor Bugbot for commit bfd6d02. Configure here.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
There are 4 total unresolved issues (including 2 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 bba0354. Configure here.
| export default defineConfig({ | ||
| schema: "./src/schemas/**/*.ts", | ||
| schema: ["./src/schemas/**/*.ts", "../judging-db/src/enums.ts"], | ||
| tablesFilter: judgingTables.map((table) => `!${table}`), |
There was a problem hiding this comment.
Push creates enums that block migrate
High Severity
Club drizzle-kit push now loads the judging enums. On a database that has not yet run panel migrate, push creates those types. The first migration then fails on unguarded create type, so judging tables never appear and switching an edition to panel cannot create its event.
Reviewed by Cursor Bugbot for commit bba0354. Configure here.
| for (let batch = 0; batch < 20; batch += 1) { | ||
| const delivered = await drainOutbox(ctx.db).catch(() => 0); | ||
| if (delivered === 0) break; | ||
| } |
There was a problem hiding this comment.
Publish hangs on webhook delivery
Medium Severity
Publishing results now drains the outbox in-request. drainOutbox posts every webhook with no timeout, so a hung subscriber blocks the publish mutation after placements are already written and the organizer sees a failure on the freeze step.
Reviewed by Cursor Bugbot for commit bba0354. Configure here.


Automated PR tracking changes from
feat/panelintodev.Note
High Risk
Large new judging surface co-located with the club API and shared database; backend switching and best-effort panel sync can affect live hackathon judging and published results if misconfigured.
Overview
Introduces Panel, a hackathon judging stack as
@query/judging-*workspaces: pure scoring/dispatch in judging-core, Postgres schema and SQL migrations in judging-db, Hono/tRPC API in judging-server, and apanelCLI (migrate, seed, doctor, import/export).The portal embeds that API at
/api/panel(same process and club Postgres pool viapackages/apipanel/panel-sync). Hackathons get ajudging_backendflag (legacy|panel); staff can switch backends, with projects/judges synced on promote/approval and published placements pulled intohackathon_result. Legacy rankings logic is refactored intoweightProjectswith a replay test against@query/judging-corerank()at pairwise weight 0. Clubdrizzle-kit pushexcludes Panel tables so deploys do not drop judging schema.Adds planning/docs (
PLAN.md,docs/judging/*, ADRs), extends root vitest to judging packages, and trackspackages/judging-db/migrationsin git.Reviewed by Cursor Bugbot for commit bba0354. Bugbot is set up for automated code reviews on this repo. Configure here.