Found by driving the full ceremony end-to-end in the browser:
- live.start + startPresentation sync LiveVotingSession (status/currentProjectId)
— jury votes silently failed when the admin never used sendToScreens
- LiveVotingForm no longer shows 'submitted' on a failed mutation; pages
re-key on votedAt so async vote data renders the right state after refresh
- reveal compose prefers DELIB_LOCKED deliberation results over jury score
order (listSessions now includes results); Arm unlocks right after save
- deliberation jury page review cards re-key on revision
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Requests consisting only of cheap public ceremony reads (+ token-gated vote
casts) get a 6000/min per-IP budget instead of 100/min. Vote integrity is
enforced by the token + AudienceFavoriteVote IP cap, not the rate limiter.
Found live: big-screen polling hit 429 within minutes in verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- submitVote resolves the caller's JuryGroupMember participant row server-side
(was comparing JuryGroupMember id to User id — every juror got FORBIDDEN)
- getSessionWithVotes now includes category projects so the ranking form has
data before finalize
- liveVoting.vote accepts any finale-ordered project (revision during
deliberation); timed window still applies to the live project
- live.sendToScreens keeps LiveVotingSession.currentProjectId/status in sync
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- finalist banner/panel render an optional-uploads state (settle to green via
hasRequired ? allRequiredUploaded : allUploaded; 'optional' copy when nothing required)
- ReviewDocsPicker admin card on the LIVE_FINAL round page to curate judge-visible docs
Grand-Final jurors couldn't find the finalist documents review: the only entry
point was a top-nav text link (hidden in the hamburger on mobile), with nothing
on the dashboard. Add a prominent dashboard banner (shown only to finals-jury
members) linking to /jury/finals-documents, and gate the nav "Finalist Documents"
link to members so other jurors don't hit a dead "No access" page.
- finalist.canReviewDocuments: lightweight boolean procedure (self-resolves active
program) so the nav can gate the link without fetching the full payload
- jury-nav: show "Finalist Documents" only when canReviewDocuments
- jury dashboard: FinalsJuryBanner server component, gated via userCanReviewFinals,
rendered above content regardless of assignment state
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The finals jury needs the teams' EXISTING submissions (pitch deck, exec summary,
business plan, videos from prior rounds) — which all 9 teams already have. So:
- listFinalistDocumentsForReview now returns ALL of each finalist team's files
across every round (labeled by doc type + round; finale uploads flagged
'Revised for finals'), with presigned URLs. NOT gated — judges always see.
- Revised re-uploads are now an admin toggle (Round.configJson.allowFinalistRevisedUploads,
default OFF): gates the banner/panel (getFinalDocumentStatusForProject), the
upload guard (getUploadUrl/deleteFile), the documents-page round, and the
reminders (manual + cron). When off, teams aren't prompted/able to upload.
- finalist.get/setRevisedUploadSetting + a Switch on the admin finale overview.
- judge review component rewritten to a per-team labeled file list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Grand Final round = the live event; document upload + judge review happen
in the lead-up BEFORE it opens. So gate them on finalist enrollment + the round
being open-for-docs (DRAFT or ACTIVE, not closed/finalized) instead of requiring
ROUND_ACTIVE. Lets the round stay DRAFT until event time.
- getOpenFinaleRound (was getActiveFinaleRound): status in {DRAFT,ACTIVE}, not finalized
- cron + userCanReviewFinals use the same open-status condition
- getUploadUrl + deleteFile allow a not-yet-closed LIVE_FINAL round
- getMyDashboard openRounds includes the enrolled DRAFT LIVE_FINAL round (finalists only)
- tests: DRAFT now works; CLOSED returns null
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-ups: mentor.getProjectFinalDocuments now filters droppedAt:null
(a dropped mentor no longer sees a team's final docs); reminder linkUrl is
relative so the in-app notification does client-side nav (email still absolutized).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The middleware matcher intercepts /api/cron/* but the prefix was absent from
publicPaths, so unauthenticated scheduler calls were 307'd to /login and the
cron handlers never ran. All 9 cron routes already enforce x-cron-secret, so
opening the prefix is safe and unblocks the new final-document-reminders cron
(and repairs the existing crons). Same class of gap as the /lunch/pick fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The external dish-picker page is reached via a signed token by attendees who
have no account. The middleware authorized() callback redirected any non
allowlisted path to /login, which is a dead end for accountless users — so the
picker shipped in 8d4f0ba was unreachable in prod (307 → /login). Add
/lunch/pick to publicPaths; data stays gated by token verification in tRPC.
Adds a regression test asserting the path is public and a protected path is not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
External lunch attendees had no way to pick their own dish — an admin had to set
it inline and no email was ever sent. (Marine added herself as an external
expecting a dish-selection link and never received one.)
Adds:
- ExternalAttendee.inviteSentAt + additive migration
- HMAC-signed external lunch token (mirrors finalist-token)
- Public no-login picker page /lunch/pick/[token] — dish + allergens + notes,
gated by the lunch change deadline, read-only after
- tRPC getExternalByToken / setExternalPick (public) + sendExternalInvite (admin)
- Auto-send invite on createExternal when an email is present; per-row resend
button + status chip (Invited / Picked / no email) in the logistics screen
- Unpicked externals chased by the lunch reminder cron + manual "Send reminders"
- sendExternalDishInviteEmail (branded). Page + email title use the configurable
venue ("Lunch at {venue}") rather than "grand finale"
Tests: token roundtrip/tamper/expiry, selectUnpickedExternals filter,
get/set-by-token happy + deadline + bad-token, createExternal auto-send,
cron external reminders. Full suite 303 passing; build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'Unassigned' rooming option used value="" which throws at runtime
(blank tab behind the error boundary). Use a sentinel value mapped to unassign.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrites hotels-tab.tsx from the removed single-hotel getHotel/upsertHotel
pattern to the new multi-hotel API: Hotels section (list/add/edit/delete with
occupancy badge) + Rooming section (per-attendee hotel+room+dates assignment,
team-assign shortcut, CSV export).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the program-level hotel.findUnique (broken after removing @unique)
with the caller's HotelStay (include hotel) on their AttendingMember.
Returns hotel: {name,address,link,notes}|null and room: {roomNumber,
checkInAt,checkOutAt}|null. MyLogisticsCard renders the Room section
(number + Monaco-time check-in/out) when room is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace getHotel/upsertHotel with listHotels/createHotel/updateHotel/deleteHotel
(multi-hotel per edition). Add listRooming, assignStay, assignTeamToHotel, and
unassignStay procedures for per-attendee room assignments. Update setFlightStatus
to include attendee's HotelStay in TRAVEL_CONFIRMED notification metadata.
Extend getTravelConfirmedTemplate to render room number and check-in/out dates.
All procedures are adminProcedure and audit-logged. 10 new unit tests green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add updateMyVisaNationality mutation: finds the caller's AttendingMember where the program has visaStatusVisibleToMembers=true and a VisaApplication exists, updates VisaApplication.nationality, and emits a VISA_NATIONALITY_SELF_SET audit log. Throws NOT_FOUND when no eligible application exists. Tests: persists update; rejects caller without a visible visa app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix getMyFinalistConfirmation to resolve project via OR [submittedByUserId, teamMembers] so a lead who submitted but has no TeamMember row can see their card.
- Add getMyLogistics query: returns projectTitle, confirmationStatus, hotel (program 1:1), myFlight (caller's AttendingMember.flightDetail), visaVisible flag, and myVisa when visible. Returns null for non-confirmed or unrelated callers.
- Tests: confirmed finalist sees hotel/flight/visa; non-finalist gets null; PENDING confirmation gets null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract selectUnpickedAttendees helper with OR filter (is null OR pickedAt null)
to fix cron missing attendees with no MemberLunchPick row at all
- Update cron route to use the helper
- sendRecap now throws TRPCError on email failure instead of silently stamping success
- Add lunch.sendReminders adminProcedure for manual on-demand reminder sends
- Add "Send reminders now" AlertDialog button to LunchRecapActions
- Tests: lunch-reminder-filter.test.ts (2 new), all 5 lunch test files pass (40 tests)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a flight is set to CONFIRMED, fire a TRAVEL_CONFIRMED in-app
notification (+ email via the existing NotificationEmailSetting pipeline)
to the attending member. When a visa status changes to one of
INVITATION_SENT|APPOINTMENT_BOOKED|GRANTED|DENIED, fire a
VISA_STATUS_UPDATE notification. Both are best-effort (try/catch, never
throw inside the mutation). Admin notes are not forwarded to metadata.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add sendDueConfirmationReminders() to finalist-confirmation.ts: queries
PENDING confirmations with no reminderSentAt whose deadline is within the
per-program LIVE_FINAL round reminderHoursBeforeDeadline window (default 12h),
sends a FINALIST_REMINDER in-app notification (+ email via pipeline) to the
team LEAD, then stamps reminderSentAt for idempotency.
Wire into the finalist-confirmations cron route alongside expirePendingPastDeadline.
Also clear reminderSentAt on re-invite in resetOrCreatePendingConfirmation so
re-invited teams get a fresh reminder window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Notifies the team LEAD with FINALIST_WITHDRAWN (in-app + email) when an admin
withdraws a grand-finale slot via adminDecline, unconfirm, or unenroll.
For unenroll, only notifies when a CONFIRMED confirmation existed before deletion.
Adds finalist-withdrawal.test.ts (4 tests) covering all three paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire notifyAdmins() into finalist lifecycle events so admins receive
in-app (+ email when enabled) notifications on each key transition:
- finalist.confirm → FINALIST_CONFIRMED
- finalist.decline / adminDecline → FINALIST_DECLINED
- expirePendingPastDeadline (per row) → FINALIST_EXPIRED
- promoteNextWaitlistEntry + manualPromote → FINALIST_WAITLIST_PROMOTED
All calls are wrapped in try/catch — comms never throw inside a mutation
or cron (CLAUDE.md: "round notifications never throw"). Added project
title to the project select where it was missing.
Tests: tests/unit/finalist-comms.test.ts — 3 tests passing (TDD).
Regression: finalist-confirmation, finalist-admin-confirm,
finalist-enrollment — 30 tests all passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>