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>
Additive nullable column for the auto document-reminder cron (fires once per
team). Migration hand-authored + applied to dev via db execute (NOT migrate dev,
per drifted-dev-history rule); prod applies it via migrate deploy on next build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirmed document set: Final Presentation, Final Business Plan, 1-min Video,
Executive Summary (all required, PDF-only docs), same for both categories.
Judge page stays a thin dedicated page reusing the existing doc viewer because
the finale has 0 assignments / empty jury group (group-based, not assignment-based).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design for surfacing the already-live Grand Final document upload (PDF + 1-min
video) to the 9 confirmed finalists via a dashboard banner, a read-only judge
review page (Finals Jury group + admins), a Final Documents panel on both the
team and mentor views, and email + in-app reminders (auto cron + manual blast).
Reuses the existing legacy FileRequirement anchor, upload mechanics, and
notification pipeline. Grounded in verified prod state.
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>
- Hotel.programId no longer unique (many hotels per edition)
- New HotelStay 1:1 with AttendingMember (hotelId, room, check-in/out)
- Program.hotel -> hotels[]
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>