mentor.bulkAssign now accepts mentorIds[] instead of a single mentorId and
creates the cartesian product of (mentor, project) assignments. Existing
active (mentor, project) pairs are skipped per-pair, not per-project, so
choosing two mentors against a project that already has one of them still
adds the second.
Email coalescing stays one-per-mentor: each mentor receives a single email
listing only their own newly-assigned projects (not the union). Each touched
project still triggers a single team-introduction email when its MENTORING
round is ROUND_ACTIVE, listing all currently-active mentors on that team.
Dialog UI swaps the radio picker for a checkbox group with a removable chip
strip for selected mentors, a live preview of the assignment count
(mentors × projects = up to N), and a submit button that names both
counts. Toast on success reports total assignments created, projects
touched, pairs skipped, and how many mentor emails went out.
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>
- 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>
- 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>
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>
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>
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 mentor.dropAssignment mutation (mentor-only, ownership-checked, reason
min 10 chars). Filters dropped MentorAssignment rows out of getMyProjects,
getCandidates mentor count, getMentorPool, and getMenteeActivity so they
no longer surface in the mentor or admin UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a project-centric ops view for mentor management:
- New mentor.getMenteeActivity tRPC procedure aggregates every project
with wantsMentorship=true and derives a status (unassigned / assigned
/ active / stalled) from the latest message + file activity.
- /admin/mentors becomes a tabbed page: existing Mentor list +
new Mentees & Activity table with status pills, search, and a
per-row Assign/Open CTA linking to /admin/projects/[id]/mentor.
- Includes 2 unit tests covering classification + program scoping.
Also: ignore .remember/ (plugin scratch dir).
mentor.getRecentMessages: last N unread messages from teams across all
of a mentor's assignments. Drives a Recent Messages card on /mentor.
applicant.getMentorConversationPreview: last 3 messages + unread count
for a given project. Drives a 'Conversation with [Mentor]' card on
/applicant — auto-hides when no mentor is assigned.
Both procedures use the existing MentorMessage(projectId, createdAt)
composite index — no new index needed.
Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
mentor.getCandidates and mentor.getMentorPool both filtered on
status='ACTIVE', which excluded every seeded mentor (status=NONE)
and any INVITED mentor. Production scenario: PR 4's Manual Picker and
PR 5's pool counts both rendered empty against real data.
Filter changed to status != SUSPENDED — admins want to see all mentors
they manage (including INVITED + NONE), but not suspended ones.
Found via Playwright smoke of PR 5: pool count read 0 against 4 seeded
mentors with roles[]=['MENTOR'], status='NONE'.
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>
- processRoundClose EVALUATION uses ranking scores + advanceMode config
(threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED
- Advancement emails generate invite tokens for passwordless users with
"Create Your Account" CTA; rejection emails have no link
- Finalization UI shows account stats (invite vs dashboard link counts)
- Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson)
- New award pool notification system: getAwardSelectionNotificationTemplate email,
notifyEligibleProjects mutation with invite token generation,
"Notify Pool" button on award detail page with custom message dialog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Align schema.prisma with add_15_features migration (15 discrepancies):
nullability, column names, PKs, missing/extra columns, onDelete behavior
- Make universal_apply_programid migration idempotent for safe re-execution
- Add reconciliation migration for missing FKs and indexes
- Fix message.ts and mentor.ts to match corrected schema field names
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security (Critical/High):
- Fix path traversal bypass in local storage provider (path.resolve + prefix check)
- Fix timing-unsafe HMAC comparison (crypto.timingSafeEqual)
- Add auth + ownership checks to email API routes (verify-credentials, change-password)
- Remove hardcoded secret key fallback in local storage provider
- Add production credential check for MinIO (fail loudly if not set)
- Remove DB error details from health check response
- Add stricter rate limiting on application submissions (5/hour)
- Add rate limiting on email availability check (anti-enumeration)
- Change getAIAssignmentJobStatus to adminProcedure
- Block dangerous file extensions on upload
- Reduce project list max perPage from 5000 to 200
Query Optimization:
- Optimize analytics getProjectRankings with select instead of full includes
- Fix N+1 in mentor.getSuggestions (batch findMany instead of loop)
- Use _count for files instead of fetching full file records in project list
- Switch to bulk notifications in assignment and user bulk operations
- Batch filtering upserts (25 per transaction instead of all at once)
UI/UX:
- Replace Inter font with Montserrat in public layout (brand consistency)
- Use Logo component in public layout instead of placeholder
- Create branded 404 and error pages
- Make admin rounds table responsive with mobile card layout
- Fix notification bell paths to be role-aware
- Replace hardcoded slate colors with semantic tokens in admin sidebar
- Force light mode (dark mode untested)
- Adjust CardTitle default size
- Improve muted-foreground contrast for accessibility (A11Y)
- Move profile form state initialization to useEffect
Code Quality:
- Extract shared toProjectWithRelations to anonymization.ts (removed 3 duplicates)
- Remove dead code: getObjectInfo, isValidImageSize, unused batch tag functions, debug logs
- Remove unused twilio dependency
- Remove redundant email index from schema
- Add actual storage object deletion when file records are deleted
- Wrap evaluation submit + assignment update in
- Add comprehensive platform review document
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Major cleanup and schema migration:
- Remove unused dynamic form builder system (ApplicationForm, ApplicationFormField, etc.)
- Complete migration from RoundProject junction table to direct Project.roundId
- Add sortOrder and entryNotificationType fields to Round model
- Add country field to User model for mentor matching
- Enhance onboarding with profile photo and country selection steps
- Fix all TypeScript errors related to roundProjects references
- Remove unused libraries (@radix-ui/react-toast, embla-carousel-react, vaul)
Files removed:
- admin/forms/* pages and related components
- admin/onboarding/* pages
- applicationForm.ts and onboarding.ts routers
- Dynamic form builder Prisma models and enums
Schema changes:
- Removed ApplicationForm, ApplicationFormField, OnboardingStep, ApplicationFormSubmission, SubmissionFile models
- Removed FormFieldType and SpecialFieldType enums
- Added Round.sortOrder, Round.entryNotificationType
- Added User.country
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 15+ styled email templates matching existing invite email design
- Wire up notification triggers in all routers (assignment, round, project, mentor, application, onboarding)
- Add test email button for each notification type in admin settings
- Add round-attached notifications: admins can configure which notification to send when projects enter a round
- Fall back to status-based notifications when round has no configured notification
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Projects now exist at the program level instead of being locked to a
single round. A new RoundProject join table enables many-to-many
relationships with per-round status tracking. Rounds have sortOrder
for configurable progression paths.
- Add RoundProject model, programId on Project, sortOrder on Round
- Migration preserves existing data (roundId -> RoundProject entries)
- Update all routers to query through RoundProject join
- Add assign/remove/advance/reorder round endpoints
- Add Assign, Advance, Remove Projects dialogs on round detail page
- Add round reorder controls (up/down arrows) on rounds list
- Show all rounds on project detail page
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth.
Includes production Dockerfile (multi-stage, port 7600), docker-compose
with registry-based image pull, Gitea Actions CI workflow, nginx config
for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>