Commit Graph

591 Commits

Author SHA1 Message Date
Matt
146691be00 fix(auth): allow /api/cron paths past middleware (self-guarded by CRON_SECRET)
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>
2026-06-09 16:03:55 +02:00
Matt
e4f13aaed4 feat(final-docs): auto pre-deadline reminder cron 2026-06-09 16:00:42 +02:00
Matt
24c7c4bc6c feat(final-docs): Final Documents panel on team + mentor views
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:53:39 +02:00
Matt
8c6a59bad9 feat(final-docs): mentor.getProjectFinalDocuments procedure
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:50:42 +02:00
Matt
b66e2071f9 refactor(final-docs): shared review component reachable by jury + admin routes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:47:56 +02:00
Matt
df0be6bb48 feat(final-docs): judge review page + entry points
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:44:37 +02:00
Matt
e9e072dda7 feat(final-docs): finalist document review service + procedure with finale-access gate
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:36:59 +02:00
Matt
61bf5a4032 feat(final-docs): admin manual reminder button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:31:18 +02:00
Matt
26709e2c9b feat(final-docs): manual admin document-reminder blast
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:26:50 +02:00
Matt
f3d3a21156 feat(final-docs): GRAND_FINAL_DOCS_REMINDER/SUBMITTED notification types + email template
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:23:44 +02:00
Matt
9e058e6ad7 feat(final-docs): finalist upload banner on applicant dashboard 2026-06-09 15:21:08 +02:00
Matt
16e0a08f16 feat(final-docs): applicant.getFinalDocumentStatus procedure 2026-06-09 15:17:47 +02:00
Matt
c53ec23109 fix(final-docs): round-scope file query + guard empty-required edge case 2026-06-09 15:15:22 +02:00
Matt
b1923cf0e1 feat(final-docs): grand-final document status service 2026-06-09 15:09:50 +02:00
Matt
a6fc697e4d fix(auth): allow /lunch/pick public access for accountless external attendees
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m30s
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>
2026-06-05 12:18:10 +02:00
Matt
8d4f0bac1e feat(logistics): external attendees self-select lunch dish via tokenized page
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m51s
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>
2026-06-05 12:04:13 +02:00
Matt
f2c8cc1e80 fix(logistics): Hotels tab crash — Radix SelectItem cannot have empty value
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m58s
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>
2026-06-04 19:50:18 +02:00
Matt
3bbc80332c feat(logistics): Hotels tab — multi-hotel management + rooming assignment
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>
2026-06-04 19:24:06 +02:00
Matt
9313eb96f0 feat(applicant): My Logistics shows assigned hotel + room
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>
2026-06-04 19:21:29 +02:00
Matt
4cd2651f9c feat(logistics): hotels CRUD + rooming + assignment procedures + travel email
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>
2026-06-04 19:19:18 +02:00
Matt
97951deb68 feat(logistics): departure-after-arrival validation + travel/visa CSV export
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m11s
- upsertFlightDetail throws BAD_REQUEST when departureAt < arrivalAt
- Travel tab: Download CSV button (project/attendee/email/flight fields/status/visa)
- Visas tab: Download CSV button (project/attendee/nationality/status/dates/notes)
- TDD: 2 new tests (rejects invalid, accepts valid); all 6 flight tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:56:22 +02:00
Matt
53b623fb20 feat(applicant): My Logistics card (hotel/flights/visa+nationality) + confirm-page dashboard link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:53:21 +02:00
Matt
74cd111e3a feat(applicant): self-service visa nationality entry
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>
2026-06-04 16:51:01 +02:00
Matt
d03c705642 feat(applicant): getMyLogistics (hotel+flight+visa) + submitter-match fix
- 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>
2026-06-04 16:50:14 +02:00
Matt
ee8d65a300 feat(logistics): enable Email Templates tab
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:41:39 +02:00
Matt
0058b2b73b feat(logistics): Email Templates tab (toggle/subject/preview/test) + logistics in global settings
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 16:41:33 +02:00
Matt
e5788b3e9d feat(notifications): renderNotificationEmail + previewEmailTemplate + logistics sample data
- Export `renderNotificationEmail` from email.ts (pure template resolver, no send)
- Refactor `sendStyledNotificationEmail` to delegate to `renderNotificationEmail`
- Hoist sampleData to module-level `NOTIFICATION_SAMPLE_DATA` in notification router
- Add 8 logistics sample entries (FINALIST_*/TRAVEL_CONFIRMED/VISA_STATUS_UPDATE)
- Add `notification.previewEmailTemplate` adminProcedure query (returns subject/html/hasStyledTemplate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:39:29 +02:00
Matt
3f25ba112b fix(lunch): reminder filter, recap failure surfacing, manual send-reminders
- 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>
2026-06-04 16:24:01 +02:00
Matt
884c96c710 feat(logistics): travel-confirmed + visa-status emails to attendees
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>
2026-06-04 16:20:21 +02:00
Matt
1b4ab6be18 feat(finalist): deadline reminder emails via cron
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>
2026-06-04 16:17:19 +02:00
Matt
d501624c56 feat(finalist): withdrawal notification to team on decline/unconfirm/unenroll
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>
2026-06-04 16:14:26 +02:00
Matt
b2826d595f feat(finalist): admin alerts on confirm/decline/expire/promote
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>
2026-06-04 16:11:17 +02:00
Matt
f529e79cd7 feat(comms): logistics notification types, templates, and email settings
- Add 8 constants to NotificationTypes (FINALIST_CONFIRMED/DECLINED/EXPIRED/
  WAITLIST_PROMOTED/REMINDER/WITHDRAWN, TRAVEL_CONFIRMED, VISA_STATUS_UPDATE)
  with matching icons and priorities in NotificationIcons/NotificationPriorities
- Add 4 branded email templates: getFinalistReminderTemplate,
  getFinalistWithdrawnTemplate, getTravelConfirmedTemplate,
  getVisaStatusTemplate — registered in NOTIFICATION_EMAIL_TEMPLATES
  (admin-alert types use generic fallback)
- Add 8 logistics seed rows to seed-notification-settings.ts; upserted to
  dev DB (idempotent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 16:06:10 +02:00
Matt
34bb2bad57 fix(finalist): program-scope guards on enroll/unenroll (code review)
- enrollFinalists: reject a roundId whose competition belongs to a
  different program than input.programId.
- unenroll: reject a project/round pair from different programs before
  any delete.
- Hoist ADMIN_CONFIRM attendee validation to a pre-pass so a bad entry
  in a multi-team batch fails before any project is partially written.
- Add regression tests for both cross-program guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:49:13 +02:00
Matt
8ee517f6ca feat(logistics): waitlist populate UI + confirmations-tab un-confirm/re-invite actions
- waitlist-card: AddToWaitlistForm sub-component wires finalist.addToWaitlist (category + project pickers sourced from listEnrollmentCandidates, filtered to exclude confirmed/waitlisted projects; appends at next rank)
- confirmations-tab: fix dead-end empty-state copy to reference Grand Final round Overview tab
- confirmations-tab: CONFIRMED rows → Un-confirm button (AlertDialog with required reason, calls finalist.unconfirm)
- confirmations-tab: DECLINED/EXPIRED rows → Re-invite button (calls enrollFinalists EMAIL mode via liveFinalRoundId from listEnrollmentCandidates)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:36:15 +02:00
Matt
2a98f0cacf feat(grand-finale): finalist enrollment card on LIVE_FINAL round page
Adds EnrollAttendeesDialog and FinalistEnrollmentCard components and
wires the card above FinalistSlotsCard on the LIVE_FINAL round Overview,
giving admins the missing UI entry point to enroll mentoring-round teams
into the Grand Final via EMAIL or ADMIN_CONFIRM mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:32:40 +02:00
Matt
e80710487c feat(finalist): listEnrollmentCandidates query for enrollment UI
Returns mentoring-round candidates grouped by category with status,
team members, quota and confirmed/pending counts; inLiveFinal flag and
attendeeCap for the enrollment UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:29:26 +02:00
Matt
375aeb08af feat(finalist): unenroll reverses round membership + confirmation
Adds finalist.unenroll(projectId, roundId) which deletes the
FinalistConfirmation (cascading AttendingMember/FlightDetail/
VisaApplication/MemberLunchPick) and the LIVE_FINAL ProjectRoundState,
then logs a FINALIST_UNENROLL audit entry. Safe no-op when no rows exist.
Tests cover ADMIN_CONFIRM enrolled teardown and the no-rows path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:23:50 +02:00
Matt
f1e62fdd3b feat(finalist): unified enrollFinalists (round membership + confirmation + email/admin-confirm)
- Add `finalist.enrollFinalists` adminProcedure: creates ProjectRoundState in
  LIVE_FINAL round (skipDuplicates) + resets/creates FinalistConfirmation in
  one step, with EMAIL and ADMIN_CONFIRM attendee modes.
- Extract `confirmAttendanceInTx` helper into finalist-enrollment.ts; reuse
  from both adminConfirm and enrollFinalists (DRY refactor, all tests green).
- Add 4 tests covering EMAIL mode, ADMIN_CONFIRM mode, re-enroll safety, and
  over-cap rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 15:20:51 +02:00
Matt
dde8ea9345 feat(finalist): re-invite-safe confirmation reset helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 15:16:52 +02:00
Matt
ca9edcd038 fix(lunch): allow non-admins to read dish list (unblocks applicant picker) 2026-06-04 15:15:14 +02:00
Matt
6b37e9bb9e fix(email): correct member-row spacing in mentor welcome emails
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m34s
Member/mentor/teammate tables used width:100%, stretching the two
columns apart and forcing names to wrap; wrapped names then misaligned
with their email (default vertical-align: middle).

Drop width:100% so tables hug content, add 16px column gap via name-cell
padding, and set vertical-align: top so emails align to the first line
of the name. Applied to getMentorBulkAssignmentTemplate and
getTeamMentorIntroductionTemplate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:11:01 +02:00
Matt
60f1a53d70 fix(users): populate roles[] with primary role on user creation
All user-creation paths (admin create, bulk invite import, public application
contact + team members, project team members, jury-group + special-award
invites) now set roles=[role] so the invariant role in roles[] holds for new
users, matching seed.ts and the role-change mutations. Prevents the empty
roles[] inconsistency that hid primary-role mentors from the mentor picker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:06:52 +02:00
Matt
828c09df6d fix(mentor): primary-role mentors are selectable/assignable
getCandidates, getMentorPool and bulkAssign matched MENTOR via roles[] only, so
a user with role=MENTOR but an empty roles[] array (legacy/seeded records, e.g.
Arnaud Blandin on prod) was excluded from the mentor picker and rejected by
bulk assign. Match MENTOR as primary role OR in roles[], mirroring userHasRole.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:34:40 +02:00
Matt
d4a77f63d3 fix(members): role tabs/filter include users with secondary roles
user.list and user.listInvitableIds filtered on the singular User.role column,
so the type tabs (Jury/Mentor/…) omitted users holding that role as a secondary
role (User.roles[]). Match the role as primary OR secondary (roles hasSome),
combined with search via AND, mirroring userHasRole / hasRole middleware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 18:51:11 +02:00
Matt
652c3ed4f2 fix(email): use shared branded wrapper (logo/footer) for mentorship emails
Migrate getMentorBulkAssignmentTemplate + getTeamMentorIntroductionTemplate to
getEmailWrapper() so they match the other ~40 platform emails: MOPC logo header,
ocean background, big-logo footer, and UTF-8 charset (fixes accent/em-dash
rendering). Body now uses sectionTitle/paragraph/infoBox/ctaButton helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 18:21:49 +02:00
Matt
bd05aaa87d feat(mentor): email all team members button on project detail
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:50:00 +02:00
Matt
0d6f71b9e1 feat(admin): send mentorship welcome/reminder button on mentoring rounds
Adds a sky-accented "Send Welcome / Reminder" button to the Notifications
grid in the round page, visible only on MENTORING rounds. Wires into
trpc.mentor.previewMentorshipWelcome / sendMentorshipWelcome via the
shared EmailPreviewDialog with optional custom note support.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:46:58 +02:00
Matt
829b082912 feat(mentor): admin preview + send mentorship welcome/reminder email
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:40:38 +02:00
Matt
32116dac75 feat(mentor): round-open emails now carry team-member contacts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:33:27 +02:00