Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,21 @@ Team docs carry the DevPost-replacement write-up (all optional; absent ⇒ legac
- **Team page (`team/[team_id]/index.js`):** `TeamProjectSection` renders first (`id="project"`, in the TOC) when `hasProjectContent`; tagline `.ohx-lead`, 16:9 `--surface-2` thumbnail box, story, built-with `.ohx-tag`s, link tiles, submission line (public: "Submitted Sat 2:58 PM MST" / "Submitted late …" / quiet "In progress"; members: "Draft — not yet submitted." + "Edit on your dashboard →" → `manageteam#project`). Masthead adds a submission `.ohx-tag` when status is non-null. The member nudge covers missing story / missing demo / not submitted (DevPost is no longer nagged). `<Head>` OG/twitter image + card and the JSON-LD come from `projectMeta`. `next/image` ONLY for `cdn.ohack.dev` (the only allowed `remotePatterns` host besides giphy) — YouTube posters are plain `<img width height loading=lazy>`.
- **Gallery (`Hackathon/TeamList.js`):** module-scope `ProjectMedia` (16:9 reserved box: thumbnail → `LiteVideoThumbnail` → Fraunces initial placeholder) tops each card; tagline 2-line clamp; first 3 built-with + `+N`; submission tag; DevPost demoted to a quiet "DevPost ↗" link (absent + member → "Add your project story →" to `manageteam#project`); `submittedOnly` toggle (`aria-pressed`, rendered only when n > 0) over a stable sort that keeps active-first as the primary order and submitted-first within it (`useMemo`); "No submitted projects yet." only while the toggle is on. Lazy per-card avatar loading is untouched. No new fetches.
- **Results (`Hackathon/HackathonResults.js` — also rendered outside a `RefinedRoot` on `/results`, so `var(--x, #hex)` fallbacks):** winner cards show `project_tagline` + `awards[]` pills (Part 9 bug #11 — awards were never rendered; guard non-array), DevPost chip only when present; module-scope `HackersChoiceCard` (new `peerVoteEnabled` prop = `!!constraints.peer_vote_enabled`, passed from the event page and `results.js`) fetches `GET /api/hackathons/<id>/peer-vote/summary` into a `minHeight 148` slot — published → winner link + "{ballots} peer ballots"; 404/unpublished → "announced at the awards ceremony"; disabled → renders nothing. `HackathonFunnel` sublabels are source-neutral. `Profile/Portfolio/TeamsShowcaseSection.jsx` imports `WINNING_STATUSES` from `constants/teamStatus` (bug #12 — no private copy).
- **Judge views — demo video only, no process change:** `pages/judge/[event_id].js` and `pages/judge/[event_id]/team/[team_id].js` read `demo_video_url || video_url` (the backend now fills the legacy `video_url` from `demo_video_url`); the scoring page embeds `VideoDisplay` (dynamic, ssr:false, 16:9 container) under the GitHub/DevPost links, gated `!isRound2` exactly like the pre-existing "Watch Pitch Video" button. Rubric, scoring form, rounds, `JudgingRound1/2/Results`, `/about/judges`, `/hackathon-judging-criteria` and `mentorCoverage.js` are untouched — verified with a prettier-normalized diff.
- **Judge views — demo video only, no process change:** `pages/judge/[event_id].js` and `pages/judge/[event_id]/team/[team_id].js` read `demo_video_url || video_url` (the backend now fills the legacy `video_url` from `demo_video_url`); the scoring page embeds `VideoDisplay` (dynamic, ssr:false, 16:9 container) under the GitHub/DevPost links, gated `!isRound2` exactly like the pre-existing "Watch Pitch Video" button. Rubric, rounds, `/about/judges`, `/hackathon-judging-criteria` and `mentorCoverage.js` are untouched. (The scoring form and `JudgingRound1/2/Results` were later rebuilt by the Oct 2026 judging pass — see "Judging experience" below — without changing the rubric or the results math beyond the documented tie-break.)

## Judging experience (Oct 2026 — judge pages + admin judging tabs)

Built for ~15 judges hitting `/judge` at once during Fall 2026. The rubric (4 categories × 2 criteria, 1–5, 40 max; Accessibility as a separate Round-1 prize), the two rounds and the results math are unchanged — this pass changed the UI, the failure modes and the admin tooling. Backend half: `api/judging/` on `feat/judging-locks-progress` (locks, progress, bulk assignments) — **deploy it first**; every new frontend call treats a 404 as "feature off".

- **`src/lib/judgeApi.js` THROWS `JudgeApiError`** (`{status, body}`; helpers `isRoundLocked`/`isNotFound`/`isForbidden`). The old client caught every error and returned fake `{success: true}` from `submitScore`/`saveDraft`/`getJudgeTeams`, so a 403/409/500 showed "Score submitted successfully!" while the score was lost, and an outage read as "no teams assigned". Never reintroduce swallow-and-stub; callers render the error.
- **No notistack provider exists in the app** — the judge pages' old `enqueueSnackbar` calls never rendered. Judge pages now use a local MUI `Snackbar`; the admin judging tabs (`JudgingRound1/2`, `JudgingResults`, `JudgeProgressPanel`) take `onSnack` from `JudgingSection` (the host page's snackbar) and mirror every toast through a `notify()` helper.
- **Shared judge pieces live in `src/components/Judge/`** (all module-scope): `judgingCriteria.js` (rubric data + pure helpers: `calculateTotal` ignores `total`/special keys, `normalizeScores(apiScores, round)`, `scoreDiff`, `isFeedbackValid` = 20 meaningful chars — the ONE source for both scoring surfaces), `ScoreScale` (segmented 1–5 radiogroup with roving tabindex — replaced the MUI Slider, which changed value on an accidental trackpad drag), `InlineScoreAdjuster`, `JudgeTeamCard`, `judgeTheme` (scoped MUI theme over `refinedFormTheme` so Portal'd Dialogs/Snackbars look refined; `quietTotalStyle`).
- **Judge dashboard `/judge/[event_id]`** (refined `RefinedRoot` page): fetches `getJudgeTeams` + `getJudgeScores` in parallel (token/user via refs, effect keyed on presence — token rotation never wipes an open editor); refetches on `visibilitychange` (15s throttle) only while no editor is open. **Inline "Adjust scores" contract (the accidental-click answer):** the editor exists only after an explicit button press on an already-scored card, values change only through `ScoreScale`, NOTHING is sent until "Save changes" (disabled until a real diff + valid feedback; Cancel/Esc discards), the footer spells out the diff ("1 change · total 24 → 26"), and a save re-submits the full score via `POST /api/judge/score`. Unscored teams go to the full page (first scoring needs the video/criteria context). Locked rounds (`locks[round].locked` from the teams payload) hide Score/Adjust and show a `role="status"` closed notice; a 409 `round_locked` on save flips the local lock and explains. Scores on cards are a small muted line ("Your score · 24 of 40"), no colored boxes.
- **Scoring page `/judge/[event_id]/team/[team_id]`**: same refined chrome; the total is a small, uncolored muted line (shoulder-surfing); draft autosave is **dirty-gated and only for not-yet-submitted scores** (the old page wrote a draft on every load — including for submitted scores, whose drafts were then never shown again because submitted wins); a submitted score shows "Submitted <when>" + "Update score" and changes are only saved on press; locked ⇒ controls disabled, no submit; submit failure stays on the page with the error (it used to navigate away on the faked success). Demo video + "Watch demo" links stay Round-1-only as before; `getRoundLocks` is fetched alongside (null-tolerant).
- **Admin `judging/JudgeProgressPanel`** (Round 1 + Round 2 tabs, defaultExpanded accordion): ONE `GET /api/judge/admin/progress/<event>/<round>` → judges complete / scores in / drafts-in-progress chips, by-judge table (progress bar, missing teams as chips, drafts, last submitted) or by-team view, CSV, and the **lock toggle** (confirm dialog → `POST /api/judge/admin/locks/<event>/<round>`). Polls every 30s while visible and incomplete, refetches on refocus; token via refs. Remounted via a `key` bump after the tab saves assignments.
- **Bulk assignment saves:** `JudgingRound1.createAssignments` / `JudgingRound2.saveRound2Configuration` call `judgeApi.bulkAssign` (`replace: true` on full panel saves so the backend set equals what's on screen; `false` for single judge/team adds) and fall back to the per-pair POST loop only on 404. `loadExistingAssignments` resolves judges from the already-loaded judge list (network lookup only for unknown ids) and loads panels in parallel.
- **`JudgingResults`** (rewritten): one `judgeApi.getBulkScores(event,'round2')` + per-panel assignments (was one request per judge × team + per judge). Ranking is `src/components/admin/judging/resultsRanking.js::rankRound2Teams` (unit-tested): average desc; **ties broken by the EARLIEST `project_submitted_at`** — the first time the team pressed "Submit your project" (`submit_project` stamps it once) — a team with no stamp loses the tie; then lower std dev, then name. Averages are compared at the DISPLAYED precision (`averageKey`, two decimals — never an epsilon, which isn't transitive and reorders averages that display differently). **Only currently assigned judges' scores count** (same rule as the old page); a score from a judge later removed from the panel shows in the dialog as "not counted". Rows carry `rank`/`tieGroup`/`tieBrokenBy`; the table shows a "Project submitted" column (event tz, with seconds) and a Tie chip; CSV exports both. The open-dialog row is derived from live state by id (stale-snapshot lesson). The scoring page has a dirty-gated unsaved-changes guard (`beforeunload` + `routeChangeStart` confirm; suppressed for the post-submit redirect).
- Tests: `src/components/Judge/__tests__/*`, `src/components/admin/judging/__tests__/resultsRanking.test.js`, `src/lib/__tests__/judgeApi.test.js`, `src/__tests__/pages/judge-event.test.js` (page-level: inline adjust end to end, locked round, failed submit stays put, retry card).

## Hackers' Choice peer vote (Sep 2026)

Expand Down
14 changes: 13 additions & 1 deletion ga-events-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,27 @@ Events added by the DevPost-replacement work (`docs/plans/team-dashboard-devpost
| `peer_vote_cta_click` | `PeerVote/PeerVoteCTA.js` | `{ event_label: "event" \| "dashboard", event_id }` | Which surface drove the voter to the page (event page vs team dashboard card). |
| `peer_vote_submitted` | `PeerVote/PeerVotePage.js` | `{ value: <number of picks>, event_id, page: "hackers_choice_vote" }` | Ballot accepted (`POST …/peer-vote/ballot`). Re-votes fire again. No tallies are ever exposed to the client. |

### Judging (Oct 2026)

Judge-side events fire via `trackEvent({ action, params })` from the refined judge dashboard; admin events via `trackStructuredEvent(EventCategory.ADMIN, …)`.

| Event Name | Source | Parameters | Notes |
|------------|--------|------------|-------|
| `judge_dash_start` | `pages/judge/index.js` | `{ event_label: <event_id>, page: "judge_dashboard" }` | "Open judging" on a hackathon card (pre-existing event, kept). |
| `judge_inline_adjust_open` | `pages/judge/[event_id].js` | `{ event_id, round, page: "judge_event" }` | Judge pressed "Adjust scores" on an already-scored team card (editor opened; nothing saved yet). |
| `judge_inline_adjust_saved` | `pages/judge/[event_id].js` | `{ event_id, round, page: "judge_event" }` | Inline "Save changes" succeeded (`POST /api/judge/score` re-submit). Ratio to `_open` = how often judges actually change their mind after a demo. |
| `judge_score_submitted` | `pages/judge/[event_id]/team/[team_id].js` | `{ event_id, round, event_label: "submit" \| "update", page: "judge_team" }` | Full scoring page submit succeeded; `update` = re-submitting an already-submitted score. |

### Admin (`event_category: "admin"`)

| Event Name | Source | Parameters | Notes |
|------------|--------|------------|-------|
| `admin_deadline_reminder_sent` | `admin/hackathon-edit/sections/DeadlinesSection.js` | `{ event_category: "admin", event_label: <event_id>, value: <hours_before 24\|6\|1> }` | Manual "Send reminder now" (the hourly cron path is server-side and untracked). |
| `admin_peer_vote_void_ballot` | `admin/PeerVoteResults.js` | `{ event_category: "admin", event_label: <event_id> }` | A ballot was voided. |
| `admin_peer_vote_publish` | `admin/PeerVoteResults.js` | `{ event_category: "admin", event_label: <event_id> }` | Hackers' Choice published (award appended + public summary written). |
| `admin_judging_lock` / `admin_judging_unlock` | `admin/judging/JudgeProgressPanel.js` | `{ event_category: "admin", event_label: "round1" \| "round2", event_id }` | Scoring for a round locked/unlocked from the Round 1 / Round 2 admin tabs (`POST /api/judge/admin/locks/<event>/<round>`). |
| `admin_submissions_csv_export` | `admin/TeamManagement.js` | `{ event_category: "admin", event_label: <event_id> }` | "Export submissions CSV" download. |
| `admin_csv_export` | `admin/ExportCsvButton.js`, `admin/volunteer/VolunteerExportDialog.js`, `admin/JudgingResults.js` | `{ event_category: "admin", event_label: <table>, value: <row count>, event_id }` — the volunteer dialog adds `{ roster: all\|yes\|no, statuses: "all" \| "a\|b", columns: all\|essentials, kept_view_filters }` | Any "Download CSV" on `/admin/hackathons/<event>`: `event_label` names the table (`volunteer_<type>`, `teams`, `team_assignments_*`, `judging_round1_*`, `judging_round2_results`, `peer_vote_*`, `checkin_people`, `nonprofits`, `schedule_countdowns`, `meals`, `links`). |
| `admin_csv_export` | `admin/ExportCsvButton.js`, `admin/volunteer/VolunteerExportDialog.js`, `admin/JudgingResults.js` | `{ event_category: "admin", event_label: <table>, value: <row count>, event_id }` — the volunteer dialog adds `{ roster: all\|yes\|no, statuses: "all" \| "a\|b", columns: all\|essentials, kept_view_filters }` | Any "Download CSV" on `/admin/hackathons/<event>`: `event_label` names the table (`volunteer_<type>`, `teams`, `team_assignments_*`, `judging_round1_*`, `judging_round2_results`, `judging_round1_progress`, `judging_round2_progress`, `peer_vote_*`, `checkin_people`, `nonprofits`, `schedule_countdowns`, `meals`, `links`). |

## Business Intelligence Applications

Expand Down
Loading
Loading