Skip to content

Feature: feat/panel to dev - #479

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

github-actions[bot] wants to merge 8 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 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 a panel CLI (migrate, seed, doctor, import/export).

The portal embeds that API at /api/panel (same process and club Postgres pool via packages/api panel / panel-sync). Hackathons get a judging_backend flag (legacy | panel); staff can switch backends, with projects/judges synced on promote/approval and published placements pulled into hackathon_result. Legacy rankings logic is refactored into weightProjects with a replay test against @query/judging-core rank() at pairwise weight 0. Club drizzle-kit push excludes Panel tables so deploys do not drop judging schema.

Adds planning/docs (PLAN.md, docs/judging/*, ADRs), extends root vitest to judging packages, and tracks packages/judging-db/migrations in git.

Reviewed by Cursor Bugbot for commit bba0354. 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
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
Comment thread packages/api/src/routers/judge/portal.ts
Comment thread packages/judging-server/src/app.ts
Comment thread packages/judging-server/src/app.ts
await options.db
.update(loginCode)
.set({ consumedAt: new Date() })
.where(eq(loginCode.id, row.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.

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.

Fix in Cursor Fix in Web

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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.

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

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

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 bba0354. Configure here.

export default defineConfig({
schema: "./src/schemas/**/*.ts",
schema: ["./src/schemas/**/*.ts", "../judging-db/src/enums.ts"],
tablesFilter: judgingTables.map((table) => `!${table}`),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bba0354. Configure here.

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