Files
MOPC-Portal/docs/superpowers/specs/2026-06-09-grand-final-documents-design.md
Matt 537de07245 docs(grand-final): spec for final-document upload, mentor surfacing, judge review, notifications
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>
2026-06-09 14:31:49 +02:00

14 KiB
Raw Blame History

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.
  • 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 14 (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).

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 FileRequirementProjectFile 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"), 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 viewsrc/app/(applicant)/applicant/mentor/page.tsx (adds a panel below the existing mentor cards / chat / workspace-files). Uses applicant.getFinalDocumentStatus.
  • Mentor viewsrc/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 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 inline FilePreview, 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.listReviewDocuments procedure (wraps listFinalistDocumentsForReview). Authorization: SUPER_ADMIN / PROGRAM_ADMIN, OR a JuryGroupMember of the active LIVE_FINAL round's jury group, via a new assertFinalsReviewAccess(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 to NotificationTypes, with a NOTIFICATION_EMAIL_TEMPLATES entry (branded) and a seed-notification-settings.ts row (category: "logistics", per-type sendEmail toggle, 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 with sendEmail default off (in-app on). Triggered from applicant.saveFileMetadata when 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 by CRON_SECRET) calling sendDueFinalDocReminders. Reminder window read from the round configJson.finalDocsReminderHoursBeforeDeadline (default 48h). Fires once per team (stamped via finalDocsReminderSentAt). 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 by finalist.sendDocumentReminderssendManualFinalDocReminders. Placed on the admin Grand Final round overview and/or the /admin/logistics Confirmations tab. Usable immediately to kick off the round.

5. Minor polish

  • Flip the two FileRequirement rows to required = 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 windowCloseAt in 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: add GRAND_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; returns null for a non-confirmed team and when no active LIVE_FINAL round; deadlinePassed reflects windowCloseAt.
  • listFinalistDocumentsForReview: returns all finalist teams with correct per-requirement file mapping and submittedCount.
  • 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; stamps finalDocsReminderSentAt; 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)

  1. Populate the "Finals Jury" group with the actual judges (existing jury-group admin UI) — required before the review page is useful to them.
  2. Extend the Grand Final windowCloseAt (currently 2026-06-11, ~2 days out) to the intended deadline.
  3. Verify/flip the two requirements to required = true.

Build sequence (shippable in phases; deadline is imminent)

  1. Banner + manual admin reminder + minor polish — makes the existing upload discoverable now (most urgent).
  2. Judge review page + access helper + entry points.
  3. Mentor-section Final Documents panel (team + mentor) + mentor.getProjectFinalDocuments.
  4. Auto reminder cron + GRAND_FINAL_DOCS_SUBMITTED on-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.