142 lines
14 KiB
Markdown
142 lines
14 KiB
Markdown
|
|
# 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 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).
|
|||
|
|
|
|||
|
|
## 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"), 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). 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
|
|||
|
|
|
|||
|
|
- 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.sendDocumentReminders` → `sendManualFinalDocReminders`. 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.
|