Voting day already active for the season
Step A1 — A single button on the season page (`admin.setVotingDay`) toggles `seasons.votingDayActive`, which switches the phase resolver for every performance from performance-day to voting-day scoring. Shown here already active (seeded that way); toggling it is demonstrated below.
Toggled off
One click deactivates voting day — every performance's scoring phase reverts to performance-day.
Toggled back on
Reactivated — the rest of this walkthrough happens with voting day live.
Reviewer sees an eligibility snapshot on their dashboard
Step A2 — `eligibility.evaluateEligibility` computes everything needed (shows attended, response average, exclusions, reason) but it's only exposed admin-side (`/api/admin/seasons/:id/eligibility`). There's no `GET /api/me/eligibility` or dashboard card yet — a reviewer has no way to see their own status before hitting the wall at submit time (see A3).
Ineligible reviewer blocked at finalization
Step A3 — This IS enforced server-side: `submitScore`'s voting-day branch checks the `enforce_voting_eligibility` policy and throws "Not eligible to finalize on voting day: …" (403), shown here as a real error from a real blocked submit attempt — not a mock. It's just not explained to the reviewer ahead of time (that's the A2 gap above).
Eligible reviewer finalizes voting-day scores
Step A4 — Impersonating an eligible reviewer (chapter-admin/platform-admin "View as"): the screen shows a Voting Day badge. Voting day writes a brand-new score row per scorer/category (the 2-phase model), separate from Finn's performance-day scores — this rubric starts blank, not pre-filled from PD.
Voting-day score submitted
First voting-day submission for this category — submitted for adjudicator review, same approval flow as performance day (see A8).
A >2-point revision requires a written reason
Step A5 — Revising a already-submitted voting-day score enough to move the category score by more than 2 points surfaces a required reason field. v2 hard-blocks this (v1 only asked on paper — 2,385 of 7,607 v1 finalized scores moved ≥2 points but only 46 reasons were ever recorded).
Revised score submitted with reason on file
The revision is now on record with its change reason, ready for adjudicator re-approval.
Adjudicator approves voting-day submissions
Step A8 — The exact same review-and-approve flow as performance day, just against voting-day-phase score rows. Production v1 reality: all 7,607 voting-day-submitted scores were approved (blanket batch-approve), so this single "Approve all" action is the real-world workflow, not an edge case.
Per-performance score aggregates exist — no season-wide leaderboard yet
Step B1 — The admin console aggregates scores per performance (what's shown here), but there's no `awards.getCategoryStandings(seasonId)` — a season-wide leaderboard ranking every nominee within a category across all performances, which is what B2's top-5 computation needs to read from.
Compute top-5 nominations per category
Step B2 — Nothing built. `award_categories.maxNominees` exists as a data field but no standings→top-N slate computation exists anywhere.
Committee reviews slates and determines winners
Step B3 — No winners view, no "confirm winner" write path, no audit trail for the decision — depends entirely on B2 existing first.
Reviewer award (perfect on-time attendance, all responses)
Step B4 — `attendance_status='late'` and `user_profiles.studentGrade` are both tracked (the raw inputs exist), but no `awards.getReviewerStandings` computation reads them into a per-grade standing.
Results feed the gala (winners export)
Step B5 — Out of scope until B2/B3 exist; would just be an export view over their output.