# Grand-Final Documents — upload visibility, mentor surfacing, judge review, notifications **Date:** 2026-06-09 **Status:** Design — pending review **Edition:** MOPC 2026 (program "Monaco Ocean Protection Challenge", competition `MOPC 2026`) ## Problem Nine finalist teams must submit two final deliverables ahead of the Grand Final — a **final PDF presentation** and a **~1-minute video** — and the upcoming Grand-Final judges need to review those documents. Today there is no discoverable upload prompt, no consolidated judge review surface, and no notifications driving teams to upload. ## Current state (verified against prod, 2026-06-09) The data layer already exists and is correctly set up: - **"Grand Final" (LIVE_FINAL) round is `ROUND_ACTIVE`** with `windowCloseAt = 2026-06-11 21:00 UTC` and the empty **"Finals Jury"** group attached (`juryGroupId` set, **0 members**). - **Two `FileRequirement` rows already exist on the Grand Final round** (legacy per-round system): **"PDF presentation support"** (`application/pdf`) and **"1 minute video"** (`video/*`). Both currently `required = false`, `maxSizeMB = null`, **0 files uploaded**. This set is being expanded to the confirmed 4-document set below. - **All 9 finalist teams are correctly enrolled**: each has a `ProjectRoundState` in both the (closed) Mentoring round and the (active) Grand Final round, and all 9 have `FinalistConfirmation.status = CONFIRMED`. No mismatches (confirmed↔enrolled↔in-mentor all aligned). All 9 share one mentor, **Camille Lopez**. Attendee counts 1–4 (program `defaultAttendeeCap = 4`). - Auto-enroll (confirmed + in mentor round → Grand Final round) is working via `finalist.enrollFinalists`; the **admin override already exists** (`finalist.adminConfirm` to mark attending without a token; `finalist.unenroll` to remove) in the `/admin/logistics` **Confirmations tab** + attendance dialog. ### What this means The **upload already works today**: `/applicant/documents` renders an upload section for every round in `openRounds`, where `openRounds` = program rounds that are `ROUND_ACTIVE` **and** the project is a member of (`applicant.getMyDashboard`). The Grand Final round qualifies, so all 9 teams can upload the PDF + video right now. The upload procedure (`applicant.getUploadUrl`) permits any team member to upload against a requirement as long as the round is `ROUND_ACTIVE` (it is). The `windowCloseAt` deadline is **advisory only** (display, not blocking). The gaps are therefore: **discoverability** (no banner/notification), **judge review** (no consolidated surface; jury can only see files for projects they are individually assigned to, and the Finals Jury group is empty), and **mentor-section surfacing** (the final documents never appear in the mentor area). ## Goals 1. Make the existing finalist upload **discoverable** via a dashboard banner and notifications. 2. Give the upcoming Grand-Final judges a **read-only review page** of all finalists' documents. 3. Surface the final documents (and a pre-deadline cue) inside **the mentor section**, on both the team's and the mentor's views. 4. Add **email + in-app notifications**, triggered **automatically** (pre-deadline reminder cron) and **manually** (admin blast). ## Document set (confirmed) The Grand Final round's `FileRequirement` rows are reconfigured to **four required documents**, identical for both categories (STARTUP and BUSINESS_CONCEPT) — the per-round `FileRequirement` model already applies one set to all teams in the round: 1. **Final Presentation** — `application/pdf` (rename of the existing "PDF presentation support" row) 2. **Final Business Plan** — `application/pdf` (new) 3. **1-minute Video** — `video/*` (existing "1 minute video" row) 4. **Executive Summary** — `application/pdf` (new) All four `required = true`. PDF-only for the three document slots (no Word). This is an additive/safe prod data change (0 files currently uploaded). If per-category document sets are ever needed, that is out of scope here (the per-round model does not support it without extra work). ## Non-goals (YAGNI) - Admin approve / needs-changes review workflow on documents. - Migrating to the heavier `SubmissionWindow` system (the legacy `FileRequirement` anchor is already set up and proven). - A mentor-milestone tracker UI (`MentorMilestone`/`MentorMilestoneCompletion` models exist but have no UI; "last steps of the mentor round" is treated as descriptive framing, surfaced as a read-only Final Documents panel, not a milestone system). - Comments/threads on the judge review page. - New admin enrollment/override controls (`adminConfirm` + `unenroll` already exist; we only ensure they are reachable from the finale round overview). ## Architecture decision Build thin additions on the **existing legacy `FileRequirement` → `ProjectFile` anchor** that is already configured on the Grand Final round. Reuse: - Upload mechanics (`applicant.getUploadUrl` / `saveFileMetadata`, presigned MinIO PUT, `RequirementUploadList`). - File preview/download (`FilePreview` / `file-viewer`, `file.getDownloadUrl`). - The notification pipeline (`createNotification`, `notifyProjectTeam`/`notifyProjectMentors`, `NotificationEmailSetting`, `NOTIFICATION_EMAIL_TEMPLATES`, `sendStyledNotificationEmail`) and the reminder-cron pattern (`sendDueConfirmationReminders`). **The deadline everywhere** (banner, mentor cue, reminder cron) is the Grand Final round's single `windowCloseAt` field, edited by admins in round settings. All deadline displays use **browser-local time + zone label** (`Intl.DateTimeFormat`), never UTC/fixed Monaco time, per the locked grand-finale timezone rule. Deadline behavior is **soft/advisory** — uploads stay open while the round is active; past the date, files are flagged "late" in the UI but still accepted. ## Shared service: `src/server/services/final-documents.ts` A new service centralizes the logic, wrapped by thin tRPC procedures: - `getFinalDocumentStatusForProject(prisma, projectId)` → `{ roundId, roundName, deadline, deadlinePassed, requirements: [{ id, name, acceptedMimeTypes, uploaded, file? }], allRequiredUploaded }` or `null` when the project is not a CONFIRMED finalist in an active LIVE_FINAL round. The single source of truth for the banner, the mentor panels, and reminder targeting. - `listFinalistDocumentsForReview(prisma, programId)` → `{ round: { name, deadline }, totalCount, submittedCount, teams: [{ projectId, teamName, category, confirmStatus, documents: [{ requirementId, requirementName, file? }] }] }`. File metadata only; presigned URLs are fetched per-file on demand by the client via existing `file.getDownloadUrl`. - `sendDueFinalDocReminders(prisma)` → cron entry. Targets CONFIRMED finalists in the active LIVE_FINAL round with at least one required document missing, whose `finalDocsReminderSentAt` is null and whose deadline is within the reminder window; creates `GRAND_FINAL_DOCS_REMINDER` notifications and stamps `finalDocsReminderSentAt`. Best-effort per row. - `sendManualFinalDocReminders(prisma, { programId, projectIds?, actorId })` → admin blast. For the given projects (default: all CONFIRMED finalists with missing required docs), create `GRAND_FINAL_DOCS_REMINDER` notifications regardless of `finalDocsReminderSentAt`. Returns `{ sent }`. ## Components ### 1. Finalist upload banner (applicant dashboard) - New auto-hiding banner component (pattern of `LunchBanner`/`MyLogisticsCard`: returns `null` when not applicable) on `src/app/(applicant)/applicant/page.tsx`. - Backed by a new query **`applicant.getFinalDocumentStatus`** (wraps `getFinalDocumentStatusForProject` for the caller's project). - Shows: heading ("Upload your Grand Final documents"), each required document with a ✓ / empty state (e.g. "2 of 4 uploaded"), deadline in browser-local time + zone, and a CTA button → `/applicant/documents`. Collapses to a "✓ Submitted" confirmation once all required documents are uploaded. Non-dismissible while incomplete. ### 2. Mentor-section "Final Documents" panel (team + mentor) A new read-only `FinalDocumentsPanel` component rendered on **both** mentor surfaces: - **Team view** — `src/app/(applicant)/applicant/mentor/page.tsx` (adds a panel below the existing mentor cards / chat / workspace-files). Uses `applicant.getFinalDocumentStatus`. - **Mentor view** — `src/app/(mentor)/mentor/workspace/[projectId]/page.tsx` (adds a "Final Documents" section or tab for the viewed project). Uses a new **`mentor.getProjectFinalDocuments`** procedure (mentor/team access check) wrapping `getFinalDocumentStatusForProject`. Behavior: before upload + as the deadline nears, a **visual cue** ("Final grand-final documents due [date] — upload now", with an upload CTA on the team view); after upload, the PDF + video appear as the team's read-only "final documents" (inline preview / video player / download), visible to both the team and their mentor. Same underlying `ProjectFile`s — no duplicate storage. ### 3. Judge review page (thin dedicated page, reusing existing components) **Why dedicated rather than baking into the existing per-project jury page** (verified in prod): the finale has **0 `Assignment` rows**, the "Finals Jury" group has **0 members**, and no `LiveVotingSession` exists. The existing jury flow is assignment-gated — the round page lists `roundAssignment.getMyAssignments` (empty for the finale) and `file.listByProject` 403s any juror without an `Assignment` to the project. The finale runs on a **group + live-session** model, not per-project assignments. Baking in would require either fabricating an assignment per judge × finalist or rewiring the assignment-based access path — more work and risk, and a worse UX (one project at a time vs. all finalists at once). So a dedicated page is the better fit *because* the existing page's access model does not apply to the finale. It stays thin by **reusing existing components** — the same `MultiWindowDocViewer` / `FilePreview` / `