Skip to content

feat(judging): refined judge pages, inline score adjustment, round locks, admin progress + project-submission tie-break - #407

Merged
gregv merged 2 commits into
developfrom
feat/judging-experience-oct-2026
Oct 22, 2026
Merged

gregv merged 2 commits into
developfrom
feat/judging-experience-oct-2026

Conversation

@gregv

@gregv gregv commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Judging experience for Fall 2026 (~15 judges on /judge at once) plus the admin judging tabs. Pairs with backend PR opportunity-hack/backend-ohack.dev#307 — deploy the backend first; every new call here treats a 404 as "feature off" (no progress panel / no locks), so the order is safe but the new features only appear once the backend is live. The rubric, rounds and results math are unchanged apart from the tie-break described below.

Bugs this fixes (worth knowing even if you skip the rest)

  • src/lib/judgeApi.js swallowed every error and returned a fake {success: true} from submitScore/saveDraft/getJudgeTeams. A 403/409/500 showed "Score submitted successfully!" while the score was lost, and a backend outage read as "You have no teams assigned". It now throws a typed error and every page renders the failure.
  • There is no notistack provider in the app, so the judge pages' and admin tabs' enqueueSnackbar calls never rendered anything. Judge pages use local MUI snackbars; the admin tabs mirror toasts through the host page's onSnack.
  • The scoring page wrote a draft on every load — including for already-submitted scores, whose drafts were then never shown again (submitted wins), so edits silently "reverted".
  • Admin Results fired one request per judge × team (+ one per judge); Round 1/2 "Save" posted one assignment per judge × team (often 90+ sequential requests).

Judge side (refined civic-editorial design)

  • /judge — assignments as calm cards with navy progress rails, tutorial band, quick reference.
  • /judge/[event] — the dashboard. Each already-scored team card has an explicit "Adjust scores" button that opens an inline editor: segmented 1–5 controls (no sliders), the diff is spelled out ("1 change · total 24 → 26"), nothing is sent until "Save changes", Cancel/Esc discards, Save is disabled until there's a real change with valid feedback. Unscored teams go to the full page (first scoring needs the video and criteria). Locked rounds hide scoring and show a closed notice. Scores on cards are a small muted line, no colored boxes. Demo videos open in one page-level dialog.
  • /judge/[event]/team/[id] — same design; the total is a small, uncolored line pinned at the top of the side column (people look over judges' shoulders); draft autosave only after a real change and only before first submit; an already-submitted score shows "Submitted " and an explicit Update score; locked ⇒ read-only; failed submits stay on the page with the error; an unsaved-changes guard on Back/unload.

Admin side

  • ?section=judging&subtab=round1|round2 — new "scoring progress & lock" panel: judges complete / scores in / drafts in progress, by-judge table (progress bar, missing teams, drafts, last submitted) or by-team view, CSV, and a Lock scoring toggle with a confirm dialog. Polls every 30s while open.
  • Round 1/2 saves use the bulk route (one request per panel, replace so the backend set matches the screen); Round 2 refuses to save with an empty judge list; Round 1 skips panels with no judges or no teams.
  • subtab=results — rewritten on ONE bulk-scores call. Ties on average score are broken by the earliest project_submitted_at (the first time the team pressed "Submit your project" on its dashboard; stamped once). A new "Project submitted" column shows that time in the event timezone with seconds, tied rows get a Tie chip with the reason, and the CSV exports both. Averages compare at the displayed two decimals (never an epsilon). Only currently assigned judges' scores count — same rule as before; a judge later removed from the panel shows in the dialog as "not counted".
  • Docs: new CLAUDE.md "Judging experience (Oct 2026)" section, ga-events-reference.md (judge_inline_adjust_open/_saved, judge_score_submitted, admin_judging_lock/_unlock).

Verification

  • npx jest --ci: 84 suites, 604 passed (new: Judge/__tests__/*, admin/judging/__tests__/resultsRanking.test.js + JudgeProgressPanel.test.js, lib/__tests__/judgeApi.test.js, src/__tests__/pages/judge-event.test.js + judge-team.test.js — inline adjust end to end, save race, incomplete-score gate, locked round, failed submit stays put, autosave never-on-load / never-when-submitted / never-when-locked, lock confirm flow, 404 → unavailable).
  • npx eslint on every touched file: clean.
  • npm run build against the local backend: exit 0 (twice — before and after the review fixes).
  • Reviewed by Claude Opus 5.1 in a separate pass; all blocker/should-fix items are in the second commit.
  • Not clicked through in a browser (auth-gated judge + admin pages) — hence the plan below.

Test plan (please click through)

Needs backend #307 deployed. Use a judge account that is assigned in Round 1 (and Round 2 once set up) for fall-2026, and an admin account.

Judge

  1. /judge — your events render as cards with "Round 1 · find the top teams" progress; "Open judging" → /judge/fall-2026.
  2. /judge/fall-2026?tab=round1 — event title in the masthead; unscored teams show Score this team; scored teams show a small "Your score · N of 40" line plus Adjust scores. Open the demo video (dialog, not a new tab).
  3. Click Adjust scores on a scored team: change one criterion with the 1–5 buttons → the footer reads "1 change · total 24 → 26", Save becomes enabled; press Cancel → nothing changed (reload to confirm). Open again, change, Save changes → toast "Saved your changes for ", the card's score line updates, the editor closes. While a save is in flight, Close and other cards' Adjust are disabled.
  4. Keyboard: Tab into a 1–5 control, use ←/→ (Home/End), Enter/Space is not needed; Esc closes the editor (except while typing in the feedback box).
  5. Phone width (≈375px): the inline editor rows wrap (label on top, 1–5 buttons under it) with no horizontal scroll.
  6. /judge/fall-2026/team/<id>?round=round1 (an unscored team): the total line is at the top of the side column and stays visible; change a score → "Unsaved changes…" then "Draft saved h:mm"; reload → the draft is restored (info toast). Submit with < 20 characters of feedback → button disabled with the hint; with feedback → confirm dialog → Submit score → back on the dashboard, team now "Scored".
  7. Open an already-submitted team on the full page: the status tag says "Submitted ", the button reads Update score, changing a value shows "Edited — press Update score to save" and NO draft is written (check the admin progress panel: no draft appears). Press Back with an unsaved change → the browser asks to confirm.
  8. Round 2 tab: finalist cards show the demo time / room tags; same Adjust flow.

Admin — /admin/hackathons/fall-2026?section=judging
9. subtab=round1 → "Round 1 scoring progress & lock" panel: judges with scores show green progress, "Still missing" lists team names, drafts show as "(draft)"; By team view lists who each team is waiting on; CSV downloads.
10. Click Lock Round 1 scoring → confirm → chip reads "Round 1 scoring locked · Locked by at ". As a judge, reload /judge/fall-2026: the round shows "Scoring closed", no Score/Adjust buttons, and the full page is read-only. Try to submit from a stale tab opened before the lock → warning toast "Round 1 scoring was just closed…", nothing saved. Unlock → judges can score again.
11. subtab=round2 → same panel for Round 2 above the Round 1 results table. Select finalists → Save Round 2 Setup → one success toast naming the pair count; saving again reports 0 new; remove a finalist and save → the removed pairs disappear from the judge's Round 2 tab. With judges not loaded (simulate by blocking the judges request) the save is refused with a message instead of wiping assignments.
12. Round 1 tab: Save Round 1 Panel with a panel that has judges but no teams → saved with a "Skipped " warning, nothing deleted.
13. subtab=results → ranking table has a "Project submitted" column (event timezone, seconds). Make two teams tie on average (e.g. one judge each, same total): the later-submitted team ranks below with a Tie chip whose tooltip names the rule; the CSV has "Tie" and "Tie broken by" columns; the podium shows the chip. A team that never pressed Submit shows "Not submitted" and loses the tie. Refresh reflects a judge just removed from Round 2 (their score shows "not counted" in the dialog and the average moves).
14. Toasts: every admin action above shows a snackbar (they used to be silent).

🤖 Generated with Claude Code

gregv and others added 2 commits October 21, 2026 23:23
…cks, admin progress + submission tie-break

Judge side
- judgeApi now throws JudgeApiError (it faked {success:true} on failed score
  submits/drafts/team loads — scores were silently lost).
- /judge, /judge/[event], /judge/[event]/team/[id] rebuilt on the refined design.
- Dashboard: explicit 'Adjust scores' inline editor per scored team — segmented
  1–5 controls, nothing persists until 'Save changes', diff summary, Cancel/Esc
  discards; locked rounds hide scoring and explain.
- Scoring page: small uncolored total (shoulder-surfing), dirty-gated draft
  autosave only before first submit, explicit 'Update score' after, read-only
  when locked, failed submits stay on the page with the error.
- Local MUI snackbars: the app has no notistack provider, so the old toasts
  never rendered.

Admin side
- judging/JudgeProgressPanel on Round 1 + Round 2: who submitted / drafts /
  missing per judge and per team, CSV, lock toggle with confirm; polls 30s.
- Round 1/2 saves use POST /api/judge/admin/assignments/bulk (one request per
  panel instead of one per judge×team), falling back on 404.
- JudgingResults rewritten on one bulk-scores call; ties broken by earliest
  project_submitted_at with a visible column + Tie chip (resultsRanking.js).
- Admin tabs mirror toasts through the host page's onSnack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y-precision ties, Round 2 wipe guard, lock state, unsaved-changes guard

Judge side
- Inline adjuster: Close and every other card's Adjust are disabled while a
  save is in flight; a result always lands on its own card (no silent 500).
- "Adjust scores" is offered only when a complete stored score exists; Save
  requires every criterion set (backend would 400 otherwise).
- Rows wrap on phones; Esc inside the feedback box no longer discards; focus
  returns to the Adjust button on close; ARIA radio arrow directions.
- Toast no longer prints the score; "Not scored yet" uses the quiet tag.
- Scoring page: total pinned at the top of a scrollable sticky aside; dirty-
  gated beforeunload + routeChangeStart guard; judge_score_submitted GA event;
  W3C reference link restored on the accessibility prize.

Admin side
- Results: ties compared at the displayed two decimals (never an epsilon —
  not transitive, reordered 14- vs 15-judge averages); only currently
  assigned judges' scores count (parity with the old page), unassigned shown
  as "not counted"; WinnerPodium at module scope; assignments no longer
  cached so Refresh refetches.
- Round 2 save refuses an empty judge list and records a new panel before
  assigning (no duplicate panels on a failed call); Round 1 save skips panels
  with no judges or no teams instead of sending an empty replace.
- JudgeProgressPanel: lock state independent of the progress load (public
  locks fallback, toggle disabled while unknown), array guards, refreshSignal
  prop instead of a key remount, orphan copy corrected.

Tests: scoring-page autosave contract (never on load / submitted / locked,
once after a change, draft restore), progress panel (render, lock flow,
404, unknown-lock fallback), dashboard save race + incomplete-score gate,
display-precision ranking.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontend-ohack-dev Ready Ready Preview Oct 22, 2026 6:42am UTC

Request Review

This branch was successfully deployed

1 active deployment
Preview — 7c4a0a9a Deployed Oct 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant