Design for surfacing the already-live Grand Final document upload (PDF + 1-min video) to the 9 confirmed finalists via a dashboard banner, a read-only judge review page (Finals Jury group + admins), a Final Documents panel on both the team and mentor views, and email + in-app reminders (auto cron + manual blast). Reuses the existing legacy FileRequirement anchor, upload mechanics, and notification pipeline. Grounded in verified prod state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
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_ACTIVEwithwindowCloseAt = 2026-06-11 21:00 UTCand the empty "Finals Jury" group attached (juryGroupIdset, 0 members). - Two
FileRequirementrows already exist on the Grand Final round (legacy per-round system): "PDF presentation support" (application/pdf) and "1 minute video" (video/*). Both currentlyrequired = false,maxSizeMB = null, 0 files uploaded. - All 9 finalist teams are correctly enrolled: each has a
ProjectRoundStatein both the (closed) Mentoring round and the (active) Grand Final round, and all 9 haveFinalistConfirmation.status = CONFIRMED. No mismatches (confirmed↔enrolled↔in-mentor all aligned). All 9 share one mentor, Camille Lopez. Attendee counts 1–4 (programdefaultAttendeeCap = 4). - Auto-enroll (confirmed + in mentor round → Grand Final round) is working via
finalist.enrollFinalists; the admin override already exists (finalist.adminConfirmto mark attending without a token;finalist.unenrollto remove) in the/admin/logisticsConfirmations 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
- Make the existing finalist upload discoverable via a dashboard banner and notifications.
- Give the upcoming Grand-Final judges a read-only review page of all finalists' documents.
- Surface the final documents (and a pre-deadline cue) inside the mentor section, on both the team's and the mentor's views.
- Add email + in-app notifications, triggered automatically (pre-deadline reminder cron) and manually (admin blast).
Non-goals (YAGNI)
- Admin approve / needs-changes review workflow on documents.
- Migrating to the heavier
SubmissionWindowsystem (the legacyFileRequirementanchor is already set up and proven). - A mentor-milestone tracker UI (
MentorMilestone/MentorMilestoneCompletionmodels 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+unenrollalready 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 }ornullwhen 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 existingfile.getDownloadUrl.sendDueFinalDocReminders(prisma)→ cron entry. Targets CONFIRMED finalists in the active LIVE_FINAL round with at least one required document missing, whosefinalDocsReminderSentAtis null and whose deadline is within the reminder window; createsGRAND_FINAL_DOCS_REMINDERnotifications and stampsfinalDocsReminderSentAt. Best-effort per row.sendManualFinalDocReminders(prisma, { programId, projectIds?, actorId })→ admin blast. For the given projects (default: all CONFIRMED finalists with missing required docs), createGRAND_FINAL_DOCS_REMINDERnotifications regardless offinalDocsReminderSentAt. Returns{ sent }.
Components
1. Finalist upload banner (applicant dashboard)
- New auto-hiding banner component (pattern of
LunchBanner/MyLogisticsCard: returnsnullwhen not applicable) onsrc/app/(applicant)/applicant/page.tsx. - Backed by a new query
applicant.getFinalDocumentStatus(wrapsgetFinalDocumentStatusForProjectfor the caller's project). - Shows: heading ("Upload your Grand Final documents"), the two deliverables each with a ✓ / empty state, 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). Usesapplicant.getFinalDocumentStatus. - Mentor view —
src/app/(mentor)/mentor/workspace/[projectId]/page.tsx(adds a "Final Documents" section or tab for the viewed project). Uses a newmentor.getProjectFinalDocumentsprocedure (mentor/team access check) wrappinggetFinalDocumentStatusForProject.
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 ProjectFiles — no duplicate storage.
3. Judge review page
- New read-only page (e.g.
src/app/(jury)/jury/finals-documents/page.tsx) listing all finalist teams grouped by category, each with its two documents: PDF via inlineFilePreview, video via inline<video>player, plus download. Missing documents show "Not yet uploaded". Header shows the deadline and "X of N submitted". - Backed by a new
finalist.listReviewDocumentsprocedure (wrapslistFinalistDocumentsForReview). Authorization: SUPER_ADMIN / PROGRAM_ADMIN, OR aJuryGroupMemberof the active LIVE_FINAL round's jury group, via a newassertFinalsReviewAccess(ctx)helper. Unauthorized → access-denied state. - Entry points: a jury sidebar link ("Finalist Documents") shown when an active LIVE_FINAL round exists, and a "Review finalist documents" link on the admin Grand Final round overview.
- Implementation note: verify the
(jury)route-group layout does not hard-redirect admins; if it does, either relax it for this page or mount the page on a neutral path reachable by both. Authorization is enforced by the procedure regardless.
4. Notifications (email + in-app; auto + manual)
- New notification type
GRAND_FINAL_DOCS_REMINDER(team-facing): added toNotificationTypes, with aNOTIFICATION_EMAIL_TEMPLATESentry (branded) and aseed-notification-settings.tsrow (category: "logistics", per-typesendEmailtoggle, subject/body overridable). In-app + email. - New notification type
GRAND_FINAL_DOCS_SUBMITTED(mentor-facing, light): when a team uploads a final document, notify the team's mentor(s) in-app so it surfaces in their mentor section. Seed row withsendEmaildefault off (in-app on). Triggered fromapplicant.saveFileMetadatawhen the file is for the LIVE_FINAL round (best-effort, never throws). - Automatic (cron): new route
src/app/api/cron/final-document-reminders/route.ts(protected byCRON_SECRET) callingsendDueFinalDocReminders. Reminder window read from the roundconfigJson.finalDocsReminderHoursBeforeDeadline(default 48h). Fires once per team (stamped viafinalDocsReminderSentAt). This doubles as the initial "documents are open" nudge. - Manual (admin): a "Remind teams to upload final documents" action with a live
EmailPreviewDialog(mirrors the existing finalist reminder-blast), backed byfinalist.sendDocumentReminders→sendManualFinalDocReminders. Placed on the admin Grand Final round overview and/or the/admin/logisticsConfirmations tab. Usable immediately to kick off the round.
5. Minor polish
- Flip the two
FileRequirementrows torequired = true(guarded prod data update at ship time, or via the admin round file-requirement editor if present). - Confirm admins can edit the round's
windowCloseAtin round settings (the admin-set deadline). If no input exists, add a small one; likely already present.
Data model changes
FinalistConfirmation.finalDocsReminderSentAt DateTime?(new nullable column) — lets the auto reminder fire once per team. Migration required (additive, safe).NotificationTypes: addGRAND_FINAL_DOCS_REMINDER,GRAND_FINAL_DOCS_SUBMITTED.seed-notification-settings.ts: add rows for both new types (auto-provisions on deploy).- Optional round config field
finalDocsReminderHoursBeforeDeadline(default 48) validated in the LIVE_FINAL round Zod config.
tRPC procedures (new)
| Procedure | Router | Auth | Purpose |
|---|---|---|---|
applicant.getFinalDocumentStatus |
applicant | protected (team member) | Banner + team mentor panel |
mentor.getProjectFinalDocuments |
mentor | mentor/team access to project | Mentor workspace panel |
finalist.listReviewDocuments |
finalist | admin OR finale jury-group member | Judge review page |
finalist.sendDocumentReminders |
finalist | admin | Manual reminder blast |
(Reminder email preview reuses the existing notification.previewEmailTemplate.)
Testing
Vitest (sequential, factories per tests/helpers.ts):
getFinalDocumentStatusForProject: both uploaded / one uploaded / none; returnsnullfor a non-confirmed team and when no active LIVE_FINAL round;deadlinePassedreflectswindowCloseAt.listFinalistDocumentsForReview: returns all finalist teams with correct per-requirement file mapping andsubmittedCount.- Authorization matrix for
finalist.listReviewDocuments: admin ✓, finale jury-group member ✓, non-finale jury member ✗, applicant ✗. sendDueFinalDocReminders: targets only CONFIRMED finalists with missing required docs inside the window; stampsfinalDocsReminderSentAt; idempotent (no double-send).finalist.sendDocumentReminders: admin only; counts correctly.
Live-UI smoke on dev (lesson learned — catches what tests/build miss): banner renders for a finalist; team mentor panel + mentor-workspace panel render; judge page renders for an admin and for a finale jury-group member and denies a non-finale juror; upload still works; a manual reminder produces an in-app + email notification.
Prerequisites / admin actions (outside code)
- Populate the "Finals Jury" group with the actual judges (existing jury-group admin UI) — required before the review page is useful to them.
- Extend the Grand Final
windowCloseAt(currently 2026-06-11, ~2 days out) to the intended deadline. - Verify/flip the two requirements to
required = true.
Build sequence (shippable in phases; deadline is imminent)
- Banner + manual admin reminder + minor polish — makes the existing upload discoverable now (most urgent).
- Judge review page + access helper + entry points.
- Mentor-section Final Documents panel (team + mentor) +
mentor.getProjectFinalDocuments. - Auto reminder cron +
GRAND_FINAL_DOCS_SUBMITTEDon-upload mentor notification + new notification types/templates/seed + migration.
Deployment
Per the prod-deploy runbook: after everything is reviewed and tested (build clean, npx vitest run green), commit to main, push to code.monaco-opc.com/MOPC/MOPC-Portal, track the Gitea CI build until it publishes mopc/mopc-portal:latest, then redeploy on prod (ssh stefan@89.58.5.223:22022, /opt/letsbe/stacks/mopc-portal): docker compose pull && docker compose up -d (NEVER -v). Confirm the additive migration applied and the new notification-settings rows seeded, then live-smoke the banner, judge page, and a manual reminder.