Commit Graph

99 Commits

Author SHA1 Message Date
Matt
97ef3e59ac feat(finale): server-stamped phase timer helper
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:04:51 +02:00
Matt
64f88890f5 fix(auth): make audience vote, live-scores and ceremony routes public
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:02:08 +02:00
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
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
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
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
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
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
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
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
0e221c3916 feat(email): mentorship senders forward contacts/note, return success
sendMentorBulkAssignmentEmail now accepts optional teamMembers per project
and a customNote, forwards both to the template, switches to getBaseUrl(),
and returns Promise<boolean> (true on success, false on empty/error).

sendTeamMentorIntroductionEmail now accepts optional teammates and customNote,
forwards both to the template, switches to getBaseUrl(), and returns
Promise<boolean> (true on success, false on empty/error).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:29:47 +02:00
Matt
a973b1316c feat(email): instructions + contact emails + optional note in mentorship templates
Export getMentorBulkAssignmentTemplate and getTeamMentorIntroductionTemplate,
adding an always-on instructions block, optional team-member/teammate contact
lists, and an optional custom note to both. Covers TDD with 4 new unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:24:31 +02:00
Matt
195fc787a9 feat(mentor): bulk assignment + coalesced emails + team intros on round open
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m53s
Round-page bulk-assign UI
- Checkboxes on every project row, header select-all, primary-tinted action
  toolbar that appears when 1+ rows are selected with an "Assign mentor…"
  CTA and Clear. Dialog lists the mentor pool with search (name/email/
  country/expertise), load indicator, and a radio picker.
- Always-visible tip strip when nothing is selected explains the bulk flow
  and offers a one-click "Select all N without a mentor" shortcut.
- New tRPC procedure `mentor.bulkAssign({ mentorId, projectIds })` assigns
  one mentor to many projects in a transaction; idempotent on the per-pair
  `(projectId, mentorId)` unique; per-project in-app notifications still
  fire for each team.
- Mutation invalidates listMentoringProjects, getProjectsNeedingMentor,
  getMentoringImportCandidates, getMentorPool, getRoundStats, project.list
  so the page reflects the new state without a refresh.

Coalesced mentor emails
- New `sendMentorBulkAssignmentEmail` (single email listing every newly-
  assigned project + workspace links) used by `mentor.bulkAssign` and
  `mentor.autoAssignBulkForRound`. The previously-silent auto-fill flow
  now emails mentors at the end of the batch, one combined email per
  mentor regardless of how many projects they received.

Team introduction emails when the round opens
- New `sendTeamMentorIntroductionEmail` lists every assigned mentor with
  name + email and a link to the workspace, so teams can reach out
  directly.
- `activateRound` (round-engine) fires the introduction for every project
  in a MENTORING round that has active mentors when the round opens.
- `mentor.assign`, `mentor.bulkAssign`, and `autoAssignBulkForRound` also
  fire the introduction immediately when the project's MENTORING round is
  already ROUND_ACTIVE — so mentors added mid-round still reach the team.
- Idempotency via the new `MentorAssignment.teamIntroducedAt` column
  (migration 20260526114936) — independent from `notificationSentAt` so
  pre-existing mentor-side stamps don't suppress the team-side email.
2026-05-26 14:04:32 +02:00
Matt
921019aaa4 fix(mentor): unbreak the mentor pipeline end-to-end
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m42s
Adding the MENTOR role from /admin/members/[id] only updated React state — the
AlertDialog "Add role" confirmation never called the server, so prod ended up
with zero users in MENTOR roles[] and /admin/mentors showed "No mentors yet".
The dialog now awaits updateUser.mutateAsync({ roles }) before closing.

Other corrections in the same area:

- DialogContent uses flex flex-col with max-h-[90vh] overflow-y-auto so tall
  modals (e.g. Add Project to Round) scroll internally instead of overflowing
  past their own rounded background.
- getProjectsNeedingMentor now matches autoAssignBulkForRound exactly: both
  filter mentorAssignments by droppedAt: null and require
  finalistConfirmation: CONFIRMED, so the toolbar count never exceeds what
  auto-fill actually processes. The toolbar surfaces hasNoMentors /
  hasNoEligible / count / all-assigned as distinct states instead of one
  misleading "All eligible projects have a mentor" line.
- New per-team table (MentoringProjectsTable) replaces ProjectStatesTable on
  the Projects tab of MENTORING rounds. Lists every project with its active
  mentors (multi-mentor aware), filter pills, search, finalist-confirmation
  badge, and a per-row link to /admin/projects/[id]/mentor for assigning.
- Applicant team page now lists ALL active mentors (PR8 Task 7) instead of
  just mentorAssignments[0].
- Hard guard in src/lib/email.ts short-circuits sendEmail when NODE_ENV=test
  or VITEST=true so test runs can never emit real notifications again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:01:05 +02:00
Matt
48e48f058d feat(mentor-workspace): inline document preview matching applicant docs pattern
- Eye toggle expands the row below to embed FilePreview from
  @/components/shared/file-viewer (PDF iframe, image, video, Office docs)
- Download button uses explicit Content-Disposition: attachment via a
  new `disposition` input on workspaceGetFileDownloadUrl
- getPresignedUrl learns `inline: true` and optional `response-content-type`
  override so PDFs/images don't get force-downloaded by MinIO's default
- Eye button only renders for previewable mime types

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:26:20 +02:00
Matt
ee47c0305f feat(mentor): add change-request procedures + admin email notification
- mentor.requestChange: applicants/admins open a PENDING MentorChangeRequest
  with a reason; one open request per (user, project) enforced
- mentor.listChangeRequests: admin-only inbox listing
- mentor.resolveChangeRequest: admin marks RESOLVED or DISMISSED with optional
  resolution note
- sendMentorChangeRequestEmail: notifies all SUPER_ADMIN/PROGRAM_ADMIN users
  when a request is opened (try/catch — never throws)
- Mentors are NOT notified of change requests, even after resolution
  (per design decision in PR8 plan)
- Audit log entries for create + resolve; raw reason redacted from audit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:59:23 +02:00
Matt
3a1eb149b6 feat(mentor-workspace): re-scope files from assignment to project for team-wide visibility
- MentorFile.projectId is the new access boundary; mentorAssignmentId stays
  as informational audit FK (nullable).
- uploadFile derives projectId from the assignment; getFiles takes projectId
  directly; deleteFile/addFileComment auth checks any mentor on the project
  OR a project team member.
- HMAC upload token now binds to projectId (in addition to assignmentId).
- promoteFile reads file.projectId directly (no more mentorAssignment null
  navigation).
- Removes 3 placeholder NOT_FOUND guards added in Task 4.
2026-05-22 16:53:07 +02:00
Matt
9152ebb399 feat(email): add sendMentorTeamAssignmentEmail for per-team mentor notifications
Fires when a mentor is added to a specific project team — distinct from the
one-time onboarding email keyed by User.mentorOnboardingSentAt. Idempotency
for this new email is enforced at the call site in Task 4 via
MentorAssignment.notificationSentAt. Wrapped in try/catch — never throws.
2026-05-22 16:16:28 +02:00
Matt
47746d79dd feat(auth): admin access link doubles as magic-login for users with passwords
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s
The original generateAccessLink branched on user state and minted either
an invite URL (forces password setup) or a reset URL (forces password
change). Both required the user to set/change a password — fine for new
users, painful for tech-illiterate sponsor jurors who already have a
working password and just need a fresh login because their JWT went
stale or their email is bouncing.

This adapts the existing invite-token flow to behave as a magic-login
when the user already has a password:

  - auth.ts credentials.authorize: only set mustSetPassword=true if the
    user has no passwordHash. Users who already set one keep it, the
    invite token is consumed, JWT is issued with their current role,
    they're signed in.
  - accept-invite/page.tsx: redirect to / after accept (was hardcoded
    to /set-password). The middleware already enforces the
    /set-password detour when mustSetPassword is true, so users who
    need it still land there; everyone else routes by role.
  - generateAccessLink: drop the reset-password branch. Always emits an
    /accept-invite URL. The flow naturally adapts: setup for new users,
    magic-login for active ones. Audit log records which behavior fired
    (kind: 'setup' | 'magic_login').
  - dialog copy: clearer description for each kind.

Net behavior: Didier (active, has password, stale JWT after role
migration) clicks his link → instant login on /jury, password preserved.
Magali (no password yet) clicks hers → /set-password → onboarding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:35:22 +02:00
Matt
7bc2b84d1d refactor(awards): remove AWARD_MASTER role, fold features into jury chair flow
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m5s
The AWARD_MASTER role split sponsor jurors into a parallel UI that hid
project files (only showed when the award was anchored to an evaluation
round) and duplicated the jury voting path with no real difference in
authority — tie-break and finalize were already governed by AwardJuror.isChair
regardless of the user's global role. Inviting a juror via the award page
defaulted to AWARD_MASTER, randomly fragmenting jury panels.

This collapses the role into JURY_MEMBER + isChair:

- specialAward.getMyAwardDetail now returns evaluation scores, chair
  visibility into other jurors' votes, and juror roster
- specialAward.submitVote accepts an optional justification per vote
- specialAward.confirmWinner moves from awardMasterProcedure to
  protectedProcedure (juror+chair check inside)
- bulkInviteJurors creates JURY_MEMBER accounts and, when the award has
  a juryGroupId, also adds them to that JuryGroup so they appear on
  the round-page jury panel
- jury award page renders justification, eval-score badges, and a
  chair tools panel with vote tally + finalize-winner CTA
- juryGroup.list includes attached SpecialAwards; the jury-list UI
  shows a trophy pill alongside round pills
- (award-master) route group, awardMasterProcedure, AWARD_MASTER role
  enum value, and AWARD_MASTER_DECISION decisionMode are deleted
- migration demotes any residual AWARD_MASTER users to JURY_MEMBER and
  recreates the UserRole enum without the value

Coup de Coeur on prod: Didier (the sponsor juror added today as
AWARD_MASTER by the buggy invite form) was migrated to JURY_MEMBER and
attached to the existing "Coup de Coeur" JuryGroup; the SpecialAward
itself was linked to that group (juryGroupId was NULL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:21:09 +02:00
Matt
55e6abc161 feat(finalization): winner email + UI for terminal rounds
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m7s
When finalizing a round with no further round to advance to, passing teams
are winners — not advancers. Detected for both special-award terminal rounds
(label = award name) and the main competition's terminal round (label =
competition name). Wording uses "a winner" so it works for both single-winner
awards and top-N main-track outcomes.

Adds AWARD_WINNER_NOTIFICATION email type + template ("Your project has won!"
with "our team will reach out about next steps" copy). Routes through the
notification dispatch table the same way ADVANCEMENT_NOTIFICATION does.

The FinalizationSummary gains a `winnerContext` field; the admin finalization
tab uses it to swap "X projects will advance to Y" → "X winners will be
notified for [label]" and renames "Advancement Message" → "Winner Message"
in the custom-message field. The email-preview button shows the winner
template when applicable.

In-app notification (bell icon) gets matching winner copy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:30:35 +02:00
Matt
6e36704bb1 feat(awards): notify jurors on assignment + admin reminder button
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m41s
The previous addJuror / bulkAddJurors / bulkInviteJurors flows silently
created AwardJuror rows with no notification when the user already had
an account. The result: assigned jurors had no idea they were assigned
unless they happened to log in and check /jury/awards manually.

Three changes:

1. New email template + sender (sendAwardJurorNotificationEmail). Tells
   the juror what the award is, how many projects are eligible, when
   voting closes, and links straight to /jury/awards/<id>. Reused for
   both the initial assignment notification and admin reminders.

2. Auto-send on assignment. addJuror / bulkAddJurors / bulkInviteJurors
   now send the email to newly-attached jurors. bulkInviteJurors checks
   for a prior AwardJuror row before sending so duplicate "Bulk Invite"
   clicks don't spam jurors who were already assigned. addJuror /
   bulkAddJurors accept a `sendEmail` flag so admin tooling can opt out.

3. New admin procedure specialAward.notifyJurors(awardId, userIds?,
   customMessage?). Surfaced in the Jurors tab as a "Send reminder to
   all" button at the top and a per-row mail icon for individual
   reminders. Audit-logged with action: 'JUROR_REMINDER'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 13:17:29 +02:00
Matt
fbc42f11fd fix(security): defang CSV formula injection in all exports
CSV cells whose first character is one of `=`, `+`, `-`, `@`, `\t`, `\r`
are interpreted as formulas by Excel and LibreOffice when the file is
opened. `=HYPERLINK(...)` and `=WEBSERVICE(...)` execute on cell focus
with no prompt and can exfiltrate row data to an attacker URL; DDE
(`=cmd|...`) reaches RCE behind the "enable content" prompt.

The platform exposes anonymous-attacker reachable sinks:

- `application.submit` is publicProcedure with `projectName` as
  `z.string().min(2).max(200)` — no character filter — so a project
  titled `=HYPERLINK("https://evil/?d="&A1,"Click")` lands in every
  admin export that includes Project.title.
- `userAgent` from any unauthenticated request is persisted to
  `AuditLog.userAgent` and dumped verbatim into the audit-log CSV.

Three independent CSV builders all only escaped commas/quotes/newlines
and missed the formula-prefix class:

- `src/components/shared/csv-export-dialog.tsx` — used by
  export.evaluations, export.assignments, export.filteringResults,
  export.auditLogs, export.projectScores
- `src/components/admin/round/ranking-dashboard.tsx`
- `src/server/routers/lunch.ts` (lunch.exportManifestCsv)

Centralized the fix in a new `src/lib/csv.ts` `csvCell` helper that
prefixes a single quote when the value starts with a formula trigger,
then applies the standard quote/escape rules. Wired into all three
builders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 04:14:42 +02:00
Matt
9d0beed02f fix(security): file storage authorization hardening
Three separate issues in the file storage layer:

1. IDOR via client-controlled object key in applicant.saveFileMetadata
   and file.replaceFile. Both procedures accepted `bucket` and `objectKey`
   from the client and stored them on a new ProjectFile row attached to
   the caller's own project. Because file.getDownloadUrl authorizes via
   `findFirst({ bucket, objectKey })` -> projectId, an attacker could
   bind another team's storage object to their own project row and then
   download the foreign object through the legitimate authorization
   path. Now both procedures require `bucket === BUCKET_NAME` and the
   `objectKey` to start with the project's sanitized title prefix
   (matches the prefix that generateObjectKey produces server-side).

   New helper `objectKeyBelongsToProject` exported from src/lib/minio.ts;
   `sanitizePath` is now exported as well so the helper can reuse it.

2. Missing per-round scope on file.getBulkDownloadUrls. The single-file
   getDownloadUrl restricts a juror to files in rounds with sortOrder
   <= their assigned round, but the bulk variant only checked that an
   Assignment row existed for the project. A juror assigned only to
   EVALUATION could pull URLs for LIVE_FINAL/DELIBERATION confidential
   files via this endpoint. Now applies the same per-round filter when
   the caller's access to the project is jury-only (mentors / team
   members / award jurors retain unrestricted access, matching
   getDownloadUrl semantics).

3. Same omission on the standalone /api/files/bulk-download REST route.
   Same fix applied there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 03:30:00 +02:00
Matt
05b0412534 feat: lunch reminder + recap email templates
Adds sendLunchReminderEmail and sendLunchRecapEmail. Templates use
Intl.DateTimeFormat with Europe/Monaco zone. Reuses existing
escapeHtml helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 02:37:17 +02:00
Matt
895be93678 feat: selectFinalists creates PENDING confirmations and sends emails
- New service module createPendingConfirmation: writes a PENDING
  FinalistConfirmation row with a signed token whose exp matches the
  computed deadline.
- selectFinalists admin mutation: reads windowHours from the round's
  configJson.confirmationWindowHours (default 24), validates category
  match + quota, then creates one confirmation per selected project
  and sends a notification email to the team lead. Email failures are
  logged but never roll back the row creation.
- New email helpers: getFinalistConfirmationTemplate +
  sendFinalistConfirmationEmail.
2026-04-28 17:55:09 +02:00
Matt
53a1e62614 feat: HMAC-signed finalist confirmation token 2026-04-28 17:50:17 +02:00
Matt
e37f3a5874 fix: render enum labels as proper title case
formatEnumLabel was leaving inputs uppercase ("TECHNOLOGY_INNOVATION"
became "TECHNOLOGY INNOVATION"); lowercasing first yields proper
title case ("Technology Innovation") and improves labels app-wide.
Apply it on the project mentor page for Ocean Issue + Category.
2026-04-28 16:28:30 +02:00
Matt
cedd188328 feat(email): mentor-onboarding email template + sender (§D)
One-shot email sent when a user is first granted the MENTOR role.
Subject: 'Welcome to MOPC mentoring'. Includes a CTA to /mentor and
a hint about the Switch View pill for multi-role users.

Idempotency lives at the call site (User.mentorOnboardingSentAt
checked in user.bulkUpdateRoles / user.updateRoles).

Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
2026-04-28 15:58:17 +02:00
Matt
2e7b545a1b feat: mentor workspace files end-to-end with secure presign
Adds generateMentorObjectKey helper producing
<projectName>/mentorship/<timestamp>-<file>. Replaces the
client-supplied bucket/objectKey on workspaceUploadFile with an
HMAC-signed upload token that binds bucket, objectKey, uploader,
and a 1h expiry — paths can no longer be forged from the client.

Adds workspaceGetUploadUrl, workspaceGetFiles,
workspaceGetFileDownloadUrl, workspaceDeleteFile procedures with
mentor-or-team-member auth. Builds <WorkspaceFilesPanel> and
wires it into the mentor workspace Files tab and the applicant
/applicant/mentor page. Replaces the file-promotion-panel mock
array with a real workspaceGetFiles query.

Tests cover token sign/verify (5), key construction (5), and
end-to-end procedure flow including auth + tampered tokens (7).

Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §F.1
Plan: docs/superpowers/plans/2026-04-28-pr2-mentor-workspace-files.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:33:18 +02:00
Matt
eb1e8a7870 fix: case-insensitive email matching in auth and password reset
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m10s
Email lookups used findUnique (case-sensitive on PostgreSQL) but user
input was lowercased, causing login failures for users with mixed-case
emails stored in the DB (e.g. Laurent_Faure@dietsmann.com). Also
normalized 7 affected emails to lowercase on the production DB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:04:31 -04:00
Matt
f61cebe6ae feat: wire AWARD_MASTER into auth redirects and role-switch navigation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:35:34 -04:00
Matt
7ead21114e fix: pipeline progress, message variables, jury invite flow, accept-invite UX
- Pipeline: SUBMISSION rounds count IN_PROGRESS + COMPLETED for progress %
- Round engine: remove phantom SubmissionFileRequirement check blocking auto-transition
- Messages: implement {{userName}}, {{projectName}}, {{roundName}}, {{programName}}, {{deadline}} substitution
- Email preview: show greeting, CTA button, and footer matching actual sent email
- Message composer: add green dot indicator for active rounds in round selector
- User create: generate invite token atomically (prevents stuck INVITED state on email failure)
- Jury invites: use jury-specific email template mentioning round context
- Bulk invite: animated progress bar, batch size hint, success/failure counts
- Accept invite: distinguish server errors (retry button) from expired tokens (redirect)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:47:42 -04:00
1356809cb1 fix: tech debt batch 1 — TS errors, vulnerabilities, dead code
- Fixed 12 TypeScript errors across analytics.ts, observer-project-detail.tsx, bulk-upload/page.tsx, settings/profile/page.tsx
- npm audit: 8 vulnerabilities resolved (1 critical, 4 high, 3 moderate)
- Deleted 3 dead files: live-control.ts (618 lines), feature-flags.ts, file-type-categories.ts
- Removed typescript.ignoreBuildErrors: true — TS errors now block builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:51:44 +01:00
94cbfec70a fix: email XSS sanitization, bulk invite concurrency, error handling (code review batch 2)
- Add escapeHtml() helper and apply to all user-supplied variables in 20+ HTML email templates
- Auto-escape in sectionTitle() and statCard() helpers for defense-in-depth
- Replace 5 instances of incomplete manual escaping with escapeHtml()
- Refactor bulkInviteTeamMembers: batch all DB writes in $transaction, then send emails via Promise.allSettled with concurrency pool of 10
- Fix inner catch block in award-eligibility-job.ts to capture its own error variable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:59:56 +01:00
a1e758bc39 feat: router.back() navigation, read-only evaluation view, auth audit logging
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m53s
- Convert all Back buttons platform-wide (38 files) to use router.back()
  for natural browser-back behavior regardless of entry point
- Add read-only view for submitted evaluations in closed rounds with
  blue banner, disabled inputs, and contextual back navigation
- Add auth audit logs: MAGIC_LINK_SENT, PASSWORD_RESET_LINK_CLICKED,
  PASSWORD_RESET_LINK_EXPIRED, PASSWORD_RESET_LINK_INVALID
- Learning Hub links navigate in same window for all roles
- Update settings descriptions to reflect all-user scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:25:56 +01:00
a556732b46 feat: observer UX overhaul — reports, projects, charts, session & email
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m2s
- Observer projects: default sort by status (rejected last), sortable status column
- Observer projects: search by country, institution, geographic zone
- Observer project detail: vertical timeline connectors between rounds
- Fix React key warning in ExpandableJurorTable and FilteringReportTabs
- Fix ScoreBadge text always white for better contrast on all backgrounds
- Remove misleading /30 denominator from heatmap juror reviewed count
- INTAKE stats: show Start-ups, Business Concepts, Countries (not States/Categories)
- DiversityMetrics: extractCountry() for country-only display in charts
- Fix nested button hydration error in filtering report mobile view
- Color project titles by outcome in filtering report (green/red/amber)
- Redesign CrossStageComparisonChart: funnel viz + metrics table with attrition %
- Center doughnut chart in StatusBreakdownChart
- Remove redundant RoundTypeStatsCards from evaluation report
- Move evaluation tab bar below overview header, rename to "Juror Assignments"
- Dev email override system (DEV_EMAIL_OVERRIDE env var)
- Session refresh on role change without re-login
- Role switcher in user dropdown menu
- formatCategory() utility for consistent category display
- Activity feed max height constraint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:37:50 +01:00
b7905a82e1 fix: impersonation, dashboard counter, logo lightbox, submission config, auth logs
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s
- Fix impersonation by bypassing useSession().update() loading gate with direct session POST
- Fix dashboard account counter defaulting to latest round with PASSED projects
- Add clickToEnlarge lightbox for project logos on admin detail page
- Remove submission eligibility config (all passed projects must upload)
- Suppress CredentialsSignin auth errors in production (minified name check)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 15:40:08 +01:00
8cdcc85555 feat: round user tracker + fix INVITED status not updating on login
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
- Replace Semi-Finalist Tracker with Round User Tracker on dashboard
- New getRoundUserStats query: round-aware account activation stats
- Round selector dropdown to view any round's passed projects
- sendAccountReminders now accepts optional roundId for scoped reminders
- Fix: signIn callback now sets status=ACTIVE for INVITED users on login
- DB fix: 5 users who logged in via magic link but stayed INVITED → ACTIVE

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:00:19 +01:00
ee8e90132e feat: forgot password flow, member page fixes, country name display
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m7s
Password reset:
- /forgot-password page: enter email, receive reset link via email
- /reset-password?token=xxx page: set new password with validation
- user.requestPasswordReset: generates token, sends styled email
- user.resetPassword: validates token, hashes new password
- Does NOT trigger re-onboarding — only resets the password
- 30-minute token expiry, cleared after use
- Added passwordResetToken/passwordResetExpiresAt to User model

Member detail page fixes:
- Hide "Expertise & Capacity" card for applicants/audience roles
- Show country names with flag emojis instead of raw ISO codes
- Login "Forgot password?" now links to /forgot-password page

Project detail page:
- Team member details show full country names with flags

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:49:43 +01:00
c6d0f90038 fix: presigned URL signatures, bucket consolidation, login & invite status
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m44s
- MinIO: use separate public client for presigned URLs so AWS V4 signature
  matches the browser's Host header (fixes SignatureDoesNotMatch on all uploads)
- Consolidate applicant/partner uploads to mopc-files bucket (removes
  non-existent mopc-submissions and mopc-partners buckets)
- Auth: allow magic links for any non-SUSPENDED user (was ACTIVE-only,
  blocking first-time CSV-seeded applicants)
- Auth: accept invite tokens for any non-SUSPENDED user (was INVITED-only)
- Ensure all 14 invite token locations set status to INVITED

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:06:17 +01:00
875c2e8f48 fix: security hardening — block self-registration, SSE auth, audit logging fixes
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Security fixes:
- Block self-registration via magic link (PrismaAdapter createUser throws)
- Magic links only sent to existing ACTIVE users (prevents enumeration)
- signIn callback rejects non-existent users (defense-in-depth)
- Change schema default role from JURY_MEMBER to APPLICANT
- Add authentication to live-voting SSE stream endpoint
- Fix false FILE_OPENED/FILE_DOWNLOADED audit events on page load
  (remove purpose from eagerly pre-fetched URL queries)

Bug fixes:
- Fix impersonation skeleton screen on applicant dashboard
- Fix onboarding redirect loop in auth layout

Observer dashboard redesign (Steps 1-6):
- Clickable round pipeline with selected round highlighting
- Round-type-specific dashboard panels (intake, filtering, evaluation,
  submission, mentoring, live final, deliberation)
- Enhanced activity feed with server-side humanization
- Previous round comparison section
- New backend queries for round-specific analytics

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:18:50 +01:00
13f125af28 feat: error audit middleware, impersonation attribution, account lockout logging
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m13s
- Add withErrorAudit middleware tracking FORBIDDEN/UNAUTHORIZED/NOT_FOUND per user
- Fix impersonation attribution: log real admin ID, prefix IMPERSONATED_ on actions
- Add ACCOUNT_LOCKED audit events on login lockout (distinct from LOGIN_FAILED)
- Audit export of assignments and audit logs (meta-audit gap)
- Update audit page UI with new security event types and colors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:28:56 +01:00
6c52e519e5 feat: impersonation system, semi-finalist detail page, tRPC resilience
- Add super-admin impersonation: "Login As" from user list, red banner
  with "Return to Admin", audit logged start/end, nested impersonation
  blocked, onboarding gate skipped during impersonation
- Fix semi-finalist stats: check latest terminal state (not any PASSED),
  use passwordHash OR status=ACTIVE for activation check
- Add /admin/semi-finalists detail page with search, category/status filters
- Add account_reminder_days setting to notifications tab
- Add tRPC resilience: retry on 503/HTML responses, custom fetch detects
  nginx error pages, exponential backoff (2s/4s/8s)
- Reduce dashboard polling intervals (60s stats, 30s activity, 120s semi)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:55:44 +01:00
b1a994a9d6 fix: enforce onboarding gate for applicants and observers
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m37s
Applicants could bypass onboarding and land directly on the dashboard.
Added onboardingCompletedAt check + redirect to /onboarding in both
the applicant and observer layouts (jury/mentor already had this gate).
Also removed premature status ACTIVE on magic-link first login — now
only completeOnboarding sets ACTIVE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:00:19 +01:00
43e21c6c6e feat: semi-finalist tracker dashboard, account reminders, search + UX fixes
- Add getSemiFinalistStats query with per-category/per-award breakdown
- Add sendAccountReminders mutation with invite token generation and dedup
- Add SemiFinalistTracker dashboard widget with progress bars and remind buttons
- Add ACCOUNT_REMINDER email template
- Extend project search to match team member name/email (7 locations)
- Fix Passed count deduplication: count distinct projects, not round-state rows
- Fix role switcher: visible pills above user section, auto-refresh session on mount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:41:03 +01:00
af03c12ae5 feat: per-round advancement selection, email preview, Docker/auth fixes
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m42s
- Bulk notification dialog: per-round checkboxes (default none selected),
  selected count badge, "Preview Email" button with rendered iframe
- Backend: roundIds filter on sendBulkPassedNotifications, new
  previewAdvancementEmail query
- Docker: add external MinIO network so app container can reach MinIO
- File router: try/catch on getPresignedUrl with descriptive error
- Auth: custom NextAuth logger suppresses CredentialsSignin stack traces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 14:31:01 +01:00
a39e27f6ff fix: applicant portal — document uploads, round filtering, auth hardening
Fix round-specific document uploads (submittedAt no longer blocks uploads),
add view/download buttons for existing files, enforce active-round-only for
uploads/deletes. Harden auth layout and set-password page. Filter applicant
portal rounds by award track membership.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:29:39 +01:00
f24bea3df2 feat: extend notification system with batch sender, bulk dialog, and logging
Add NotificationLog schema extensions (nullable userId, email, roundId,
projectId, batchId fields), batch notification sender service, and bulk
notification dialog UI. Include utility scripts for debugging and seeding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:29:06 +01:00