Comprehensive spec for upcoming MENTORING round (R6): config form completeness, mentor-specific admin views, manual + auto-fill assignment UX, multi-role juror→mentor flow, juror preferences filter, workspace messaging/file UX with server-side path enforcement, and test coverage. Phased into six independently-shippable PRs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
28 KiB
Mentor Round Readiness — End-to-End Design
Date: 2026-04-28 Author: Matt + Claude (brainstorming session) Status: Draft, awaiting review
Motivation
R5 (Semi-Final Evaluation) is about to close. Next is R6 (Mentoring) for projects that request or are assigned a mentor, then R7 (Grand Final). The MENTORING backend exists but has gaps that block operational use:
- Admin Config form omits two
MentoringConfigSchemafields (mentoringRequestDeadlineDays,passThroughIfNoRequest) - Round Overview shows generic stats only — no mentor-specific dashboard
/admin/projects/[id]/mentorexposes only AI suggestions; manual mentor selection is missing entirely from the UI- File uploads (
mentor.workspaceUploadFile) accept client-controlledbucket/objectKey— security/consistency hole - Juror "Confirm Your Evaluation Preferences" banner pulls in LIVE_FINAL groups (not appropriate for a live ceremony)
- Multi-role users (juror + mentor) land on primary role's dashboard only; no quick path for an admin to bulk-promote jurors
- Zero tests for MENTORING round behavior
This spec covers all of the above plus workspace messaging/file UX polish, in one design with phased PRs.
Goals
- Admin can fully configure a MENTORING round from the UI (no DB-direct edits needed for any
MentoringConfigSchemafield). - Admin can see at a glance: who requested mentoring, who has a mentor, who doesn't, who's mentoring whom, what the mentor pool looks like.
- Admin can manually assign a mentor to any project, AND auto-fill all unassigned projects in one action.
- Files uploaded in the mentor workspace land at
<projectName>/mentorship/<file>in the configured bucket, with paths constructed server-side. - Mentors and applicant teams see recent messages on their respective dashboards.
- A juror who is also a mentor can switch dashboards in one click, without seeing irrelevant LIVE_FINAL preference cards.
- The MENTORING round behavior (pass-through, eligibility, advancement) is covered by integration tests.
Non-goals
- Redesigning messaging or notifications from scratch.
- Replacing the AI mentor-matching service with a different model.
- Building a mentor scheduling/calendar feature.
- Bulk-promoting jurors to mentors via CSV import (per-row checkbox + bulk action is enough for this iteration).
- Migrating any existing mentor file objects in MinIO (none exist yet — spec asserts a pre-flight check).
Out-of-scope but adjacent
- Grand Finale (R7 LIVE_FINAL) UX — explicitly deferred per user direction (handled separately, much further build-out planned).
- Mentor pool capacity / load-balancing algorithm changes — covered only by surfacing existing fields in the admin view.
High-level architecture
No new top-level architecture. Extending existing patterns:
- Storage path: new helper
generateMentorObjectKey(projectTitle, fileName)insrc/lib/minio.tsthat returns<sanitizedProjectName>/mentorship/<timestamp>-<sanitizedFileName>— exact same shape asgenerateObjectKey()withroundName="mentorship". Server-side only. - Config schema: no Prisma migration. The two missing fields (
mentoringRequestDeadlineDays,passThroughIfNoRequest) already exist inMentoringConfigSchemaand are read byround-engine.tsandapplicant.ts— only the form needs updating. - Multi-role dashboards: existing
User.roles UserRole[]array drives everything; logic-only changes (post-login redirect priority, bulk-promote bulk action, fix CSS layering on impersonation banner). - Preferences filter: single Prisma query change in
getOnboardingContext. - Workspace dashboards: reuse existing
MentorMessagetable; new tRPC procedures return last-N message previews.
Phasing / PR plan
Six PRs, ordered smallest-blast-radius first:
| PR | Section | Risk | What ships |
|---|---|---|---|
| 1 | §E | Low | Filter getOnboardingContext to review-only rounds |
| 2 | §F.1 | Low | Server-side objectKey enforcement + generateMentorObjectKey helper |
| 3 | §A | Med | Config form completeness (2 missing inputs + General Settings cleanup + Launch Readiness gate relax) |
| 4 | §C | Med | Manual mentor picker + bulk auto-fill + AI fallback |
| 5 | §B | Med | Mentor-specific Round Overview + un-redirect /admin/mentors |
| 6 | §D + §F.2 | Med | Multi-role redirect priority + bulk-promote + impersonation banner fix + dashboard message previews |
| (continuous) | §G | Low | Tests added in each PR for the surface changing in that PR |
A standalone test PR is not planned — tests ride with the change they cover.
§A. MENTORING round Config form
Files:
src/components/admin/round-config/mentoring-config.tsx(likely path; locate the round-type-specific config component used by(admin)/admin/rounds/[roundId]Config tab)src/components/admin/round-config/launch-readiness.tsx(or similar — the component that renders the 0/3 readiness checklist)
Changes:
- Add "Mentoring Request Window" section to the Config form:
- Numeric input bound to
configJson.mentoringRequestDeadlineDays— int, min 1, max 90, default 14. - Help text: "Number of days from round opening during which teams may request mentoring. After this window, no new requests are accepted."
- Numeric input bound to
- Add "Pass-through behavior" toggle bound to
configJson.passThroughIfNoRequest:- Default
true(matches schema default). - Off-state label: "Hold all projects in PENDING until mentor is assigned (manual gate)"
- On-state label: "Auto-PASS projects that don't request mentoring (default)"
- Default
- Replace empty "General Settings" section header. Either:
- Delete the empty header (preferred — fewer questions); OR
- Move the eligibility dropdown into it (so the section has content).
- Relax Launch Readiness "File requirements set" gate for MENTORING rounds:
- Required only when
configJson.filePromotionEnabled === trueANDconfigJson.promotionTargetWindowIdis set (i.e., the round is configured to promote mentor-authored files into a downstream submission window). - Otherwise treat the readiness item as N/A and don't count it against the 0/3 (it becomes 0/2 for mentoring rounds without promotion configured).
- Required only when
- Help-text added to the existing Eligibility dropdown explaining each option:
requested_only— only projects that flagmentoringRequestedparticipate (default).all_advancing— every project advancing into this round gets a mentor.admin_selected— admin manually picks which projects participate.
Tests (in PR 3): one per MentoringConfigSchema field — render with default config, change input, submit, assert config persisted via the existing config-save mutation.
§B. Mentoring-specific admin views
Files:
src/app/(admin)/admin/rounds/[roundId]/page.tsx(Round Overview tab)src/app/(admin)/admin/rounds/[roundId]/projects-tab.tsx(Projects tab — exact filename to confirm during impl)src/app/(admin)/admin/mentors/page.tsx(currently a redirect stub — replace with a real list page)src/app/(admin)/admin/mentors/[id]/page.tsx(also a stub today; replace with mentor detail)- New tRPC procedures on
mentorrouter (admin-gated):getRoundStats,getMentorPool,getMentorDetail
Round Overview — replace generic Round Details with a mentoring-specific stats card when round.roundType === 'MENTORING':
- Top-line counts (single row of stat cards):
- Total projects in round
- Requested mentoring (count + % of total)
- Mentor assigned (count + % of total)
- Awaiting assignment (= requested - assigned)
- Request window card:
- Deadline (computed from
windowOpenAt + mentoringRequestDeadlineDays) - Time remaining (live countdown, using existing
formatCountdownhelper) - "Closes in N days" pill, turns amber within 48 hours, red within 12 hours
- Deadline (computed from
- Mentor pool card:
- Pool size (count of users with MENTOR role in the program)
- Average load (assigned projects ÷ pool size)
- Capacity remaining (sum of
User.maxAssignmentsOverrideminus current load, where overrides exist) - Link →
/admin/mentors
- Workspace activity card:
- Total messages exchanged (sum across all assignments in round)
- Total files uploaded
- Total milestones completed
- "Last activity" timestamp
Round Details panel stays at the bottom of the Overview tab when round is MENTORING (the existing panel is still useful for type/status/position/dates), but with these field-level adjustments:
- Replace "Jury Group: —" row with "Mentor Pool: N members" (link to
/admin/mentors). - Keep "Type", "Status", "Position", "Opens", "Closes" rows unchanged.
- The new "mentoring stats card" (top-line counts, request window, mentor pool, workspace activity) renders above the Round Details panel, not in place of it.
Projects tab — when round is MENTORING, the per-project row shows:
- Project title + team lead
- "Requested mentoring" badge (yes/no)
- "Mentor assigned" cell — mentor name + expertise overlap chip, OR "Unassigned" with inline "Assign" button → opens the manual-pick drawer (see §C)
- "Workspace activity" small-text summary (msgs / files / milestones)
- Bulk action bar (when ≥1 project selected): "Auto-fill mentors for selected" → calls
mentor.autoAssignBulk
/admin/mentors — un-redirect, replace stub with a real list page:
- Searchable/filterable list of all users with MENTOR role in the current edition.
- Columns: name, email, country, expertise tags (chips), assigned-projects count, completed count, capacity remaining, last activity.
- Row →
/admin/mentors/[id]detail page (existing route, replace stub):- Mentor profile + expertise + bio
- List of assigned projects (link to per-project workspace)
- Per-project status (in_progress / completed / paused)
- Recent activity feed (messages / file uploads / milestone completions across all assignments)
- Admin actions: reassign / unassign
Tests (in PR 5): integration test for getRoundStats returning correct counts; render-test for round overview when round.roundType=MENTORING.
§C. Manual + auto-fill mentor assignment
Files:
src/app/(admin)/admin/projects/[id]/mentor/page.tsx(rewrite)src/server/services/mentor-matching.ts(add expertise-tag fallback)src/server/routers/mentor.ts(getCandidatesnew procedure for manual picker; ensureautoAssignBulkexposes a "skip already assigned" param — confirm and document)
Page rewrite — three sections, all visible at once (not tabs):
- Project Context card (top):
- Project title, ocean issue, country, team size, expertise needs (project tags)
- Round being assigned for (linked)
- Mentoring requested? Yes/no
- Currently Assigned card:
- If assigned: mentor name, email, country, expertise overlap chips, "Assigned by [admin], 3 days ago, method: MANUAL/AUTO", actions: Unassign | Swap
- If unassigned: empty state with copy "No mentor assigned yet — pick one below or use AI"
- Pick a mentor card with a tab strip:
- Tab 1 — Manual picker (default selected):
- Searchable input
- Sortable table of all MENTOR-role users in the program: name, expertise tags, country, current load, capacity, expertise overlap with this project (computed: count of shared tags / total project tags, displayed as a percentage chip)
- Default sort: highest expertise overlap first
- Per-row "Assign" button → calls
mentor.assign({ projectId, mentorId, method: 'MANUAL' })
- Tab 2 — AI suggestions:
- Existing pane (loads
getSuggestions). - Fallback: if AI fails (no
OPENAI_API_KEY, network error, or returns empty) — show expertise-tag-overlap ranking as the suggestion source instead, with a banner: "AI matching unavailable — showing expertise-tag overlap instead". (The fallback ranking is the same algorithm as Tab 1's default sort, so the lists may look similar — that's fine.)
- Existing pane (loads
- Tab 1 — Manual picker (default selected):
Auto-fill remainder (bulk action):
- On round Projects tab + Round Overview, button: "Auto-fill mentors for unassigned projects".
- Call
mentor.autoAssignBulkwith the round ID; the service filters to projects-in-round-without-MentorAssignment, scoped further by the round'seligibilityconfig:requested_only→ only projects withmentoringRequested=trueall_advancing→ every project in the roundadmin_selected→ button disabled (admins must pick manually for this mode)
- Confirm the existing service already skips projects with a MentorAssignment (any method); if it doesn't, fix in the same PR.
- Result toast: "Assigned N projects, skipped M already-assigned, K unassignable (no matching mentor)".
Tests (in PR 4):
mentor.assignround-trips with method=MANUALmentor.autoAssignBulkskips manually-assigned projectsgetCandidatesreturns expected expertise-overlap ordering- Fallback path used when AI unavailable
§D. Juror→mentor multi-role UX
Files:
src/app/page.tsx(post-login redirect)src/app/(admin)/admin/members/page.tsx(bulk action)src/components/layouts/role-nav.tsx(no change — switcher already correct)src/components/layouts/impersonation-banner.tsx(or wherever the banner lives — find via grep)src/server/routers/user.ts(newbulkUpdateRolesmutation if not exists)src/lib/email/templates/mentor-onboarding.tsx(new)src/server/services/notifications.ts(or equivalent — call site to send mentor-onboarding email when MENTOR role is freshly added to a user)
1. Post-login redirect — priority-based on roles[]:
Replace single-role switch in src/app/page.tsx with priority order:
const ROLE_DASHBOARD_PRIORITY: Array<[UserRole, Route]> = [
['SUPER_ADMIN', '/admin'],
['PROGRAM_ADMIN', '/admin'],
['AWARD_MASTER', '/award-master'],
['JURY_MEMBER', '/jury'],
['MENTOR', '/mentor'],
['OBSERVER', '/observer'],
['APPLICANT', '/applicant'],
['AUDIENCE', '/audience'],
]
const userRoles = session.user.roles ?? [session.user.role]
const target = ROLE_DASHBOARD_PRIORITY.find(([role]) => userRoles.includes(role))?.[1]
if (target) redirect(target)
Decision: priority-based, not "remember last view". The "remember last view" approach requires a new column on User and adds login-side complexity. Priority is deterministic, easy to explain, and the role-switcher dropdown handles the case where the user wants a different view. Revisit if users complain.
2. Bulk juror→mentor promotion on /admin/members:
- Add row checkboxes to the Members table (already a table — confirm during impl).
- When ≥1 row selected, surface a bulk action toolbar with "Add role…" dropdown (OBSERVER / MENTOR / AWARD_MASTER) and "Remove role…".
- Call new
user.bulkUpdateRoles({ userIds, addRole?, removeRole? })mutation. Server-side: only SUPER_ADMIN/PROGRAM_ADMIN, log aDecisionAuditLogentry per user changed. - After success, refresh the table and toast "Added MENTOR role to N users; M already had it (no-op)".
3. Mentor-onboarding email (one-shot):
- New email template at
src/lib/email/templates/mentor-onboarding.tsx: brief welcome, explanation of mentor responsibilities, link to/mentor, link to "Switch View" doc/walkthrough. - Trigger: in
user.bulkUpdateRolesand the existing single-userupdateRolesmutation, when MENTOR is newly added (i.e., wasn't inroles[]before this update) → enqueue the email. Idempotent on subsequent edits that keep MENTOR inroles. - Add a
User.mentorOnboardingSentAt: DateTime?column for idempotency. Migration: nullable column, no backfill needed.
4. Fix impersonation banner pointer-events:
- Locate the banner component (grep
Impersonating/bg-red-600 fixed top-0). - Restructure: banner sits in a flex container above the header rather than being
position: fixedover it. The header height stays unchanged; the banner pushes content down. - Alternative (smaller change): keep
position: fixedbutpointer-events: noneon the banner div and re-enablepointer-events: autoon the inner "Return to Admin" button only. Either fixes the menu intercept. - Pick the simpler diff at impl time; document choice in PR.
5. Banner shows all roles:
- When
session.user.roles.length > 1, render comma-separated list: "Impersonating Dr. Sophie Laurent (JURY MEMBER, MENTOR)".
Tests (in PR 6):
- Post-login redirect honors priority for multi-role user.
bulkUpdateRolesadds MENTOR to N users and sends N onboarding emails.- Idempotency: second
bulkUpdateRoleswith same input does NOT resend email. - Impersonation banner does not intercept clicks on user dropdown (Playwright e2e if available).
§E. Filter juror preferences to review-only rounds (PR 1)
File: src/server/routers/user.ts:1397-1422 (getOnboardingContext)
Change: Query the membership's jury group, including its linked rounds. Filter out memberships where every linked round is LIVE_FINAL or DELIBERATION. Keep memberships where at least one linked round is INTAKE / FILTERING / EVALUATION / SUBMISSION / MENTORING.
const memberships = await ctx.prisma.juryGroupMember.findMany({
where: {
userId: ctx.user.id,
juryGroup: {
rounds: {
some: {
roundType: {
in: ['INTAKE', 'FILTERING', 'EVALUATION', 'SUBMISSION', 'MENTORING'],
},
},
},
},
},
include: { juryGroup: { select: { id: true, name: true, defaultMaxAssignments: true } } },
})
(Confirm the relation field name rounds on JuryGroup during impl — Prisma schema field may be Round[] named differently.)
Tests (in PR 1):
- Juror with memberships in (Screening: FILTERING) + (Finals: LIVE_FINAL) → only Screening returned.
- Juror with memberships in (Mixed: EVALUATION + LIVE_FINAL) → returned (group has at least one review round).
- Juror with only (Finals: LIVE_FINAL) → no memberships returned.
Risk: very low. Single procedure, additive Prisma filter, easy to revert.
§F. Workspace messaging + files end-to-end
§F.1 — Server-side path enforcement (PR 2)
Files:
src/lib/minio.ts(add helper)src/server/routers/mentor.ts(workspaceUploadFileprocedure + presign procedure)src/server/services/mentor-workspace.ts(uploadFileservice)
New helper in src/lib/minio.ts:
export function generateMentorObjectKey(projectTitle: string, fileName: string): string {
return generateObjectKey(projectTitle, fileName, 'mentorship')
}
This produces <sanitizedProjectName>/mentorship/<timestamp>-<sanitizedFileName>, matching the existing project-file scheme.
Procedure changes:
- Add a presign procedure (if not present):
mentor.presignWorkspaceUpload({ mentorAssignmentId, fileName, mimeType, size })→- Loads the
MentorAssignment+ linkedProject(server-side). - Authorizes: user is the assigned mentor OR a project team member (mentorProcedure for mentors; protectedProcedure with project-team check for applicants).
- Constructs
objectKey = generateMentorObjectKey(project.title, fileName). - Returns
{ uploadUrl, bucket, objectKey }— the presigned PUT URL is short-lived (1h).
- Loads the
- Change
workspaceUploadFileto accept ONLY{ uploadToken, description? }(whereuploadTokenis an opaque value returned by the presign call). The presign procedure stores{ token → { mentorAssignmentId, fileName, mimeType, size, bucket, objectKey } }in a short-lived cache (in-memory or Redis if configured, 1h TTL). The upload procedure looks up the token, validates that the user is the same one who called presign, then writes theMentorFilerow using the cached values. This eliminates any client-controlled path entirely. - Mirror the same change for applicant-side uploads to mentor workspace (if a separate procedure exists).
Migration: Pre-flight — confirm MentorFile table is empty (or only test data) in production. If it has any rows, migrate objectKeys to the new scheme via a one-shot script; otherwise skip migration.
Tests (in PR 2):
- Presign returns key matching
<projectName>/mentorship/<timestamp>-<file>shape. workspaceUploadFilerejects payloads that includebucketorobjectKey(input schema rejects unknown fields via Zod).- Authorization: mentor uploading to a workspace they're NOT assigned to → throws TRPCError UNAUTHORIZED.
§F.2 — Dashboard message previews (PR 6)
Files:
- New component:
src/components/mentor/recent-messages-card.tsx - New component:
src/components/applicant/mentor-conversation-card.tsx src/app/(mentor)/mentor/page.tsx— embed RecentMessagesCardsrc/app/(applicant)/applicant/page.tsx— embed MentorConversationCard (only render when project has mentorAssignment + workspace enabled)src/server/routers/mentor.ts— new proceduregetRecentMessagesForMentor(returns last N msgs across all assignments)src/server/routers/applicant.ts— new proceduregetMentorConversationPreview({ projectId })(returns last 3 msgs + unread count for one project)
Mentor dashboard preview:
- Card title: "Recent Messages"
- Shows last 5 unread messages across ALL assignments (sender name + project + first 100 chars + relative timestamp).
- Each row links to
/mentor/workspace/<projectId>(jumps to that conversation). - "View all" link →
/mentor/messages(existing or new index — confirm during impl). - Empty state: "No new messages. Your mentees will appear here when they reach out."
Applicant dashboard preview (only when project has assigned mentor + workspace enabled):
- Card title: "Conversation with [Mentor Name]"
- Shows last 3 messages (sender name + content + timestamp).
- Unread count badge.
- "Send a message" inline composer or "Open chat" button →
/applicant/mentor. - Empty state: "Say hi to your mentor — they're here to help you sharpen your project."
Performance: both queries use indexed lookups on MentorMessage(workspaceId, createdAt). Add an index migration if not present.
Tests (in PR 6):
getRecentMessagesForMentorreturns N most-recent unread messages across assignments.getMentorConversationPreviewreturns 3 most-recent messages + correct unread count.- Renders gracefully when no assignment / no messages.
§F.3 — End-to-end verification scenario (covered in §G)
A single integration test walking through the full happy path. See §G.
§G. Tests
New test files:
tests/unit/mentor-config.test.ts(PR 3) — Config form persistence per fieldtests/unit/mentor-key-construction.test.ts(PR 2) —generateMentorObjectKeyshape + sanitizationtests/integration/mentor-assignment.test.ts(PR 4) — manual + auto + bulk + skiptests/integration/mentor-round-engine.test.ts(NEW for PR 3 or PR 5) — pass-through behavior, eligibility variants, advancementtests/integration/mentor-workspace.test.ts(PR 6) — message + file lifecycle, dashboard previews, milestone auto-completetests/unit/jury-preferences-filter.test.ts(PR 1) —getOnboardingContextfilter
End-to-end happy path (tests/integration/mentor-round-e2e.test.ts, ships with PR 6):
- Admin creates a MENTORING round, sets dates + eligibility=requested_only + 14-day deadline.
- Admin activates round.
- Project A has
mentoringRequested=true, project B does not. - Round-engine activation: B auto-PASSED (pass-through), A stays PENDING.
- Admin manually assigns mentor M1 to project A. A flips PENDING → IN_PROGRESS. Mentor + team get assignment notification.
- M1 sends a message in workspace; team replies. Both messages appear in respective dashboard previews.
- M1 uploads a file. ObjectKey matches
<projectA-title>/mentorship/<timestamp>-.... Team comments on the file. - M1 marks all required milestones complete → assignment.completionStatus = "completed".
- Admin closes round. A and B both PASSED; A also COMPLETED.
This single test covers the operational path the user actually cares about for the upcoming round.
Open questions
generateMentorObjectKey— which "project name" field do we pass?Project.titleis the obvious choice (it's whatgenerateObjectKeyfor submission files uses). Confirm during impl that there's no team-name-specific field we should prefer.- Does
JuryGrouphave a directroundsPrisma relation? Spec assumes it; confirm field name during impl. If it'sRound.juryGroupIdonly (no back-relation), use a nestedRoundquery. - Mentor-onboarding email content — copy needs writing. Owned by admin, not blocking impl; can ship with placeholder copy and finalize before going live.
mentor.autoAssignBulk— does it already skip manually-assigned? Spec assumes yes; confirm by reading source during PR 4. If no, change is small (addwhere: { method: { not: 'MANUAL' } }to its query).- Pre-flight check on existing mentor files in prod MinIO before §F.1 — must be empty or migrated, not orphaned. Confirm via
prisma db queryagainst prod read replica before deploying PR 2.
Risks
| Risk | Severity | Mitigation |
|---|---|---|
| Existing mentor files in prod use legacy keys | High if hit | Pre-flight check; migration script ready before deploy |
bulkUpdateRoles accidentally removes a critical role |
Med | Server-side guard: SUPER_ADMIN cannot be self-demoted; audit log all changes |
| Multi-role redirect priority surprises some users | Low | Document the priority order; role switcher exists for override |
| AI fallback ordering doesn't match prior AI suggestions | Low | UX banner clearly states fallback is in use; keep logic simple |
Filter on getOnboardingContext accidentally hides valid memberships |
Low | Tests cover the three cases; ship behind no flag, easy to revert |
Migration plan
- §A: no migration.
- §B: no migration.
- §C: no migration.
- §D: one Prisma migration adding nullable
User.mentorOnboardingSentAt: DateTime?. No backfill (treat all existing users as not-yet-onboarded; on next role edit, email fires once). - §E: no migration.
- §F.1: optional one-shot script to rewrite legacy
MentorFile.objectKeyrows to the new scheme. Only runs if pre-flight check finds rows. The script copies objects to the new key path then updates DB rows in a transaction; old keys remain readable until manual cleanup. - §F.2: optional Prisma index on
MentorMessage(workspaceId, createdAt DESC)if not present.
Rollback
Each PR independently revertable. PRs 1, 2, 4 ship with no migration → straight git revert. PR 6 has a migration → revert PR + one-line down migration to drop the column. PR 3 has no migration; PR 5 has no migration.
Acceptance criteria (per phase)
PR 1 (§E):
- Sophie Laurent (member of Screening, Expert, Finals jury groups) sees Screening + Expert preferences only — not Finals.
PR 2 (§F.1):
- New mentor file uploads write to
<projectName>/mentorship/<timestamp>-<file>in MinIO. - Removing
bucket/objectKeyfrom aworkspaceUploadFilecall still succeeds. - Old
objectKeyupload payloads now fail Zod validation.
PR 3 (§A):
- All
MentoringConfigSchemafields are editable from the Config tab. - A draft MENTORING round with no document-promotion configured can pass Launch Readiness without a "File requirements set" check.
PR 4 (§C):
- Admin can manually assign any MENTOR-role user to any project from
/admin/projects/[id]/mentor. - Round Projects tab "Auto-fill remaining" assigns to all
mentoringRequested=trueprojects without a mentor. - Page renders sensibly with no
OPENAI_API_KEYset (expertise-tag fallback).
PR 5 (§B):
- MENTORING round Overview shows live counts (requested / assigned / unassigned), deadline countdown, mentor pool size, workspace activity totals.
/admin/mentorsshows real list of MENTOR-role users with current assignments.
PR 6 (§D + §F.2):
- Multi-role user (jury+mentor) lands on
/juryafter login (priority order). Role switcher dropdown shows "Mentor View". /admin/membersallows multi-select + "Add MENTOR role to selected" → all selected users get email + role.- Impersonation banner doesn't intercept clicks on the user dropdown.
- Mentor
/mentordashboard shows "Recent Messages" card; applicant/applicantdashboard shows "Conversation with [Mentor]" card.