diff --git a/docs/superpowers/plans/2026-06-01-mentorship-comms-and-welcome-email.md b/docs/superpowers/plans/2026-06-01-mentorship-comms-and-welcome-email.md
new file mode 100644
index 0000000..80fb04a
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-01-mentorship-comms-and-welcome-email.md
@@ -0,0 +1,1113 @@
+# Mentorship Communications & Welcome/Reminder Email — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Give mentors a one-click "email all team members" button, and turn the mentoring round-open assignment emails into instructional welcome emails (with contact addresses) that an admin can also re-send on demand, with a live preview.
+
+**Architecture:** Two existing email templates in `src/lib/email.ts` are enhanced in place (always-on instructions block; optional team-contact lists and custom note) so they serve as a single source of truth used by both the round-open auto-send and a new admin reminder action. Two new admin tRPC procedures (`previewMentorshipWelcome` query, `sendMentorshipWelcome` mutation) live in `src/server/routers/mentor.ts`. A new admin button reuses the existing `EmailPreviewDialog`. A small client-only mailto button is added to the mentor project page.
+
+**Tech Stack:** Next.js 15 App Router, tRPC 11 (Zod), Prisma 6, React, shadcn/ui, Vitest 4, sonner toasts, Nodemailer.
+
+**Spec:** `docs/superpowers/specs/2026-06-01-mentorship-comms-and-welcome-email-design.md`
+
+**Refinements vs spec (discovered during planning):** preview reuses `EmailPreviewDialog` with a stacked Mentor/Team preview rather than a custom tabbed iframe; the manual blast logs via `logAudit` (matching how these emails already work) rather than `NotificationLog`; new template inputs are optional for backward compatibility.
+
+---
+
+## File Structure
+
+| File | Responsibility | Action |
+|---|---|---|
+| `src/lib/email.ts` | Email templates + senders | Modify: export + enhance two templates and two senders; export `getBaseUrl` |
+| `src/server/services/round-engine.ts` | Round state machine | Modify: pass team-member/teammate data into the upgraded emails on round-open |
+| `src/server/routers/mentor.ts` | Mentor tRPC procedures | Modify: add `previewMentorshipWelcome` + `sendMentorshipWelcome` |
+| `src/components/admin/round/send-mentorship-welcome-button.tsx` | Admin reminder button | Create |
+| `src/app/(admin)/admin/rounds/[roundId]/page.tsx` | Round detail page | Modify: wire button into Notifications section (mentoring only) |
+| `src/app/(mentor)/mentor/projects/[id]/page.tsx` | Mentor project detail | Modify: add "Email all team members" mailto button |
+| `tests/unit/mentor-welcome-email.test.ts` | Template unit tests | Create |
+| `tests/unit/mentorship-welcome-send.test.ts` | tRPC procedure tests | Create |
+
+---
+
+## Task 1: Enhance & export the two mentorship email templates (TDD)
+
+**Files:**
+- Modify: `src/lib/email.ts` (templates at lines 2835–2913 and 2949–3015; helper `getBaseUrl` at lines 225–228)
+- Test: `tests/unit/mentor-welcome-email.test.ts` (create)
+
+- [ ] **Step 1: Write the failing template tests**
+
+Create `tests/unit/mentor-welcome-email.test.ts`:
+
+```typescript
+import { describe, it, expect } from 'vitest'
+import {
+ getMentorBulkAssignmentTemplate,
+ getTeamMentorIntroductionTemplate,
+} from '@/lib/email'
+
+describe('getMentorBulkAssignmentTemplate', () => {
+ it('includes team-member emails, the instructions block, and a custom note', () => {
+ const t = getMentorBulkAssignmentTemplate(
+ 'Alice',
+ [
+ {
+ title: 'Reef Project',
+ url: 'https://x/mentor/workspace/1',
+ teamMembers: [{ name: 'Bob', email: 'bob@team.com' }],
+ },
+ ],
+ 'https://x/mentor',
+ 'Please reach out this week.',
+ )
+ expect(t.html).toContain('bob@team.com')
+ expect(t.html).toContain('How to mentor on MOPC')
+ expect(t.html).toContain('Please reach out this week.')
+ expect(t.text).toContain('bob@team.com')
+ expect(t.text).toContain('How to mentor on MOPC')
+ })
+
+ it('renders without team members or note (backward compatible)', () => {
+ const t = getMentorBulkAssignmentTemplate(
+ 'Alice',
+ [{ title: 'P', url: 'https://x/p' }],
+ 'https://x/mentor',
+ )
+ expect(t.html).toContain('How to mentor on MOPC')
+ // Custom-note box uses the #fff7ed background; absent when no note passed.
+ expect(t.html).not.toContain('#fff7ed')
+ })
+})
+
+describe('getTeamMentorIntroductionTemplate', () => {
+ it('includes mentor + teammate emails, the instructions block, and a custom note', () => {
+ const t = getTeamMentorIntroductionTemplate(
+ 'Bob',
+ 'Reef Project',
+ [{ name: 'Alice', email: 'alice@mentor.com' }],
+ 'https://x/applicant/mentor',
+ [{ name: 'Carol', email: 'carol@team.com' }],
+ 'Welcome aboard!',
+ )
+ expect(t.html).toContain('alice@mentor.com')
+ expect(t.html).toContain('carol@team.com')
+ expect(t.html).toContain('Working with your mentor')
+ expect(t.html).toContain('Welcome aboard!')
+ })
+
+ it('renders without teammates or note (backward compatible)', () => {
+ const t = getTeamMentorIntroductionTemplate(
+ 'Bob',
+ 'Reef Project',
+ [{ name: 'Alice', email: 'alice@mentor.com' }],
+ 'https://x/applicant/mentor',
+ )
+ expect(t.html).toContain('Working with your mentor')
+ expect(t.html).not.toContain('#fff7ed')
+ })
+})
+```
+
+- [ ] **Step 2: Run the tests to verify they fail**
+
+Run: `npx vitest run tests/unit/mentor-welcome-email.test.ts`
+Expected: FAIL — `getMentorBulkAssignmentTemplate`/`getTeamMentorIntroductionTemplate` are not exported (import error), or the new params/sections are missing.
+
+- [ ] **Step 3: Export `getBaseUrl`**
+
+In `src/lib/email.ts`, change the declaration at line 225 from:
+
+```typescript
+function getBaseUrl(): string {
+```
+
+to:
+
+```typescript
+export function getBaseUrl(): string {
+```
+
+- [ ] **Step 4: Replace `getMentorBulkAssignmentTemplate` (lines 2949–3015) with the enhanced, exported version**
+
+```typescript
+export function getMentorBulkAssignmentTemplate(
+ name: string,
+ projects: {
+ title: string
+ url: string
+ teamMembers?: { name: string | null; email: string }[]
+ }[],
+ mentorDashboardUrl: string,
+ customNote?: string,
+): EmailTemplate {
+ const count = projects.length
+ const subject =
+ count === 1
+ ? `You've been assigned to a new MOPC project: "${projects[0].title}"`
+ : `You've been assigned to ${count} new MOPC projects`
+ const greeting = name ? `Hi ${name},` : 'Hi there,'
+
+ const textBlocks = projects.map((p) => {
+ const members =
+ p.teamMembers && p.teamMembers.length > 0
+ ? '\n' +
+ p.teamMembers
+ .map((m) => ` - ${m.name ?? 'Team member'}: ${m.email}`)
+ .join('\n')
+ : ''
+ return ` • ${p.title} — ${p.url}${members}`
+ })
+ const text = [
+ greeting,
+ '',
+ ...(customNote ? [customNote, ''] : []),
+ count === 1
+ ? `You have been assigned as a mentor to a new project:`
+ : `You have been assigned as a mentor to ${count} new projects:`,
+ '',
+ ...textBlocks,
+ '',
+ 'How to mentor on MOPC:',
+ ' - Open each project workspace from your Mentor Dashboard to chat with the team, share files, and track milestones.',
+ ' - Messages you send in the workspace notify the team by email automatically.',
+ ' - You can also email team members directly using the addresses listed above.',
+ '',
+ `Open your mentor dashboard: ${mentorDashboardUrl}`,
+ '',
+ 'The MOPC team',
+ ].join('\n')
+
+ const customNoteHtml = customNote
+ ? `
${escapeHtml(customNote)}
`
+ : ''
+
+ const htmlList = projects
+ .map((p) => {
+ const members =
+ p.teamMembers && p.teamMembers.length > 0
+ ? `${p.teamMembers
+ .map(
+ (m) =>
+ `${escapeHtml(m.name ?? 'Team member')} ${escapeHtml(m.email)} `,
+ )
+ .join('')}
`
+ : ''
+ return `${escapeHtml(p.title)} ${members} `
+ })
+ .join('')
+
+ const instructionsHtml = `
+
+
How to mentor on MOPC
+
+ Open each project workspace from your Mentor Dashboard to chat with the team, share files, and track milestones.
+ Messages you send in the workspace notify the team by email automatically.
+ You can also email team members directly using the addresses listed above.
+
+
`
+
+ const html = `
+
+
+
+
+
+
${count === 1 ? 'New mentor assignment' : `${count} new mentor assignments`}
+
+
+
${name ? `Hi ${escapeHtml(name)},` : 'Hi there,'}
+ ${customNoteHtml}
+
${count === 1 ? 'You have been assigned as a mentor to a new project:' : `You have been assigned as a mentor to ${count} new projects:`}
+
+ ${instructionsHtml}
+
+ Open Mentor Dashboard
+
+
+
+ Monaco Ocean Protection Challenge
+
+
+
+
+ `.trim()
+
+ return { subject, text, html }
+}
+```
+
+- [ ] **Step 5: Replace `getTeamMentorIntroductionTemplate` (lines 2835–2913) with the enhanced, exported version**
+
+```typescript
+export function getTeamMentorIntroductionTemplate(
+ recipientName: string | null,
+ projectTitle: string,
+ mentors: { name: string | null; email: string }[],
+ workspaceUrl: string,
+ teammates?: { name: string | null; email: string }[],
+ customNote?: string,
+): EmailTemplate {
+ const count = mentors.length
+ const subject =
+ count === 1
+ ? `Your mentor for "${projectTitle}" on MOPC`
+ : `Your ${count} mentors for "${projectTitle}" on MOPC`
+ const greeting = recipientName ? `Hi ${recipientName},` : 'Hi there,'
+
+ const mentorTextLines = mentors
+ .map((m) => ` • ${m.name ?? 'Mentor'} — ${m.email}`)
+ .join('\n')
+ const teammateTextLines =
+ teammates && teammates.length > 0
+ ? ['', 'Your team:', ...teammates.map((t) => ` • ${t.name ?? 'Team member'} — ${t.email}`)]
+ : []
+
+ const text = [
+ greeting,
+ '',
+ ...(customNote ? [customNote, ''] : []),
+ count === 1
+ ? `The mentoring round is now open, and your project "${projectTitle}" has a mentor:`
+ : `The mentoring round is now open, and your project "${projectTitle}" has ${count} mentors:`,
+ '',
+ mentorTextLines,
+ ...teammateTextLines,
+ '',
+ 'Working with your mentor:',
+ ' - Go to the Mentoring section of your applicant portal to message your mentor directly — they are notified by email when you write.',
+ ' - Share documents and questions early; your mentor is here to help you sharpen your project before the finals.',
+ ' - You can also email your mentor directly using the address above.',
+ '',
+ `Open your mentoring page: ${workspaceUrl}`,
+ '',
+ 'The MOPC team',
+ ].join('\n')
+
+ const customNoteHtml = customNote
+ ? `${escapeHtml(customNote)}
`
+ : ''
+
+ const mentorHtmlList = mentors
+ .map(
+ (m) => `
+
+ ${escapeHtml(m.name ?? 'Mentor')}
+ ${escapeHtml(m.email)}
+ `,
+ )
+ .join('')
+
+ const teammatesHtml =
+ teammates && teammates.length > 0
+ ? `
+ Your team
+ ${teammates
+ .map(
+ (t) => `
+
+ ${escapeHtml(t.name ?? 'Team member')}
+ ${escapeHtml(t.email)}
+ `,
+ )
+ .join('')}
`
+ : ''
+
+ const instructionsHtml = `
+
+
Working with your mentor
+
+ Go to the Mentoring section of your applicant portal to message your mentor directly — they are notified by email when you write.
+ Share documents and questions early; your mentor is here to help you sharpen your project before the finals.
+ You can also email your mentor directly using the address above.
+
+
`
+
+ const html = `
+
+
+
+
+
+
${count === 1 ? 'Meet your mentor' : `Meet your ${count} mentors`}
+
+
+
${recipientName ? `Hi ${escapeHtml(recipientName)},` : 'Hi there,'}
+ ${customNoteHtml}
+
${count === 1
+ ? `The mentoring round is now open and a mentor has been assigned to your project ${escapeHtml(projectTitle)} :`
+ : `The mentoring round is now open and ${count} mentors have been assigned to your project ${escapeHtml(projectTitle)} :`}
+
+ ${teammatesHtml}
+ ${instructionsHtml}
+
+ Open Mentor Workspace
+
+
+
+ Monaco Ocean Protection Challenge
+
+
+
+
+ `.trim()
+
+ return { subject, text, html }
+}
+```
+
+- [ ] **Step 6: Run the tests to verify they pass**
+
+Run: `npx vitest run tests/unit/mentor-welcome-email.test.ts`
+Expected: PASS (4 tests).
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/lib/email.ts tests/unit/mentor-welcome-email.test.ts
+git commit -m "feat(email): instructions + contact emails + optional note in mentorship templates"
+```
+
+---
+
+## Task 2: Update the two senders to forward new data and return success
+
+**Files:**
+- Modify: `src/lib/email.ts` (`sendTeamMentorIntroductionEmail` lines 2921–2947; `sendMentorBulkAssignmentEmail` lines 3023–3044)
+
+- [ ] **Step 1: Replace `sendMentorBulkAssignmentEmail` (lines 3023–3044)**
+
+```typescript
+export async function sendMentorBulkAssignmentEmail(
+ email: string,
+ name: string | null,
+ projects: { id: string; title: string; teamMembers?: { name: string | null; email: string }[] }[],
+ customNote?: string,
+): Promise {
+ try {
+ if (projects.length === 0) return false
+ const baseUrl = getBaseUrl()
+ const enriched = projects.map((p) => ({
+ title: p.title,
+ url: `${baseUrl}/mentor/workspace/${p.id}`,
+ teamMembers: p.teamMembers,
+ }))
+ const template = getMentorBulkAssignmentTemplate(name || '', enriched, `${baseUrl}/mentor`, customNote)
+ await sendEmail({ to: email, subject: template.subject, text: template.text, html: template.html })
+ return true
+ } catch (error) {
+ console.error('[sendMentorBulkAssignmentEmail] failed', { email, error })
+ return false
+ }
+}
+```
+
+- [ ] **Step 2: Replace `sendTeamMentorIntroductionEmail` (lines 2921–2947)**
+
+```typescript
+export async function sendTeamMentorIntroductionEmail(
+ recipientEmail: string,
+ recipientName: string | null,
+ projectTitle: string,
+ projectId: string,
+ mentors: { name: string | null; email: string }[],
+ teammates?: { name: string | null; email: string }[],
+ customNote?: string,
+): Promise {
+ try {
+ if (mentors.length === 0) return false
+ const baseUrl = getBaseUrl()
+ const workspaceUrl = `${baseUrl}/applicant/mentor`
+ const template = getTeamMentorIntroductionTemplate(
+ recipientName,
+ projectTitle,
+ mentors,
+ workspaceUrl,
+ teammates,
+ customNote,
+ )
+ await sendEmail({
+ to: recipientEmail,
+ subject: template.subject,
+ text: template.text,
+ html: template.html,
+ })
+ return true
+ } catch (error) {
+ console.error('[sendTeamMentorIntroductionEmail] failed', { recipientEmail, projectId, error })
+ return false
+ }
+}
+```
+
+- [ ] **Step 3: Verify nothing broke — typecheck**
+
+Run: `npm run typecheck`
+Expected: PASS. Existing callers (`mentor.ts:132`, `mentor.ts:893`, `mentor.ts:1375`, `round-engine.ts:261`, `round-engine.ts:340`) still compile because all new params are optional and the boolean return is ignored by them.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/lib/email.ts
+git commit -m "feat(email): mentorship senders forward contacts/note, return success"
+```
+
+---
+
+## Task 3: Pass team-member & teammate data into round-open emails
+
+**Files:**
+- Modify: `src/server/services/round-engine.ts` (mentor query lines 225–237 + grouping 238–270; team query 288–312 + loop 313–341)
+- Test: `tests/unit/mentor-email-deferral.test.ts` (existing — must still pass)
+
+- [ ] **Step 1: Add `teamMembers` to the mentor-assignment query (lines 225–237)**
+
+Replace the `select` block's `project` line so the query reads:
+
+```typescript
+ const pendingAssignments = await prisma.mentorAssignment.findMany({
+ where: {
+ droppedAt: null,
+ notificationSentAt: null,
+ project: { projectRoundStates: { some: { roundId } } },
+ },
+ select: {
+ id: true,
+ mentorId: true,
+ mentor: { select: { name: true, email: true } },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ teamMembers: { select: { user: { select: { name: true, email: true } } } },
+ },
+ },
+ },
+ })
+```
+
+- [ ] **Step 2: Carry team members into the per-mentor bucket (lines 238–270)**
+
+Update the `Map` generic and the push so the grouping block reads:
+
+```typescript
+ const perMentor = new Map<
+ string,
+ {
+ email: string | null
+ name: string | null
+ assignmentIds: string[]
+ projects: { id: string; title: string; teamMembers: { name: string | null; email: string }[] }[]
+ }
+ >()
+ for (const a of pendingAssignments) {
+ if (!a.mentor?.email) continue
+ const bucket = perMentor.get(a.mentorId) ?? {
+ email: a.mentor.email,
+ name: a.mentor.name,
+ assignmentIds: [],
+ projects: [],
+ }
+ bucket.assignmentIds.push(a.id)
+ bucket.projects.push({
+ id: a.project.id,
+ title: a.project.title,
+ teamMembers: a.project.teamMembers
+ .filter((tm) => tm.user?.email)
+ .map((tm) => ({ name: tm.user.name, email: tm.user.email })),
+ })
+ perMentor.set(a.mentorId, bucket)
+ }
+```
+
+The existing `sendMentorBulkAssignmentEmail(bucket.email, bucket.name, bucket.projects)` call (line ~261) needs no change — `bucket.projects` now carries `teamMembers`.
+
+- [ ] **Step 3: Pass teammates into the team-introduction call (lines 313–341)**
+
+Inside the `for (const p of projectsToIntroduce)` loop, after `mentors` is built and before the recipient loop, add an `allMembers` list, and pass per-recipient teammates into the send call:
+
+```typescript
+ const allMembers = p.teamMembers
+ .filter((tm) => tm.user?.email)
+ .map((tm) => ({ name: tm.user.name, email: tm.user.email }))
+
+ for (const [email, { name }] of recipients) {
+ const teammates = allMembers.filter((m) => m.email !== email)
+ await sendTeamMentorIntroductionEmail(email, name, p.title, p.id, mentors, teammates)
+ }
+```
+
+(Replace the existing `for (const [email, { name }] of recipients) { await sendTeamMentorIntroductionEmail(email, name, p.title, p.id, mentors) }` block.)
+
+- [ ] **Step 4: Run the existing round-open email test + typecheck**
+
+Run: `npx vitest run tests/unit/mentor-email-deferral.test.ts`
+Expected: PASS (behavior unchanged — emails still deferred on draft, sent on active).
+
+Run: `npm run typecheck`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/server/services/round-engine.ts
+git commit -m "feat(mentor): round-open emails now carry team-member contacts"
+```
+
+---
+
+## Task 4: Add `previewMentorshipWelcome` + `sendMentorshipWelcome` procedures (TDD)
+
+**Files:**
+- Modify: `src/server/routers/mentor.ts` (imports near lines 1–9; append procedures before the router close at line 3290)
+- Test: `tests/unit/mentorship-welcome-send.test.ts` (create)
+
+- [ ] **Step 1: Write the failing tRPC test**
+
+Create `tests/unit/mentorship-welcome-send.test.ts`:
+
+```typescript
+import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
+
+vi.mock('@/lib/email', async () => {
+ const actual = await vi.importActual('@/lib/email')
+ return {
+ ...actual,
+ sendMentorBulkAssignmentEmail: vi.fn(async () => true),
+ sendTeamMentorIntroductionEmail: vi.fn(async () => true),
+ }
+})
+
+import { prisma, createCaller } from '../setup'
+import {
+ createTestProgram,
+ createTestCompetition,
+ createTestRound,
+ createTestProject,
+ createTestProjectRoundState,
+ createTestUser,
+ cleanupTestData,
+} from '../helpers'
+import { mentorRouter } from '../../src/server/routers/mentor'
+import {
+ sendMentorBulkAssignmentEmail,
+ sendTeamMentorIntroductionEmail,
+} from '@/lib/email'
+
+describe('mentor.sendMentorshipWelcome / previewMentorshipWelcome', () => {
+ let programId: string
+ const userIds: string[] = []
+ let roundId: string
+ let memberEmail: string
+ let mentorEmail: string
+
+ beforeAll(async () => {
+ const program = await createTestProgram()
+ programId = program.id
+ const competition = await createTestCompetition(program.id)
+ const round = await createTestRound(competition.id, {
+ roundType: 'MENTORING',
+ status: 'ROUND_ACTIVE',
+ })
+ roundId = round.id
+
+ const project = await createTestProject(program.id)
+ await createTestProjectRoundState(project.id, round.id)
+
+ const mentor = await createTestUser('MENTOR')
+ const member = await createTestUser('APPLICANT')
+ mentorEmail = mentor.email
+ memberEmail = member.email
+ userIds.push(mentor.id, member.id)
+
+ await prisma.teamMember.create({
+ data: { projectId: project.id, userId: member.id, role: 'LEAD' },
+ })
+ await prisma.mentorAssignment.create({
+ data: { projectId: project.id, mentorId: mentor.id, method: 'MANUAL' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanupTestData(programId, userIds)
+ })
+
+ it('sends to mentors and team members and reports counts', async () => {
+ const admin = await createTestUser('SUPER_ADMIN')
+ userIds.push(admin.id)
+ const caller = createCaller(mentorRouter, {
+ id: admin.id,
+ email: admin.email,
+ role: 'SUPER_ADMIN',
+ })
+
+ const res = await caller.sendMentorshipWelcome({ roundId, customNote: 'Reminder!' })
+
+ expect(res.sent).toBeGreaterThan(0)
+ expect(sendMentorBulkAssignmentEmail).toHaveBeenCalled()
+ expect(sendTeamMentorIntroductionEmail).toHaveBeenCalled()
+ })
+
+ it('does NOT stamp the one-time flags (re-sendable reminder)', async () => {
+ const assignment = await prisma.mentorAssignment.findFirst({
+ where: { project: { projectRoundStates: { some: { roundId } } } },
+ })
+ expect(assignment?.notificationSentAt).toBeNull()
+ expect(assignment?.teamIntroducedAt).toBeNull()
+ })
+
+ it('preview returns non-empty mentor + team HTML with real contacts', async () => {
+ const admin = await createTestUser('SUPER_ADMIN')
+ userIds.push(admin.id)
+ const caller = createCaller(mentorRouter, {
+ id: admin.id,
+ email: admin.email,
+ role: 'SUPER_ADMIN',
+ })
+
+ const pv = await caller.previewMentorshipWelcome({ roundId })
+
+ expect(pv.recipientCount).toBeGreaterThan(0)
+ expect(pv.html).toContain('Mentor version')
+ expect(pv.html).toContain('Team version')
+ expect(pv.html).toContain(memberEmail)
+ expect(pv.html).toContain(mentorEmail)
+ })
+})
+```
+
+- [ ] **Step 2: Run the test to verify it fails**
+
+Run: `npx vitest run tests/unit/mentorship-welcome-send.test.ts`
+Expected: FAIL — `caller.sendMentorshipWelcome is not a function` / `caller.previewMentorshipWelcome is not a function`.
+
+- [ ] **Step 3: Add the email imports to `mentor.ts`**
+
+In the existing import from `@/lib/email` near the top of `src/server/routers/mentor.ts`, add the three new names (keep `sendMentorBulkAssignmentEmail` / `sendTeamMentorIntroductionEmail`, which are already imported):
+
+```typescript
+import {
+ sendMentorBulkAssignmentEmail,
+ sendTeamMentorIntroductionEmail,
+ getMentorBulkAssignmentTemplate,
+ getTeamMentorIntroductionTemplate,
+ getBaseUrl,
+} from '@/lib/email'
+```
+
+(If the existing import lists other names, preserve them and just append these. `logAudit` is already imported in this file — it is used by the `assign` procedure.)
+
+- [ ] **Step 4: Append the two procedures before the router's closing `})` (line 3290)**
+
+```typescript
+ previewMentorshipWelcome: adminProcedure
+ .input(z.object({ roundId: z.string(), customNote: z.string().max(2000).optional() }))
+ .query(async ({ ctx, input }) => {
+ const { roundId, customNote } = input
+ const baseUrl = getBaseUrl()
+
+ const assignments = await ctx.prisma.mentorAssignment.findMany({
+ where: { droppedAt: null, project: { projectRoundStates: { some: { roundId } } } },
+ select: {
+ mentorId: true,
+ mentor: { select: { name: true, email: true } },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ teamMembers: { select: { user: { select: { name: true, email: true } } } },
+ },
+ },
+ },
+ })
+
+ const mentorIds = new Set()
+ const teamEmails = new Set()
+ for (const a of assignments) {
+ if (a.mentor?.email) mentorIds.add(a.mentorId)
+ for (const tm of a.project.teamMembers) {
+ if (tm.user?.email) teamEmails.add(tm.user.email)
+ }
+ }
+ const recipientCount = mentorIds.size + teamEmails.size
+
+ const firstMentor = assignments.find((a) => a.mentor?.email)
+ const mentorTemplate = firstMentor
+ ? getMentorBulkAssignmentTemplate(
+ firstMentor.mentor!.name || '',
+ assignments
+ .filter((a) => a.mentorId === firstMentor.mentorId)
+ .map((a) => ({
+ title: a.project.title,
+ url: `${baseUrl}/mentor/workspace/${a.project.id}`,
+ teamMembers: a.project.teamMembers
+ .filter((tm) => tm.user?.email)
+ .map((tm) => ({ name: tm.user.name, email: tm.user.email })),
+ })),
+ `${baseUrl}/mentor`,
+ customNote,
+ )
+ : getMentorBulkAssignmentTemplate(
+ 'Sample Mentor',
+ [
+ {
+ title: 'Sample Project',
+ url: `${baseUrl}/mentor`,
+ teamMembers: [{ name: 'Sample Applicant', email: 'applicant@example.com' }],
+ },
+ ],
+ `${baseUrl}/mentor`,
+ customNote,
+ )
+
+ const firstProject = assignments.find((a) => a.mentor?.email)
+ let teamTemplate
+ if (firstProject) {
+ const projMentors = assignments
+ .filter((a) => a.project.id === firstProject.project.id && a.mentor?.email)
+ .map((a) => ({ name: a.mentor!.name, email: a.mentor!.email }))
+ const teammates = firstProject.project.teamMembers
+ .filter((tm) => tm.user?.email)
+ .map((tm) => ({ name: tm.user.name, email: tm.user.email }))
+ teamTemplate = getTeamMentorIntroductionTemplate(
+ teammates[0]?.name ?? null,
+ firstProject.project.title,
+ projMentors,
+ `${baseUrl}/applicant/mentor`,
+ teammates.slice(1),
+ customNote,
+ )
+ } else {
+ teamTemplate = getTeamMentorIntroductionTemplate(
+ 'Sample Applicant',
+ 'Sample Project',
+ [{ name: 'Sample Mentor', email: 'mentor@example.com' }],
+ `${baseUrl}/applicant/mentor`,
+ [{ name: 'Sample Teammate', email: 'teammate@example.com' }],
+ customNote,
+ )
+ }
+
+ const isSample = !firstMentor
+ const banner = (label: string) =>
+ `${label}
`
+ const sampleNote = isSample
+ ? `No assignments in this round yet — showing sample data.
`
+ : ''
+
+ const html = `${sampleNote}${banner('Mentor version')}${mentorTemplate.html}${banner('Team version')}${teamTemplate.html}`
+
+ return { html, recipientCount }
+ }),
+
+ sendMentorshipWelcome: adminProcedure
+ .input(z.object({ roundId: z.string(), customNote: z.string().max(2000).optional() }))
+ .mutation(async ({ ctx, input }) => {
+ const { roundId, customNote } = input
+
+ const assignments = await ctx.prisma.mentorAssignment.findMany({
+ where: { droppedAt: null, project: { projectRoundStates: { some: { roundId } } } },
+ select: {
+ id: true,
+ mentorId: true,
+ mentor: { select: { name: true, email: true } },
+ project: {
+ select: {
+ id: true,
+ title: true,
+ teamMembers: { select: { user: { select: { name: true, email: true } } } },
+ submittedByEmail: true,
+ submittedBy: { select: { name: true } },
+ },
+ },
+ },
+ })
+
+ let sent = 0
+ let failed = 0
+
+ // Mentor emails (coalesced per mentor).
+ const perMentor = new Map<
+ string,
+ {
+ email: string
+ name: string | null
+ projects: { id: string; title: string; teamMembers: { name: string | null; email: string }[] }[]
+ }
+ >()
+ for (const a of assignments) {
+ if (!a.mentor?.email) continue
+ const bucket = perMentor.get(a.mentorId) ?? {
+ email: a.mentor.email,
+ name: a.mentor.name,
+ projects: [],
+ }
+ bucket.projects.push({
+ id: a.project.id,
+ title: a.project.title,
+ teamMembers: a.project.teamMembers
+ .filter((tm) => tm.user?.email)
+ .map((tm) => ({ name: tm.user.name, email: tm.user.email })),
+ })
+ perMentor.set(a.mentorId, bucket)
+ }
+ for (const bucket of perMentor.values()) {
+ const ok = await sendMentorBulkAssignmentEmail(bucket.email, bucket.name, bucket.projects, customNote)
+ if (ok) sent++
+ else failed++
+ }
+
+ // Team emails (per project, to all members + original submitter).
+ const byProject = new Map()
+ for (const a of assignments) {
+ const arr = byProject.get(a.project.id) ?? []
+ arr.push(a)
+ byProject.set(a.project.id, arr)
+ }
+ for (const projAssignments of byProject.values()) {
+ const p = projAssignments[0].project
+ const mentors = projAssignments
+ .filter((a) => a.mentor?.email)
+ .map((a) => ({ name: a.mentor!.name, email: a.mentor!.email }))
+ if (mentors.length === 0) continue
+ const allMembers = p.teamMembers
+ .filter((tm) => tm.user?.email)
+ .map((tm) => ({ name: tm.user.name, email: tm.user.email }))
+ const recipients = new Map()
+ for (const m of allMembers) recipients.set(m.email, { name: m.name })
+ if (p.submittedByEmail && !recipients.has(p.submittedByEmail)) {
+ recipients.set(p.submittedByEmail, { name: p.submittedBy?.name ?? null })
+ }
+ for (const [email, { name }] of recipients) {
+ const teammates = allMembers.filter((m) => m.email !== email)
+ const ok = await sendTeamMentorIntroductionEmail(email, name, p.title, p.id, mentors, teammates, customNote)
+ if (ok) sent++
+ else failed++
+ }
+ }
+
+ try {
+ await logAudit({
+ prisma: ctx.prisma,
+ userId: ctx.user.id,
+ action: 'MENTORSHIP_WELCOME_SENT',
+ entityType: 'Round',
+ entityId: roundId,
+ detailsJson: { sent, failed, hasCustomNote: !!customNote },
+ ipAddress: ctx.ip,
+ userAgent: ctx.userAgent,
+ })
+ } catch (err) {
+ console.error('[sendMentorshipWelcome] audit failed', err)
+ }
+
+ return { sent, failed }
+ }),
+```
+
+- [ ] **Step 5: Run the test to verify it passes**
+
+Run: `npx vitest run tests/unit/mentorship-welcome-send.test.ts`
+Expected: PASS (3 tests).
+
+- [ ] **Step 6: Typecheck**
+
+Run: `npm run typecheck`
+Expected: PASS. (If `logAudit` is not already imported in `mentor.ts`, add `import { logAudit } from '@/server/utils/audit'` — but it is used by the existing `assign` procedure, so it should already be present.)
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/server/routers/mentor.ts tests/unit/mentorship-welcome-send.test.ts
+git commit -m "feat(mentor): admin preview + send mentorship welcome/reminder email"
+```
+
+---
+
+## Task 5: Admin "Send welcome/reminder" button (wired into the round page)
+
+**Files:**
+- Create: `src/components/admin/round/send-mentorship-welcome-button.tsx`
+- Modify: `src/app/(admin)/admin/rounds/[roundId]/page.tsx` (imports near line 127; Notifications grid lines 1441–1445)
+
+- [ ] **Step 1: Create the button component**
+
+`src/components/admin/round/send-mentorship-welcome-button.tsx`:
+
+```tsx
+'use client'
+
+import { useState } from 'react'
+import { trpc } from '@/lib/trpc/client'
+import { toast } from 'sonner'
+import { Mail } from 'lucide-react'
+import { EmailPreviewDialog } from './email-preview-dialog'
+
+interface SendMentorshipWelcomeButtonProps {
+ roundId: string
+}
+
+export function SendMentorshipWelcomeButton({ roundId }: SendMentorshipWelcomeButtonProps) {
+ const [open, setOpen] = useState(false)
+ const [customMessage, setCustomMessage] = useState()
+
+ const preview = trpc.mentor.previewMentorshipWelcome.useQuery(
+ { roundId, customNote: customMessage },
+ { enabled: open },
+ )
+
+ const sendMutation = trpc.mentor.sendMentorshipWelcome.useMutation({
+ onSuccess: (data) => {
+ toast.success(
+ `Sent ${data.sent} email${data.sent !== 1 ? 's' : ''}${data.failed ? ` (${data.failed} failed)` : ''}`,
+ )
+ setOpen(false)
+ },
+ onError: (err) => toast.error(err.message),
+ })
+
+ return (
+ <>
+ setOpen(true)}
+ className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-sky-500 hover:-translate-y-0.5 hover:shadow-md transition-all text-left w-full"
+ >
+
+
+
Send Welcome / Reminder
+
+ Email all mentors & teams how to use mentorship
+
+
+
+
+ sendMutation.mutate({ roundId, customNote: msg })}
+ isSending={sendMutation.isPending}
+ onRefreshPreview={(msg) => setCustomMessage(msg)}
+ />
+ >
+ )
+}
+```
+
+- [ ] **Step 2: Import the button in the round page**
+
+In `src/app/(admin)/admin/rounds/[roundId]/page.tsx`, after line 127 (the `BulkInviteButton` import), add:
+
+```typescript
+import { SendMentorshipWelcomeButton } from '@/components/admin/round/send-mentorship-welcome-button'
+```
+
+- [ ] **Step 3: Render it (mentoring rounds only) in the Notifications grid**
+
+Replace the Notifications grid (lines 1441–1445) so it reads:
+
+```tsx
+
+
+
+
+ {isMentoring && }
+
+```
+
+- [ ] **Step 4: Typecheck**
+
+Run: `npm run typecheck`
+Expected: PASS.
+
+- [ ] **Step 5: Manual verification (no component-test harness exists in this repo)**
+
+Run: `npm run dev`, log in as an admin, open a MENTORING round's detail page. Confirm: the "Send Welcome / Reminder" card appears in Notifications (and is absent on non-mentoring rounds). Click it → the dialog opens, shows a recipient count, renders the stacked **Mentor version / Team version** preview, updates when you type a note and refresh, and the toast reports a count on send. (To eyeball the raw HTML earlier/offline, call the `previewMentorshipWelcome` query via the tRPC panel or temporarily write `pv.html` to a file in the test.)
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/components/admin/round/send-mentorship-welcome-button.tsx "src/app/(admin)/admin/rounds/[roundId]/page.tsx"
+git commit -m "feat(admin): send mentorship welcome/reminder button on mentoring rounds"
+```
+
+---
+
+## Task 6: Mentor "Email all team members" button
+
+**Files:**
+- Modify: `src/app/(mentor)/mentor/projects/[id]/page.tsx` (Team Members card; `CardContent` begins around line 350, members at 332–427; `Button` and `Mail` already imported)
+
+- [ ] **Step 1: Add the button at the top of the Team Members `CardContent`**
+
+Immediately after the `` opening tag of the Team Members card, insert:
+
+```tsx
+ {(() => {
+ const emails = (project.teamMembers ?? [])
+ .map((m) => m.user.email)
+ .filter((e): e is string => !!e)
+ if (emails.length === 0) return null
+ const mailto = `mailto:${emails.join(',')}?subject=${encodeURIComponent(
+ `MOPC Mentorship — ${project.title}`,
+ )}`
+ return (
+
+ )
+ })()}
+```
+
+- [ ] **Step 2: Typecheck**
+
+Run: `npm run typecheck`
+Expected: PASS. (`Button` is imported at line 14; `Mail` at line 46; `project.title` and `project.teamMembers[].user.email` are part of `getProjectDetail`.)
+
+- [ ] **Step 3: Manual verification**
+
+In `npm run dev`, open a mentor's project detail page. Confirm the "Email all team members" button shows in the Team Members card, and clicking it opens the OS mail client with every team member's address in the To: field and the subject pre-filled. Confirm it is hidden when a project has no team members.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add "src/app/(mentor)/mentor/projects/[id]/page.tsx"
+git commit -m "feat(mentor): email all team members button on project detail"
+```
+
+---
+
+## Task 7: Full verification
+
+- [ ] **Step 1: Run the full test suite**
+
+Run: `npx vitest run`
+Expected: PASS (all existing tests + the two new files). Pay attention that `tests/unit/mentor-email-deferral.test.ts` still passes.
+
+- [ ] **Step 2: Typecheck + lint + build**
+
+Run: `npm run typecheck && npm run lint && npm run build`
+Expected: all PASS (CLAUDE.md requires a green build before push).
+
+- [ ] **Step 3: Final commit (if lint/format changed anything)**
+
+```bash
+git add -A
+git commit -m "chore(mentor): lint/format for mentorship comms feature" || echo "nothing to commit"
+```
+
+---
+
+## Self-Review Notes
+
+- **Spec coverage:** Email-all button → Task 6. Upgraded auto-intro emails (instructions + contacts) → Tasks 1–3. Re-sendable manual reminder with optional note → Task 4. Tailored content with embedded contact emails → Task 1. Preview → Task 4 (query) + Task 5 (dialog). Both-audience default, no toggle → Task 4. Email in `To:` → Task 6. Teammates + mentor in team email → Tasks 1 & 4.
+- **Type consistency:** `getMentorBulkAssignmentTemplate(name, projects[{title,url,teamMembers?}], dashUrl, customNote?)`, `getTeamMentorIntroductionTemplate(recipientName, projectTitle, mentors, workspaceUrl, teammates?, customNote?)`, senders return `Promise`, procedures return `{ html, recipientCount }` and `{ sent, failed }` — used consistently by the dialog (`recipientCount`, `data.sent`, `data.failed`).
+- **No placeholders:** every code step contains full code; instruction copy is final text.