Compare commits
71 Commits
5cae78fe0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5b7cdf670 | ||
|
|
90f36ac9b2 | ||
|
|
a921731c52 | ||
| fc8e58f985 | |||
|
|
e547d2bd03 | ||
|
|
f731f96a0a | ||
|
|
09049d2911 | ||
|
|
3fb0d128a1 | ||
|
|
5965f7889d | ||
|
|
b2279067e2 | ||
|
|
014bb15890 | ||
|
|
f12c29103c | ||
|
|
65a22e6f19 | ||
|
|
989db4dc14 | ||
|
|
5e0c8b2dfe | ||
|
|
85a0fa5016 | ||
|
|
c707899179 | ||
|
|
4d40afec6e | ||
|
|
effc078918 | ||
|
|
763b2ef0f5 | ||
|
|
86fa542371 | ||
|
|
079468d2ca | ||
| de73a6f080 | |||
| 80c9e35971 | |||
| 93f4ad4b31 | |||
| 8e5fc18da6 | |||
| 845554fdb8 | |||
| 7f334ed095 | |||
| f572336781 | |||
| 2fb26d4734 | |||
| 4c0efb232c | |||
| fbb194067d | |||
| ac970fc6a0 | |||
| 0b4e56b287 | |||
| 1233cdd22c | |||
| 6ca39c976b | |||
| 9ab4717f96 | |||
|
|
382570cebd | ||
|
|
c634982835 | ||
|
|
c321d4711e | ||
|
|
2d91ce02fc | ||
|
|
3975b5c51f | ||
|
|
b5425e705e | ||
|
|
e56e143a40 | ||
|
|
9ee767b6cd | ||
|
|
0afd4d97c6 | ||
|
|
2a374195c4 | ||
| c88f540633 | |||
| ae0ac58547 | |||
| 59f90ccc37 | |||
| 70cfad7d46 | |||
| 451b483880 | |||
| 7d1c87e938 | |||
| 31225b099e | |||
| dc075f8969 | |||
| 68d3dde1aa | |||
| 331b67dae0 | |||
|
|
8a328357e3 | ||
| 52cdca1b85 | |||
| 7b85fd9602 | |||
| 2a5fa463b3 | |||
| b5d90d3c26 | |||
| bd9cd310fc | |||
| ce4069bf92 | |||
| 98f4a957cc | |||
| 09091d7c08 | |||
| 74515768f5 | |||
| 7e3d600eed | |||
| 0631dbb64f | |||
| d787a24921 | |||
| 5c8d22ac11 |
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Deployment/runtime scripts must stay LF for Linux containers/shells.
|
||||||
|
*.sh text eol=lf
|
||||||
|
Dockerfile text eol=lf
|
||||||
|
**/Dockerfile text eol=lf
|
||||||
|
|
||||||
|
# Keep YAML and env-ish config files LF across platforms.
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.env text eol=lf
|
||||||
|
*.sql text eol=lf
|
||||||
@@ -6,8 +6,8 @@ on:
|
|||||||
- main
|
- main
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: code.letsbe.solutions
|
REGISTRY: code.monaco-opc.com
|
||||||
IMAGE_NAME: letsbe/mopc-app
|
IMAGE_NAME: mopc/mopc-portal
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
559
CLAUDE.md
559
CLAUDE.md
@@ -1,388 +1,171 @@
|
|||||||
# MOPC Platform - Claude Code Context
|
# CLAUDE.md
|
||||||
|
|
||||||
## Project Overview
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
**MOPC (Monaco Ocean Protection Challenge)** is a secure jury online voting platform for managing project selection rounds. The platform enables jury members to evaluate submitted ocean conservation projects, with Phase 1 supporting two selection rounds:
|
## Project Overview
|
||||||
|
|
||||||
- **Round 1**: ~130 projects → ~60 semi-finalists
|
**MOPC (Monaco Ocean Protection Challenge)** — a secure jury voting platform for managing multi-round project selection. Jury members evaluate ocean conservation projects through configurable competitions with rounds (intake, filtering, evaluation, submission, mentoring, live finals, deliberation). Domain: `monaco-opc.com`.
|
||||||
- **Round 2**: ~60 projects → 6 finalists
|
|
||||||
|
## Common Commands
|
||||||
**Domain**: `monaco-opc.com`
|
|
||||||
|
```bash
|
||||||
The platform is designed for future expansion into a comprehensive program management system including learning hub, communication workflows, and partner modules.
|
# Development
|
||||||
|
npm run dev # Next.js dev (Turbopack)
|
||||||
## Key Decisions
|
npm run build # Production build (always test before push)
|
||||||
|
npm run typecheck # tsc --noEmit
|
||||||
| Decision | Choice |
|
|
||||||
|----------|--------|
|
# Database
|
||||||
| Evaluation Criteria | Fully configurable per round (admin defines) |
|
npx prisma generate # Regenerate client after schema changes
|
||||||
| CSV Import | Flexible column mapping (admin maps columns) |
|
npx prisma migrate dev # Create + apply migration
|
||||||
| Max File Size | 500MB (for videos) |
|
npx prisma studio # GUI database browser
|
||||||
| Observer Role | Included in Phase 1 |
|
npm run db:seed # Run seed (tsx prisma/seed.ts)
|
||||||
| First Admin | Database seed script |
|
|
||||||
| Past Evaluations | Visible read-only after submit |
|
# Testing (vitest, not jest)
|
||||||
| Grace Period | Admin-configurable per juror/project |
|
npx vitest # Run all tests (watch mode)
|
||||||
| Smart Assignment | AI-powered (GPT) + Smart Algorithm fallback + geo-diversity, familiarity, COI scoring |
|
npx vitest run # Run all tests once
|
||||||
| AI Data Privacy | All data anonymized before sending to GPT |
|
npx vitest run tests/unit/round-engine.test.ts # Single file
|
||||||
| Evaluation Criteria Types | `numeric`, `text`, `boolean`, `section_header` (backward-compatible) |
|
npx vitest run -t 'test name' # Single test by name
|
||||||
| COI Workflow | Mandatory declaration before evaluation, admin review |
|
|
||||||
| Evaluation Reminders | Cron-based email reminders with countdown urgency |
|
# Code Quality
|
||||||
|
npm run lint # ESLint
|
||||||
## Brand Identity
|
npm run format # Prettier
|
||||||
|
|
||||||
| Name | Hex | Usage |
|
# Docker (production)
|
||||||
|------|-----|-------|
|
docker compose -f docker/docker-compose.yml up -d
|
||||||
| Primary Red | `#de0f1e` | CTAs, alerts |
|
docker compose -f docker/docker-compose.dev.yml up -d # Dev stack
|
||||||
| Dark Blue | `#053d57` | Headers, sidebar |
|
```
|
||||||
| White | `#fefefe` | Backgrounds |
|
|
||||||
| Teal | `#557f8c` | Links, secondary |
|
## Tech Stack
|
||||||
|
|
||||||
**Typography**: Montserrat (600/700 for headings, 300/400 for body)
|
Next.js 15 (App Router) | TypeScript 5 (strict) | Tailwind CSS 4 | shadcn/ui | tRPC 11 (superjson) | Prisma 6 | PostgreSQL 16 | NextAuth 5 (Auth.js) | Vitest 4 | OpenAI SDK 6 | MinIO (S3) | Nodemailer + Poste.io
|
||||||
|
|
||||||
## Tech Stack
|
## Architecture
|
||||||
|
|
||||||
| Layer | Technology | Version |
|
### Data Flow: Prisma → tRPC → React
|
||||||
|-------|-----------|---------|
|
|
||||||
| **Framework** | Next.js (App Router) | 15.x |
|
End-to-end type safety: `prisma/schema.prisma` defines models → `src/server/routers/*.ts` expose tRPC procedures with Zod validation → `src/lib/trpc/client.ts` provides typed React hooks → components call `trpc.domain.procedure.useQuery()`.
|
||||||
| **Language** | TypeScript | 5.x |
|
|
||||||
| **UI Components** | shadcn/ui | latest |
|
### tRPC Middleware Hierarchy (`src/server/trpc.ts`)
|
||||||
| **Styling** | Tailwind CSS | 3.x |
|
|
||||||
| **API Layer** | tRPC | 11.x |
|
All role-based access is enforced via procedure types:
|
||||||
| **Database** | PostgreSQL | 16.x |
|
|
||||||
| **ORM** | Prisma | 6.x |
|
| Procedure | Roles Allowed |
|
||||||
| **Authentication** | NextAuth.js (Auth.js) | 5.x |
|
|-----------|---------------|
|
||||||
| **AI** | OpenAI GPT | 4.x SDK |
|
| `publicProcedure` | Anyone (no auth) |
|
||||||
| **Animation** | Motion (Framer Motion) | 11.x |
|
| `protectedProcedure` | Any authenticated user |
|
||||||
| **Notifications** | Sonner | 1.x |
|
| `adminProcedure` | SUPER_ADMIN, PROGRAM_ADMIN |
|
||||||
| **Command Palette** | cmdk | 1.x |
|
| `superAdminProcedure` | SUPER_ADMIN only |
|
||||||
| **File Storage** | MinIO (S3-compatible) | External |
|
| `juryProcedure` | JURY_MEMBER only |
|
||||||
| **Email** | Nodemailer + Poste.io | External |
|
| `mentorProcedure` | SUPER_ADMIN, PROGRAM_ADMIN, MENTOR |
|
||||||
| **Containerization** | Docker Compose | 2.x |
|
| `observerProcedure` | SUPER_ADMIN, PROGRAM_ADMIN, OBSERVER |
|
||||||
| **Reverse Proxy** | Nginx | External |
|
| `awardMasterProcedure` | SUPER_ADMIN, PROGRAM_ADMIN, AWARD_MASTER |
|
||||||
|
| `audienceProcedure` | Any authenticated user |
|
||||||
## Architecture Principles
|
|
||||||
|
### Route Groups (Next.js App Router)
|
||||||
1. **Type Safety First**: End-to-end TypeScript from database to UI via Prisma → tRPC → React
|
|
||||||
2. **Mobile-First Responsive**: All components designed for mobile, enhanced for desktop
|
- `src/app/(auth)/` — Public auth pages (login, verify, accept-invite, onboarding)
|
||||||
3. **Full Control**: No black-box services; every component is understood and maintainable
|
- `src/app/(admin)/` — Admin dashboard, competition management, round config
|
||||||
4. **Extensible Data Model**: JSON fields for future attributes without schema migrations
|
- `src/app/(jury)/` — Jury evaluation interface, round assignments, live voting
|
||||||
5. **Security by Default**: RBAC, audit logging, secure file access with pre-signed URLs
|
- `src/app/(applicant)/` — Applicant dashboard, competition progress, document uploads
|
||||||
|
- `src/app/(mentor)/` — Mentor workspace
|
||||||
## File Structure
|
|
||||||
|
### Competition System
|
||||||
```
|
|
||||||
mopc-platform/
|
The core domain model. A **Competition** represents a complete evaluation cycle:
|
||||||
├── CLAUDE.md # This file - project context
|
|
||||||
├── docs/
|
```
|
||||||
│ └── architecture/ # Architecture documentation
|
Competition → has many Rounds (ordered)
|
||||||
│ ├── README.md # System overview
|
```
|
||||||
│ ├── database.md # Database design
|
|
||||||
│ ├── api.md # API design
|
- **Competition** (`src/server/routers/competition.ts`): Top-level competition config with program link, status, and settings
|
||||||
│ ├── infrastructure.md # Deployment docs
|
- **Round** (`RoundType: INTAKE | FILTERING | EVALUATION | SUBMISSION | MENTORING | LIVE_FINAL | DELIBERATION`): Each competition has ordered rounds with type-specific config in `configJson`
|
||||||
│ └── ui.md # UI/UX patterns
|
|
||||||
├── src/
|
Key models:
|
||||||
│ ├── app/ # Next.js App Router pages
|
- `JuryGroup` — Named jury panel with chair/member/observer roles
|
||||||
│ │ ├── (auth)/ # Public auth routes (login, verify)
|
- `AdvancementRule` — Auto-advance, score threshold, top-N, admin selection between rounds
|
||||||
│ │ ├── (admin)/ # Admin dashboard (protected)
|
- `SubmissionWindow` — File submission deadlines per round
|
||||||
│ │ ├── (jury)/ # Jury interface (protected)
|
- `AssignmentPolicy` / `AssignmentIntent` — Governance layer for jury assignment
|
||||||
│ │ ├── api/ # API routes
|
- `ProjectRoundState` — Per-project state within each round
|
||||||
│ │ │ ├── trpc/ # tRPC endpoint
|
- `DeliberationSession` / `DeliberationVote` / `ResultLock` — Structured deliberation and result finalization
|
||||||
│ │ │ └── cron/
|
|
||||||
│ │ │ └── reminders/ # Cron endpoint for evaluation reminders (F4)
|
Key services:
|
||||||
│ │ ├── layout.tsx # Root layout
|
- `src/server/services/round-engine.ts` — State machine for round transitions
|
||||||
│ │ └── page.tsx # Home/landing
|
- `src/server/services/round-assignment.ts` — Jury assignment generation with policy enforcement
|
||||||
│ ├── components/
|
- `src/server/services/submission-manager.ts` — File submission + filtering with duplicate detection
|
||||||
│ │ ├── ui/ # shadcn/ui components
|
- `src/server/services/deliberation.ts` — Deliberation session management and vote tallying
|
||||||
│ │ ├── admin/ # Admin-specific components
|
- `src/server/services/result-lock.ts` — Result finalization and unlock governance
|
||||||
│ │ │ └── evaluation-summary-card.tsx # AI summary display
|
- `src/server/services/live-control.ts` — Live ceremony cursor management
|
||||||
│ │ ├── forms/ # Form components
|
- `src/server/services/competition-context.ts` — Cross-cutting competition context resolver
|
||||||
│ │ │ ├── evaluation-form.tsx # With progress indicator (F1)
|
|
||||||
│ │ │ ├── coi-declaration-dialog.tsx # COI blocking dialog (F5)
|
Competition types: `src/types/competition.ts`, `src/types/competition-configs.ts`
|
||||||
│ │ │ └── evaluation-form-with-coi.tsx # COI-gated wrapper (F5)
|
|
||||||
│ │ ├── layouts/ # Layout components (sidebar, nav)
|
### AI Services (`src/server/services/ai-*.ts`)
|
||||||
│ │ └── shared/ # Shared components
|
|
||||||
│ │ └── countdown-timer.tsx # Live countdown with urgency (F4)
|
All AI calls anonymize data before sending to OpenAI. Services:
|
||||||
│ ├── lib/
|
- `ai-filtering.ts` — AI-powered project screening with rubric
|
||||||
│ │ ├── auth.ts # NextAuth configuration
|
- `ai-assignment.ts` — GPT-suggested jury-project matching
|
||||||
│ │ ├── prisma.ts # Prisma client singleton
|
- `ai-evaluation-summary.ts` — Strengths/weaknesses synthesis from evaluations
|
||||||
│ │ ├── trpc/ # tRPC client & server setup
|
- `ai-tagging.ts` — Auto-tagging projects
|
||||||
│ │ ├── minio.ts # MinIO client
|
- `ai-award-eligibility.ts` — Award eligibility assessment
|
||||||
│ │ └── email.ts # Email utilities
|
- `ai-shortlist.ts` — AI-powered shortlist recommendations
|
||||||
│ ├── server/
|
- `anonymization.ts` — Strips PII before AI calls
|
||||||
│ │ ├── routers/ # tRPC routers by domain
|
|
||||||
│ │ │ ├── program.ts
|
### Auth System
|
||||||
│ │ │ ├── round.ts
|
|
||||||
│ │ │ ├── project.ts
|
NextAuth v5 with two providers: **Email** (magic links) and **Credentials** (password + invite token). Failed login tracking with 5-attempt lockout (15 min). Session includes `user.role` for RBAC.
|
||||||
│ │ │ ├── user.ts
|
|
||||||
│ │ │ ├── assignment.ts
|
### Docker Entrypoint (`docker/docker-entrypoint.sh`)
|
||||||
│ │ │ ├── evaluation.ts
|
|
||||||
│ │ │ ├── audit.ts
|
On container start: retry `prisma migrate deploy` → `prisma generate` → auto-seed if User table is empty → `node server.js`. A `docker compose down -v && docker compose up -d` will run all migrations from scratch and seed.
|
||||||
│ │ │ ├── settings.ts
|
|
||||||
│ │ │ ├── gracePeriod.ts
|
## Testing Infrastructure
|
||||||
│ │ │ ├── export.ts # CSV export incl. filtering results (F2)
|
|
||||||
│ │ │ ├── analytics.ts # Reports/analytics (observer access, F3)
|
- **Framework**: Vitest 4 with `fileParallelism: false` and `pool: 'forks'` (tests run sequentially)
|
||||||
│ │ │ └── mentor.ts # Mentor messaging endpoints (F10)
|
- **Setup**: `tests/setup.ts` provides `prisma` client (uses `DATABASE_URL_TEST` or `DATABASE_URL`), `createTestContext(user)`, `createCaller(router, user)`
|
||||||
│ │ ├── services/ # Business logic services
|
- **Factories**: `tests/helpers.ts` has `createTestUser()`, `createTestProgram()`, `createTestCompetition()`, `createTestRound()`, `uid()` helper, and `cleanupTestData()`
|
||||||
│ │ │ ├── smart-assignment.ts # With geo/familiarity/COI scoring (F8)
|
- **Pattern**: Create data with factories → build caller with `createCaller(routerModule, user)` → call procedures → assert → cleanup in `afterAll`
|
||||||
│ │ │ ├── evaluation-reminders.ts # Email reminder service (F4)
|
|
||||||
│ │ │ └── ai-evaluation-summary.ts # GPT summary generation (F7)
|
## User Roles
|
||||||
│ │ └── middleware/ # RBAC & auth middleware
|
|
||||||
│ ├── hooks/ # React hooks
|
`SUPER_ADMIN` | `PROGRAM_ADMIN` | `JURY_MEMBER` | `OBSERVER` | `MENTOR` | `APPLICANT` | `AWARD_MASTER` | `AUDIENCE`
|
||||||
│ ├── types/ # Shared TypeScript types
|
|
||||||
│ └── utils/ # Utility functions
|
## Coding Standards
|
||||||
├── prisma/
|
|
||||||
│ ├── schema.prisma # Database schema
|
- **TypeScript**: Strict mode, `type` over `interface`, prefer `unknown` over `any`
|
||||||
│ ├── migrations/ # Migration files
|
- **Files**: kebab-case. **Components**: PascalCase. **DB models**: PascalCase in Prisma
|
||||||
│ └── seed.ts # Seed data
|
- **React**: Server Components by default, `'use client'` only when needed
|
||||||
├── public/ # Static assets
|
- **Styling**: Tailwind utility classes, mobile-first (`md:`, `lg:` breakpoints), shadcn/ui as base
|
||||||
├── docker/
|
- **tRPC**: Group by domain (`trpc.competition.create()`), Zod input validation, `TRPCError` for errors
|
||||||
│ ├── Dockerfile # Production build
|
- **Brand colors**: Primary Red `#de0f1e`, Dark Blue `#053d57`, White `#fefefe`, Teal `#557f8c`
|
||||||
│ ├── docker-compose.yml # Production stack
|
- **Typography**: Montserrat (600/700 headings, 300/400 body)
|
||||||
│ └── docker-compose.dev.yml # Development stack
|
|
||||||
├── tests/
|
## Key Constraints
|
||||||
│ ├── unit/ # Unit tests
|
|
||||||
│ └── e2e/ # End-to-end tests
|
1. Jury can only see assigned projects (enforced at query level)
|
||||||
└── config files... # package.json, tsconfig, etc.
|
2. Voting windows are strict — submissions blocked outside active window
|
||||||
```
|
3. All admin actions are audited via `DecisionAuditLog`
|
||||||
|
4. Files accessed via MinIO pre-signed URLs only (no public bucket)
|
||||||
## Coding Standards
|
5. COI declaration required before evaluation (blocking dialog)
|
||||||
|
6. Smart assignment skips COI-declared jurors, applies geo-diversity penalty and familiarity bonus
|
||||||
### TypeScript
|
7. Cron endpoints protected by `CRON_SECRET` header
|
||||||
- Strict mode enabled
|
8. Round notifications never throw — all errors caught and logged
|
||||||
- Explicit return types for functions
|
|
||||||
- Use `type` over `interface` for consistency (unless extending)
|
## Security
|
||||||
- Prefer `unknown` over `any`
|
|
||||||
|
- CSRF: tRPC uses `application/json` (triggers CORS preflight). Do NOT add permissive CORS headers.
|
||||||
### React/Next.js
|
- Rate limiting: 100 req/min tRPC, 10 req/min auth, 5-attempt lockout
|
||||||
- Use Server Components by default
|
- AI privacy: All data anonymized before OpenAI calls
|
||||||
- `'use client'` only when needed (interactivity, hooks)
|
|
||||||
- Collocate components with their routes when specific to that route
|
## Seed Data
|
||||||
- Use React Query (via tRPC) for server state
|
|
||||||
|
`prisma/seed.ts` imports from `docs/Candidatures 2026 *.csv` with special handling for non-breaking spaces (U+00A0) around French guillemets. Includes ALL CSV rows (no filtering/dedup) — duplicate detection happens in `submission-manager.ts` at runtime.
|
||||||
### Naming Conventions
|
|
||||||
- **Files**: kebab-case (`user-profile.tsx`)
|
## Environment Variables
|
||||||
- **Components**: PascalCase (`UserProfile`)
|
|
||||||
- **Functions/Variables**: camelCase (`getUserById`)
|
Required: `DATABASE_URL`, `NEXTAUTH_URL`, `NEXTAUTH_SECRET`, `MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_BUCKET`, `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `EMAIL_FROM`, `OPENAI_API_KEY`, `CRON_SECRET`
|
||||||
- **Constants**: SCREAMING_SNAKE_CASE (`MAX_FILE_SIZE`)
|
|
||||||
- **Database Tables**: PascalCase in Prisma (`User`, `Project`)
|
External services (pre-existing on VPS): MinIO `:9000`, Poste.io `:587`, Nginx reverse proxy with SSL.
|
||||||
- **Database Columns**: camelCase in Prisma (`createdAt`)
|
|
||||||
|
## Git
|
||||||
### Styling
|
|
||||||
- Tailwind CSS utility classes
|
Remote: `code.monaco-opc.com/MOPC/MOPC-Portal`. Branch: `main`. Always `npm run build` before pushing.
|
||||||
- Mobile-first: base styles for mobile, `md:` for tablet, `lg:` for desktop
|
|
||||||
- Use shadcn/ui components as base, customize via CSS variables
|
|
||||||
- No inline styles; no separate CSS files unless absolutely necessary
|
|
||||||
|
|
||||||
### API Design (tRPC)
|
|
||||||
- Group by domain: `trpc.program.create()`, `trpc.round.list()`
|
|
||||||
- Use Zod for input validation
|
|
||||||
- Return consistent response shapes
|
|
||||||
- Throw `TRPCError` with appropriate codes
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
npm run dev # Start Next.js dev server
|
|
||||||
npm run db:studio # Open Prisma Studio
|
|
||||||
npm run db:push # Push schema changes (dev only)
|
|
||||||
npm run db:migrate # Run migrations
|
|
||||||
npm run db:seed # Seed database
|
|
||||||
|
|
||||||
# Testing
|
|
||||||
npm run test # Run unit tests
|
|
||||||
npm run test:e2e # Run E2E tests
|
|
||||||
npm run test:coverage # Test with coverage
|
|
||||||
|
|
||||||
# Build & Deploy
|
|
||||||
npm run build # Production build
|
|
||||||
npm run start # Start production server
|
|
||||||
docker compose up -d # Start Docker stack
|
|
||||||
docker compose logs -f app # View app logs
|
|
||||||
|
|
||||||
# Code Quality
|
|
||||||
npm run lint # ESLint
|
|
||||||
npm run format # Prettier
|
|
||||||
npm run typecheck # TypeScript check
|
|
||||||
```
|
|
||||||
|
|
||||||
## Windows Development Notes
|
|
||||||
|
|
||||||
**IMPORTANT**: On Windows, all Docker commands AND all npm/node commands must be run using PowerShell (`powershell -ExecutionPolicy Bypass -Command "..."`), not bash/cmd. This is required for proper Docker Desktop integration and Node.js execution.
|
|
||||||
|
|
||||||
**IMPORTANT**: When invoking PowerShell from bash, always use `-ExecutionPolicy Bypass` to skip the user profile script which is blocked by execution policy:
|
|
||||||
```bash
|
|
||||||
powershell -ExecutionPolicy Bypass -Command "..."
|
|
||||||
```
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
```bash
|
|
||||||
# npm commands
|
|
||||||
powershell -ExecutionPolicy Bypass -Command "npm install"
|
|
||||||
powershell -ExecutionPolicy Bypass -Command "npm run build"
|
|
||||||
powershell -ExecutionPolicy Bypass -Command "npx prisma generate"
|
|
||||||
|
|
||||||
# Docker commands
|
|
||||||
powershell -ExecutionPolicy Bypass -Command "docker compose -f docker/docker-compose.dev.yml up -d"
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Docker commands on Windows (use PowerShell)
|
|
||||||
docker compose -f docker/docker-compose.dev.yml up -d
|
|
||||||
docker compose -f docker/docker-compose.dev.yml build --no-cache app
|
|
||||||
docker compose -f docker/docker-compose.dev.yml logs -f app
|
|
||||||
docker compose -f docker/docker-compose.dev.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Database
|
|
||||||
DATABASE_URL="postgresql://user:pass@localhost:5432/mopc"
|
|
||||||
|
|
||||||
# NextAuth
|
|
||||||
NEXTAUTH_URL="https://monaco-opc.com"
|
|
||||||
NEXTAUTH_SECRET="your-secret-key"
|
|
||||||
|
|
||||||
# MinIO (existing separate stack)
|
|
||||||
MINIO_ENDPOINT="http://localhost:9000"
|
|
||||||
MINIO_ACCESS_KEY="your-access-key"
|
|
||||||
MINIO_SECRET_KEY="your-secret-key"
|
|
||||||
MINIO_BUCKET="mopc-files"
|
|
||||||
|
|
||||||
# Email (Poste.io - existing)
|
|
||||||
SMTP_HOST="localhost"
|
|
||||||
SMTP_PORT="587"
|
|
||||||
SMTP_USER="noreply@monaco-opc.com"
|
|
||||||
SMTP_PASS="your-smtp-password"
|
|
||||||
EMAIL_FROM="MOPC Platform <noreply@monaco-opc.com>"
|
|
||||||
|
|
||||||
# OpenAI (for smart assignment and AI evaluation summaries)
|
|
||||||
OPENAI_API_KEY="your-openai-api-key"
|
|
||||||
|
|
||||||
# Cron (for scheduled evaluation reminders)
|
|
||||||
CRON_SECRET="your-cron-secret-key"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Architectural Decisions
|
|
||||||
|
|
||||||
### 1. Next.js App Router over Pages Router
|
|
||||||
**Rationale**: Server Components reduce client bundle, better data fetching patterns, layouts system
|
|
||||||
|
|
||||||
### 2. tRPC over REST
|
|
||||||
**Rationale**: End-to-end type safety without code generation, excellent DX with autocomplete
|
|
||||||
|
|
||||||
### 3. Prisma over raw SQL
|
|
||||||
**Rationale**: Type-safe queries, migration system, works seamlessly with TypeScript
|
|
||||||
|
|
||||||
### 4. NextAuth.js over custom auth
|
|
||||||
**Rationale**: Battle-tested, supports magic links, session management built-in
|
|
||||||
|
|
||||||
### 5. MinIO (external) over local file storage
|
|
||||||
**Rationale**: S3-compatible, pre-signed URLs for security, scalable, already deployed
|
|
||||||
|
|
||||||
### 6. JSON fields for extensibility
|
|
||||||
**Rationale**: `metadata_json`, `settings_json` allow adding attributes without migrations
|
|
||||||
|
|
||||||
### 7. Soft deletes with status fields
|
|
||||||
**Rationale**: Audit trail preservation, recovery capability, referential integrity
|
|
||||||
|
|
||||||
## User Roles (RBAC)
|
|
||||||
|
|
||||||
| Role | Permissions |
|
|
||||||
|------|------------|
|
|
||||||
| **SUPER_ADMIN** | Full system access, all programs, user management |
|
|
||||||
| **PROGRAM_ADMIN** | Manage specific programs, rounds, projects, jury |
|
|
||||||
| **JURY_MEMBER** | View assigned projects only, submit evaluations, declare COI |
|
|
||||||
| **OBSERVER** | Read-only access to dashboards, all analytics/reports |
|
|
||||||
| **MENTOR** | View assigned projects, message applicants via `mentorProcedure` |
|
|
||||||
| **APPLICANT** | View own project status, upload documents per round, message mentor |
|
|
||||||
|
|
||||||
## Important Constraints
|
|
||||||
|
|
||||||
1. **Jury can only see assigned projects** - enforced at query level
|
|
||||||
2. **Voting windows are strict** - submissions blocked outside active window
|
|
||||||
3. **Evaluations are versioned** - edits create new versions
|
|
||||||
4. **All admin actions are audited** - immutable audit log
|
|
||||||
5. **Files accessed via pre-signed URLs** - no public bucket access
|
|
||||||
6. **Mobile responsiveness is mandatory** - every view must work on phones
|
|
||||||
7. **File downloads require project authorization** - jury/mentor must be assigned to the project
|
|
||||||
8. **Mentor endpoints require MENTOR role** - uses `mentorProcedure` middleware
|
|
||||||
9. **COI declaration required before evaluation** - blocking dialog gates evaluation form; admin reviews COI declarations
|
|
||||||
10. **Evaluation form supports multiple criterion types** - `numeric`, `text`, `boolean`, `section_header`; defaults to `numeric` for backward compatibility
|
|
||||||
11. **Smart assignment respects COI** - jurors with declared conflicts are skipped entirely; geo-diversity penalty and prior-round familiarity bonus applied
|
|
||||||
12. **Cron endpoints protected by CRON_SECRET** - `/api/cron/reminders` validates secret header
|
|
||||||
13. **Project status changes tracked** - every status update creates a `ProjectStatusHistory` record
|
|
||||||
14. **Per-round document management** - `ProjectFile` supports `roundId` scoping and `isLate` deadline tracking
|
|
||||||
|
|
||||||
## Security Notes
|
|
||||||
|
|
||||||
### CSRF Protection
|
|
||||||
tRPC mutations are protected against CSRF attacks because:
|
|
||||||
- tRPC uses `application/json` content type, which triggers CORS preflight on cross-origin requests
|
|
||||||
- Browsers block cross-origin JSON POSTs by default (Same-Origin Policy)
|
|
||||||
- NextAuth's own routes (`/api/auth/*`) have built-in CSRF token protection
|
|
||||||
- No custom CORS headers are configured to allow external origins
|
|
||||||
|
|
||||||
**Do NOT add permissive CORS headers** (e.g., `Access-Control-Allow-Origin: *`) without also implementing explicit CSRF token validation on all mutation endpoints.
|
|
||||||
|
|
||||||
### Rate Limiting
|
|
||||||
- tRPC API: 100 requests/minute per IP
|
|
||||||
- Auth endpoints: 10 POST requests/minute per IP
|
|
||||||
- Account lockout: 5 failed password attempts triggers 15-minute lockout
|
|
||||||
|
|
||||||
## External Services (Pre-existing)
|
|
||||||
|
|
||||||
These services are already running on the VPS in separate Docker Compose stacks:
|
|
||||||
|
|
||||||
- **MinIO**: `http://localhost:9000` - S3-compatible storage
|
|
||||||
- **Poste.io**: `localhost:587` - SMTP server for emails
|
|
||||||
- **Nginx**: Host-level reverse proxy with SSL (certbot)
|
|
||||||
|
|
||||||
The MOPC platform connects to these via environment variables.
|
|
||||||
|
|
||||||
## Phase 1 Scope
|
|
||||||
|
|
||||||
### In Scope
|
|
||||||
- Round management (create, configure, activate/close)
|
|
||||||
- Project import (CSV) and file uploads
|
|
||||||
- Jury invitation (magic link)
|
|
||||||
- Manual project assignment (single + bulk)
|
|
||||||
- Evaluation form (configurable criteria)
|
|
||||||
- Autosave + final submit
|
|
||||||
- Voting window enforcement
|
|
||||||
- Progress dashboards
|
|
||||||
- CSV export
|
|
||||||
- Audit logging
|
|
||||||
- **F1: Evaluation progress indicator** - sticky status bar with percentage tracking across criteria, global score, decision, feedback
|
|
||||||
- **F2: Export filtering results as CSV** - dynamic AI column flattening from `aiScreeningJson`
|
|
||||||
- **F3: Observer access to reports/analytics** - all 8 analytics procedures use `observerProcedure`; observer reports page with round selector, tabs, charts
|
|
||||||
- **F4: Countdown timer + email reminders** - live countdown with urgency colors; `EvaluationRemindersService` with cron endpoint (`/api/cron/reminders`)
|
|
||||||
- **F5: Conflict of Interest declaration** - `ConflictOfInterest` model; blocking dialog before evaluation; admin COI review page
|
|
||||||
- **F6: Bulk status update UI** - checkbox selection, floating toolbar, `ProjectStatusHistory` tracking
|
|
||||||
- **F7: AI-powered evaluation summary** - `EvaluationSummary` model; GPT-generated strengths/weaknesses, themes, scoring stats
|
|
||||||
- **F8: Smart assignment improvements** - `geoDiversityPenalty`, `previousRoundFamiliarity`, `coiPenalty` scoring factors
|
|
||||||
- **F9: Evaluation form flexibility** - extended criterion types (`numeric`, `text`, `boolean`, `section_header`); conditional visibility, section grouping
|
|
||||||
- **F10: Applicant portal enhancements** - `ProjectStatusHistory` timeline; per-round document management (`roundId` + `isLate` on `ProjectFile`); `MentorMessage` model for mentor-applicant chat
|
|
||||||
|
|
||||||
### Out of Scope (Phase 2+)
|
|
||||||
- Typeform/Notion integrations
|
|
||||||
- WhatsApp notifications
|
|
||||||
- Learning hub
|
|
||||||
- Partner modules
|
|
||||||
- Public website
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
- **Unit Tests**: Business logic, utilities, validators
|
|
||||||
- **Integration Tests**: tRPC routers with test database
|
|
||||||
- **E2E Tests**: Critical user flows (Playwright)
|
|
||||||
- **Manual Testing**: Responsive design on real devices
|
|
||||||
|
|
||||||
## Documentation Links
|
|
||||||
|
|
||||||
- [Architecture Overview](./docs/architecture/README.md)
|
|
||||||
- [Database Design](./docs/architecture/database.md)
|
|
||||||
- [API Design](./docs/architecture/api.md)
|
|
||||||
- [Infrastructure](./docs/architecture/infrastructure.md)
|
|
||||||
- [UI/UX Patterns](./docs/architecture/ui.md)
|
|
||||||
|
|||||||
645
DEPLOYMENT.md
645
DEPLOYMENT.md
@@ -1,318 +1,327 @@
|
|||||||
# MOPC Platform - Server Deployment Guide
|
# MOPC Platform - Server Deployment Guide
|
||||||
|
|
||||||
Deployment guide for the MOPC platform on a Linux VPS with Docker.
|
Deployment guide for the MOPC platform on a Linux VPS with Docker.
|
||||||
|
|
||||||
**Domain**: `portal.monaco-opc.com`
|
**Domain**: `portal.monaco-opc.com`
|
||||||
**App Port**: 7600 (behind Nginx reverse proxy)
|
**App Port**: 7600 (behind Nginx reverse proxy)
|
||||||
**CI/CD**: Gitea Actions (Ubuntu runner) builds and pushes Docker images
|
**CI/CD**: Gitea Actions (Ubuntu runner) builds and pushes Docker images
|
||||||
|
|
||||||
## CI/CD Pipeline
|
## CI/CD Pipeline
|
||||||
|
|
||||||
The app is built automatically by a Gitea runner on every push to `main`:
|
The app is built automatically by a Gitea runner on every push to `main`:
|
||||||
|
|
||||||
1. Gitea Actions workflow builds the Docker image on Ubuntu
|
1. Gitea Actions workflow builds the Docker image on Ubuntu
|
||||||
2. Image is pushed to the Gitea container registry
|
2. Image is pushed to the Gitea container registry
|
||||||
3. On the server, you pull the latest image and restart
|
3. On the server, `docker compose up -d` refreshes the image and restarts the app
|
||||||
|
|
||||||
### Gitea Setup
|
### Gitea Setup
|
||||||
|
|
||||||
Configure the following in your Gitea repository settings:
|
Configure the following in your Gitea repository settings:
|
||||||
|
|
||||||
**Repository Variables** (Settings > Actions > Variables):
|
**Repository Variables** (Settings > Actions > Variables):
|
||||||
|
|
||||||
| Variable | Value |
|
| Variable | Value |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| `REGISTRY_URL` | Your Gitea registry URL (e.g. `gitea.example.com/your-org`) |
|
| `REGISTRY_URL` | Your Gitea registry URL (e.g. `gitea.example.com/your-org`) |
|
||||||
|
|
||||||
**Repository Secrets** (Settings > Actions > Secrets):
|
**Repository Secrets** (Settings > Actions > Secrets):
|
||||||
|
|
||||||
| Secret | Value |
|
| Secret | Value |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| `REGISTRY_USER` | Gitea username with registry access |
|
| `REGISTRY_USER` | Gitea username with registry access |
|
||||||
| `REGISTRY_PASSWORD` | Gitea access token or password |
|
| `REGISTRY_PASSWORD` | Gitea access token or password |
|
||||||
|
|
||||||
The workflow file is at `.gitea/workflows/build.yml`.
|
The workflow file is at `.gitea/workflows/build.yml`.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Linux VPS (Ubuntu 22.04+ recommended)
|
- Linux VPS (Ubuntu 22.04+ recommended)
|
||||||
- Docker Engine 24+ with Compose v2
|
- Docker Engine 24+ with Compose v2
|
||||||
- Nginx installed on the host
|
- Nginx installed on the host
|
||||||
- Certbot for SSL certificates
|
- Certbot for SSL certificates
|
||||||
|
|
||||||
### Install Docker (if needed)
|
### Install Docker (if needed)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://get.docker.com | sh
|
curl -fsSL https://get.docker.com | sh
|
||||||
sudo usermod -aG docker $USER
|
sudo usermod -aG docker $USER
|
||||||
# Log out and back in
|
# Log out and back in
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install Nginx & Certbot (if needed)
|
### Install Nginx & Certbot (if needed)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install nginx certbot python3-certbot-nginx
|
sudo apt install nginx certbot python3-certbot-nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
## First-Time Deployment
|
## First-Time Deployment
|
||||||
|
|
||||||
### 1. Clone the repository
|
### 1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <your-repo-url> /opt/mopc
|
git clone <your-repo-url> /opt/mopc
|
||||||
cd /opt/mopc
|
cd /opt/mopc
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Configure environment variables
|
### 2. Configure environment variables
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp docker/.env.production docker/.env
|
cp docker/.env.production docker/.env
|
||||||
nano docker/.env
|
nano docker/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
Fill in all `CHANGE_ME` values. Generate secrets with:
|
Fill in all `CHANGE_ME` values. Generate secrets with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
```
|
```
|
||||||
|
|
||||||
Required variables:
|
Required variables:
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `REGISTRY_URL` | Gitea registry URL (e.g. `gitea.example.com/your-org`) |
|
| `REGISTRY_URL` | Gitea registry URL (e.g. `gitea.example.com/your-org`) |
|
||||||
| `DB_PASSWORD` | PostgreSQL password |
|
| `DB_PASSWORD` | PostgreSQL password |
|
||||||
| `NEXTAUTH_SECRET` | Auth session secret (openssl rand) |
|
| `NEXTAUTH_SECRET` | Auth session secret (openssl rand) |
|
||||||
| `NEXTAUTH_URL` | `https://portal.monaco-opc.com` |
|
| `NEXTAUTH_URL` | `https://portal.monaco-opc.com` |
|
||||||
| `MINIO_ENDPOINT` | MinIO internal URL (e.g. `http://localhost:9000`) |
|
| `MINIO_ENDPOINT` | MinIO internal URL (e.g. `http://localhost:9000`) |
|
||||||
| `MINIO_ACCESS_KEY` | MinIO access key |
|
| `MINIO_ACCESS_KEY` | MinIO access key |
|
||||||
| `MINIO_SECRET_KEY` | MinIO secret key |
|
| `MINIO_SECRET_KEY` | MinIO secret key |
|
||||||
| `MINIO_BUCKET` | MinIO bucket name (`mopc-files`) |
|
| `MINIO_BUCKET` | MinIO bucket name (`mopc-files`) |
|
||||||
| `SMTP_HOST` | SMTP server host |
|
| `SMTP_HOST` | SMTP server host |
|
||||||
| `SMTP_PORT` | SMTP port (587) |
|
| `SMTP_PORT` | SMTP port (587) |
|
||||||
| `SMTP_USER` | SMTP username |
|
| `SMTP_USER` | SMTP username |
|
||||||
| `SMTP_PASS` | SMTP password |
|
| `SMTP_PASS` | SMTP password |
|
||||||
| `EMAIL_FROM` | Sender address |
|
| `EMAIL_FROM` | Sender address |
|
||||||
|
|
||||||
### 3. Run the deploy script
|
### 3. Run the deploy script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x scripts/deploy.sh scripts/seed.sh scripts/update.sh
|
chmod +x scripts/deploy.sh scripts/seed.sh scripts/update.sh
|
||||||
./scripts/deploy.sh
|
./scripts/deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
- Log in to the container registry
|
- Log in to the container registry
|
||||||
- Pull the latest app image
|
- Pull the latest app image
|
||||||
- Start PostgreSQL + the app
|
- Start PostgreSQL + the app
|
||||||
- Run database migrations automatically on startup
|
- Run database migrations automatically on startup
|
||||||
- Wait for the health check
|
- Wait for the health check
|
||||||
|
|
||||||
### 4. Seed the database (one time only)
|
### 4. Seed the database (one time only)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/seed.sh
|
./scripts/seed.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This seeds:
|
This seeds:
|
||||||
- Super admin user (`matt.ciaccio@gmail.com`)
|
- Super admin user (`matt.ciaccio@gmail.com`)
|
||||||
- System settings
|
- System settings
|
||||||
- Program & Round 1 configuration
|
- Program & Round 1 configuration
|
||||||
- Evaluation form
|
- Evaluation form
|
||||||
- All candidature data from CSV
|
- All candidature data from CSV
|
||||||
|
|
||||||
### 5. Set up Nginx
|
### 5. Set up Nginx
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s /opt/mopc/docker/nginx/mopc-platform.conf /etc/nginx/sites-enabled/
|
sudo ln -s /opt/mopc/docker/nginx/mopc-platform.conf /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t
|
sudo nginx -t
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Set up SSL
|
### 6. Set up SSL
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo certbot --nginx -d portal.monaco-opc.com
|
sudo certbot --nginx -d portal.monaco-opc.com
|
||||||
```
|
```
|
||||||
|
|
||||||
Auto-renewal is configured by default. Test with:
|
Auto-renewal is configured by default. Test with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo certbot renew --dry-run
|
sudo certbot renew --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. Verify
|
### 7. Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl https://portal.monaco-opc.com/api/health
|
curl https://portal.monaco-opc.com/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected response:
|
Expected response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"status":"healthy","timestamp":"...","services":{"database":"connected"}}
|
{"status":"healthy","timestamp":"...","services":{"database":"connected"}}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Updating the Platform
|
## Updating the Platform
|
||||||
|
|
||||||
After Gitea CI builds a new image (push to `main`):
|
After Gitea CI builds a new image (push to `main`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/mopc
|
cd /opt/mopc
|
||||||
./scripts/update.sh
|
./scripts/update.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This pulls the latest image from the registry, restarts only the app container (PostgreSQL stays running), runs migrations via the entrypoint, and waits for the health check.
|
This pulls the latest image from the registry, restarts only the app container (PostgreSQL stays running), runs migrations via the entrypoint, and waits for the health check.
|
||||||
|
|
||||||
## Manual Operations
|
Manual equivalent:
|
||||||
|
|
||||||
### View logs
|
```bash
|
||||||
|
cd /opt/mopc/docker
|
||||||
```bash
|
docker compose up -d --pull always --force-recreate app
|
||||||
cd /opt/mopc/docker
|
```
|
||||||
docker compose logs -f app # App logs
|
|
||||||
docker compose logs -f postgres # Database logs
|
`prisma migrate deploy` runs automatically in the container entrypoint before the app starts.
|
||||||
```
|
|
||||||
|
## Manual Operations
|
||||||
### Run migrations manually
|
|
||||||
|
### View logs
|
||||||
```bash
|
|
||||||
cd /opt/mopc/docker
|
```bash
|
||||||
docker compose exec app npx prisma migrate deploy
|
cd /opt/mopc/docker
|
||||||
```
|
docker compose logs -f app # App logs
|
||||||
|
docker compose logs -f postgres # Database logs
|
||||||
### Open a shell in the app container
|
```
|
||||||
|
|
||||||
```bash
|
### Run migrations manually
|
||||||
cd /opt/mopc/docker
|
|
||||||
docker compose exec app sh
|
```bash
|
||||||
```
|
cd /opt/mopc/docker
|
||||||
|
docker compose exec app npx prisma migrate deploy
|
||||||
### Restart services
|
```
|
||||||
|
|
||||||
```bash
|
### Open a shell in the app container
|
||||||
cd /opt/mopc/docker
|
|
||||||
docker compose restart app # App only
|
```bash
|
||||||
docker compose restart # All services
|
cd /opt/mopc/docker
|
||||||
```
|
docker compose exec app sh
|
||||||
|
```
|
||||||
### Stop everything
|
|
||||||
|
### Restart services
|
||||||
```bash
|
|
||||||
cd /opt/mopc/docker
|
```bash
|
||||||
docker compose down # Stop containers (data preserved)
|
cd /opt/mopc/docker
|
||||||
docker compose down -v # Stop AND delete volumes (data lost!)
|
docker compose restart app # App only
|
||||||
```
|
docker compose restart # All services
|
||||||
|
```
|
||||||
## Database Backups
|
|
||||||
|
### Stop everything
|
||||||
### Create a backup
|
|
||||||
|
```bash
|
||||||
```bash
|
cd /opt/mopc/docker
|
||||||
docker exec mopc-postgres pg_dump -U mopc mopc | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
|
docker compose down # Stop containers (data preserved)
|
||||||
```
|
docker compose down -v # Stop AND delete volumes (data lost!)
|
||||||
|
```
|
||||||
### Restore a backup
|
|
||||||
|
## Database Backups
|
||||||
```bash
|
|
||||||
gunzip < backup_20260130_020000.sql.gz | docker exec -i mopc-postgres psql -U mopc mopc
|
### Create a backup
|
||||||
```
|
|
||||||
|
```bash
|
||||||
### Set up daily backups (cron)
|
docker exec mopc-postgres pg_dump -U mopc mopc | gzip > backup_$(date +%Y%m%d_%H%M%S).sql.gz
|
||||||
|
```
|
||||||
```bash
|
|
||||||
sudo mkdir -p /data/backups/mopc
|
### Restore a backup
|
||||||
|
|
||||||
cat > /opt/mopc/scripts/backup-db.sh << 'SCRIPT'
|
```bash
|
||||||
#!/bin/bash
|
gunzip < backup_20260130_020000.sql.gz | docker exec -i mopc-postgres psql -U mopc mopc
|
||||||
BACKUP_DIR=/data/backups/mopc
|
```
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
docker exec mopc-postgres pg_dump -U mopc mopc | gzip > $BACKUP_DIR/mopc_$DATE.sql.gz
|
### Set up daily backups (cron)
|
||||||
find $BACKUP_DIR -name "mopc_*.sql.gz" -mtime +30 -delete
|
|
||||||
SCRIPT
|
```bash
|
||||||
|
sudo mkdir -p /data/backups/mopc
|
||||||
chmod +x /opt/mopc/scripts/backup-db.sh
|
|
||||||
echo "0 2 * * * /opt/mopc/scripts/backup-db.sh >> /var/log/mopc-backup.log 2>&1" | sudo tee /etc/cron.d/mopc-backup
|
cat > /opt/mopc/scripts/backup-db.sh << 'SCRIPT'
|
||||||
```
|
#!/bin/bash
|
||||||
|
BACKUP_DIR=/data/backups/mopc
|
||||||
## Architecture
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
docker exec mopc-postgres pg_dump -U mopc mopc | gzip > $BACKUP_DIR/mopc_$DATE.sql.gz
|
||||||
```
|
find $BACKUP_DIR -name "mopc_*.sql.gz" -mtime +30 -delete
|
||||||
Gitea CI (Ubuntu runner)
|
SCRIPT
|
||||||
|
|
|
||||||
v (docker push)
|
chmod +x /opt/mopc/scripts/backup-db.sh
|
||||||
Container Registry
|
echo "0 2 * * * /opt/mopc/scripts/backup-db.sh >> /var/log/mopc-backup.log 2>&1" | sudo tee /etc/cron.d/mopc-backup
|
||||||
|
|
```
|
||||||
v (docker pull)
|
|
||||||
Linux VPS
|
## Architecture
|
||||||
|
|
|
||||||
v
|
```
|
||||||
Nginx (host, port 443) -- SSL termination
|
Gitea CI (Ubuntu runner)
|
||||||
|
|
|
|
||||||
v
|
v (docker push)
|
||||||
mopc-app (Docker, port 7600) -- Next.js standalone
|
Container Registry
|
||||||
|
|
|
|
||||||
v
|
v (docker pull)
|
||||||
mopc-postgres (Docker, port 5432) -- PostgreSQL 16
|
Linux VPS
|
||||||
|
|
|
||||||
External services (separate Docker stacks):
|
v
|
||||||
- MinIO (port 9000) -- S3-compatible file storage
|
Nginx (host, port 443) -- SSL termination
|
||||||
- Poste.io (port 587) -- SMTP email
|
|
|
||||||
```
|
v
|
||||||
|
mopc-app (Docker, port 7600) -- Next.js standalone
|
||||||
## Troubleshooting
|
|
|
||||||
|
v
|
||||||
### App won't start
|
mopc-postgres (Docker, port 5432) -- PostgreSQL 16
|
||||||
|
|
||||||
```bash
|
External services (separate Docker stacks):
|
||||||
cd /opt/mopc/docker
|
- MinIO (port 9000) -- S3-compatible file storage
|
||||||
docker compose logs app
|
- Poste.io (port 587) -- SMTP email
|
||||||
docker compose exec postgres pg_isready -U mopc
|
```
|
||||||
```
|
|
||||||
|
## Troubleshooting
|
||||||
### Can't pull image
|
|
||||||
|
### App won't start
|
||||||
```bash
|
|
||||||
# Re-authenticate with registry
|
```bash
|
||||||
docker login <your-registry-url>
|
cd /opt/mopc/docker
|
||||||
|
docker compose logs app
|
||||||
# Check image exists
|
docker compose exec postgres pg_isready -U mopc
|
||||||
docker pull <your-registry-url>/mopc-app:latest
|
```
|
||||||
```
|
|
||||||
|
### Can't pull image
|
||||||
### Migration fails
|
|
||||||
|
```bash
|
||||||
```bash
|
# Re-authenticate with registry
|
||||||
# Check migration status
|
docker login <your-registry-url>
|
||||||
docker compose exec app npx prisma migrate status
|
|
||||||
|
# Check image exists
|
||||||
# Reset (DESTROYS DATA):
|
docker pull <your-registry-url>/mopc-app:latest
|
||||||
docker compose exec app npx prisma migrate reset
|
```
|
||||||
```
|
|
||||||
|
### Migration fails
|
||||||
### SSL certificate issues
|
|
||||||
|
```bash
|
||||||
```bash
|
# Check migration status
|
||||||
sudo certbot certificates
|
docker compose exec app npx prisma migrate status
|
||||||
sudo certbot renew --force-renewal
|
|
||||||
```
|
# Reset (DESTROYS DATA):
|
||||||
|
docker compose exec app npx prisma migrate reset
|
||||||
### Port conflict
|
```
|
||||||
|
|
||||||
The app runs on port 7600. If something else uses it:
|
### SSL certificate issues
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ss -tlnp | grep 7600
|
sudo certbot certificates
|
||||||
```
|
sudo certbot renew --force-renewal
|
||||||
|
```
|
||||||
## Security Checklist
|
|
||||||
|
### Port conflict
|
||||||
- [ ] SSL certificate active and auto-renewing
|
|
||||||
- [ ] `docker/.env` has strong, unique passwords
|
The app runs on port 7600. If something else uses it:
|
||||||
- [ ] `NEXTAUTH_SECRET` is randomly generated
|
|
||||||
- [ ] Gitea registry credentials secured
|
```bash
|
||||||
- [ ] Firewall allows only ports 80, 443, 22
|
sudo ss -tlnp | grep 7600
|
||||||
- [ ] Docker daemon not exposed to network
|
```
|
||||||
- [ ] Daily backups configured
|
|
||||||
- [ ] Nginx security headers active
|
## Security Checklist
|
||||||
|
|
||||||
|
- [ ] SSL certificate active and auto-renewing
|
||||||
|
- [ ] `docker/.env` has strong, unique passwords
|
||||||
|
- [ ] `NEXTAUTH_SECRET` is randomly generated
|
||||||
|
- [ ] Gitea registry credentials secured
|
||||||
|
- [ ] Firewall allows only ports 80, 443, 22
|
||||||
|
- [ ] Docker daemon not exposed to network
|
||||||
|
- [ ] Daily backups configured
|
||||||
|
- [ ] Nginx security headers active
|
||||||
|
|||||||
78
MOPC-redesign-report.md
Normal file
78
MOPC-redesign-report.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Final Comparative Analysis of Round System Redesign Proposals
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
After a detailed review of the design documents, and with special consideration for the directive that a full architectural replacement ("nuking" the system) is an acceptable strategy, the **"Claude" proposal is unequivocally the superior plan.**
|
||||||
|
|
||||||
|
While both proposals advocate for a fundamental redesign, the "Claude" plan presents a complete, well-reasoned, and deeply detailed target architecture. The "Codex" plan, in contrast, offers a rigid and process-heavy methodology for a redesign but fails to provide the substantive architectural details of the proposed replacement.
|
||||||
|
|
||||||
|
The "Claude" plan is better not because it is less risky, but because it is a **complete and actionable engineering proposal.** The "Codex" plan is an empty process document by comparison.
|
||||||
|
|
||||||
|
The "GLM-5" proposal could not be evaluated due to corrupted and unreadable files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Introduction
|
||||||
|
|
||||||
|
This report provides a comparative analysis of two proposals for a major redesign of the MOPC round system: "Claude" and "Codex". The analysis was guided by the principle that a complete, "rip and replace" overhaul of the existing architecture is a valid and encouraged option. The goal is to determine which plan is the most comprehensive, well-thought-through, creative, and technically impressive in its vision for this new architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Philosophy of Redesign
|
||||||
|
|
||||||
|
Both plans embrace the idea of replacing the old, organic round system with a new, purpose-built architecture. They even converge on a similar conceptual model (`Pipeline -> Track -> Stage`). However, their approaches to achieving this goal are diametrically opposed.
|
||||||
|
|
||||||
|
- **"Claude" - The Architect's Approach:** This plan focuses on the **end-state architecture first.** It presents a meticulously detailed domain model and schema, explaining the "why" behind every component. It then proposes a professional, phased implementation plan to build and deploy this new architecture, using a temporary dual-write strategy to ensure a smooth and safe transition before decommissioning the old system. This is a complete replacement, executed with engineering precision.
|
||||||
|
|
||||||
|
- **"Codex" - The Process-Manager's Approach:** This plan focuses on the **process of execution first.** It builds an elaborate and rigid framework of rules, gates, and documentation requirements for the project. It advocates for a one-time, destructive cutover. However, it critically fails to define the very architecture it plans to build. Key documents like the `schema-domain-model.md` are little more than lists of names, lacking the actual schema, fields, relationships, and rationale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Comparative Analysis
|
||||||
|
|
||||||
|
### Comprehensiveness & Technical Impressiveness
|
||||||
|
|
||||||
|
**Winner: Claude**
|
||||||
|
|
||||||
|
This is the most critical distinction. The "Claude" proposal is technically impressive because of its content and substance. The `02-schema-design.md` document is a masterclass in software architecture, providing:
|
||||||
|
- A clear `Pipeline -> Track -> Stage` hierarchy.
|
||||||
|
- Complete `prisma` schema definitions for 12 new models.
|
||||||
|
- An explicit, configurable state machine (`StageTransition`) to replace hard-coded logic.
|
||||||
|
- First-class support for parallel award tracks (`RoutingRule`).
|
||||||
|
- A robust data model for live-event management (`LiveStageControl`).
|
||||||
|
- Detailed consideration of indexes, validation, and migration.
|
||||||
|
|
||||||
|
The "Codex" proposal is comprehensive only in its process. It specifies *that* a schema should be built, but doesn't contain the schema. It specifies *that* API contracts should exist, but the substance is missing. Its technical impressiveness is superficial—an illusion created by a mountain of process formalism with no architectural core. **The "Claude" plan *is* a new architecture; the "Codex" plan is a set of instructions for *how to create* a new architecture.**
|
||||||
|
|
||||||
|
### Thoughtfulness & Strategy
|
||||||
|
|
||||||
|
**Winner: Claude**
|
||||||
|
|
||||||
|
Given that a "nuke" is acceptable, the question becomes: what is the most thoughtful way to execute it?
|
||||||
|
|
||||||
|
- The "Codex" strategy of a single, destructive cutover is brittle. It assumes perfection in execution and leaves no room for error. A single failure during the final push could render the entire platform inoperable and require a complex, stressful rollback.
|
||||||
|
- The "Claude" strategy of a phased cutover with a dual-write period is far more thoughtful. It is still a complete "nuke," as the old `Round` model is ultimately removed. However, it allows the new system to be validated in a live production environment alongside the old one before the final switch is flipped. This de-risks the deployment immensely and is the hallmark of a mature engineering organization.
|
||||||
|
|
||||||
|
### Creativity & Innovation
|
||||||
|
|
||||||
|
**Winner: Claude**
|
||||||
|
|
||||||
|
The most creative and innovative element in this entire project is the **new system architecture itself.** The `Pipeline -> Track -> Stage` model, the explicit state machine for transitions, and the flexible routing rules are the true innovations that will solve the platform's problems.
|
||||||
|
|
||||||
|
Since the "Claude" plan is the only one to actually *document* this innovative architecture in detail, it wins by default. The creativity of the "Codex" plan is confined to its rigid project management methodology, which is less a useful innovation and more of a high-risk academic exercise.
|
||||||
|
|
||||||
|
### Efficiency
|
||||||
|
|
||||||
|
**Winner: Claude**
|
||||||
|
|
||||||
|
The "Claude" plan's phased approach allows for incremental progress and testing. The development team can build, test, and validate each part of the new system in isolation. The "Codex" plan, with its massive documentation overhead and "all-or-nothing" integration at the very end, is a recipe for inefficiency, bottlenecks, and massive rework when unforeseen issues inevitably arise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Conclusion & Recommendation
|
||||||
|
|
||||||
|
The directive to allow for a full architectural replacement does not change the final recommendation; it strengthens it. The "Codex" proposal pays lip service to the idea of a replacement but provides no actual architectural substance. It is a hollow and risky process document.
|
||||||
|
|
||||||
|
The "Claude" proposal embraces the spirit of a full redesign by presenting a truly impressive, detailed, and well-reasoned new architecture. It then pairs this excellent design with a professional and pragmatic implementation plan that respects the complexities of migrating a live system.
|
||||||
|
|
||||||
|
**The "Claude" plan is the only viable path forward. It is a complete, thoughtful, and technically superior proposal that should be adopted without reservation.**
|
||||||
804
Notes.md
804
Notes.md
@@ -1,402 +1,402 @@
|
|||||||
Below is a “technical requirements” rendering (not an architecture diagram), structured so you can hand it to a dev team and derive system architecture + backlog from it. Phase 1 is the only critical deliverable; Phase 2+ are explicitly extendable.
|
Below is a “technical requirements” rendering (not an architecture diagram), structured so you can hand it to a dev team and derive system architecture + backlog from it. Phase 1 is the only critical deliverable; Phase 2+ are explicitly extendable.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 0) Product scope and phasing
|
## 0) Product scope and phasing
|
||||||
|
|
||||||
### Phase 1 (critical, delivery in ~2 weeks)
|
### Phase 1 (critical, delivery in ~2 weeks)
|
||||||
|
|
||||||
**Secure Jury Online Voting Module** to run two selection rounds:
|
**Secure Jury Online Voting Module** to run two selection rounds:
|
||||||
|
|
||||||
* Round 1: ~130 projects → ~60 semi-finalists (Feb 18–23 voting window)
|
* Round 1: ~130 projects → ~60 semi-finalists (Feb 18–23 voting window)
|
||||||
* Round 2: ~60 projects → 6 finalists (~April 13 week voting window)
|
* Round 2: ~60 projects → 6 finalists (~April 13 week voting window)
|
||||||
* Voting is asynchronous, online, with assigned project access, scoring + feedback capture, and reporting dashboards.
|
* Voting is asynchronous, online, with assigned project access, scoring + feedback capture, and reporting dashboards.
|
||||||
|
|
||||||
### Phase 2+ (mid-term)
|
### Phase 2+ (mid-term)
|
||||||
|
|
||||||
Centralized MOPC platform:
|
Centralized MOPC platform:
|
||||||
|
|
||||||
* Applications/projects database
|
* Applications/projects database
|
||||||
* Document management (MinIO S3)
|
* Document management (MinIO S3)
|
||||||
* Jury spaces (history, comments, scoring)
|
* Jury spaces (history, comments, scoring)
|
||||||
* Learning hub / resources
|
* Learning hub / resources
|
||||||
* Communication workflows (email + possibly WhatsApp)
|
* Communication workflows (email + possibly WhatsApp)
|
||||||
* Partner/sponsor visibility modules
|
* Partner/sponsor visibility modules
|
||||||
* Potential website integration / shared back office
|
* Potential website integration / shared back office
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1) Users, roles, permissions (RBAC)
|
## 1) Users, roles, permissions (RBAC)
|
||||||
|
|
||||||
### Core roles
|
### Core roles
|
||||||
|
|
||||||
1. **Platform Super Admin**
|
1. **Platform Super Admin**
|
||||||
|
|
||||||
* Full system configuration, security policies, integrations, user/role management, data export, audit access.
|
* Full system configuration, security policies, integrations, user/role management, data export, audit access.
|
||||||
2. **Program Admin (MOPC Admin)**
|
2. **Program Admin (MOPC Admin)**
|
||||||
|
|
||||||
* Manages cycles/rounds, projects, jury members, assignments, voting windows, criteria forms, dashboards, exports.
|
* Manages cycles/rounds, projects, jury members, assignments, voting windows, criteria forms, dashboards, exports.
|
||||||
3. **Jury Member**
|
3. **Jury Member**
|
||||||
|
|
||||||
* Can access only assigned projects for active rounds; submit evaluations; view own submitted evaluations; optionally view aggregated results only if permitted.
|
* Can access only assigned projects for active rounds; submit evaluations; view own submitted evaluations; optionally view aggregated results only if permitted.
|
||||||
4. **Read-only Observer (optional)**
|
4. **Read-only Observer (optional)**
|
||||||
|
|
||||||
* Internal meeting viewer: can see dashboards/aggregates but cannot edit votes.
|
* Internal meeting viewer: can see dashboards/aggregates but cannot edit votes.
|
||||||
|
|
||||||
### Permission model requirements
|
### Permission model requirements
|
||||||
|
|
||||||
* **Least privilege by default**
|
* **Least privilege by default**
|
||||||
* **Round-scoped permissions**: access can be constrained per selection round/cycle.
|
* **Round-scoped permissions**: access can be constrained per selection round/cycle.
|
||||||
* **Project-scoped access control**: jury sees only assigned projects (unless admin toggles “all projects visible”).
|
* **Project-scoped access control**: jury sees only assigned projects (unless admin toggles “all projects visible”).
|
||||||
* **Admin override controls**: reassign projects, revoke access, reopen/lock evaluations, extend voting windows, invalidate votes with reason logging.
|
* **Admin override controls**: reassign projects, revoke access, reopen/lock evaluations, extend voting windows, invalidate votes with reason logging.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2) Core domain objects (data model concepts)
|
## 2) Core domain objects (data model concepts)
|
||||||
|
|
||||||
### Entities
|
### Entities
|
||||||
|
|
||||||
* **Program** (e.g., “MOPC 2026”)
|
* **Program** (e.g., “MOPC 2026”)
|
||||||
* **Selection Cycle / Round**
|
* **Selection Cycle / Round**
|
||||||
|
|
||||||
* Attributes: name, start/end of voting window, status (draft/active/closed/archived), required reviews per project (default ≥3), scoring form version, jury cohort.
|
* Attributes: name, start/end of voting window, status (draft/active/closed/archived), required reviews per project (default ≥3), scoring form version, jury cohort.
|
||||||
* **Project**
|
* **Project**
|
||||||
|
|
||||||
* Attributes: title, team name, description, tags, status (submitted/eligible/assigned/semi-finalist/finalist/etc.), submission metadata, external IDs (Typeform/Notion), files (exec summary, PDF deck, intro video).
|
* Attributes: title, team name, description, tags, status (submitted/eligible/assigned/semi-finalist/finalist/etc.), submission metadata, external IDs (Typeform/Notion), files (exec summary, PDF deck, intro video).
|
||||||
* **File Asset**
|
* **File Asset**
|
||||||
|
|
||||||
* Stored in MinIO (S3-compatible): object key, bucket, version/etag, mime type, size, upload timestamp, retention policy, access policy.
|
* Stored in MinIO (S3-compatible): object key, bucket, version/etag, mime type, size, upload timestamp, retention policy, access policy.
|
||||||
* **Jury Member**
|
* **Jury Member**
|
||||||
|
|
||||||
* Profile: name, email, organization (optional), role, expertise tags, status (invited/active/suspended).
|
* Profile: name, email, organization (optional), role, expertise tags, status (invited/active/suspended).
|
||||||
* **Expertise Tag**
|
* **Expertise Tag**
|
||||||
|
|
||||||
* Managed vocabulary or free-form with admin approval.
|
* Managed vocabulary or free-form with admin approval.
|
||||||
* **Assignment**
|
* **Assignment**
|
||||||
|
|
||||||
* Connects Jury Member ↔ Project ↔ Round
|
* Connects Jury Member ↔ Project ↔ Round
|
||||||
* Attributes: assignment method (manual/auto), created by, created timestamp, required review flag, completion status.
|
* Attributes: assignment method (manual/auto), created by, created timestamp, required review flag, completion status.
|
||||||
* **Evaluation (Vote)**
|
* **Evaluation (Vote)**
|
||||||
|
|
||||||
* Per assignment: criterion scores + global score + binary decision + qualitative feedback
|
* Per assignment: criterion scores + global score + binary decision + qualitative feedback
|
||||||
* Metadata: submitted_at, last_edited_at, finalization flag, versioning, IP/user-agent logging (optional), conflict handling.
|
* Metadata: submitted_at, last_edited_at, finalization flag, versioning, IP/user-agent logging (optional), conflict handling.
|
||||||
* **Audit Log**
|
* **Audit Log**
|
||||||
|
|
||||||
* Immutable events: login, permission changes, voting window changes, assignments, overrides, exports, vote invalidations.
|
* Immutable events: login, permission changes, voting window changes, assignments, overrides, exports, vote invalidations.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3) Phase 1 functional requirements
|
## 3) Phase 1 functional requirements
|
||||||
|
|
||||||
### 3.1 Jury authentication & access
|
### 3.1 Jury authentication & access
|
||||||
|
|
||||||
* Invite flow:
|
* Invite flow:
|
||||||
|
|
||||||
* Admin imports jury list (CSV) or adds manually.
|
* Admin imports jury list (CSV) or adds manually.
|
||||||
* System sends invitation email with secure link + account activation.
|
* System sends invitation email with secure link + account activation.
|
||||||
* Authentication options (choose one for Phase 1, keep others pluggable):
|
* Authentication options (choose one for Phase 1, keep others pluggable):
|
||||||
|
|
||||||
* Email magic link (recommended for speed)
|
* Email magic link (recommended for speed)
|
||||||
* Password + MFA optional
|
* Password + MFA optional
|
||||||
* Session requirements:
|
* Session requirements:
|
||||||
|
|
||||||
* Configurable session duration
|
* Configurable session duration
|
||||||
* Forced logout on role revocation
|
* Forced logout on role revocation
|
||||||
* Access gating:
|
* Access gating:
|
||||||
|
|
||||||
* Jury can only view projects for **active** rounds and only those assigned.
|
* Jury can only view projects for **active** rounds and only those assigned.
|
||||||
|
|
||||||
### 3.2 Project ingestion & management
|
### 3.2 Project ingestion & management
|
||||||
|
|
||||||
Phase 1 can support either:
|
Phase 1 can support either:
|
||||||
|
|
||||||
* **Option A (fastest): Manual import**
|
* **Option A (fastest): Manual import**
|
||||||
|
|
||||||
* Admin uploads CSV with project metadata + file links or uploads.
|
* Admin uploads CSV with project metadata + file links or uploads.
|
||||||
* **Option B (semi-integrated): Sync from Notion/Typeform**
|
* **Option B (semi-integrated): Sync from Notion/Typeform**
|
||||||
|
|
||||||
* Read projects from existing Notion DB and/or Typeform export.
|
* Read projects from existing Notion DB and/or Typeform export.
|
||||||
|
|
||||||
Minimum capabilities:
|
Minimum capabilities:
|
||||||
|
|
||||||
* Admin CRUD on projects (create/update/archive)
|
* Admin CRUD on projects (create/update/archive)
|
||||||
* Project tagging (from “Which issue does your project address?” + additional admin tags)
|
* Project tagging (from “Which issue does your project address?” + additional admin tags)
|
||||||
* Attach required assets:
|
* Attach required assets:
|
||||||
|
|
||||||
* Executive summary (PDF/doc)
|
* Executive summary (PDF/doc)
|
||||||
* PDF presentation
|
* PDF presentation
|
||||||
* 30s intro video (mp4)
|
* 30s intro video (mp4)
|
||||||
* File storage via MinIO (see Section 6)
|
* File storage via MinIO (see Section 6)
|
||||||
|
|
||||||
### 3.3 Assignment system (≥3 reviews/project)
|
### 3.3 Assignment system (≥3 reviews/project)
|
||||||
|
|
||||||
Admin can:
|
Admin can:
|
||||||
|
|
||||||
* Manually assign projects to jury members (bulk assign supported)
|
* Manually assign projects to jury members (bulk assign supported)
|
||||||
* Auto-assign (optional but strongly recommended):
|
* Auto-assign (optional but strongly recommended):
|
||||||
|
|
||||||
* Input: jury expertise tags + project tags + constraints
|
* Input: jury expertise tags + project tags + constraints
|
||||||
* Constraints:
|
* Constraints:
|
||||||
|
|
||||||
* Each project assigned to at least N jurors (N configurable; default 3)
|
* Each project assigned to at least N jurors (N configurable; default 3)
|
||||||
* Load balancing across jurors (minimize variance)
|
* Load balancing across jurors (minimize variance)
|
||||||
* Avoid conflicts (optional): disallow assignment if juror marked conflict with project
|
* Avoid conflicts (optional): disallow assignment if juror marked conflict with project
|
||||||
* Output: assignment set + summary metrics (coverage, per-juror load, unmatched tags)
|
* Output: assignment set + summary metrics (coverage, per-juror load, unmatched tags)
|
||||||
* Reassignment rules:
|
* Reassignment rules:
|
||||||
|
|
||||||
* Admin can reassign at any time
|
* Admin can reassign at any time
|
||||||
* If an evaluation exists, admin can:
|
* If an evaluation exists, admin can:
|
||||||
|
|
||||||
* keep existing evaluation tied to original juror
|
* keep existing evaluation tied to original juror
|
||||||
* or invalidate/lock it (requires reason + audit event)
|
* or invalidate/lock it (requires reason + audit event)
|
||||||
|
|
||||||
### 3.4 Evaluation form & scoring logic
|
### 3.4 Evaluation form & scoring logic
|
||||||
|
|
||||||
Per project evaluation must capture:
|
Per project evaluation must capture:
|
||||||
|
|
||||||
* **Criterion scores** (scale-based, define exact scale as configurable; e.g., 1–5 or 1–10)
|
* **Criterion scores** (scale-based, define exact scale as configurable; e.g., 1–5 or 1–10)
|
||||||
|
|
||||||
1. Need clarity
|
1. Need clarity
|
||||||
2. Solution relevance
|
2. Solution relevance
|
||||||
3. Gap analysis (market/competitors)
|
3. Gap analysis (market/competitors)
|
||||||
4. Target customers clarity
|
4. Target customers clarity
|
||||||
5. Ocean impact
|
5. Ocean impact
|
||||||
* **Global score**: 1–10
|
* **Global score**: 1–10
|
||||||
* **Binary decision**: “Select as semi-finalist?” (Yes/No)
|
* **Binary decision**: “Select as semi-finalist?” (Yes/No)
|
||||||
* **Qualitative feedback**: long text
|
* **Qualitative feedback**: long text
|
||||||
|
|
||||||
Form requirements:
|
Form requirements:
|
||||||
|
|
||||||
* Admin-configurable criteria text, ordering, scales, and whether fields are mandatory
|
* Admin-configurable criteria text, ordering, scales, and whether fields are mandatory
|
||||||
* Autosave drafts
|
* Autosave drafts
|
||||||
* Final submit locks evaluation by default (admin can allow edits until window closes)
|
* Final submit locks evaluation by default (admin can allow edits until window closes)
|
||||||
* Support multiple rounds with potentially different forms (versioned forms per round)
|
* Support multiple rounds with potentially different forms (versioned forms per round)
|
||||||
|
|
||||||
### 3.5 Voting windows and enforcement (must-have)
|
### 3.5 Voting windows and enforcement (must-have)
|
||||||
|
|
||||||
Admins must be able to configure and enforce:
|
Admins must be able to configure and enforce:
|
||||||
|
|
||||||
* Voting window start/end **per round** (date-time, timezone-aware)
|
* Voting window start/end **per round** (date-time, timezone-aware)
|
||||||
* States:
|
* States:
|
||||||
|
|
||||||
* Draft (admins only)
|
* Draft (admins only)
|
||||||
* Active (jury can submit)
|
* Active (jury can submit)
|
||||||
* Closed (jury read-only)
|
* Closed (jury read-only)
|
||||||
* Archived (admin/export only)
|
* Archived (admin/export only)
|
||||||
* Enforcement rules:
|
* Enforcement rules:
|
||||||
|
|
||||||
* Jury cannot submit outside the active window
|
* Jury cannot submit outside the active window
|
||||||
* Admin “grace period” toggle to accept late submissions for specific jurors/projects
|
* Admin “grace period” toggle to accept late submissions for specific jurors/projects
|
||||||
* Admin can extend the window (global or subset) with audit logging
|
* Admin can extend the window (global or subset) with audit logging
|
||||||
* Dashboard countdown + clear messaging for jurors
|
* Dashboard countdown + clear messaging for jurors
|
||||||
|
|
||||||
### 3.6 Dashboards & outputs
|
### 3.6 Dashboards & outputs
|
||||||
|
|
||||||
Must produce:
|
Must produce:
|
||||||
|
|
||||||
* **Jury member view**
|
* **Jury member view**
|
||||||
|
|
||||||
* Assigned projects list, completion status, quick access to files, evaluation status (not started/draft/submitted)
|
* Assigned projects list, completion status, quick access to files, evaluation status (not started/draft/submitted)
|
||||||
* **Admin dashboards**
|
* **Admin dashboards**
|
||||||
|
|
||||||
* Coverage: projects with <N evaluations
|
* Coverage: projects with <N evaluations
|
||||||
* Progress: submission rates by juror
|
* Progress: submission rates by juror
|
||||||
* Aggregates per project:
|
* Aggregates per project:
|
||||||
|
|
||||||
* Average per criterion
|
* Average per criterion
|
||||||
* Average global score
|
* Average global score
|
||||||
* Distribution (min/max, std dev optional)
|
* Distribution (min/max, std dev optional)
|
||||||
* Count of “Yes” votes
|
* Count of “Yes” votes
|
||||||
* Qualitative comments list (with juror identity visible only to admins, configurable)
|
* Qualitative comments list (with juror identity visible only to admins, configurable)
|
||||||
* Shortlisting tools:
|
* Shortlisting tools:
|
||||||
|
|
||||||
* Filter/sort by aggregate score, yes-vote ratio, tag, missing reviews
|
* Filter/sort by aggregate score, yes-vote ratio, tag, missing reviews
|
||||||
* Export shortlist (e.g., top 60 / top 6) with manual override controls
|
* Export shortlist (e.g., top 60 / top 6) with manual override controls
|
||||||
* Exports (Phase 1):
|
* Exports (Phase 1):
|
||||||
|
|
||||||
* CSV/Excel export for:
|
* CSV/Excel export for:
|
||||||
|
|
||||||
* Evaluations (row per evaluation)
|
* Evaluations (row per evaluation)
|
||||||
* Aggregates (row per project)
|
* Aggregates (row per project)
|
||||||
* Assignment matrix
|
* Assignment matrix
|
||||||
* PDF export (optional) for meeting packs
|
* PDF export (optional) for meeting packs
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4) Admin console requirements (robust)
|
## 4) Admin console requirements (robust)
|
||||||
|
|
||||||
### 4.1 Governance & configuration
|
### 4.1 Governance & configuration
|
||||||
|
|
||||||
* Create/manage Programs and Rounds
|
* Create/manage Programs and Rounds
|
||||||
* Set:
|
* Set:
|
||||||
|
|
||||||
* Required reviews per project (N)
|
* Required reviews per project (N)
|
||||||
* Voting windows (start/end) + grace rules
|
* Voting windows (start/end) + grace rules
|
||||||
* Evaluation form version
|
* Evaluation form version
|
||||||
* Visibility rules (whether jurors can see aggregates, whether jurors can see their past submissions after close)
|
* Visibility rules (whether jurors can see aggregates, whether jurors can see their past submissions after close)
|
||||||
* Manage tags:
|
* Manage tags:
|
||||||
|
|
||||||
* Tag taxonomy, synonyms/merging, locked tags
|
* Tag taxonomy, synonyms/merging, locked tags
|
||||||
|
|
||||||
### 4.2 User management & security controls
|
### 4.2 User management & security controls
|
||||||
|
|
||||||
* Bulk invite/import
|
* Bulk invite/import
|
||||||
* Role assignment & revocation
|
* Role assignment & revocation
|
||||||
* Force password reset / disable account
|
* Force password reset / disable account
|
||||||
* View user activity logs
|
* View user activity logs
|
||||||
* Configure:
|
* Configure:
|
||||||
|
|
||||||
* Allowed email domains (optional)
|
* Allowed email domains (optional)
|
||||||
* MFA requirement (optional)
|
* MFA requirement (optional)
|
||||||
* Session lifetime (optional)
|
* Session lifetime (optional)
|
||||||
|
|
||||||
### 4.3 Assignment controls
|
### 4.3 Assignment controls
|
||||||
|
|
||||||
* Manual assignment UI (single + bulk)
|
* Manual assignment UI (single + bulk)
|
||||||
* Auto-assignment wizard:
|
* Auto-assignment wizard:
|
||||||
|
|
||||||
* select round
|
* select round
|
||||||
* choose balancing strategy (e.g., “maximize tag match”, “balance load first”)
|
* choose balancing strategy (e.g., “maximize tag match”, “balance load first”)
|
||||||
* preview results
|
* preview results
|
||||||
* apply
|
* apply
|
||||||
* Conflict of interest handling:
|
* Conflict of interest handling:
|
||||||
|
|
||||||
* Admin can mark conflicts (juror ↔ project)
|
* Admin can mark conflicts (juror ↔ project)
|
||||||
* Auto-assign must respect conflicts
|
* Auto-assign must respect conflicts
|
||||||
|
|
||||||
### 4.4 Data integrity controls
|
### 4.4 Data integrity controls
|
||||||
|
|
||||||
* Vote invalidation (requires reason)
|
* Vote invalidation (requires reason)
|
||||||
* Reopen evaluation (admin-only, logged)
|
* Reopen evaluation (admin-only, logged)
|
||||||
* Freeze round (hard lock)
|
* Freeze round (hard lock)
|
||||||
* Immutable audit log export
|
* Immutable audit log export
|
||||||
|
|
||||||
### 4.5 Integrations management
|
### 4.5 Integrations management
|
||||||
|
|
||||||
* Connectors toggles (Typeform/Notion/email provider/WhatsApp) with credentials stored securely
|
* Connectors toggles (Typeform/Notion/email provider/WhatsApp) with credentials stored securely
|
||||||
* MinIO bucket configuration + retention policies
|
* MinIO bucket configuration + retention policies
|
||||||
* Webhook management (optional)
|
* Webhook management (optional)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5) Non-functional requirements (Phase 1)
|
## 5) Non-functional requirements (Phase 1)
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
* TLS everywhere
|
* TLS everywhere
|
||||||
* RBAC + project-level access control
|
* RBAC + project-level access control
|
||||||
* Secure file access (pre-signed URLs with short TTL; no public buckets)
|
* Secure file access (pre-signed URLs with short TTL; no public buckets)
|
||||||
* Audit logging for admin actions + exports
|
* Audit logging for admin actions + exports
|
||||||
* Basic anti-abuse:
|
* Basic anti-abuse:
|
||||||
|
|
||||||
* rate limiting login endpoints
|
* rate limiting login endpoints
|
||||||
* brute-force protection if password auth used
|
* brute-force protection if password auth used
|
||||||
|
|
||||||
### Reliability & performance
|
### Reliability & performance
|
||||||
|
|
||||||
* Support:
|
* Support:
|
||||||
|
|
||||||
* Round 1: 15 jurors, 130 projects, min 390 evaluations
|
* Round 1: 15 jurors, 130 projects, min 390 evaluations
|
||||||
* Round 2: ~30 jurors, 60 projects
|
* Round 2: ~30 jurors, 60 projects
|
||||||
* Fast page load for dashboards and project pages
|
* Fast page load for dashboards and project pages
|
||||||
* File streaming for PDFs/videos (avoid timeouts)
|
* File streaming for PDFs/videos (avoid timeouts)
|
||||||
|
|
||||||
### Compliance & privacy (baseline)
|
### Compliance & privacy (baseline)
|
||||||
|
|
||||||
* Store only necessary personal data for jurors/candidates
|
* Store only necessary personal data for jurors/candidates
|
||||||
* Retention policies configurable (especially for candidate files)
|
* Retention policies configurable (especially for candidate files)
|
||||||
* Access logs available for security review
|
* Access logs available for security review
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6) File storage requirements (MinIO S3)
|
## 6) File storage requirements (MinIO S3)
|
||||||
|
|
||||||
### Storage design (requirements-level)
|
### Storage design (requirements-level)
|
||||||
|
|
||||||
* Use MinIO as S3-compatible object store for:
|
* Use MinIO as S3-compatible object store for:
|
||||||
|
|
||||||
* project documents (exec summary, deck)
|
* project documents (exec summary, deck)
|
||||||
* video files
|
* video files
|
||||||
* optional assets (logos, exports packs)
|
* optional assets (logos, exports packs)
|
||||||
* Buckets:
|
* Buckets:
|
||||||
|
|
||||||
* Separate buckets or prefixes by Program/Round to simplify retention + permissions
|
* Separate buckets or prefixes by Program/Round to simplify retention + permissions
|
||||||
* Access pattern:
|
* Access pattern:
|
||||||
|
|
||||||
* Upload: direct-to-S3 (preferred) or via backend proxy
|
* Upload: direct-to-S3 (preferred) or via backend proxy
|
||||||
* Download/view: **pre-signed URLs** generated by backend per authorized user
|
* Download/view: **pre-signed URLs** generated by backend per authorized user
|
||||||
* Optional features:
|
* Optional features:
|
||||||
|
|
||||||
* Object versioning enabled
|
* Object versioning enabled
|
||||||
* Antivirus scanning hook (Phase 2)
|
* Antivirus scanning hook (Phase 2)
|
||||||
* Lifecycle rules (auto-expire after X months)
|
* Lifecycle rules (auto-expire after X months)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7) “Current process” integration mapping (future-proof)
|
## 7) “Current process” integration mapping (future-proof)
|
||||||
|
|
||||||
### Existing flow
|
### Existing flow
|
||||||
|
|
||||||
* Typeform application → confirmation email → Tally upload → Notion tracking → Google Drive manual upload
|
* Typeform application → confirmation email → Tally upload → Notion tracking → Google Drive manual upload
|
||||||
|
|
||||||
### Platform integration targets
|
### Platform integration targets
|
||||||
|
|
||||||
Phase 1 (minimal):
|
Phase 1 (minimal):
|
||||||
|
|
||||||
* Allow admin to ingest projects and upload assets (replace Drive for jury-facing access)
|
* Allow admin to ingest projects and upload assets (replace Drive for jury-facing access)
|
||||||
|
|
||||||
Phase 2 options:
|
Phase 2 options:
|
||||||
|
|
||||||
* Typeform: pull submissions via API/webhooks
|
* Typeform: pull submissions via API/webhooks
|
||||||
* Tally: capture uploads directly to MinIO (or via platform upload portal)
|
* Tally: capture uploads directly to MinIO (or via platform upload portal)
|
||||||
* Notion: sync project status + metadata (one-way or two-way)
|
* Notion: sync project status + metadata (one-way or two-way)
|
||||||
* Email automation: reminder workflows for incomplete applications
|
* Email automation: reminder workflows for incomplete applications
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8) Additional ideas as “technical backlog candidates”
|
## 8) Additional ideas as “technical backlog candidates”
|
||||||
|
|
||||||
### Automated follow-ups for incomplete applications (Phase 2)
|
### Automated follow-ups for incomplete applications (Phase 2)
|
||||||
|
|
||||||
* State machine for applications: registered → awaiting docs → complete → expired
|
* State machine for applications: registered → awaiting docs → complete → expired
|
||||||
* Scheduler:
|
* Scheduler:
|
||||||
|
|
||||||
* send reminders at configurable intervals (e.g., +2d, +5d, +7d)
|
* send reminders at configurable intervals (e.g., +2d, +5d, +7d)
|
||||||
* stop on completion
|
* stop on completion
|
||||||
* Channels:
|
* Channels:
|
||||||
|
|
||||||
* Email must-have
|
* Email must-have
|
||||||
* WhatsApp optional (requires compliance + provider; store consent + opt-out)
|
* WhatsApp optional (requires compliance + provider; store consent + opt-out)
|
||||||
|
|
||||||
### Learning hub access (semi-finalists only)
|
### Learning hub access (semi-finalists only)
|
||||||
|
|
||||||
* Resource library stored in MinIO + metadata in DB
|
* Resource library stored in MinIO + metadata in DB
|
||||||
* Access controlled by cohort + passwordless login or access tokens
|
* Access controlled by cohort + passwordless login or access tokens
|
||||||
* Expiring invite links
|
* Expiring invite links
|
||||||
|
|
||||||
### Website integration
|
### Website integration
|
||||||
|
|
||||||
* Shared identity/back office (SSO-ready) OR separate admin domains
|
* Shared identity/back office (SSO-ready) OR separate admin domains
|
||||||
* Public-facing site remains content-only; platform is operational hub
|
* Public-facing site remains content-only; platform is operational hub
|
||||||
* Requirement: clear separation between “public content” and “private jury/applicant data”
|
* Requirement: clear separation between “public content” and “private jury/applicant data”
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9) Acceptance criteria checklist (Phase 1)
|
## 9) Acceptance criteria checklist (Phase 1)
|
||||||
|
|
||||||
1. Admin can create a round, set voting window (start/end), and activate it.
|
1. Admin can create a round, set voting window (start/end), and activate it.
|
||||||
2. Admin can import projects + upload/attach required files to MinIO.
|
2. Admin can import projects + upload/attach required files to MinIO.
|
||||||
3. Admin can import jurors, invite them, and jurors can log in securely.
|
3. Admin can import jurors, invite them, and jurors can log in securely.
|
||||||
4. Admin can assign projects (manual + bulk). Auto-assign is optional but if included must guarantee ≥3 reviews/project.
|
4. Admin can assign projects (manual + bulk). Auto-assign is optional but if included must guarantee ≥3 reviews/project.
|
||||||
5. Juror sees only assigned projects, can view files, and submit evaluation form.
|
5. Juror sees only assigned projects, can view files, and submit evaluation form.
|
||||||
6. System blocks submissions outside the voting window (unless admin-granted exception).
|
6. System blocks submissions outside the voting window (unless admin-granted exception).
|
||||||
7. Admin dashboard shows progress + aggregates per project; admin can export results.
|
7. Admin dashboard shows progress + aggregates per project; admin can export results.
|
||||||
8. All critical admin actions are audit-logged.
|
8. All critical admin actions are audit-logged.
|
||||||
9. File access is protected (no public links; pre-signed URLs with TTL).
|
9. File access is protected (no public links; pre-signed URLs with TTL).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
If you want, I can turn this into:
|
If you want, I can turn this into:
|
||||||
|
|
||||||
* a clean PRD-style document (Dev-ready) **plus**
|
* a clean PRD-style document (Dev-ready) **plus**
|
||||||
* a ticket breakdown (Epics → user stories → acceptance tests) for Phase 1 delivery.
|
* a ticket breakdown (Epics → user stories → acceptance tests) for Phase 1 delivery.
|
||||||
|
|||||||
288
build-check.txt
Normal file
288
build-check.txt
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
▲ Next.js 15.5.10
|
||||||
|
- Environments: .env.local, .env
|
||||||
|
|
||||||
|
Creating an optimized production build ...
|
||||||
|
✓ Compiled successfully in 10.0s
|
||||||
|
Linting and checking validity of types ...
|
||||||
|
Collecting page data ...
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
Generating static pages (0/37) ...
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
[Error [PrismaClientInitializationError]: Unable to require(`C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node`).
|
||||||
|
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
|
||||||
|
|
||||||
|
Details: \\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node is not a valid Win32 application.
|
||||||
|
\\?\C:\Repos\MOPC\node_modules\.prisma\client\query_engine-windows.dll.node] {
|
||||||
|
clientVersion: '6.19.2',
|
||||||
|
errorCode: undefined,
|
||||||
|
retryable: undefined
|
||||||
|
}
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /accept-invite couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /accept-invite couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /error couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /error couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Generating static pages (9/37)
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /verify-email couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /verify-email couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /verify couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /verify couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /onboarding couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /onboarding couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /login couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /login couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Auth check failed in auth layout: Error: Dynamic server usage: Route /set-password couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error
|
||||||
|
at s (C:\Repos\MOPC\.next\server\chunks\4586.js:1:28328)
|
||||||
|
at m (C:\Repos\MOPC\.next\server\chunks\2171.js:437:9047)
|
||||||
|
at <unknown> (C:\Repos\MOPC\.next\server\chunks\2171.js:404:57912)
|
||||||
|
at h (C:\Repos\MOPC\.next\server\app\(auth)\error\page.js:1:4755)
|
||||||
|
at stringify (<anonymous>) {
|
||||||
|
description: "Route /set-password couldn't be rendered statically because it used `headers`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
|
||||||
|
digest: 'DYNAMIC_SERVER_USAGE'
|
||||||
|
}
|
||||||
|
Generating static pages (18/37)
|
||||||
|
Generating static pages (27/37)
|
||||||
|
✓ Generating static pages (37/37)
|
||||||
|
Finalizing page optimization ...
|
||||||
|
Collecting build traces ...
|
||||||
|
|
||||||
|
Route (app) Size First Load JS
|
||||||
|
┌ ƒ / 182 B 112 kB
|
||||||
|
├ ○ /_not-found 171 B 103 kB
|
||||||
|
├ ƒ /accept-invite 5.17 kB 141 kB
|
||||||
|
├ ƒ /admin 3.54 kB 275 kB
|
||||||
|
├ ƒ /admin/audit 8.76 kB 172 kB
|
||||||
|
├ ƒ /admin/awards 4.82 kB 141 kB
|
||||||
|
├ ƒ /admin/awards/[id] 13.2 kB 196 kB
|
||||||
|
├ ƒ /admin/awards/[id]/edit 6.02 kB 182 kB
|
||||||
|
├ ƒ /admin/awards/new 5.71 kB 182 kB
|
||||||
|
├ ƒ /admin/learning 180 B 106 kB
|
||||||
|
├ ƒ /admin/learning/[id] 10.5 kB 190 kB
|
||||||
|
├ ƒ /admin/learning/new 7.85 kB 184 kB
|
||||||
|
├ ƒ /admin/members 12.5 kB 191 kB
|
||||||
|
├ ƒ /admin/members/[id] 5.71 kB 197 kB
|
||||||
|
├ ƒ /admin/members/invite 6.6 kB 188 kB
|
||||||
|
├ ƒ /admin/mentors 171 B 103 kB
|
||||||
|
├ ƒ /admin/mentors/[id] 171 B 103 kB
|
||||||
|
├ ƒ /admin/partners 180 B 106 kB
|
||||||
|
├ ƒ /admin/partners/[id] 7.44 kB 187 kB
|
||||||
|
├ ƒ /admin/partners/new 4.2 kB 181 kB
|
||||||
|
├ ƒ /admin/programs 2.47 kB 147 kB
|
||||||
|
├ ƒ /admin/programs/[id] 180 B 106 kB
|
||||||
|
├ ƒ /admin/programs/[id]/apply-settings 12.4 kB 224 kB
|
||||||
|
├ ƒ /admin/programs/[id]/edit 6.22 kB 186 kB
|
||||||
|
├ ƒ /admin/programs/new 4.83 kB 150 kB
|
||||||
|
├ ƒ /admin/projects 16.9 kB 207 kB
|
||||||
|
├ ƒ /admin/projects/[id] 10.1 kB 205 kB
|
||||||
|
├ ƒ /admin/projects/[id]/edit 7.67 kB 220 kB
|
||||||
|
├ ƒ /admin/projects/[id]/mentor 8.78 kB 154 kB
|
||||||
|
├ ƒ /admin/projects/import 10.5 kB 201 kB
|
||||||
|
├ ƒ /admin/projects/new 4.42 kB 195 kB
|
||||||
|
├ ƒ /admin/projects/pool 6.56 kB 186 kB
|
||||||
|
├ ƒ /admin/reports 4.6 kB 308 kB
|
||||||
|
├ ƒ /admin/rounds 9.6 kB 211 kB
|
||||||
|
├ ƒ /admin/rounds/[id] 16.4 kB 210 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/assignments 16.2 kB 196 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/coi 8.73 kB 188 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/edit 10.2 kB 240 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/filtering 504 B 103 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/filtering/results 7.69 kB 187 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/filtering/rules 8.18 kB 188 kB
|
||||||
|
├ ƒ /admin/rounds/[id]/live-voting 8.68 kB 169 kB
|
||||||
|
├ ƒ /admin/rounds/new 3.72 kB 227 kB
|
||||||
|
├ ƒ /admin/settings 21.6 kB 226 kB
|
||||||
|
├ ƒ /admin/settings/tags 8.33 kB 214 kB
|
||||||
|
├ ƒ /api/auth/[...nextauth] 171 B 103 kB
|
||||||
|
├ ƒ /api/cron/reminders 171 B 103 kB
|
||||||
|
├ ƒ /api/email/change-password 171 B 103 kB
|
||||||
|
├ ƒ /api/email/verify-credentials 171 B 103 kB
|
||||||
|
├ ƒ /api/health 171 B 103 kB
|
||||||
|
├ ƒ /api/storage/local 171 B 103 kB
|
||||||
|
├ ƒ /api/trpc/[trpc] 171 B 103 kB
|
||||||
|
├ ƒ /apply/[slug] 954 B 387 kB
|
||||||
|
├ ƒ /apply/edition/[programSlug] 959 B 388 kB
|
||||||
|
├ ○ /email/change-password 6.79 kB 118 kB
|
||||||
|
├ ƒ /error 4.01 kB 118 kB
|
||||||
|
├ ƒ /jury 4.37 kB 119 kB
|
||||||
|
├ ƒ /jury/assignments 180 B 106 kB
|
||||||
|
├ ƒ /jury/awards 3.11 kB 139 kB
|
||||||
|
├ ƒ /jury/awards/[id] 5.59 kB 151 kB
|
||||||
|
├ ƒ /jury/learning 5.09 kB 137 kB
|
||||||
|
├ ƒ /jury/live/[sessionId] 7.21 kB 149 kB
|
||||||
|
├ ƒ /jury/projects/[id] 4.12 kB 144 kB
|
||||||
|
├ ƒ /jury/projects/[id]/evaluate 13.2 kB 225 kB
|
||||||
|
├ ƒ /jury/projects/[id]/evaluation 2.09 kB 116 kB
|
||||||
|
├ ƒ /live-scores/[sessionId] 6.93 kB 139 kB
|
||||||
|
├ ƒ /login 6.09 kB 120 kB
|
||||||
|
├ ƒ /mentor 7.77 kB 144 kB
|
||||||
|
├ ƒ /mentor/projects 5.51 kB 141 kB
|
||||||
|
├ ƒ /mentor/projects/[id] 8.58 kB 148 kB
|
||||||
|
├ ƒ /mentor/resources 5.12 kB 137 kB
|
||||||
|
├ ƒ /my-submission 6.82 kB 146 kB
|
||||||
|
├ ƒ /my-submission/[id] 11.7 kB 155 kB
|
||||||
|
├ ƒ /my-submission/[id]/team 8 kB 216 kB
|
||||||
|
├ ƒ /observer 2.98 kB 114 kB
|
||||||
|
├ ƒ /observer/reports 5.63 kB 309 kB
|
||||||
|
├ ƒ /onboarding 5.44 kB 313 kB
|
||||||
|
├ ƒ /set-password 7.55 kB 143 kB
|
||||||
|
├ ƒ /settings/profile 4.25 kB 210 kB
|
||||||
|
├ ƒ /verify 180 B 106 kB
|
||||||
|
└ ƒ /verify-email 171 B 103 kB
|
||||||
|
+ First Load JS shared by all 103 kB
|
||||||
|
├ chunks/1255-39d374166396f9e9.js 45.7 kB
|
||||||
|
├ chunks/4bd1b696-100b9d70ed4e49c1.js 54.2 kB
|
||||||
|
└ other shared chunks (total) 2.87 kB
|
||||||
|
|
||||||
|
|
||||||
|
ƒ Middleware 86.5 kB
|
||||||
|
|
||||||
|
○ (Static) prerendered as static content
|
||||||
|
ƒ (Dynamic) server-rendered on demand
|
||||||
|
|
||||||
@@ -51,8 +51,9 @@ COPY --from=builder /app/node_modules ./node_modules
|
|||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder /app/prisma ./prisma
|
||||||
COPY --from=builder /app/package.json ./package.json
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
|
||||||
# Copy CSV data file for manual seeding
|
# Copy files needed for seeding (tsx needs tsconfig for path resolution)
|
||||||
COPY --from=builder /app/docs/candidatures_2026.csv ./docs/candidatures_2026.csv
|
COPY --from=builder /app/docs/Candidatures2026.csv ./docs/Candidatures2026.csv
|
||||||
|
COPY --from=builder /app/tsconfig.json ./tsconfig.json
|
||||||
|
|
||||||
# Copy entrypoint script
|
# Copy entrypoint script
|
||||||
COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh
|
COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
# MinIO and Poste.io are external services connected via environment variables.
|
# MinIO and Poste.io are external services connected via environment variables.
|
||||||
#
|
#
|
||||||
# The app image is built by Gitea CI and pushed to the container registry.
|
# The app image is built by Gitea CI and pushed to the container registry.
|
||||||
# To pull the latest image: docker compose pull app
|
# `pull_policy: always` ensures `docker compose up -d` checks for newer app images.
|
||||||
# To deploy: docker compose up -d
|
# The app entrypoint runs `prisma migrate deploy` before starting Next.js.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
image: ${REGISTRY_URL}/mopc-app:latest
|
image: ${REGISTRY_URL}/mopc-app:latest
|
||||||
|
pull_policy: always
|
||||||
container_name: mopc-app
|
container_name: mopc-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
dns:
|
dns:
|
||||||
|
|||||||
@@ -1,11 +1,37 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -eu
|
||||||
|
|
||||||
echo "==> Resolving any failed migrations..."
|
MAX_MIGRATION_RETRIES="${MIGRATION_MAX_RETRIES:-30}"
|
||||||
npx prisma migrate resolve --rolled-back 20260207000000_universal_apply_programid 2>/dev/null || true
|
MIGRATION_RETRY_DELAY_SECONDS="${MIGRATION_RETRY_DELAY_SECONDS:-2}"
|
||||||
|
ATTEMPT=1
|
||||||
|
|
||||||
echo "==> Running database migrations..."
|
echo "==> Running database migrations (with retry)..."
|
||||||
npx prisma migrate deploy
|
until npx prisma migrate deploy; do
|
||||||
|
if [ "$ATTEMPT" -ge "$MAX_MIGRATION_RETRIES" ]; then
|
||||||
|
echo "ERROR: Migration failed after ${MAX_MIGRATION_RETRIES} attempts."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Migration attempt ${ATTEMPT} failed. Retrying in ${MIGRATION_RETRY_DELAY_SECONDS}s..."
|
||||||
|
ATTEMPT=$((ATTEMPT + 1))
|
||||||
|
sleep "$MIGRATION_RETRY_DELAY_SECONDS"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Generating Prisma client..."
|
||||||
|
npx prisma generate
|
||||||
|
|
||||||
|
# Auto-seed on first startup: check if Users table is empty
|
||||||
|
USER_COUNT=$(node -e "
|
||||||
|
const { PrismaClient } = require('@prisma/client');
|
||||||
|
const p = new PrismaClient();
|
||||||
|
p.user.count().then(c => { console.log(c); p.\$disconnect(); }).catch(() => { console.log('0'); p.\$disconnect(); });
|
||||||
|
" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$USER_COUNT" = "0" ]; then
|
||||||
|
echo "==> Empty database detected — running seed..."
|
||||||
|
npx prisma db seed || echo "WARNING: Seed script failed."
|
||||||
|
else
|
||||||
|
echo "==> Database already seeded ($USER_COUNT users found), skipping seed."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> Starting application..."
|
echo "==> Starting application..."
|
||||||
exec node server.js
|
exec node server.js
|
||||||
|
|||||||
1023
docs/Candidatures2026.csv
Normal file
1023
docs/Candidatures2026.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
|||||||
Notes: Filtering Round: Criteria- Older than 3 years (for all in the startup category), those who submit something random (like a spam project) (AI?)
|
Notes: Filtering Round: Criteria- Older than 3 years (for all in the startup category), those who submit something random (like a spam project) (AI?)
|
||||||
|
|
||||||
-Add filters to the page (who sent documents, etc.)
|
-Add filters to the page (who sent documents, etc.)
|
||||||
|
|
||||||
-Partners section should be a semi-crm system to track possible sponsors and partners
|
-Partners section should be a semi-crm system to track possible sponsors and partners
|
||||||
|
|
||||||
-No translation into french (no localization)
|
-No translation into french (no localization)
|
||||||
|
|
||||||
-Ameliorate the user experience (make it more simple)
|
-Ameliorate the user experience (make it more simple)
|
||||||
|
|
||||||
-Special Awards- Specific Jury Members (jury members will have to choose amongst projects that fit specific criteria (like the country they're based))- Spotlight on Africa, Coup de Coeur - Make a special award section and make a special case for judge invitation for special awards and allow us to make special awards and assign them to the selection of judges for the special awards specifically (And give them their own login space to see everything)
|
-Special Awards- Specific Jury Members (jury members will have to choose amongst projects that fit specific criteria (like the country they're based))- Spotlight on Africa, Coup de Coeur - Make a special award section and make a special case for judge invitation for special awards and allow us to make special awards and assign them to the selection of judges for the special awards specifically (And give them their own login space to see everything)
|
||||||
-Invite jury member (with tag for special awards) --> Make special award (Criteria needed, Add a tag for special award (so for example, if a location is italy it will auto have the tag for COup de Coeur (since it's criteria is it only exists in certain countries))) - This is also a separate Jury Round - Use AI to sort through elligible projects based on the plain-language criteria (so the AI interprets the criteria and all projects and smart-assigns them to the round) - Make sure this round allows them to simply choose which project will win (since they have independent criteria) - Make a mix of voting for which project wins the project (or recommended project for the award), but also in the round they can simply assign a project to the award without any criteria requirements and such
|
-Invite jury member (with tag for special awards) --> Make special award (Criteria needed, Add a tag for special award (so for example, if a location is italy it will auto have the tag for COup de Coeur (since it's criteria is it only exists in certain countries))) - This is also a separate Jury Round - Use AI to sort through elligible projects based on the plain-language criteria (so the AI interprets the criteria and all projects and smart-assigns them to the round) - Make sure this round allows them to simply choose which project will win (since they have independent criteria) - Make a mix of voting for which project wins the project (or recommended project for the award), but also in the round they can simply assign a project to the award without any criteria requirements and such
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,594 +0,0 @@
|
|||||||
Full name,Application status,Category,"Comment ",Country,Date of creation,E-mail,How did you hear about MOPC?,Issue,Jury 1 attribués,MOPC team comments,Mentorship,PHASE 1 - Submission,PHASE 2 - Submission,Project's name,Team members,Tri par zone,Téléphone,University
|
|
||||||
Chaima BEN GRIRA,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,,TN,2023-01-19,cbengrira@blueeconomy.ogs.it,,Reduction of pollution (plastics chemicals noise light...),,,false,,,Bluepsol,"Eskander ALAYA, Chaima BEN GRIRA, Nabil FOGHRI, Ahmed BACCOUCHE, Adel JELJLI","Africa, Tunisia",+393508394071,
|
|
||||||
James Carter-Johnson,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"To Farm Giant Kelp at Scale, on special racks, capturing 30x more C02 than forrest per hecare. Harvest 4 times a year for oganic fertilizer, algin and materials for bio-plastics. All these replace highly polluting oil based products on land.",GB,2024-06-06,james@bigkelp.com,You contacted me I think.,Mitigation of climate change and sea-level rise,,,false,https://drive.google.com/drive/folders/1R5-IfGbETFri6ZX0RnJY8W6wan7cLoz-?usp=drive_link,,Big Kelp,James Carter-Johnson MA MBA; Prof. Carole Llewelyn MSc PhD; Vincent Doumeizel; Carlos Vanegas MSc PhD; James Sainty BA MBA; Akhthar Swaebe BT MSc MBA; Peter Rivera MSc PhD; Alessio Massironi MSc PhD; Johannes van der Merwe ME CE PhD; Oliver Parker BSc MSc,UK,+447899791166,
|
|
||||||
Silvia Ruiz-Berdejo,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We are a biofoodtech startup specializing in microalgae and plant-based functional ingredients within the blue economy. Our R&D targets sectors like Functional Food Formulation, Precision Food Nutrition, and Nutricosmetics.
|
|
||||||
|
|
||||||
We develop new ingredients that replace fats, sugars, and additives in ultra-processed foods while replicating traditional textures, colors, and flavors to ease consumer transitions to healthier diets.
|
|
||||||
|
|
||||||
Our clean-label formulations support easy industrial integration and rapid scale-up for B2B clients in the food industry, health and wellness groups, innovative food brands, and sports teams. This advances sustainable functional nutrition aligned with blue economy principles",ES,2024-01-11,silvia@omnivorus.com,Linkedlin,Other,,,true,https://drive.google.com/drive/folders/1A8jzY7h4pfebbQKvCtg0Fc0AKzUE1F_q?usp=drive_link,,Omnivorus Smartfood,"Silvia rui-Berdejo CEO -Cofounder , Toni Gonzalez CPO - Cofounder, Luis Pascual CFO , Jose Tornero R&D Funtional Food , Carlota Villanueva-Tobaldo R&D Nutro cosmetic","Europe, Spain",+34622381855,
|
|
||||||
Achyut Karn,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"OceanGuardian AI: Predictive Ocean Protection Through Autonomous Intelligence
|
|
||||||
|
|
||||||
The Problem We're Solving
|
|
||||||
|
|
||||||
Ocean conservation today operates in crisis mode. We discover dead zones after they form, find pollution after it spreads, and detect coral bleaching after ecosystems collapse. Current monitoring methods are expensive, sporadic, and reactive—providing data only after irreversible damage occurs. The ocean needs an early warning system, not an autopsy report.
|
|
||||||
|
|
||||||
Critical gaps in current approaches:
|
|
||||||
- Monitoring covers less than 5% of critical marine zones
|
|
||||||
- Research-grade equipment costs $50,000+ per unit, limiting deployment
|
|
||||||
- Data collection happens quarterly or annually—far too slow for dynamic threats
|
|
||||||
- No predictive capability to prevent ecosystem collapse before it happens
|
|
||||||
- Communities lack real-time information to protect their local waters
|
|
||||||
|
|
||||||
Our Innovation: The World's First Predictive Ocean Protection Network
|
|
||||||
|
|
||||||
OceanGuardian AI deploys networks of affordable, solar-powered autonomous underwater drones that create continuous, real-time monitoring of marine ecosystems. But we don't just collect data—our AI predicts threats 2-8 weeks before critical damage occurs, enabling intervention while ecosystems can still be saved.
|
|
||||||
|
|
||||||
Core Technology Components:
|
|
||||||
|
|
||||||
1. Affordable Autonomous Drones ($800/unit)
|
|
||||||
- Solar and wave-energy powered for perpetual operation
|
|
||||||
- Multi-sensor array monitors 15+ parameters simultaneously
|
|
||||||
- Computer vision and acoustic sensors for marine life tracking
|
|
||||||
- Swarm intelligence enables coordinated monitoring
|
|
||||||
- Modular design adapts for different missions
|
|
||||||
|
|
||||||
2. Predictive AI Engine
|
|
||||||
- Machine learning models trained on oceanographic data
|
|
||||||
- Predicts coral bleaching events, harmful algal blooms, oxygen depletion
|
|
||||||
- Identifies microplastic accumulation hotspots
|
|
||||||
- Detects illegal fishing and pollution incidents in real-time
|
|
||||||
- Creates digital twin models of monitored ecosystems
|
|
||||||
|
|
||||||
3. Real-Time Intervention System
|
|
||||||
- Automated alerts to authorities, NGOs, and c",IN,,achyut.karn.2025@sse.ac.in,Linkedin,Technology & innovations,,,true,,,OceanGuardian AI,"Rishan Narula, Saanvi Mahajan",Asia,+916204778589,Symbiosis School of Economics
|
|
||||||
Laurent BUOB,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Whisper 360, a foiling boat with the performance of a thermal boat, powered by electricity: 45 knots, 100 nautical miles, zero emissions.",FR,2024-09-30,l.buob@whisper-ef.com,We were incubated at Monaco Tech,Sustainable shipping & yachting,,,false,,,Whisper eF,Vincent Lebeault,"Europe, France",+33675090543,
|
|
||||||
Adrien BARRAU,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Seavium is an AI platform that reduces the environmental footprint of offshore operations by eliminating unnecessary vessel movements.
|
|
||||||
Fragmented data and inefficient sourcing lead to avoidable transits, excess fuel use and emissions across the sector.
|
|
||||||
|
|
||||||
Seavium matches each offshore need with the closest, most suitable vessel in real time, using technical data and AIS availability. This optimisation cuts transit miles and fuel consumption at scale.
|
|
||||||
|
|
||||||
Early results show 18–25% fewer miles sailed and 5–12% fuel savings per operation.
|
|
||||||
With 20 000+ vessels mapped and 118 companies already engaged, the model is globally scalable.
|
|
||||||
|
|
||||||
Seavium combines a SaaS subscription with performance-based fees, ensuring that environmental impact increases with platform adoption.",FR,2024-04-01,adrien@seavium.com,via GreenwaterFoundation,Technology & innovations,,,true,https://drive.google.com/drive/folders/1fUCrWCyXQHWEcacseTa338-RPn53KnZy?usp=drive_link,,SEAVIUM,Adrien BARRAU / Samuel DRAI,"Europe, France",+33646221977,
|
|
||||||
Nitya Gunturu,Received,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"We aim to create innovative, sustainable mycelium-based packaging materials designed to replace single-use Styrofoam and plastic in transportation and e-commerce sectors.
|
|
||||||
Problem and Solution Statements
|
|
||||||
|
|
||||||
Problem 1: Plastic Pollution (Land, Water, Air) and Non-Biodegradability
|
|
||||||
The Problem:
|
|
||||||
Over 300 million tons of plastic are produced globally each year, with around 45% being single-use packaging. Styrofoam and plastic foams take up to 500 years or more to decompose, causing persistent pollution.
|
|
||||||
|
|
||||||
Our Solution:
|
|
||||||
We develop 100% biodegradable mycelium packaging that decomposes naturally in 30 to 90 days, enabling a circular economy.
|
|
||||||
|
|
||||||
Problem 2: High Carbon Footprint of Production
|
|
||||||
The Problem:
|
|
||||||
Plastic production contributes about 3.4% of global greenhouse gas emissions, heavily reliant on fossil fuels.
|
|
||||||
|
|
||||||
Our Solution:
|
|
||||||
Our process uses renewable agricultural waste and fungal growth, reducing carbon emissions by up to 70–90% compared to plastics.
|
|
||||||
|
|
||||||
Problem 3: Less Use of Plants and Other Natural Resources
|
|
||||||
The Problem:
|
|
||||||
Conventional bio-packaging often requires dedicated crops, which leads to over-exploitation of valuable land and water resources.
|
|
||||||
|
|
||||||
Our Solution:
|
|
||||||
We convert locally sourced agricultural waste into packaging, requiring significantly less land or water resources.
|
|
||||||
|
|
||||||
Problem 4: Agricultural Waste Mismanagement
|
|
||||||
The Problem:
|
|
||||||
India produces over 500 million tons of crop residue annually, much of which is burned, causing severe air pollution impacting millions.
|
|
||||||
|
|
||||||
Our Solution:
|
|
||||||
We utilize this waste as raw material, reducing harmful burning and creating economic value for rural producers.",IN,,nityagunturu95@gmail.com,University,Reduction of pollution (plastics chemicals noise light...),,,true,https://drive.google.com/drive/folders/1322p0iOzB-d66xlZV85oBEOf9gWOsNkq?usp=sharing,,MycoWrap,Nitya Gunturu and Avni Mishra,Asia,+917680093169,"Ashoka University, India"
|
|
||||||
Hasan Noor Ahmed,Received,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"The Blue Coast Guardians Initiative is a youth-led, community-centered program designed by Bilan Awdal Organization to combat coastal pollution, restore marine ecosystems, and create sustainable blue-economy opportunities along the Somaliland/Somalia coastline.
|
|
||||||
Our approach combines innovative low-cost technologies, community livelihoods, and education, enabling coastal communities to protect the ocean while improving their economic resilience.
|
|
||||||
|
|
||||||
The project targets urgent threats in the region, including plastic pollution, illegal fishing, coastal erosion, and the loss of marine biodiversity.",SO,,biland.awdal.org@gmail.com,Fund for NGO,Capacity building for coastal communities,,,false,https://drive.google.com/drive/folders/1Oz9lQCfhQqw818QegNj9S_SvArSQwZFw?usp=drive_link,,BlueGuard Africa – Community-Driven Ocean & Coastal Protection Innovation Hub,"Hasan Noor Ahmed – Chairman & Founder Amina Abdillahi Ibrahim – Program Director (Health & Nutrition) Mohamed Abdi Warsame – Finance & Administration Officer Hodan Ismail Ali – Climate & Environment Program Lead Abdirahman Yusuf Farah – Monitoring, Evaluation & Learning Officer Fardowsa Ahmed Jama – Community Outreach & Protection Coordinator","Africa, Somalia",+491737752964,Bilan Awdal Organization – Training & Capacity Development Unit
|
|
||||||
ssentubiro billy,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,allow needy children access quality education,UG,2016-08-13,lemanfoundation16@gmail.com,via social media,Capacity building for coastal communities,,,true,,,schoolarships,Nakayulu Grace and ssentubiro billy,"Africa, Ouganda",+256708630034,
|
|
||||||
Ramsay Bader,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"PosidoniaGuard is a turnkey service that helps Mediterranean marinas and coastal authorities stop anchor damage to Posidonia oceanica seagrass meadows by installing seagrass-safe “eco-moorings”, managing no-anchoring zones via a simple booking app, and quantifying the blue-carbon and biodiversity benefits for funders and regulators. Posidonia meadows are critical “blue forests” that store large amounts of carbon, support fisheries and protect coasts, but up to about 34% have already been lost, with tens of thousands of hectares damaged annually by anchoring.
|
|
||||||
|
|
||||||
Objectives:
|
|
||||||
|
|
||||||
1. Protect and restore Posidonia meadows by replacing destructive chain moorings and ad-hoc anchoring with certified eco-moorings in high-pressure bays.
|
|
||||||
|
|
||||||
2. Guide boaters away from seagrass using a digital map and reservation system that clearly marks no-anchor zones and available eco-moorings.
|
|
||||||
|
|
||||||
3. Measure and monetise impact by estimating hectares of seagrass protected and associated blue-carbon storage and ecosystem-service value, creating reporting for marinas, municipalities and impact investors.",US,,Ramsay.Bader@gmail.com,Through my University.,Blue Carbon,,,true,,,PosidoniaGuard,Ramsay Bader. Caroline Hulbert.,US,+16468972588,University of St Andrews. United Kingdom.
|
|
||||||
Adrian Colline Odira,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Our project aims to build a fully circular, climate smart aquaculture model that reduces pressure on overfished natural water bodies while empowering coastal and lakeside communities. By integrating sustainable fish production, renewable energy systems (biogas and solar), digital traceability, and community led cage farming, we create an alternative source of affordable, high quality protein that eases exploitation of lake and ocean ecosystems.
|
|
||||||
|
|
||||||
Objectives
|
|
||||||
|
|
||||||
Reduce dependence on open water fishing by scaling sustainable cage and pond aquaculture systems.
|
|
||||||
|
|
||||||
Empower women and youth with ownership of production units, fair market access, and technical training.
|
|
||||||
|
|
||||||
Increase ocean and freshwater protection by promoting regenerative practices, responsible feed use, and cold-chain efficiency to minimise post harvest loss.
|
|
||||||
|
|
||||||
Deploy digital tools to track origin, ensure transparency, and support ecosystem friendly decision making.
|
|
||||||
|
|
||||||
This approach strengthens food security, grows blue economy incomes, and protects aquatic ecosystems through a scalable, community-centered model.",KE,2018-08-20,adrian@riofish.co.ke,LinkedIn,Sustainable fishing and aquaculture & blue food,,,true,,,Rio Fish Limited,"Adrian Colline Odira, Loren Edwina Odira","Africa, Kenya",+254742838455,
|
|
||||||
Mohammad Badran,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Vision
|
|
||||||
Our vision is to be a global supporter to marine and coastal ecosystems’ stewardship, fostering a future where tropical and subtropical marine environments thrive in harmony with human activities. We envision vibrant resilient marine ecosystems that support biodiversity, enhance climate stability, and contribute to viable sustainable development with diversified livelihoods for the local communities.
|
|
||||||
|
|
||||||
Mission
|
|
||||||
Our mission is to deliver innovative and sustainable management solutions that advance development in tropical and subtropical marine and coastal areas maintaining ecosystems’ health and resilience. We endeavor to harness broad stakeholders’ involvement, community engagement, scientific research, local knowledge, and cutting-edge technology for supporting development in tropical seas to protect and restore ecosystems’ biodiversity and functionality while achieving stakeholders’ interests and local communities’ contentment.
|
|
||||||
|
|
||||||
Approach
|
|
||||||
Our approach is to harness the local knowledge and expertise in all our projects. We will do consultancy work and target nationally, regionally and internationally supported initiatives. We will keep a small team for coordination and management, but our heavy weight will be the local performers in the field. Implementing multiple local projects, we will build an effective Platform for Global Dialogue and Exchange of Experience
|
|
||||||
|
|
||||||
Objectives
|
|
||||||
Our objectives are highly ambitious and divers. We realize the hard work ample time they need to be achieved. But we trust that our approach that counts on the local knowledge and expertise will make our mission achievable. Our objectives include:
|
|
||||||
Conservation and Restoration
|
|
||||||
o Develop and implement science-based and local knowledge strategies for conservation and restoration of critical marine habitats and the biodiversity they support, including coral reefs, mangroves, and seagrass beds.
|
|
||||||
o Monitor and assess coastal and marine ecosystems’ health and the stressors they face to gu",JO,2024-08-30,ceo@martropic.com,From Canada's Ocean Supercluster,Restoration of marine habitats & ecosystems,,,true,,,MarTropic Canada Inc.,Mohammad Badran and Hala Marouf,Asia,+18733557575,
|
|
||||||
Danail Marinov,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"What it is (TRL 4–5): Pilot-ready, AI collaborative platform for GHG emissions Scope 1–3 monitoring, compliance reporting and forecasting for ports/terminals/shipping companies. RedGet.io was among the selected companies and participated in ADT4Blue, EY Startup Academy Germany, Blue Readiness Assistance and Green Marine Med (by Port of Barcelona) programs.
|
|
||||||
Value: Up to 60% reduction in reporting efforts and costs, emission forecasting for EU-ETS regulations, AI maritime assistant and decision-ready visibility to plan and verify decarbonization.
|
|
||||||
Status & partners: Confirmed pilot with Port of Gdynia (Jan 2026) and Port of Talling (Jan 2026); negotiations with Port of Valencia, Port of Huelva, and EY Bulgaria.",BG,2024-12-01,dmarinov@redget.io,A friend of mine shared this opportunity to me,Technology & innovations,,,false,,,RedGet.io,"Danail Marinov, Dobromir Balabanov, Alexander Valchev","Bulgaria, Europe",+359895497694,
|
|
||||||
Shelby Thomas,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Ocean Rescue Alliance International, through its Coastal Resilience Solutions for-profit arm and the We Restore initiative, deploys scalable living shoreline and hybrid reef technologies to restore degraded coastal and marine ecosystems while enhancing climate resilience for vulnerable communities. The project’s objective is to deliver measurable ocean biodiversity recovery, erosion reduction, and carbon co-benefits through science-based, nature-positive infrastructure that can be replicated regionally and globally.",US,2019-12-01,admin@oceanrescuealliance.org,via Email Newsletter,Restoration of marine habitats & ecosystems,,,true,,,Coastal Resilience Solutions: WeRestore,"Dr. Shelby Thomas, Dr. David Weinstein, Lindsay Humbles,",US,+13866897675,
|
|
||||||
Maaire Gyengne Francis,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Problem and Solution
|
|
||||||
|
|
||||||
Urban cities across Africa face a severe plastic waste crisis driven by rapid population growth, heavy consumption of plastic-packaged products, and inadequate formal waste management infrastructure. Most households lack convenient, reliable, and affordable waste disposal options, forcing them to depend on informal collectors with limited capacity and inconsistent schedules - or resort to harmful practices such as burning, burying, or illegally dumping plastic waste in gutters, waterways, and open spaces. This results in widespread pollution, health hazards, clogged drainage systems, flooding, and the loss of valuable recyclable material that could support local and global circular economy markets. Also, recycling companies lack consistent, traceable, and high-quality access to plastic feedstock.
|
|
||||||
|
|
||||||
Our solution is to develop an AI-powered platform that helps urban households dispose of plastic waste by connecting them with local collectors through image, video, or weight-based pricing and cashless payments. It tackles severe plastic pollution in African cities caused by limited collection capacity and unsafe disposal practices. With millions of households generating increasing waste, the market potential is vast across Ghana and other rapidly urbanizing regions. Once consistent collection volumes are reached, WasteTrack will expand into a global plastic trading marketplace, enabling recyclers worldwide to buy verified, traceable plastic waste - positioning the startup as a major player in the circular plastics economy.
|
|
||||||
|
|
||||||
Our AI-driven waste management and digital payment solution is designed to make plastic disposal easy, convenient, and traceable for urban households. Key features will include photo, video, or weight-based AI analysis to estimate disposal fees; secure digital payments; GPS-linked pickup requests; and unique tracking codes for every waste package. The platform also supports community micro-dumpsites for flexible drop-off and pr",GH,2025-01-01,gyengnefrancis90@gmail.com,Google search,Reduction of pollution (plastics chemicals noise light...),,,true,https://drive.google.com/drive/folders/1Rv9W6h5zQESX7A68bQio5JWy5TML86rH?usp=drive_link,,WasteTrack,"Frank Faarkuu, Prosper Dorfiah","Africa, Ghana",+233208397960,
|
|
||||||
Vincent Kneefel,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,,NL,2024-04-16,vincent@vitalocean.io,Linkedin,Technology & innovations,,,true,,,Vital Ocean,Joi Danielson,"Europe, Netherland",+31622514465,
|
|
||||||
Raismin Kotta,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,Sustainability fisheries and Aquaculture,ID,,raisminkotta88@gmail.com,I hear and read MOPC in website and interested to apply,Sustainable fishing and aquaculture & blue food,,,true,,,The Pearls cultuvation & Pearls jewelry,"Raismin Kotta, aya sophia, Lalu harianza,asril junaidy",Asia,+6281342018565,"45 University, Mataram Indonesia"
|
|
||||||
Anastasiia,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,Take technology onto another level,UA,,grozdova.anastasiia@gmail.com,Social media marketing,Technology & innovations,,,true,,,Innovations in ocean environment,Darina Mitina,"Europe, Ukraine",+380680650309,
|
|
||||||
Raphaëlle Guénard,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Filae transforms end-of-life fishing nets into ultra-light, modular supports for plant-based shading and greening (façades and canopies), helping cool down dense urban areas without heavy structures.
|
|
||||||
Our goal is to scale a Mediterranean circular model, from local net collection to on-site deployment, reducing waste and embodied carbon while boosting thermal comfort and biodiversity through real-world pilots.",FR,2025-03-21,contact@filae.eu,"from Marine Jacq-Pietri, Coordinatrice du Monaco Ocean Protection Challenge",Reduction of pollution (plastics chemicals noise light...),,,true,,,Filae,Raphaëlle Guénard & Killian Bossé,"Europe, France",+33663688277,
|
|
||||||
Pavel Kartashov,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Scalable and capital-light hybrid ocean energy platforms harvesting wave, sun and wind energy in near-shore areas for shore and offshore energy end-users",MK,2025-03-05,pavel.k@wavespark.co,Social media post,Technology & innovations,,,false,https://drive.google.com/drive/folders/1vdcWHlPUURdN69T-Ek7wsqOTrLNaODq0?usp=drive_link,,WaveSpark Green Marine Energies,"Pavel Kartashov, Rodrigo Caba, Francisco Perez, Glib Ivanov","Europe, Macedonia",+38975588771,
|
|
||||||
Coral Bisson,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,- Reduction of ocean plastics through development of swimwear using recycled ocean plastics,JE,,coralbisson@icloud.com,University,Reduction of pollution (plastics chemicals noise light...),,,true,,,Corali,Coral Bisson,"Europe, Jersey",+377643915342,International University of Monaco
|
|
||||||
Carol Nkawaga Moonga,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,,ZM,2024-07-11,moongacaroln@gmail.com,I saw an advertisement on LinkedIn,Sustainable fishing and aquaculture & blue food,,,true,https://drive.google.com/drive/folders/1wEWiGREhq-dWPkFqGqmSK89PcuOjhsXX?usp=drive_link,,Kacachi General Dealers,Cathrine Kapesha,"Africa, Zambia",+260979164462,
|
|
||||||
Peter Teye Busumprah,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"This initiative aims to bridge the gap in ocean data across Africa by establishing a standardized platform for accessing African Ocean Biodiversity information. The project involves developing an African Ocean Biodiversity Atlas that provides detailed data on Blue Carbon and Fisheries ecosystems, including GPS coordinates, high-resolution images, and videos illustrating the state of coastal environments throughout Africa. To ensure accessibility, we are utilizing affordable, locally developed technologies and multifunctional ocean applications to map key ecosystems such as fisheries, seaweeds, seagrasses, mangroves, and other ocean biodiversity ecosystems along the continent’s coastlines.
|
|
||||||
|
|
||||||
Our team has grown significantly from 8 to 40 members, representing 20 African nations. Currently, over 800 users are engaged, and a pilot map encompasses ten African countries. We anticipate generating approximately $240,000 annually from app downloads and technology sales, with projected monthly revenues of about $20,000. This includes $7,000 from subscriptions, $7,000 from data sales, $3,000 from licensing, and $3,000 from consulting services.
|
|
||||||
|
|
||||||
The database is designed for policymakers and academic institutions, offering precise data crucial for policy formulation, research, and publication activities. Additionally, we aim to involve private sector stakeholders who depend on reliable data to inform their investments in a sustainable blue economy.
|
|
||||||
|
|
||||||
Key features include the development of a Fisheries Atlas and a Blue Carbon Biodiversity initiative focused on Africa’s landing beaches, providing strategic recommendations for the establishment of Marine Protected Areas (MPAs). The project also promotes data sharing among local indigenous fishermen and enhances understanding aligned with the UN Ocean Decade objectives. It will create a comprehensive data repository covering various marine species, including fish, mangroves, algae, and seaweeds.
|
|
||||||
|
|
||||||
Links:
|
|
||||||
https://oceandecade.org/action",GH,2024-01-01,petervegan1223@gmail.com,MOPC Linkedin.,Technology & innovations,,,true,,,African Ocean Biodiversity Atlas,Mavis Essilfie,"Africa, Ghana",+233544671951,
|
|
||||||
Nilas Neuhauser,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"The NAUTILUS team is developing the latest generation, and most advanced Autonomous Underwater Glider with the goal of flexibly facilitating the collection of crucial data for aquatic research. By doing so, we seek to create a cost-effective and minimally invasive aquatic research robot.
|
|
||||||
After conducting first successful tests this year, we seek to continue testing our glider in Swiss lakes until summer and then, in September, set off for a 2 week mission to test in the Norwegian Ocean.
|
|
||||||
Find our website here: https://aris-space.ch/our-projects/nautilus/",CH,,nilas.neuhauser@aris-space.ch,from the 1000 Ocean Startups LinkedIn,Technology & innovations,,,true,,,Nautilus,45+ members (Management -> PM: Phillip Zenger ; DPM: Nilas Neuhauser ; SE: Matias Betschen),"Europe, Switzerland",+41792977194,"ETH Zurich, Zurich"
|
|
||||||
Aki Allahgholi,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We will solve the extreme coral restoration bottleneck when it comes to outplanting. The logistical limitations of farming, transporting and outplanting cannot be overcome through the classical methods as of now. Our patented coral paint and spraying mechanism will solve that hurdle.",CH,2025-08-13,aki@corall.eco,LinkedIn,Restoration of marine habitats & ecosystems,,,false,https://drive.google.com/drive/folders/1M8KGN87ZSTEqFP8T2eUccYOE7K7DZNrV?usp=drive_link,,CORAlliance,"Chris Glaser, Peach Zwyssig, Tamaki Bieri, Dave Gulko","Europe, Switzerland",+41763879261,
|
|
||||||
Irina Kharitonova,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"EcoPlaton Tracker is a digital educational and action-oriented platform aimed at protecting oceans by addressing the root causes of pollution on land. The project helps children and families understand how everyday habits—plastic use, chemical products, water consumption, and carbon footprint—affect rivers, lakes, seas, and ultimately the oceans.
|
|
||||||
The platform combines carbon and water impact tracking, eco-challenges, audio guides, and storytelling, including stories about lakes, oceans, and industrial water pollution. It guides users from awareness to action and delivers real environmental impact: part of the project’s revenue supports reforestation and environmental initiatives, with over 1,300 trees already planted in industrial regions of Kazakhstan.
|
|
||||||
EcoPlaton Tracker integrates a Water & Ocean Impact Tracker module that visualizes the “land–water–ocean” pollution pathway and encourages measurable behavior change.",KZ,2025-07-07,irinakharitonova0201@gmail.com,We learned about the Monaco Ocean Protection Challenge last year through Instagram and have been preparing our application since then.,Consumer awareness and education,,,true,,,EcoPlaton Tracker: From Land to Ocean,"Irina Kharitonova, Alexandra Kharitonova, Platon Nechayev",Asia,+77012141077,
|
|
||||||
Fritz Noel Bayong Momha,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"GeoCosta : Application of Geodesign to understand and Innovate in Coastal Protection Planning / Balaz Studio
|
|
||||||
Objectives :
|
|
||||||
-Understand the development of coastal protection in order to contribute to a concerted management focused on adaptation and coastal resilience
|
|
||||||
- Use the concepts of Geodesign and coastal resilience, landscape approach, and consultation in our diagnosis of the protective planning process
|
|
||||||
- Mapping of infrastructures and different actors will illustrate the actions and scenarios of the future vision of this site.",CM,2021-02-07,fbayong@balazstudio.com,LinkedIn,Technology & innovations,,,true,,,GeoCosta : Application of Geodesign to understand and Innovate in Coastal Protection Planning /Balaz Studio,Fritz Bayong,"Africa, Cameroun",+32467868495,
|
|
||||||
Rasmus Borgstrøm,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"FlowMinerals captures CO₂ from seawater and converts it into fossil-free calcium carbonate, contributing to the mitigation of ocean acidification while reducing reliance on land-based limestone mining. The solution enables industrial decarbonization using ocean-compatible materials, with a strong focus on environmental safety and minimal marine impact.
|
|
||||||
www.FlowMinerals.com",DK,2023-09-24,rasmus@blueplanetinnovators.com,LinkedIn,Mitigation of ocean acidification,,,true,,,FlowMinerals,"Rasmus Borgstrøm, Esben Jessen","Denmark, Europe",+4527117113,
|
|
||||||
Amelia Martin,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,We manufacture an eco-friendly alternative to marine foam (marine grade styrofoam).,US,2023-06-13,amelia@mudratsurf.com,Google!,Reduction of pollution (plastics chemicals noise light...),,,true,,,Mud Rat,"Jack Tarka, Patricio Acevedo, Brian Lassy",US,+18606824426,
|
|
||||||
James Kalo Malau,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,,VU,2026-01-01,malau_jk@hotmail.com,Funds for NGOs Premium,Sustainable fishing and aquaculture & blue food,,,true,,,Coral Reforestation,"John Maliu, Josue Jimmy, Nalo Samuel, Manu Roy, James Sulu",Oceania,+6787774965,
|
|
||||||
Jonas Wüst,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Tethys Robotics builds compact autonomous underwater robots that replace emission-intensive vessel operations with remote, low-impact subsea inspection. Our goal is to make offshore maintenance safer and more sustainable by reducing CO₂ emissions, preventing environmental damage through early detection, and improving the reliability of renewable marine infrastructure.",CH,2024-08-15,jonas@tethys-robotics.ch,BRIDGE by Innosuisse forward us.,Technology & innovations,,,false,,,Tethys Robotics,Pragash Sivananthaguru,"Europe, Switzerland",+41766307924,
|
|
||||||
João Manuel de Gouveia Firmino,Received,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Project idea: Convert local fish discards on Madeira into a hygienic, fermented fish sauce (small-batch artisanal → scalable).
|
|
||||||
|
|
||||||
Objectives: Reduce waste; add value for fishers; create local jobs; supply restaurants/retail; position as circular blue-economy premium product.
|
|
||||||
|
|
||||||
Key details: Source = local landings; partners = fishers + certified processor + food-safety lab; compliance = HACCP/food regs; go-to-market = horeca, gourmet stores, e-commerce; pilot → scale path.",PT,,9822@novalaw.unl.pt,Through Fondation Prince Albert II de Monaco.,Other,,,false,https://drive.google.com/drive/folders/1Pbf4FwTfAfqklel_a94CYA7dZsmvPfGH?usp=drive_link,,Atlantic Fish Sauce,João Firmino / Duarte Fernandes,"Europe, Portugal",+351969136436,NOVA University Lisbon (Nova School of Business & Economics) / University of Madeira (Faculty of Sciences and Engineering)
|
|
||||||
Francesco Ruscio,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,Enhance monitoring of benthic habitats using robotics and artificial intelligence.,IT,,francesco.ruscio@ing.unipi.it,linkedin,Technology & innovations,,,false,,,PerSEAve,"Francesco Ruscio, Simone Tani, Alessandro Gentili","Europe, Italia",+393756436501,"University of Pisa, Pisa, Italy"
|
|
||||||
Lorna Mudegu,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"WAVU is a market and aggregation platform that connects verified aquaculture producers to buyers through organised, predictable supply chains.
|
|
||||||
|
|
||||||
In many coastal and inland markets, fish buyers source from informal channels where farmed fish and wild-caught fish are indistinguishable. This lack of separation sustains demand for capture fisheries and contributes to overfishing in already stressed marine and freshwater ecosystems. By aggregating aquaculture producers, forecasting demand, and directing buyers toward farm-based supply, WAVU helps shift market demand away from unregulated wild catch.
|
|
||||||
|
|
||||||
As more buyers rely on planned aquaculture sourcing, pressure on wild fisheries is reduced while livelihoods are supported through sustainable fish production. Each tonne of farmed fish absorbed into formal markets represents demand that would otherwise be met through extraction from natural fish stocks.
|
|
||||||
|
|
||||||
WAVU builds on ongoing operations in East Africa and offers a scalable, market-driven pathway to reducing pressure on wild fisheries in regions facing overfishing and informal fish trade.",KE,2024-07-30,lornaafwandi@gmail.com,LinkedIn,Sustainable fishing and aquaculture & blue food,,,true,https://drive.google.com/drive/folders/1_Y9YW-Y_kd2Tpz80fH5juc9Ol1TR7DKq?usp=drive_link,,WAVU,Don Okoth | Vincent Oduor | Chris Munialo | Loise Mudegu,"Africa, Kenya",+254718059337,
|
|
||||||
Shamim Wasii Nyanda,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"SUNWAVE provides small-scale fishers in Tanzania with solar-powered ice-making units to reduce fish spoilage. These machines, powered by solar energy, offer a sustainable and cost-effective solution to fish preservation, especially in remote areas where access to the power grid is limited. By keeping fish fresh for longer, these units help fishers reduce spoilage, maintain higher-quality products, and increase income. The ice-making machines are operated by trained personnel to ensure proper use and efficiency.",TZ,2024-03-01,shamim@sunwaveltd.com,It was shared by SUNWAVE's Advisory Board member.,Sustainable fishing and aquaculture & blue food,,,true,,,SUNWAVE,Ridhiwan Mseya,"Africa, Tanzania",+255764190074,
|
|
||||||
Olaleye Rofiat Olayinka,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Eco Heroes is an incentive-based, tech-enabled recycling solution that prevents ocean-bound plastic waste from entering rivers and marine ecosystems. The project mobilizes communities to collect and exchange post-consumer plastic for rewards such as cash and essential services, creating a reliable supply of recovered plastic while improving livelihoods. Recovered materials are recycled and transformed into value-added products, including sewing threads, ensuring financial sustainability and scalable impact. The objective is to measurably reduce plastic pollution, create local economic value, and build a replicable model for coastal and river-connected communities.",NG,2021-11-08,olaleyerofiatyinka@gmail.com,I learned about the Monaco Ocean Protection Challenge through my involvement in an entrepreneurship and innovation programs focused on the blue economy and plastic pollution solutions.,Reduction of pollution (plastics chemicals noise light...),,,true,,,Eco Heroes Nigeria limited,Olaleye Rofiat Olayinka Salaam Lateef Oladimeji Akinsanya Dorcas Olaleye Hassan Ogundairo Ganiyat,"Africa, Nigeria",+2348038877293,
|
|
||||||
Christian Mwijage,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Every year, 9 million tonnes of plastic waste enter our oceans, polluting marine ecosystems and threatening ocean life. At this rate, by 2050 the ocean could contain more plastic than fish. At the same time, the world loses over 2 billion trees annually to meet the demand for timber in the furniture and construction industries—making deforestation the second leading driver of climate change.
|
|
||||||
We address both crises through a chemical-free, energy-efficient, AI-powered technology that transforms ocean-bound plastics and post-consumer packaging waste into high-quality, sustainable materials for furniture, building, and construction applications. By converting low-value, hard-to-recycle multi-layer plastic (MLP) waste into durable products, we are advancing the circular economy and giving new life to materials that would otherwise damage the environment.
|
|
||||||
We address one of the most persistent challenges in the plastics value chain: waste streams that lack viable conventional recycling pathways. We focus specifically on two difficult-to-recycle categories - multi-layer plastics (MLP), which combine multiple plastic layers and/or aluminum foil, and mixed plastic waste that cannot be economically or efficiently segregated. Globally, an estimated 6 billion tons of plastic waste have been generated, approximately 14% of which consists of MLP. Due to technical and economic limitations, these materials are typically landfilled, incinerated, or left uncollected, contributing significantly to environmental pollution and ecosystem degradation.",TZ,2022-12-21,chrissmwijage@gmail.com,Social Media,Reduction of pollution (plastics chemicals noise light...),,,true,,,ECOACT Tanzania,"• Mr. Bernard Ernest, Technical Director overseeing all production activities, holds a Master of Engineering in Biochemical Engineering. Mr. Christian Mwijage, Managing Director responsible for overall operations, holds a Bachelor’s degree in Business Administration and Marketing. Ms. Elineca Ndowo, Chief Finance Officer, holds a Master’s degree in Project Management and Financing from the University of Dar es Salaam.","Africa, Tanzania",+255711457346,
|
|
||||||
Rasheed Aliu,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Land-based pollution is the largest contributor to ocean degradation, yet sanitation failures in coastal communities remain overlooked. In flood-prone coastal regions of Africa, fragile septic systems collapse during flooding, releasing untreated human waste into groundwater, rivers, lagoons, and ultimately the ocean.
|
|
||||||
I witnessed this firsthand in coastal Lagos, Nigeria, when flooding destroyed local sanitation systems and a neighbour’s 4-year-old daughter died from cholera, a preventable waterborne disease. This tragedy reflects a systemic failure. Over 90% of Nigerian households rely on sanitation systems that leak sewage, contributing to 117,000 annual child deaths from waterborne diseases, according to UNICEF.
|
|
||||||
In the absence of centralized wastewater infrastructure, outdated septic tanks costly to build and maintain are frequently evacuated or overflow during floods, with waste discharged into coastal waters. This drives marine pollution, eutrophication, biodiversity loss, and degradation of near-shore ecosystems critical to food security.
|
|
||||||
At Pod we design and manufacture LoopBox, LoopBox is a solar-powered, IoT-enabled, self-contained sanitation system designed for coastal and flood-prone communities. Unlike traditional soakaway pits that leak, our tech uses embedded sensors and microbial treatment to track, treat, and recycle human waste into reusable water. Through our cloud dashboard, users and local authorities can monitor sanitation performance and water quality remotely. We also provide nearby borehole treatment as a service. LoopBox is 5x more cost-effective than conventional systems, eliminates 100 dollars/year in waste evacuation costs, and requires minimal space. Built with scalable hardware and software, it is designed to be deployed across low-income, climate-vulnerable communities bringing safety, sustainability, and data-driven decision-making to sanitation in Nigeria and Africa.
|
|
||||||
The project delivers a flood-resilient, decentralized sanitation s",NG,2025-05-06,rasheedofpod@gmail.com,BFA Global TECA Alumni Group( Tyler),Reduction of pollution (plastics chemicals noise light...),,,true,https://drive.google.com/drive/folders/1Ur6FAveOOAtS77TOGXuLGbVfqTF8mG9p?usp=drive_link,,Pod,"Rasheed Aliu, Gabriel Simon , Habeeb Lasisi and MaryJudith Chiamaka","Africa, Nigeria",+2348160238021,
|
|
||||||
Chelsey Karbowski,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Mikjikj Mniku, Mi’kmaq for “Turtle Island”, is an Indigenous-led consulting firm working at the intersection of ocean protection, community governance, and workforce development.
|
|
||||||
|
|
||||||
We help governments, philanthropies, and conservation organizations design ocean and climate initiatives that last beyond funding cycles by embedding Indigenous knowledge, ethical engagement, and local stewardship from the start.
|
|
||||||
|
|
||||||
Our work focuses on strengthening Indigenous and coastal governance, building inclusive workforce pathways in fisheries, marine monitoring, and ocean-adjacent clean energy, and making social impact measurable and defensible through socio-economic and SROI frameworks.
|
|
||||||
|
|
||||||
In a global push to protect more ocean faster, we ensure protection efforts are community-supported, socially resilient, and future-proofed, because conservation only succeeds when the people closest to the ocean are empowered to carry it forward.",CA,2025-03-01,chelsey.m.karbowski@gmail.com,Linkedin,Other,,,true,,,Mikjikj Mniku Consulting Ltd.,Chelsey Karbowski,Canada,+19026314362,
|
|
||||||
OLUTOKI FEYISHAYO FUNMI,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"To develop and inplement a sustainable food production (crops and animals) that specifically reduces a known threat to the Ocean (e. go, pollution, overfishing pressure, habitat destruction).
|
|
||||||
|
|
||||||
We will take measures using circular economy, by developing a system where waste products from our farm are treated and used in a way that prevent them from entering marine ecosystem
|
|
||||||
|
|
||||||
We will work on water management and pollution reduction and sustainable sourcing /supply chain",NG,2024-12-12,rebugssolutions@gmail.com,,Consumer awareness and education,,,true,https://drive.google.com/drive/folders/1xCJ_8EpTEdBORiJHYwIRZO22z8e54fbx?usp=drive_link,,Operation feed the children,"Adewuyi Feranmi, Olutoki sewafunmi victor, Ayodele joy, Babalola gbenga","Africa, Nigeria",+2348038226106,
|
|
||||||
Anshika Sarraf,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Auralis Blue is tackling a problem few people see but that is harming our oceans: underwater noise pollution. Ships, ports, and offshore construction create constant sound that travels far underwater, interfering with how whales, dolphins, and fish communicate, migrate, and reproduce. Auralis Blue measures this invisible threat and turns it into clear, actionable data, helping maritime stakeholders protect marine life while continuing sustainable operations.
|
|
||||||
|
|
||||||
Underwater noise is an invisible threat, but its effects are very real: studies show that marine mammals rely on sound to survive, and high noise levels can cause stress, confusion, and even death in fish populations. Despite this, there are almost no tools that measure or manage noise systematically. Auralis Blue fills this gap, providing a science-based, scalable solution that can protect marine ecosystems worldwide.
|
|
||||||
|
|
||||||
Objectives:
|
|
||||||
1) Measure and Map noise pollution
|
|
||||||
2) Marine life protection
|
|
||||||
3) Encourage better and sustainable practices
|
|
||||||
4) Support policy, investment & encourage systemic change in blue economy",IN,,anshika.sarraf_ug2024@ashoka.edu.in,LinkedIn,Reduction of pollution (plastics chemicals noise light...),,,true,,,Auralis Blue,Anshika Sarraf,Asia,+917897130506,Ashoka University + Sonipat
|
|
||||||
Neville Agesa,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"The Tsunza Community, located on Kenya’s South Coast in Kwale County, is a vital ecological hub linking mangrove forests, wetlands, and the Mwache River estuary. These interconnected ecosystems support fisheries, biodiversity, and local livelihoods but face increasing pressure from degradation, pollution, and declining fish stocks.
|
|
||||||
|
|
||||||
This project aims to protect and restore mangrove and wetland ecosystems while strengthening sustainable blue livelihoods. Through community-led mangrove restoration, marine pollution awareness, and youth and women engagement in sustainable fisheries and aquaculture practices, the project promotes ocean protection alongside economic resilience.
|
|
||||||
|
|
||||||
By integrating nature-based solutions, environmental education, and livelihood innovation, the initiative positions Tsunza as a scalable model for community-driven ocean conservation and sustainable development.",KE,2023-02-01,agesanevil@gmail.com,Gensea opportunities,Sustainable fishing and aquaculture & blue food,,,true,,,Sustainable Blue Food & Livelihoods Innovation,"Robert Meya,Hannah Mathenge,JohnChaka","Africa, Kenya",+254796438122,
|
|
||||||
Veronica Nzuu,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"My project focuses on empowering children and youth in my community to take action on plastic pollution through simple, community led learning and action. The objective is to build awareness, responsibility, and leadership by combining environmental education with practical activities such as waste segregation, plastic collection, creative upcycling, and community dialogue. By using participatory and inclusive approaches, especially for girls and marginalized youth, the project aims to strengthen community ownership of sustainability solutions and inspire long term behavior change at the local level.",KE,2023-05-29,veramichael2000@gmail.com,Social Media linked in,Consumer awareness and education,,,true,,,Furies,Angelo Mulu,"Africa, Kenya",+254748488312,
|
|
||||||
Fiona McOmish,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,We replace toxic PFAS chemicals in textiles with a water- and fire-resistant coating made 100% from seaweed. We sell our high-performing solution to textile manufacturers and formulators in a 'drop-in' format.,IT,2024-12-16,fiona.mcomish@algae-scope.com,LinkedIn,Technology & innovations,,,true,,,Algae Scope,Natasha Yamamura; Alejandra Noren; Farshid Pahlevani,"Europe, Italia",+447722083419,
|
|
||||||
Nesphory Mwambai,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"seamo.earth initiative focused on utilizing artificial intelligence (AI) to explore, document, monitor, and preserve the mariculture and seascapes of the Pwani regions. This project aims to enhance our understanding and protection of marine environments through the development of eco-friendly and climate adaptive technologies.",KE,2024-08-22,mwambai@seamo.earth,email news letter,Restoration of marine habitats & ecosystems,,,true,https://drive.google.com/drive/folders/1eOyDGZwwlNNAzbwwC-CUVmJi4gM3kDLI?usp=drive_link,,seamo.earth,"Nesphory Mwambai, Lewis Kimaru","Africa, Kenya",+254714520023,
|
|
||||||
Yahuza Sani Hudu,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"CleanUp Multi Dyna mic Concept (CleanUp MDC) is a Nigeria-based social enterprise advancing inclusive climate-tech solutions within the circular economy. Our flagship innovation, JoliTrash, is a toll-free, AI-powered, voice-based recycling platform that allows households and informal waste actors to sort and sell recyclable waste using a simple AI phone call in their local language without the need for smartphones, internet access, or digital literacy. Nigeria generates about 2.5 million tons of plastic waste annually, yet less than 10% is recycled (World Bank). At the same time, over 70% of Nigerians lack easy access to recycling facilities, locations, or clear recycling processes (NESREA, 2022), and 48% of the population has poor or no internet connectivity (NCC, 2023), making most app-based recycling platforms inaccessible to low-income and marginalized communities. CleanUp MDC was created to bridge this gap by enabling users to dial a toll-free number on any basic phone (cell-phone) and interact with our AI in Hausa, Yoruba, Igbo, Pidgin, or English with no language barrier, our AI identify users location, connect with nearby verified waste collectors, and user earn income from recyclables. Our target market includes low-income households, women, youth, informal waste pickers, and underserved urban and peri-urban communities across Nigeria, as well as recycling agents and aggregators seeking reliable recyclable feedstock. To date, we have onboarded over 30,163 active users from underserved communities, 17,907 of them women, and facilitated the recovery of more than 10,000 tons of plastic waste, positioning our operations to contribute to an estimated 25,000 tons of CO₂ emissions reduction annually, equivalent to removing about 4,000 fuel-powered cars from the road each year. We partner with the Waste Pickers Association of Nigeria (WAPAN), we are scaling nationwide with the long-term goal of expanding across Africa. Our main goals are to expand access to recycli",NG,2024-02-26,ysanihudu@gmail.com,"The Commissioner for Environment and Natural Resources of Kaduna State Government, Nigeria Share's the link with my startup",Reduction of pollution (plastics chemicals noise light...),,,true,,,CleanUp MDC,"Abner Ayuba Atuga, Ameer Saeed","Africa, Nigeria",+2348146036089,
|
|
||||||
Emeka Nwachinemere,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Pelagos is developing autonomous, bio-hybrid ocean regeneration machines that restore marine ecosystems while capturing atmospheric carbon. These AI-guided ocean drones re-mineralize seawater to combat acidification, stimulate safe plankton growth to enhance blue carbon sequestration, and support coral regeneration in degraded reefs.
|
|
||||||
|
|
||||||
Objectives:
|
|
||||||
|
|
||||||
Restore ocean health and biodiversity at scale
|
|
||||||
|
|
||||||
Enhance natural blue carbon capture and climate resilience
|
|
||||||
|
|
||||||
Provide real-time ocean intelligence data
|
|
||||||
|
|
||||||
Build a commercially viable, globally scalable blue-economy solution
|
|
||||||
|
|
||||||
Pelagos aims to transform oceans into self-healing climate engines while creating measurable environmental, social, and economic value.",NG,,nwachinemere.emeka@gmail.com,Linkedin,Restoration of marine habitats & ecosystems,,,true,,,Pelagos,"Nwachinemere Emeka, Nduka Miracle","Africa, Nigeria",+2348062148183,"University of Nigeria, Nsukka"
|
|
||||||
Rodrick Nyendwa,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Mitigation about climate change and its impact and issue that community are aware .,ZM,2023-12-12,rodricknyendwa2016@gmail.com,Through social media on funds for NGOs,Mitigation of climate change and sea-level rise,,,true,https://drive.google.com/drive/folders/1RlybRQMKzhAdcU9vqg8XDZHbtpSzLOCN?usp=drive_link,,"Complehensive HIV prevention ,Treatment care support","Rodrick Nyendwa,Executive Director, Mumbi Micheal - Finace Manager, Ementy Mweemba- Programme Manager, Winter Musonda - Human Resource Mnager, Josiah Ndjovu -Community Liason Officer, Simata Mate - Monitoring and Evaluation Manager , Edith Bwalya -Data Entry Officer, Brona Kapindo - Office Assistant , Sylvester Chisanga - Front office Assistant","Africa, Zambia",+260977339071,
|
|
||||||
Nasibu Mtambo,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Blue EcoponicX is a climate-tech initiative that transforms marine plastic waste into 3D printed hydroponic towers for urban farming. The project addresses two interconnected challenges; ocean plastic pollution and urban food insecurity by converting waste into smart, productive food-growing systems designed for cities.
|
|
||||||
|
|
||||||
Project Objectives
|
|
||||||
|
|
||||||
1. Reduce Marine Plastic Pollution
|
|
||||||
Collect and recycle marine plastic waste, preventing it from entering landfills or degrading in the ocean.
|
|
||||||
|
|
||||||
2. Improve Urban Food Security
|
|
||||||
Enable affordable, space-efficient food production for households, youth groups, and small-scale urban farmers.
|
|
||||||
|
|
||||||
3. Lower Urban Carbon Emissions
|
|
||||||
Reduce food miles, optimize resource use, and promote localized production using energy-efficient systems.
|
|
||||||
|
|
||||||
4. Promote Climate-Smart Agriculture
|
|
||||||
Use IoT technology to minimize water, nutrient, and energy waste while maximizing crop yields.
|
|
||||||
|
|
||||||
5. Empower Communities Through Technology
|
|
||||||
Make modern farming accessible through easy-to-use smart systems, training, and data insights.
|
|
||||||
|
|
||||||
Key Features
|
|
||||||
|
|
||||||
1. Circular Economy Design: Hydroponic towers made from recycled marine plastics
|
|
||||||
|
|
||||||
2. IoT Integration: Real-time monitoring of water, nutrients, and system health
|
|
||||||
|
|
||||||
3. Low Resource Use: Up to 90% less water than traditional farming
|
|
||||||
|
|
||||||
4. Urban-Friendly: Suitable for rooftops, balconies, schools, and community spaces
|
|
||||||
|
|
||||||
5. Scalable & Modular: Easy to expand from household to community-scale deployment
|
|
||||||
|
|
||||||
Target Beneficiaries
|
|
||||||
|
|
||||||
1. Urban small-scale farmers
|
|
||||||
|
|
||||||
2. Youth and women-led agribusinesses
|
|
||||||
|
|
||||||
3. Schools and training institutions
|
|
||||||
|
|
||||||
4. Cities seeking climate-resilient food systems
|
|
||||||
|
|
||||||
Expected Impact
|
|
||||||
|
|
||||||
1. Reduced plastic pollution in coastal and marine ecosystems
|
|
||||||
|
|
||||||
2. Increased access to fresh, nutritious food in urban areas
|
|
||||||
|
|
||||||
3. Lower carbon emissions from food transport and waste
|
|
||||||
|
|
||||||
4. Creation of green jobs in recycling, manufacturing, and urban agriculture
|
|
||||||
|
|
||||||
5. Stronger climate resilience for cities",KE,2025-05-20,mtamboduke@gmail.com,Through LinkendIn,Reduction of pollution (plastics chemicals noise light...),,,true,,,Blue EcoponicX,"Tabitha Shali, Mohammed Athman, Terry Okwanyo","Africa, Kenya",+254742051141,
|
|
||||||
Faith Mutisya,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Tumbe sea weed farmers is a community based organization in Msambweni kwale Kenya. We focus on empowering coastal communities especially young women and youth with skills in sustainable sea weed farming. This is because as women in kwale county we face alot of challenges such as early marriages and pregnancies most especially because women are not given the same schooling privilege as men. Therefore so many young mothers don't have any skills to provide for their young ones. Therefore Tumbe sea weed farmers has taken the initiative to empower them , and through the farming they are able to support themselves financially and at the same time contribute to global efforts in fighting climate change because weed plays an important role as a carbon sink . And we also contribute to increase in biodiversity by provide nursery and nurturing bay for fish and other aquatic organisms",KE,2023-03-02,faithmutisya56@gmail.com,Through linked in,Capacity building for coastal communities,,,true,,,Tumbe sea weed farmers,Faith Mutisya - founder and Trainer 2. Hanifa wendo- secretary/field manager 3. Mwanamisi Mwadzumba - Treasurer,"Africa, Kenya",+254711627836,
|
|
||||||
李涵凝,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"The 10cm transparent eco-jellyfish robot carries ocean-beneficial materials, moving by mechanical legs and drifting with waves to reduce marine pollution and repair ecosystems, quietly improving ocean health",CN,2026-01-01,xbm_0201@qq.com,I found out about MOPC through an online search.,Reduction of pollution (plastics chemicals noise light...),,,,https://drive.google.com/drive/folders/1duMty6mbpLCOoataogbZEShA6keuK2fy?usp=drive_link,,Environmentally Friendly Jellyfish,李涵凝,Asia,+8618618164803,
|
|
||||||
Kabir Olaosebikan,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Craft Planet – Blue Guard for the Ocean is an integrated ocean-protection initiative that prevents plastic pollution before it reaches the sea. Using AI-enabled drones, we identify high-risk waste leakage points along riverbanks and coastal areas, enabling rapid collection of plastic waste before it enters rivers and oceans. Recovered plastics are recycled into durable construction materials—interlocking blocks, eco-bricks, floor and roof tiles—which are used to improve public school infrastructure, including classrooms, toilets, desks, and chairs. The project also builds capacity among coastal communities, teachers, and students through environmental education, waste management training, and circular economy skills, creating local ownership, green jobs, and long-term ocean stewardship.",NG,2023-04-17,kabir@craftplanet.org,Through online sustainability platforms and ocean innovation networks.,Reduction of pollution (plastics chemicals noise light...),,,true,https://drive.google.com/drive/folders/1jUFqGLk1zZ6afP4BysRPpp_jRXsw_9Kz?usp=drive_link,,Craft Planet - Blue Guard,"Kabir Olaosebikan, Aminat Abdulazeez, Promise Dalero, Hanatu Abdulakeem","Africa, Nigeria",+2348142123656,
|
|
||||||
Karl Mihhels,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"The project is about converting fast-growing species of algae, with a high cellulose content (Cladophorales) into a direct replacement for wood based cellulose and cellulose products, such as paper.",FI,,karl.mihhels@aalto.fi,2nd EU Algae Awareness Summit held in Berlin on October 17th 2025,Blue Carbon,,,true,,,Shaving the Seas,Karl Mihhels,"Europe, Finland",+358447627444,"Aalto University School of Chemical Engineering, Finland"
|
|
||||||
SENI Abd-Ramane,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"_Project Title_: OceanClean Tech
|
|
||||||
|
|
||||||
_Objective_: The OceanClean Tech project aims to reduce plastic pollution in the oceans by developing a marine plastic waste collection system. The main goal is to clean up polluted marine areas and prevent new plastic waste from entering marine ecosystems.
|
|
||||||
|
|
||||||
_Innovation_: The project's innovation lies in the use of autonomous drones equipped with artificial intelligence (AI) technologies to locate and collect plastic waste at sea. The drones are capable of navigating autonomously, identifying plastic waste using sensors and image recognition algorithms, and collecting it for transport to a treatment point.
|
|
||||||
|
|
||||||
_Impact_: The OceanClean Tech project has several expected impacts:
|
|
||||||
1. _Environmental_: Significant reduction of plastic waste in the oceans, protecting marine biodiversity and ecosystems.
|
|
||||||
2. _Social_: Raising public awareness of marine pollution and involving local communities in clean-up actions.
|
|
||||||
3. _Economic_: Creating new economic opportunities related to sustainable marine waste management and the development of clean technologies.",BJ,2025-12-29,seniramane@gmail.com,"I heard about the Monaco Ocean Protection Challenge on LinkedIn, it immediately caught my attention!",Reduction of pollution (plastics chemicals noise light...),,,true,,,OceanClean Tech,"SENI Abd-Ramane, DJIBRIL Samir, SOULÉ SEIDOU Mansoura","Africa, Bénin",+2290161149564,
|
|
||||||
Omoding Olinga Simon,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Dagim Fisheries directly advances equitable access to safe, nutritious, affordable food while improving planetary health through zero-waste processing and sustainable fishing. Our multidisciplinary approach integrates nutrition science, food engineering, supply chain management, environmental conservation, and economics. We address malnutrition, reduce waste, empower fishing communities, and protect Lake Victoria's and Kyoga's ecosystem creating regenerative food systems scalable across East Africa toward the billion-lives impact goal.",UG,2024-01-05,simonomoding.ace@gmail.com,Through Linkedinn social media,Sustainable fishing and aquaculture & blue food,,,true,,,Dagim Fisheries (U) Ltd,"Omoding Simon, Ilukat Musa, Omiel Peter, Omongole Richard, Fellista Nakatabirwa","Africa, Ouganda",+256773351242,
|
|
||||||
Mutave Nelly,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Hrkb,KE,1997-01-24,mutavenelly.mn@gmail.com,Friend shared link,Technology & innovations,,,true,,,Revamp Flips,Nthatisi Lesala,"Africa, Kenya",+254704458380,
|
|
||||||
Ketty Shamakamba,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Lake Farms is establishing an academy dedicated to preserving Lake Kariba and its communities. It is a center for training, innovation, and direct action.
|
|
||||||
|
|
||||||
Core Mission: Halt fish stock depletion and foster a sustainable blue economy.
|
|
||||||
|
|
||||||
Key Initiatives:
|
|
||||||
|
|
||||||
Training Hub: Equip local fishers with skills in sustainable aquaculture, ecosystem management, and cooperative business.
|
|
||||||
|
|
||||||
Innovation & Deployment: Design and deploy ethical, lake-friendly cage systems and restorative practices to rebuild wild stocks.
|
|
||||||
|
|
||||||
Community Enterprises: Launch community-owned ""Aqua-Hubs"" that provide food security, create livelihoods, and empower women.
|
|
||||||
|
|
||||||
This academy would create a lasting legacy of ecological restoration, poverty reduction, and resilience for Lake Kariba's people, directly honoring a commitment to ocean and freshwater preservation.",ZM,2021-11-30,ilovesolarfreezers@gmail.com,"We learned about the Monaco Ocean Protection Challenge through the communication channels of the Prince Albert II of Monaco Foundation and its associated networks, which highlight pioneering solutions for ocean and freshwater conservation.",Capacity building for coastal communities,,,true,,,LAKE FARMS AND FISHING LODGE LIMITED,"Board and Management Team Chisanga Mambwe – Board Chairperson (Strategic oversight) Provides governance leadership, investor relations support, and high-level oversight of the executive team. Ketty Shamakamba – Chief Executive Officer (CEO) Leads overall strategy, fundraising, partnerships, gender lens work, and company growth. Oversees business development, climate initiatives, and solar cold-chain expansion. Chiozya Mwanza – Chief Operations Officer (COO) Responsible for day-to-day operations, cage management, production planning, logistics coordination, and community engagement with fishers and women traders. Hamando Hamalabbi – Chief Financial Officer (CFO) (Accountant) Manages finance, accounting, compliance, investment reporting, budgeting, and financial controls. Muzalema Zimba – Chief Marketing Officer (CMO) (Sales & Marketing Manager) Oversees sales strategy, distribution channels, branding, customer acquisition, and premium market relationships (hotels, restaurants, wholesalers). Joshua Mwanza – Chief Operations Manager / Deputy COO (Operations Manager) Supports operations, distribution logistics, procurement, cold-chain coordination, and team supervision. Micheck Chulaula – Chief Farm Manager (CFM) (Farm Manager) Oversees cage management, feeding regimes, harvesting, processing coordination, and ensuring biosecurity and aquaculture standards. Mabel Kaunda – Chief Human Resources Officer (CHRO) (HR Manager/Secretary) Manages staff welfare, recruitment, training, compliance, and gender-inclusive workforce policies. Our operations team includes experts in farm management, logistics, and business development, ensuring efficient production, processing, and distribution. The finance and technology staff oversee solar freezer leasing, mobile payments, and digital monitoring systems, enabling scalable, sustainable impact. Many team members, including the founders, have personal connections to the communities we serve, which drives our ","Africa, Zambia",+260971094443,
|
|
||||||
Torrigiani Aurore,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Sea Blocks develops modular, low-tech artificial reefs designed to restore marine habitats in port environments.
|
|
||||||
|
|
||||||
Each reef is co-designed and assembled through participatory workshops involving companies, citizens and local stakeholders, then installed in partnership with ports. The modules are made from low-carbon materials and locally sourced shell waste, enhancing ecological functionality and accelerating colonisation by marine species.
|
|
||||||
|
|
||||||
The project combines ecological restoration, circular economy and awareness-raising, with scientific monitoring conducted by marine biology experts to assess biodiversity recovery and long-term impact.",FR,2021-02-03,seablocksrecif@gmail.com,Through professional networks and partners involved in ocean and coastal innovation.,Restoration of marine habitats & ecosystems,,,true,,,Sea Blocks,Olivier Meynard,"Europe, France",+33647780342,
|
|
||||||
Godfrey Noel,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Project Title: Bamboo Stewardship for Mangrove Protection: Building Sustainable Livelihoods Along the East African Coast
|
|
||||||
|
|
||||||
The Problem
|
|
||||||
East African coastal mangrove ecosystems spanning from Somalia to Mozambique face catastrophic degradation, with communities harvesting mangroves for fuel and construction because alternative income sources remain unavailable. This extraction destroys critical carbon sinks, eliminates natural storm surge barriers, and collapses fish nursery habitats that sustain coastal food security. Traditional conservation approaches exclude communities from protected areas without providing viable economic alternatives, guaranteeing enforcement failure and continued ecosystem loss.
|
|
||||||
|
|
||||||
Our Solution
|
|
||||||
Kilimora, in strategic partnership with EarthLungs, is implementing a bamboo based mangrove protection system that transforms coastal communities from ecosystem exploiters into paid ecosystem stewards. We employ community members to cultivate and harvest fast growing bamboo (Bambusa species with 3 to 5 year harvest cycles and continuous regrowth capacity) in designated buffer zones adjacent to mangrove forests. This bamboo provides sustainable construction materials and biomass fuel alternatives that eliminate economic pressure on mangrove stands while generating verifiable income for participating households.
|
|
||||||
|
|
||||||
Technical Innovation
|
|
||||||
The initiative integrates drone based mangrove health monitoring with ground truth verification by community stewards, creating high resolution ecosystem data that supports both conservation management and carbon credit generation. Kilimora provides the artificial intelligence powered verification infrastructure and blockchain based transparent payment systems ensuring stewards receive direct compensation tied to measurable mangrove protection outcomes. EarthLungs contributes marine ecosystem expertise, coastal community organizing capacity, and connections to corporate blue carbon credit buyers.
|
|
||||||
|
|
||||||
Scale and Impact
|
|
||||||
The program cu",KE,2024-01-04,gnoel@kilimora.africa,LinkedIn network,Capacity building for coastal communities,,,,https://drive.google.com/drive/folders/1Ouz8-deBPfgUl7VYIwofxUwEYG4D9iMw?usp=drive_link,,Kilimora CLG,"Godfrey Noel, Zuhra Nagib, Matthew Muange, Hildah Gichuru, Ezra Maruti, Hildah Gichuru","Africa, Kenya",+254795647634,
|
|
||||||
Hellen flavine akinyi,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Plastic pollution from urban markets and streets flows into rivers ad ultimately into the ocean. single-use plastic paper bags are among the most common sources of marine debris.preventing plastic waste at the source is the most effective ad affordable solution than ocean multi-million clean up efforts,KE,2021-02-09,artworkspace1@gmail.com,thro. funds- for- Ngo newsletters,Consumer awareness and education,,,true,https://drive.google.com/drive/folders/1bFnRFeWaxyD2g52MG0l_Y6q_rQOCYbnc?usp=drive_link,,"STOPING OCEAN PLASTICS AT THE SOURCE;DIGITAL ECO-PACKAGING SOLUTION LED BY A YOUNG AFRICA WOMAN,KENYA 2026026","KIMBERLY ADHIAMBO CONIE, MAISON JOHN &PETER WAMBURA","Africa, Kenya",+27631484516,
|
|
||||||
Tochukwu Uwakeme,Received,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Coastal Blue-Skills Hubs by Pikia is a scalable, community-led training and microenterprise program that equips coastal youth and women with practical skills, tools, and starter microgrants to reduce ocean pollution and strengthen climate-resilient livelihoods through waste-to-value (plastic collection/sorting), sustainable fishing practices, and mangrove/coastal restoration. The program will run through local “Hub” partners (NGOs/co-ops/schools), a lightweight mobile curriculum, and a train-the-trainer model, paired with verified community monitoring (simple metrics + photo evidence) to prove impact and unlock blue-economy buyers and sponsors.
|
|
||||||
Objectives:
|
|
||||||
• Cut land-to-ocean leakage by organizing community collection, sorting, and resale of plastics, with tracked volumes diverted.
|
|
||||||
• Increase resilient incomes by training and supporting community micro-enterprises (waste-to-value, eco-services, sustainable seafood handling) and link them to off takers.
|
|
||||||
• Restore natural coastal defenses through mangrove/coastal habitat restoration tied to local stewardship incentives and verified survival rates.
|
|
||||||
• Create a repeatable “Hub-in-a-box” model that can scale across coastal regions quickly with clear KPIs and partner networks delivering positive, measurable ocean impact in the short to medium term, consistent with MOPC’s focus on ocean-positive business concepts",US,,uwakemet@bu.edu,United Nations SDGs Newsletter.,Capacity building for coastal communities,,,true,https://drive.google.com/drive/folders/1ph6DBmqeSGvSSqxQkymPx9rlU-ZmGFnr?usp=drive_link,,Pikia Marine,"Tochukwu Uwakeme, Moses Imoleyo, Ihuoma Ohaegbulam",US,+12024255839,Boston University / United States
|
|
||||||
Veronica Nzuu,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"My project focuses on community-based climate and ocean education for children and youth, using storytelling, play, and interactive learning to build awareness around plastic pollution, waste segregation, and environmental responsibility. The objective is to transform how young people and families understand and relate to plastic consumption moving from awareness to everyday action. Through games, facilitated sessions, and community learning spaces, the project empowers children to become informed advocates within their households and neighborhoods, strengthening long-term behavior change and community ownership of sustainability solutions.",KE,2023-05-23,veramichael2000@gmail.com,Social media linked in,Consumer awareness and education,,,true,,,Furies,Angelo Mulu,"Africa, Kenya",+254748488312,
|
|
||||||
Cristiano da Silva Palma,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Project Idea & Scientific Context
|
|
||||||
|
|
||||||
The project develops a next-generation modular OTEC (Ocean Thermal Energy Conversion) system, combining innovative deep-ocean structures, ultra-optimized thermodynamic cycles, and AI-based monitoring to deliver continuous (24/7) clean energy, with initial pilot operation targeted from 2027. The system is designed for scalable deployment in tropical and island regions, validated through a pilot-scale OTEC unit operating with deep-water intake (~1000 m or more) and real-time intelligent control.
|
|
||||||
|
|
||||||
Sur le plan scientifique, la technologie OTEC repose sur l’exploitation de la différence de température entre les eaux de surface chaudes et les eaux profondes froides afin d’alimenter un cycle thermodynamique de production d’électricité, conformément aux analyses reconnues par la Convention-cadre des Nations Unies sur les changements climatiques (UNFCCC).
|
|
||||||
|
|
||||||
Dans les régions tropicales, où les eaux de surface peuvent dépasser 25 °C tandis que les eaux profondes se situent autour de 5 °C, le différentiel thermique (ΔT) peut excéder 20 °C, condition généralement considérée comme favorable à une application efficace de l’OTEC, comme le soulignent de nombreuses publications académiques, notamment celles de la MDPI.
|
|
||||||
|
|
||||||
En revanche, dans le bassin méditerranéen, y compris autour de la Principauté de Monaco, les données actuelles indiquent un ΔT généralement inférieur aux seuils classiques de viabilité de l’OTEC à grande échelle. Toutefois, à partir de 2027, evolving ocean temperature profiles, combined with AI-assisted thermodynamic optimization, high-efficiency working fluids, operation restricted to periods of maximum thermal contrast (summer), intake at greater depths, and high thermal-efficiency piping, may enable experimental and seasonal OTEC operation, positioning the Mediterranean as a future testbed for advanced ocean energy technologies.
|
|
||||||
|
|
||||||
By aligning scientific rigor with technological innovation, the project contributes to ocean protection",BR,2024-08-09,cristianospalma@yahoo.com.br,"I learned about the Monaco Ocean Protection Challenge through institutional email exchanges within the framework of the United Nations Framework Convention on Climate Change (UNFCCC), including communications with the UNFCCC Global Secretariat, notably Simon Stiell, Executive Secretary, as well as with UNFCCC National Focal Points in Monaco and France. These included Carl Dudek (Ministry of Foreign Affairs and Cooperation of the Principality of Monaco), Dietmar Petrausch and Wilfred Suddath-Deville (Ministry for Europe and Foreign Affairs of France), and Yue Dong and Bénédicte Jenot (French Ministry for the Ecological Transition).",Technology & innovations,,,true,,,Tabernacle Space Islands,Cristiano da Silva Palma,South America,+5511978020540,
|
|
||||||
Titus Nyandoro,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We are a Kenyan-based, ocean-minded for-profit fintech venture for fishing coastal communities that are dedicated to the sustainable blue economy",KE,2024-01-01,ktnyandoch@gmail.com,WhatsApp,Technology & innovations,,,true,https://drive.google.com/drive/folders/1twpoOtR1RIei27iSRXNquyV4iMBA9HdC?usp=drive_link,,VUA SOLUTIONS,"Matthew Egessa, Titus Nyandoro","Africa, Kenya",+254743378884,
|
|
||||||
Mzuvukile Benayo,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Spatial Planning Collective more about engaging stakeholders and driving education.,ZA,2011-07-07,mzuvukilejames@gmail.com,FundsforNGOS email,Capacity building for coastal communities,,,true,,,Youth Innovation Programme,Zenande Mnethu,"Africa, South Africa",+27738223994,
|
|
||||||
Abdoulaye Sarr Ndour,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"AI-powered gamified learning platform for ocean conservation education (Duolingo-style for oceans). The platform features an AI tutor powered by ChatGPT-4/Claude, 4 educational modules covering Biodiversity, Climate, Threats, and Solutions, gamification elements including XP points, badges, and mini-games, plus professional certifications.
|
|
||||||
|
|
||||||
Target customers include B2C users (parents/students) paying 9.99 EUR/month subscriptions, schools paying 800-1,500 EUR/year for licenses, corporations paying 2K-50K EUR for CSR training programs, and professionals purchasing certifications for 49-299 EUR each.
|
|
||||||
|
|
||||||
Year 1 objectives: 10,000 users generating 207K EUR revenue. Year 3 objectives: 200,000 users generating 5.8M EUR revenue. Overall mission: 1 million ocean-literate people by 2030.
|
|
||||||
|
|
||||||
Tech stack: Next.js frontend, Supabase backend (PostgreSQL + Auth + Storage), OpenAI API for AI tutor functionality.
|
|
||||||
|
|
||||||
Timeline: 90 days to launch following MVP development, beta testing with 100 users, then public launch.
|
|
||||||
|
|
||||||
Initial budget required: 15-30K EUR covering development, educational content creation, and marketing expenses.",SN,,ndour.ecobox@gmail.com,Linkedin,Technology & innovations,,,true,,,OceanEdu AI,"Omar Cissé Faye, Fatou Cissé, Coumba Gueye","Africa, Senegal",+221775110218,"Saint Louis Gaston Berger University, Senegal"
|
|
||||||
Christopher Enriquez Urban,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Project: AI-powered offshore infrastructure for Sargassum monitoring, harvesting, and conversion into industrial biomass.
|
|
||||||
|
|
||||||
Problem: Massive Sargassum blooms devastate Caribbean coasts but remain unused due to unpredictable availability and high logistics costs.
|
|
||||||
|
|
||||||
Solution: Neural-operator forecasting systems predict bloom movements with high accuracy, guiding automated offshore platforms that harvest and preprocess algae at sea—delivering consistent, industrial-grade feedstock.
|
|
||||||
|
|
||||||
Objectives: Create reliable supply chains for bio-based materials, reduce coastal environmental damage, generate jobs and enable circular economy applications in construction, energy, and agriculture.
|
|
||||||
|
|
||||||
Impact: Transforms environmental crisis into economic opportunity while addressing climate goals through fossil material substitution.",DE,,christopher@algrid.tech,LinkedIn,Other,,,true,,,Algrid,Valentina Iunosheva,"Europe, Germany",+4915679760251,"University of Leeds, Leeds, UK"
|
|
||||||
Sarfraaz Khan AYAZ KHAN,Received,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Poseidon transforms ocean-recovered plastic into certified, customizable, high-end solid surface materials for architects, designers, artists, and sustainability-driven businesses.
|
|
||||||
Our advanced material development and manufacturing ensure durability, aesthetic quality, and long-term reuse as alternatives to conventional surfaces - integrating ocean plastic back into the economy. Each sheet removes approximately 15–30 kg of ocean plastic and includes a digital product passport that provides full traceability from collection to final use. Incubated at MonacoTech, Poseidon aligns with multiple UN Sustainable Development Goals and empowers creative professionals to lead eco-innovation, contributing to ocean cleanup, circularity, and measurable environmental impact.",MC,,info@poseidon-monaco.com,"Last year MOPC competition , JCI CCE event and news letter",Reduction of pollution (plastics chemicals noise light...),,,true,https://drive.google.com/drive/folders/11GEc6IYyLaZnQ_rtkgNHeafVPnuOZ2bz?usp=drive_link,,POSEIDON,Sarfraaz Khan AYAZ KHAN,"Europe, Monaco",+33745384992,SKEMA Business School and POLIMI Graduate School of Management
|
|
||||||
Francesca Rose Turner Prichard,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Restore society’s relationship with the ocean by building connections between women and the ocean through sport, art and conservation activities.",ES,,francescaroseturner@gmail.com,Linkedin,Consumer awareness and education,,,true,,,Residensea,"Francesca Turner, Aoife Martin, Alberto Rangel","Europe, Spain",+34671298357,Southampton Solent University
|
|
||||||
Brian Ochieng Aliech,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Our project seeks to address the pervasive challenge of plastic pollution that has afflicted urban centers and aquatic ecosystems across the globe.
|
|
||||||
By converting discarded plastic into durable construction materials, we aim to alleviate the strain on finite natural resources traditionally employed in the building industry, thereby reducing both costs and inefficiencies.
|
|
||||||
In addition, this initiative seeks to generate meaningful employment opportunities for young people in underserved communities, empowering them to achieve economic stability and dignity. Ultimately, our vision is nothing less than to contribute to the preservation and renewal of our planet.",KE,,ochiengaliech@gmail.com,Through a friend.,Reduction of pollution (plastics chemicals noise light...),,,true,,,NOLA AFRICA,"Brian Aliech, Charles Okutah, Hussein Hezekiah, Kevin Onsongo, Lidah Makena","Africa, Kenya",+254757008417,"University of Nairobi, Nairobi"
|
|
||||||
Adhithi Mugundha Kumar,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,We aim to provide a solution to invasive blue crabs in the Mediterranean by developing a bait-induced fishing method.,GB,2026-01-06,Adhithimukhundh@gmail.com,,Sustainable fishing and aquaculture & blue food,,,true,,,Blue crabs,Xenia Anagnostou,UK,+447512296331,
|
|
||||||
THIERRY BOUSSION,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Yuniboat develops an industrial model dedicated to the eco-reconditioning of leisure and professional boats, designed to significantly reduce the environmental impact of boating activities.
|
|
||||||
|
|
||||||
By extending the lifespan of existing boats rather than building new ones, Yuniboat directly contributes to the protection of oceans and marine ecosystems. Reconditioning avoids the extraction of new raw materials, limits fiberglass and plastic waste, and reduces emissions linked to manufacturing and end-of-life destruction.
|
|
||||||
|
|
||||||
Key Environmental Impacts
|
|
||||||
Reduction of marine pollution by preventing abandoned and end-of-life boats from becoming waste at sea or in ports.
|
|
||||||
Preservation of marine fauna and flora through lower emissions, reduced noise pollution, and cleaner propulsion systems (electric, biofuel, hybrid).
|
|
||||||
Lower pressure on natural resources, with up to 80% of boat components reused.
|
|
||||||
Decrease in carbon footprint, contributing to climate action and healthier marine ecosystems.
|
|
||||||
|
|
||||||
Project Objectives
|
|
||||||
Make boating more compatible with ocean preservation.
|
|
||||||
Support professionals (fishing, rental fleets) in meeting decarbonation goals by 2030.
|
|
||||||
Offer a sustainable, economically viable alternative to new boat construction.
|
|
||||||
Deploy a scalable industrial model capable of transforming the nautical and maritime sectors.
|
|
||||||
Yuniboat’s ambition is to position eco-reconditioning as a key lever for ocean protection, combining circular economy, innovation, and long-term impact on marine biodiversity.",FR,2022-06-01,t.boussion@yuniboat.com,we follow your activities on Linkedin and Instagram,Reduction of pollution (plastics chemicals noise light...),,,true,,,Yuniboat,Thierry Boussion,"Europe, France",+33621220023,
|
|
||||||
Daniele Tassara,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"MareNetto is a yacht-focused climate platform that automatically calculates and offsets superyacht CO2 emissions from AIS/MMSI data, then issues verifiable certificates that owners and charter managers use for marketing and ESG compliance",IT,,daniele.tassara@outlook.com,I lived in Monaco and i knew about this project,Technology & innovations,,,true,,,MareNetto,"Giambattista Figari, Giorgio Mussini","Europe, Italia",+393466376215,"Universita di Genova, Genova"
|
|
||||||
Gaia Minopoli,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Ogyre is a startup tackling marine plastic pollution through a Fishing for Litter model, working directly with fishing communities worldwide. Its mission is to clean the Ocean while turning plastic waste into a resource. By financially supporting fishers to recover marine litter during their daily activities, and by involving local partners for sorting and recycling, Ogyre delivers measurable environmental and social impact. The entire process is fully traceable through a blockchain-enabled platform, allowing companies to monitor progress and impact in real time. Active across Europe, South America, Africa, and Asia, Ogyre has already recovered over 800 tons of marine waste and proven a financially sustainable model—now scaling its impact globally to reach 30M kg of cumulated collection by 2030!",IT,2020-01-21,gaia.minopoli@ogyre.com,Scientific attaché of Italian Embassy in Paris,Reduction of pollution (plastics chemicals noise light...),,,true,,,Ogyre,Agnese Antoci Alessandro Serra Alice Casella Andrea Faldella Andrea Scatolero Antonio Augeri Chiara Maggiolini Davide Brugola Filippo Ferraris Gaia Minopoli Gian Piero Seregni Lorenzo Gastaldo Matteo Quaglio Mattia De Serio Michele Migliau Alessandro Sciarpelletti Francesco Carletto francesco notari Irene Eustazio Jurgen Ametaj Lorenzo Varas Marta Berardini Lucrezia Napoletano Gabriele Cusimano Enrica Sandigliano,"Europe, Italia",+393393499607,
|
|
||||||
Yajaira Cristina Alquinga Salazar,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"The general objective of this research plan is to study the dynamics of coastal dunes in the southwest of Buenos Aires Province, with special emphasis on the foredune, and its relationship with climatic, oceanographic, and anthropogenic factors. In particular, the study aims to determine the degree of influence of each of these factors, especially in areas where urban settlements have been established over the last 80 years, in comparison with adjacent sectors subjected to similar environmental conditions but without anthropogenic influence.",AR,,cristinalquinga@gmail.com,LinkedIn,Mitigation of climate change and sea-level rise,,,true,,,Dynamics of Coastal Dune Fields in the Southwest of Buenos Aires Province,"Bsc. Yajaira Cristina Alquinga Salazar, Dr. Gerardo M. E. Perillo and Dr Sibila A. Genchi",South America,+541136132787,Universidad Nacional del Sur
|
|
||||||
Jovana,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Project name: Symphony of the Blue
|
|
||||||
|
|
||||||
The Idea: Converting real-time oceanographic data (currents, temperature, pH levels) into immersive musical compositions using mathematical algorithms.
|
|
||||||
|
|
||||||
Objectives:
|
|
||||||
|
|
||||||
Emotional Data Visualization: Making the ""silent"" problems of the ocean audible to the public and investors through music.
|
|
||||||
|
|
||||||
Eco-Funding: Generating revenue for marine conservation through the sale of these unique, data-driven symphonies.
|
|
||||||
|
|
||||||
Ocean Literacy: Educating younger generations by integrating science, math, and art.",RS,2026-01-07,jovanaperisic059@gmail.com,,Technology & innovations,,,false,,,EcoMath,Jovana Perišić,"Europe, Serbia",+381645655226,
|
|
||||||
Amelia Martin,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Mud Rat is a biomaterials startup creating an eco friendly alternative to marine foams.,US,2023-06-14,amelia@mudratsurf.com,Google!,Consumer awareness and education,,,true,https://drive.google.com/drive/folders/1GzXe6ugfJQCFdqcZxj3lZrN4H7DSMAIE?usp=drive_link,,Mud Rat,"Jack Tarka, Patricio Acevedo, Brian Lassy",US,+18606824426,
|
|
||||||
Mulowoza Grace,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We are tackling plastic pollution through innovative upcyling solutions, our work is centered around four main objectives;
|
|
||||||
1. Reduce plastic pollution through innovative upcyling, we are transforming plastic waste into valuable resources.
|
|
||||||
2. Promote waste separation and proper waste management practices.
|
|
||||||
3. Raise awareness about the importance of environmental conservation.
|
|
||||||
4. Empower youth to take action in environmental conservation.
|
|
||||||
Our project, combat plastic pollution through circular economy innovation aims to reduce plastic pollution which can end up into oceans by promoting circular approaches that emphasize reduction, reuse, recycling and sustainable alternatives.
|
|
||||||
Through transforming plastic waste into economic and social opportunities our team contribute to environmental protection, green job creation and sustainable development.",UG,2022-11-07,mulowozagrace@gmail.com,Facebook,Reduction of pollution (plastics chemicals noise light...),,,true,,,Divine youth environment initiative,"Mulowoza Grace, Nassaazi phiona, Sseruga ibraheem, Male simon , kirume Vivian Deborah","Africa, Ouganda",+256705620491,
|
|
||||||
Suraj Kumar Hota,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,Using Indian knowledge system to manage overfishing,IN,,surajkumarhota23@gmail.com,,Sustainable fishing and aquaculture & blue food,,,true,,,No project,SubhaKant Dalei,Asia,+919776476665,Berhampur University India
|
|
||||||
Shiva,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,,IN,,shiv@gmail.com,Collage,Technology & innovations,,,true,,,NA,NA,Asia,+918529637418,
|
|
||||||
Sebastian Marzetti,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Underwater acoustics monitoring using easy to deploy systems
|
|
||||||
Our low power systems allow real-time alerts and data for immediate action",FR,2026-03-01,marzettisebastian@gmail.com,Linkedin,Technology & innovations,,,false,,,Intelligent Acoustics,Valentin Barchasz - Valentin Gies - Hervé Glotin,"Europe, France",+33766861456,
|
|
||||||
Emana Bilalović,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Ocean Asylum Certificates (OAC) establish legally protected micro-zones in the ocean by converting conservation into binding contractual commitments.
|
|
||||||
The project enables regulated no-exploitation areas that directly influence shipping and yachting behavior through enforceable restrictions, transparent monitoring, and long-term accountability.
|
|
||||||
Its objective is to embed ocean protection into maritime governance rather than rely on voluntary sustainability pledges.",XK,2025-08-18,emanabilalovic12@gmail.com,Instagram of University of Monaco,Sustainable shipping & yachting,,,true,,,Ocean Asylum Certificates,"Emana Bilalović, Alzana Bajrami","Europe, Kosovo",+381656075770,
|
|
||||||
Sabira Ayesha Bokhari,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,A gamified app to encourage sustainable coastal tourism,IN,,sabirabokhari@gmail.com,Ocean Oppurtunities,Other,,,true,,,Eco-Pirates,Aimen Akhtar,Asia,+33753635938,Universidad Catholica de Valencia
|
|
||||||
Vera Emma Porcher,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Idea:
|
|
||||||
Eco-engineered reef systems, built on circular-economy principles, repurposing surplus marine-grade concrete and recycled oyster shells into high-performance aquatic habitats that enhance and restore biodiversity and ecosystem services, support food security, and protect coastal communities and infrastructure at scale.
|
|
||||||
|
|
||||||
Objectives:
|
|
||||||
-Support long-term food security by restoring, conserving and enhancing productive marine habitats.
|
|
||||||
|
|
||||||
-Strengthen coastal protection by designing and deploying high-performance eco-structures that act as natural breakwaters, reducing wave energy and coastal erosion.
|
|
||||||
|
|
||||||
-Continuous tracking of ecosystem health in real time through automated ecological monitoring using AI-driven analysis to maximise reef performance.
|
|
||||||
|
|
||||||
- Scalable nature-inclusive designs and eco-structure integration for offshore oil & gas and offshore wind infrastructure to enhance ecological performance and biodiversity protection.
|
|
||||||
|
|
||||||
Other relevant details:
|
|
||||||
We are currently testing prototypes in Australia and developing an autonomous monitoring system, with early results showing very positive outcomes and remarkable improvements in biodiversity.",AU,2023-11-29,veraporcher20@gmail.com,Linkedin,Restoration of marine habitats & ecosystems,,,true,,,In-Depth Innovations,"Vera Porcher, Kane Dysart and Tynan Bartolo",Oceania,+61466053917,
|
|
||||||
Lee patrick EKOUAGUET,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"OCEAN-PATCH is an intelligent, autonomous maritime safety patch designed to protect human lives at sea.
|
|
||||||
It detects critical situations (man overboard, distress, abnormal conditions) in real time and transmits alerts and data without batteries, using body or environmental energy.
|
|
||||||
|
|
||||||
The project aims to improve maritime safety while generating valuable ocean data to support prevention, monitoring, and smarter decision-making through AI.",FR,2023-10-23,ogoouecorpstechnologies@gmail.com,Through online research and innovation platforms focused on ocean protection and blue tech.,Technology & innovations,,,true,https://drive.google.com/drive/folders/1BEc9s5h5H41vf2bRxpqvz4AHBWMZS1Xm?usp=drive_link,,OGOOUE CORPS TECHNOLOGIES,"ANDRE BIAYOUMOU, NGABOU PASCAL XAVIER, LYNDA NGARBAHAM, DUPUIS NOUILE NICOLAS","Europe, France",+33778199372,
|
|
||||||
Tshephiso Kola,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"LumiNet is our solution to the fishing industry’s two biggest headaches: catching the wrong fish and losing expensive gear that pollutes the ocean forever. We are replacing standard nylon nets with a smart, dual-action material that actually works with nature. First, our nets glow with a specific light underwater that sharks and turtles instinctively avoid, which keeps them out of the net while the target fish swim right in. Second, we’ve solved the ghost gear problem with a built-in fail-safe: as long as the net is used in the sun, it stays strong, but if it gets lost and sinks into the dark ocean, it rapidly breaks down and turns into fish food. Our goal is simple: to stop plastic pollution at the source and make fishing more efficient, saving marine life and money at the same time.",ZA,,kolatshepisho@gmail.com,Social Media,Sustainable fishing and aquaculture & blue food,,,true,,,Luminet,Tshephiso Kola,Africa,+27671509841,"University of the Witwatersrand, Johannesburg"
|
|
||||||
Eric & Aurélie Viard,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Use of organic edible seaweeds in daily food and gastronomy,FR,2007-03-11,eric@biovie.fr,We have been invited directly by Marine Jacq-Pietri to submit our project,Consumer awareness and education,,,true,,,Algues au quotidien,"Eric Viard, Aurélie Viard","Europe, France",+33695360436,
|
|
||||||
BARHOUMI Nawress,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"The project aims to develop an autonomous intelligent robot for cleaning marine environments, specifically targeting oil spills, human hair, and other pollutants. It focuses on sustainable technology, environmental protection, and smart control systems. The robot is built using recovered and recycled plastic materials, reinforcing the project’s commitment to circular economy principles and eco-friendly engineering.",TN,2024-05-05,nawressbarhoumigf@gmail.com,Newsletters,Technology & innovations,,,false,,,El Makina,"Mustapha Zoghlami, Nawress Barhoumi","Africa, Tunisia",+21621898617,
|
|
||||||
Yao Yinan,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"BluePulse is an innovative project that combines art, design, and technology to raise awareness about ocean pollution and marine conservation. Its main objective is to educate and inspire the public through creative visual campaigns, interactive installations, and sustainable product concepts that highlight the importance of protecting our oceans. The project also explores solutions to reduce plastic and chemical pollution, fostering a culture of environmental responsibility.",CN,,yyn982715367@outlook.com,I found out about the Monaco Ocean Protection Challenge through the organisers listed on the UArctic Congress 2026 website: https://www.uarcticcongress.fo/about,Consumer awareness and education,,,true,,,"BluePulse – Design, Protect, Inspire",Yinan Yao,Asia,+8615221826163,"Communication University of China, Nanjing(Location: Nanjing, China)"
|
|
||||||
Antalya Fadiyatullathifah,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,xxx,ID,2024-11-11,Antallathifah@gmail.com,xxxx,Blue Carbon,,,true,,,Environmental Consultant,xxx,Asia,+6281110115560,
|
|
||||||
Moramade Blanc,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Le projet SIRECOP – Suivi Intelligent des Récifs Coralliens et de la Pêche à Belle-Anse vise à renforcer la résilience des récifs coralliens du Parc Naturel National Lagon des Huîtres (PNN-LdH) et à promouvoir une pêche durable dans le Sud-Est d’Haïti.
|
|
||||||
|
|
||||||
Face aux pressions climatiques et anthropiques, il combine des technologies innovantes (capteurs environnementaux, drones, caméras sous-marines et intelligence artificielle) et une approche participative impliquant les communautés de pêcheurs.
|
|
||||||
|
|
||||||
Le projet permettra de suivre la santé des récifs, de restaurer les zones dégradées et d’améliorer la gestion des ressources halieutiques, contribuant ainsi à la conservation des écosystèmes marins, à la sécurité alimentaire et au développement durable des communautés côtières de Belle-Anse.",HT,,blamo82@yahoo.fr,Through my university and professional networks and partnerships,Technology & innovations,,,true,,,« Suivi Intelligent des Récifs Coralliens et de la Pêche à Belle-Anse » « SIRECOP »,"Moramade Blanc, Wedeline Pierre, Chralens Calixte, Jacky Duvil,Ruth Catia Bernadin",Haïti,+50940809002,"Sorbonne Universite, France"
|
|
||||||
Samuel Nnaji,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Project: Zero Ocean
|
|
||||||
|
|
||||||
Idea: Digital platform for transparent, efficient, and sustainable clean fuel supply chain in maritime
|
|
||||||
|
|
||||||
Objectives:
|
|
||||||
- Optimize clean fuel procurement and reduce emissions
|
|
||||||
- Ensure compliance with global regulations
|
|
||||||
- Enhance bunkering efficiency and audit trails
|
|
||||||
Key Features: eBDN, AI-driven analytics, real-time tracking, supplier integration",NG,,realstard247@gmail.com,WhatsApp,Sustainable shipping & yachting,,,true,,,Zero Ocean,Benjamin Odusanya,"Africa, Nigeria",+2348161502448,University of Nigeria
|
|
||||||
Hannah Gillespie,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"SeaBrew is an early-stage food and drink start-up developing a seaweed-reinforced coffee designed to improve micronutrient intake through an existing daily habit. Our product combines sustainably sourced seaweed with coffee to deliver nutrients such as magnesium, while maintaining taste and consumer acceptability. We have already conducted a blind taste test with positive consumer feedback and recently pitched SeaBrew to EIT Food, where we were awarded second place, which has encouraged us to progress towards more rigorous technical validation and compliance ahead of scaling.",GB,,hggillespie12@gmail.com,The Ocean Opportunity Lab (TOOL),Sustainable fishing and aquaculture & blue food,,,true,,,SeaBrew Coffee,"Anne Moullier, Joseph Flynn, Hannah Gillespie, Laura Coombs, Ronan Cooney",UK,+447887479247,"University of Cambridge, Cambridge, UK"
|
|
||||||
Rhea Thoppil,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"PhytOFlight
|
|
||||||
Plant-based mitigation of plastic pollution in Kerala’s backwaters
|
|
||||||
|
|
||||||
PhytOFlight is a nature-based initiative that uses phytoremediation and native aquatic vegetation to mitigate plastic and microplastic pollution in Kerala’s backwaters. Inspired by the “fight or flight” response, the project uses plants as active ecological defenders that intercept, trap, and reduce plastic waste while restoring ecosystem health.
|
|
||||||
|
|
||||||
Kerala’s backwaters are ecologically and economically vital, yet increasingly threatened by plastic pollution from domestic waste, and tourism. Conventional cleanup methods are costly and short-lived. PhytOFlight offers a low-cost, sustainable, and scalable alternative that works with natural processes rather than relying solely on mechanical removal.
|
|
||||||
|
|
||||||
Objectives
|
|
||||||
Reduce macroplastic and microplastic pollution in targeted backwater zones, improve water quality and support aquatic biodiversity and engage local communities in monitoring, maintenance and environmental awareness of such areas
|
|
||||||
|
|
||||||
PhytOFlight integrates ecological restoration with pollution control, offering a cost-effective, climate-resilient solution tailored to Kerala’s backwaters in India.",IN,,rmthoppil@gmail.com,Through my university,Reduction of pollution (plastics chemicals noise light...),,,true,,,phytoflight,Rhea Thoppil,Asia,+33745764372,"Sorbonne University, France"
|
|
||||||
Ethan Jezek,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"I have been developing an AI integrated app called OceanID that helps users identify marine species (vegetation, algae, and animals) by uploading photographs. By doing so, and by providing key and exciting information to users, I have ambitions of improving and better establishing community education and outreach, as well as marine networking in communities around the globe. Upon identifying an organism, users are presented with key ecological and economical information about the organism they captured on camera, recent publications, distribution, and if the species is currently a foodstuffs, will be presented with recipes, information on how to safely and sustainably harvest, and sustainable producers where a user could buy ingredients for said recipe . For higher level users, e.g. ocean users such as fishers, farmers, and researchers, information on permitting, local processors, producers, and developers is also provided (this information is provided for all users but intended to be helpful and beneficial for higher-level users).
|
|
||||||
|
|
||||||
Other functions on the app include; a database of all species the app has identified, a community tab that displays the discoveries of nearby and followed users, a map function where users can see community discoveries and the location of permit zones, and key economic players (see above) in relation to their location, and a cookbook that saves all of the recipes that a user has collected.",US,,ejezek12@gmail.com,I heard of the MOPC through colleagues I have on LinkedIN,Consumer awareness and education,,,true,,,OceanID,Ethan Jezek,US,+18178996766,"I have started this concept myself in Dallas, Texas but I am also a PhD candidate at the University of Waikato in New Zealand"
|
|
||||||
Nnaji Samuel Ebube,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"🌟 *Project: OceanFin - Boosting Nigeria's Blue Economy 🌊*
|
|
||||||
- *Idea*: Empower coastal communities with digital financial services for sustainable ocean-based livelihoods 🐟
|
|
||||||
- *Objectives*:
|
|
||||||
- Increase financial services 📈
|
|
||||||
- Improve financial inclusion for fishermen, traders 💸
|
|
||||||
- Promote sustainable ocean practices 🌿
|
|
||||||
- *Key features*: Digital payments, loans, insurance, FX services, international partnerships 🌍",NG,,nnajisamuel2448@gmail.com,Online,Capacity building for coastal communities,,,true,,,OceanFin,"Ifeoma Odusanya, Benjamin Odusanya","Africa, Nigeria",+2348161502448,University of Nigeria Nsukka
|
|
||||||
Sofie Boggio Sella,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"The project develops an AI-driven system to predict where coral reefs are most likely to survive under future climate conditions. By fusing seafloor structure, reef imagery, environmental data, and biodiversity indicators into a single probabilistic model, it moves beyond mapping what exists today to forecasting where restoration and protection will be most effective tomorrow. Its objective is to identify climate-resilient “safe havens” and restoration hotspots, providing actionable, uncertainty-aware maps for scientists and conservation practitioners. This enables smarter allocation of limited resources, transforming coral conservation from reactive damage control into a proactive strategy for long-term reef resilience.",IT,,boggiosellasofie@gmail.com,Linkedln,Restoration of marine habitats & ecosystems,,,true,,,PMRF: Probabilistic Multi Reef Fusion pipeline,"Sofie Boggio Sella, Lily Lewis, Mohammad Jahanbakht","Europe, Italia",+61448568796,James Cook University Australia
|
|
||||||
Christine Kurz,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,,,,christine.a.kurz@gmail.com,,,,,,,,Xy,Xy,,+4917622904612,
|
|
||||||
Antonella Bongiovanni,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"EVE Biofactory is a deep-biotech company leveraging microalgae to build the most scalable nano drug-delivery platform on the market.
|
|
||||||
Inefficient drug delivery causes treatment failure, patient harm, and up to $40B in annual losses from underperforming bioactives.
|
|
||||||
Inspired by the smallest ocean organisms, EVE develops Nanoalgosomes: naturally occurring exosomes produced from microalgae, the only delivery system that is scalable, circular, and fully biological.
|
|
||||||
Nanoalgosomes are cost-competitive, biologically active, and more efficient than synthetic nanoparticles, enabling lower drug doses and reducing the release of medicines and persistent nanomaterials into wastewater that today impact river and ocean ecosystems.",IT,2022-09-29,info@evebiofactory.com,Our mentor Alessandro ROmano pointed out the challenge and recommended our project would be a good fit.,Technology & innovations,,,true,,,EVE Biofactory,Antonella Bongiovanni - Natasa Zarovni - Mauro Manno - Paolo de Stefanis - Lorenzo Sbizzera - Gabriella Pocsfalvi - Paola Gargano,"Europe, Italia",+393286093034,
|
|
||||||
Justyna Grosjean,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"The Antifouling Coating of Tomorrow.
|
|
||||||
Lower Costs. Cleaner Oceans. Decarbonating Shipping.",DE,2021-05-11,justyna@cleanoceancoatings.com,Through the Fondation Prince Albert II de Monaco,Sustainable shipping & yachting,,,true,,,Clean Ocean Coatings GmbH,"Christina Linke, Jens Deppe, Friederike Bartels, Johana Chen, Sandra Lötsch, Patricia Greim","Europe, Germany",+33685638357,
|
|
||||||
Erick Patrick dos Anjos Vilhena,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Sustainable fish leather production is more than just an exotic alternative; it addresses critical issues within the fashion and food industries, as well as the environment.
|
|
||||||
|
|
||||||
Here are the main problems this solution solves:
|
|
||||||
|
|
||||||
1. Waste in the Fishing Industry (Circular Economy)
|
|
||||||
Currently, the vast majority of fish skins resulting from human consumption are discarded as organic waste.
|
|
||||||
|
|
||||||
The Problem: Thousands of tons of skins end up in landfills or are thrown back into rivers and oceans, causing pollution due to excess organic matter.
|
|
||||||
|
|
||||||
The Solution: It transforms a by-product (waste) into a high-value material, closing the loop of the circular economy.
|
|
||||||
|
|
||||||
2. Environmental Impact of Bovine Leather
|
|
||||||
Traditional (cow) leather carries a heavy ecological footprint that fish leather helps to mitigate.
|
|
||||||
|
|
||||||
Deforestation: Cattle ranching is a leading cause of deforestation. Fish production does not require new pastures.
|
|
||||||
|
|
||||||
Water Consumption: Raising cattle consumes massive volumes of water compared to existing aquaculture or artisanal fishing.
|
|
||||||
|
|
||||||
Carbon Emissions: Producing fish leather emits significantly fewer greenhouse gases than the beef industry supply chain.
|
|
||||||
|
|
||||||
3. Toxicity in Processing (Tanning)
|
|
||||||
Industrial tanning of common leathers often uses Chromium, a heavy metal that is highly polluting if disposed of incorrectly.
|
|
||||||
|
|
||||||
The Difference: Sustainable fish leather solutions focus on vegetable tanning (using tannins extracted from tree barks and plants). This eliminates toxic waste and results in a biodegradable product that is safe for both artisans and consumers.
|
|
||||||
|
|
||||||
4. Durability vs. Aesthetics
|
|
||||||
Many leather alternatives (such as ""synthetic leather"" made of plastic/PU) have low durability and pollute the environment with microplastics.
|
|
||||||
|
|
||||||
The Solution: Fish leather has a cross-fiber structure (unlike the parallel fibers in bovine leather), making it extremely strong and tear-resistant despite being thin. It solves the dilemma for those seeking a material that is delicate, durable, and eco-",BR,2023-01-01,e.vilhena@hotmail.com,Linkedln,Sustainable fishing and aquaculture & blue food,,,true,,,sustainable fish leather,Andria Carrilho,South America,+5596981337237,
|
|
||||||
Amaia Rodriguez,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,Clean plastic from the sea with fishermen and transform the waste into materials for construction and architecture.,ES,2020-05-18,amaia@thegravitywave.com,A friend sent it to me,Reduction of pollution (plastics chemicals noise light...),,,true,https://drive.google.com/drive/folders/11McKvPyKzbUgYiFd2gfeWvLrlGGPhyP2?usp=sharing,,GRAVITY WAVE,"Amaia Rodriguez, Julen Rodriguez, Naiara Lopez, Alvaro Garcia, Camila Lago, Norberto De Rodrigo, Irene Hurtado","Europe, Spain",+34606655862,
|
|
||||||
Dr Mumthas Yahiya,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"“Nature-Based Solutions for Mitigating Ocean Acidification through Coastal Blue Carbon Ecosystems - Project Idea
|
|
||||||
|
|
||||||
This project focuses on mitigating ocean acidification by enhancing and restoring blue carbon ecosystems such as mangroves, seagrasses, and salt marshes. These ecosystems absorb atmospheric CO₂, increase local alkalinity, and act as natural buffers against pH reduction in coastal waters. The study will evaluate their potential as cost-effective, climate-resilient mitigation strategies.
|
|
||||||
|
|
||||||
Objectives
|
|
||||||
|
|
||||||
To assess the role of mangroves and seagrass meadows in reducing coastal seawater acidity.
|
|
||||||
|
|
||||||
To quantify carbon sequestration and alkalinity enhancement in selected coastal habitats.
|
|
||||||
|
|
||||||
To evaluate ecosystem-based management practices as mitigation tools for ocean acidification.
|
|
||||||
|
|
||||||
To provide policy-relevant recommendations for integrating blue carbon ecosystems into coastal climate action plans.
|
|
||||||
|
|
||||||
Relevance
|
|
||||||
|
|
||||||
The project supports climate change mitigation, marine biodiversity conservation, and sustainable coastal management while addressing the growing threat of ocean acidification.",,,mumthasy@gmail.com,IUCN,Mitigation of ocean acidification,,,true,,,Migratory birds,Thamanna K,US,+917012789400,Kerala
|
|
||||||
yvano voigt,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Reducing plastic waste
|
|
||||||
|
|
||||||
Reducing skin cancer rates
|
|
||||||
|
|
||||||
Reducing coral reef destruction",FR,,yvano.voigt@gmail.com,thanks to the oceanography museum,Reduction of pollution (plastics chemicals noise light...),,,true,,,Totem by FrenchKiss suncare,"yvano voigt, Elsa Delpace","Europe, France",+33652294558,"Ipag business school, Nice France"
|
|
||||||
Lily Atussa Payton,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Oyster Club NYC is a fledgling organization aimed at giving New Yorkers a hands-on connection to their maritime past, present, and future through the lens of ocean sustainability. Rending New Yorkers that NYC is truly their oyster, and that some of the strongest communities are built around the smallest of creatures. Specifically, we create bespoke events aimed at bringing together people to create community, discuss how making small choices can benefit our oceans, such as eating oysters, all while having fun in the process. This has manifested in a Learn-to-Shuck Holiday Party in December and a monthly oyster happy hour at various locations across the city. Our specific objectives are threefold:
|
|
||||||
- use oysters as a catalyst to expose New Yorkers to sustainable and regenerative food in a social environment,
|
|
||||||
- embed an oceans-focused mindset into an island city that often forgets its connection to the water, and
|
|
||||||
- build a climate-minded community across the five boroughs.",US,,lily.a.payton@gmail.com,Online research,Consumer awareness and education,,,true,,,Oyster Club NYC,"Lily Payton, Kelsey Burkin, Savannah Harker",US,+13015297789,N/A
|
|
||||||
Gary Molano,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We breed better seaweed using genomic breeding techniques. We started with kelp, and have achieved 4fold harvestable yield gains in 5 years of breeding, a 10x speed advancement compared to breeding efforts in Asia. We are currently targeting traits that increase the value of seaweed, such as lower iodine and higher bioactive composition (fucoidan, alginate, laminarin, etc), to help make farmed kelp more competitive with wild harvests. We also have a breeding scheme that produces ""sterile"" kelp to protect local ecosystems from farmed kelp. This sterile kelp is produced using non-GMO techniques.",US,2023-07-19,gary@macrobreed.com,Through the ocean exchange newsletter,Sustainable fishing and aquaculture & blue food,,,true,,,MacroBreed,"Scott Lindell, Charles Yarish, Filipe Alberto",US,+12135198233,
|
|
||||||
Qendresa Krasniqi,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We are developing a specialized ROV designed to restore marine ecosystems. Coastal ecosystems, such as the Oslofjord, are currently threatened by invasive species and marine debris. Specifically, the Pacific oyster is spreading rapidly, requiring efficient and noninvasive methods for removal to protect local biodiversity. Our current prototype is part of a joint venture with 'Matfat Oslofjorden,' where it will harvest invasive oysters from the Oslo Fjord to be repurposed as a sustainable food source. Our solution is efficient, non-invasive, and fully programmable for diverse oceanic habitats and tasks. Navier USN is not starting from scratch with a proven track record in developing autonomous surface vehicles (ASVs), our startup concept expands this expertise into the underwater domain. We are a seasoned technical and commercial team with a proven track record in autonomous maritime technology, including multiple world championship titles. Supported by prominent industry partners, we have the proven competence and scale to transform maritime environmental management",NO,2022-10-06,qendresa04@gmail.com,1000 Ocean StartUps,Restoration of marine habitats & ecosystems,,,true,,,Aegir by Navier USN,"Qendresa Krasniqi, Hedda Collin, Markus Marstad","Europe, Norway",+4798474602,
|
|
||||||
Dorra Fadhloun,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,PlastiTrack's goal is to turn citizen smartphone photos into citywide microplastic pollution heatmaps that municipalities use to prioritize cleanup investments.,TN,,dorra.fadhloun@msb.tn,LinkedIn,Technology & innovations,,,true,,,Oceani,Samar,"Africa, Tunisia",+21629508048,Mediterranean School of Business
|
|
||||||
Ahamed Adhnaf,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"AquaHorizon is a holistic ocean innovation hub that transforms ocean challenges into solutions while empowering coastal communities. Our objectives are to develop sustainable practices for marine conservation, reduce pollution, provide education and capacity building for coastal populations, and create a collaborative space where innovators can design and implement solutions for a healthier ocean and thriving communities.",LK,,anaadhnaf413@gmail.com,I learned about the Monaco Ocean Protection Challenge through a friend.,Capacity building for coastal communities,,,true,,,AquaHorizon,"Ahamed Adhnaf , Kaveesha Gunarathna, Mohammed Rifath","Africa, Sri Lanka",+94760270097,National Institute of Social Development - Sri Lanka
|
|
||||||
Robert Kunzmann,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"According to the UN, only 9% of 8.3 billion tons of plastic waste have been recycled over the past 65 years.
|
|
||||||
Most of the plastic waste ends up burned, burried or in the oceans. This profound impact on our environment is not fully understood yet, but micro plastics have been found in all the way from glaciers, to human placentas.
|
|
||||||
Today, recycling is not economically feasible in most situations. There are several reasons for this. Firstly, many recycling methods cannot handle mixed waste and therefore require waste to be sorted. Where chemical recycling can accept mixed plastic, the high temperature or pressure requirements lead to high costs. This is why recycling rates remain low. Plastalyst makes it possible to break down waste into core chemicals such as monomers or hydrogen and carbon monoxide (syngas for SAF and biodiesel).
|
|
||||||
Organic waste is decomposed into alcohols and syngas, whereas plastic is decomposed into methanol, alkanes or monomers. It uses only water, waste, and a reusable catalyst as input. The reaction occurred at a temperature of only 200°C. Compared to other methods, our method has significant advantages such as low energy use, which results in lower operational cost. Next, we use water as a solvent, drying of waste is not needed, therefore it will cut the cost of preparing the material. Lastly, no solvent is needed and no CO2 is emitted in the reaction. Unlike organic methods, such as biodigestion that require a lot of time and emit a lot of CO2, Plastalyst is a fast chemical method that emits no CO2.",LU,2019-04-01,robert.kunzmann@acbiode.com,Climate KIC,Reduction of pollution (plastics chemicals noise light...),,,true,,,AC Biode,Robert Kunzmann,"Europe, Luxembourg",+441751026862,
|
|
||||||
Mayoro MBAYE,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"The MER SEA GUEDJI center designated according to the "" Educational Archipelago"" concept ,composed of self- contained educational modules connected by landscaped pathways . This lightweight,reversible,and bioclimatic architecture respect the public martime domain integrates harmoniously into the natural and social environment .",SN,,dg@kma-international.com,Dr Manon Aminatou,Capacity building for coastal communities,,,true,,,MER SEA GUEDJI,Mayoro MBAYE +Mbacké SECK+Ali DOUCOURÈ,"Africa, Senegal",+221776441916,
|
|
||||||
Divin Arnaud KOUEBATOUKA,Received,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Our project transforms invasive water hyacinth, a major threat to rivers, coastal lagoons and marine ecosystems, into 100% organic absorbent solutions used to control oil and chemical pollution.
|
|
||||||
By harvesting water hyacinth before it reaches estuaries and coastal zones, we prevent ecosystem degradation while supplying industries and ports with sustainable spill-response materials.
|
|
||||||
Our flagship product, KUKIA®, absorbs hydrocarbons efficiently and is later recycled into alternative fuel for cement plants, creating a circular, zero-waste model.
|
|
||||||
The project combines ocean and freshwater protection, industrial pollution control, and community empowerment, generating income for women-led harvesting groups while reducing marine contamination risks.
|
|
||||||
This scalable solution contributes directly to ocean conservation, blue economy resilience, and sustainable industrial practices in Africa and beyond.",CG,2022-07-27,divinkoueba@gmail.com,"I learned about the Monaco Ocean Protection Challenge through professional networks and sustainability-focused opportunity monitoring platforms, including LinkedIn and grant-funding communities dedicated to ocean and climate innovation.",Restoration of marine habitats & ecosystems,,,false,https://drive.google.com/drive/folders/1uHEFuI-iosKap2OPSUdQsbXuih07g7n0?usp=drive_link,,Green Tech Africa,"Our lean startup is primarily run by a team of 3: Divin, Osvaldo, and Jessica, alongside a great supportive team of advisors. Using his skills in tech and as a civil & environmental engineer, Divin has designed and built several innovations geared towards sustainability, including the solar dryer now being used across Congo to revive the pyrethrum industry, smart roads, and smart pipes. The Central Africa Community recently awarded him the best innovator in Congo. He is the CEO. Working for L'Oréal, Osvaldo is well-versed in manufacturing and running supply chain processes. This experience, in addition to being an engineer, makes him an ideal CTO. Jessica has an extensive background in tech and finance. She has been a Microsoft Ambassador and Hult Prize coordinator. Her experience in handling projects and relationships with global agencies and customers is handy in her CFO role. She also hails from Homabay, Congo, where hyacinth surrounds the island for days and weeks at times. This blocks waterways and sometimes prevents children from going to school. The team has achieved several milestones, such as making scientific validation and proof of concept of the products, raising over CFA 3M in funding, and winning significant international awards, including the Central Africa Youth for Climate Action Award, Best Manufacturing Startup in Congo, Best Innovation in Congo by EAC, the World Engineering Day Hackathon by UNESCO, the TotalEnergies Startup of the Year, and Falling Walls Lab Brazzaville.","Africa, Congo",+242069323235,
|
|
||||||
Paul Schmitzberger,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"We decouple the production of marine protein from the ocean by replicating aquatic ecosystems in modular, automated, plug-and-play systems. We combine hardware, software and biology in products called LARA and Vortex. These modular units are controlled and operated by AI agents under the remote supervision of our biologists and system engineers. The agents optimize the growth of microalgae, zooplankton and fish or shrimps for human consumption. Our goal is to establish large-scale, environmentally friendly aquaculture operations in diverse environments, advancing global food security and sustainability.",AT,2019-11-15,laura@blue-planet-ecosystems.com,Online search,Sustainable fishing and aquaculture & blue food,,,false,,,Blue Planet Ecosystems,"Paul Schmitzberger, Cécile Deterre, Stephan Mayrhofer, Jens Cormier, Pierre De Villiers, Stephan Sergides, Jakob Weber, Romana Zabojnikova, Laura Belz","Austria, Europe",+436642347890,
|
|
||||||
Julia Denkmayr,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"NEREIA develops non-toxic antifouling technology using functional surfaces to prevent biofouling on ship hulls, reducing drag and improving fuel efficiency. Eliminating hull fouling could save the shipping industry up to $30bn in fuel costs and 200 Mt of CO2 annually.",AT,2026-04-30,julia@nereia-coatings.com,"Through a Carbon 13 domain expert, Thibaut Monfort Micheo, as well as LinkedIn and other social media.",Sustainable shipping & yachting,,,false,,,NEREIA,Rimah Darawish,"Austria, Europe",+393203476632,
|
|
||||||
Tara Lepine,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"OceanSeed deploys mobile hatchery units as emergency response infrastructure to restore marine ecosystems and fisheries after collapse, often caused by climate change. Its objectives are to rapidly produce native juvenile shellfish, rebuild ecosystems and keystone populations, protect biodiversity, and support local livelihoods. OceanSeed can be scaled globally through a network of mobile hatcheries, using aquaculture as a tool for conservation and climate adaptation.",CA,,tlep171@aucklanduni.ac.nz,Communication from our university department head.,Sustainable fishing and aquaculture & blue food,,,false,,,OceanSeed,Tara Lepine,Canada,+64273401929,"University of Auckland, Auckland, New Zealand"
|
|
||||||
Reid Barnett,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Our project is using our proprietary technology to grow plants directly on the surface of natural and manmade surface waters. This allows us to sequester carbon, nutirents, and other chemical pollutants at scale in both ocean systems and the freshwater systems upstream. After the plants are fully grown the entire system, material and biomass, can be pyrolyzed to generate carbon negative energy and lock carbon away in a stable form. This process is highly efficient and extremely inexpensive. When we pyrolyze the system we generate over three times more revenue than the total cost of the system and its operation, while opening up opportunites for blue carbon credits and creating bio-oil which can be refined for various uses.
|
|
||||||
This project is about creating an entirely new pathway for pollutants in the environment to redirect them from where they cause harm and towards where they can generate value and do good.",US,2024-05-01,reidbarnett@ceretunellc.com,We were connected through the team at Ocean Exchange.,Reduction of pollution (plastics chemicals noise light...),,,true,,,Ceretune LLC,Reid Barnett and Blake Parrish,US,+19198010336,
|
|
||||||
Ryan Borotra,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"Sentry Labs is developing graphene field-effect transistor (GFET)–based molecular sensors for sustainable fishing, aquaculture, and blue food systems. The project focuses on real-time, in-situ detection of biologically and chemically relevant signals in seawater to enable earlier identification of environmental and biological risks affecting farmed and wild stocks. Our objective is to provide robust, reproducible sensing systems that support healthier stocks, reduced losses, and more sustainable management of marine food production.",CA,2025-10-20,ryan@sentrylabs.cc,LinkedIn,Sustainable fishing and aquaculture & blue food,,,true,,,Sentry Labs,"Ryan Borotra, Martin Chaperot, Andrei Bogza",Canada,+16479659526,
|
|
||||||
Nadine Hakim,,the « Start-ups » category: Open to students fresh graduates & entrepreneurs with an existing comp,"SEAMOSS is a sustainable biodesign and coastal livelihood project focused on the cultivation and transformation of sea moss (marine macroalgae) as a nature-based solution to environmental and social challenges in coastal communities. The project combines regenerative aquaculture, biomaterial development, and community-led value chains to reduce pressure on marine ecosystems while creating local economic opportunities.
|
|
||||||
|
|
||||||
The core idea is to cultivate native sea moss species using low-impact, regenerative methods and transform the biomass into biodegradable materials and functional products that can replace plastic-based alternatives, particularly in packaging, design, and everyday consumer goods.",CO,2025-10-01,nadinehakimm@gmail.com,,Sustainable fishing and aquaculture & blue food,,,true,,,SEAMOSS COLOMBIA,Sandra Bessudo and Irene Arroyave,South America,+573205421979,
|
|
||||||
Maria Ester Faiella,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"ThermoShield is a modular underwater panel system that passively reduces local heat from coastal infrastructure. Its objective is to prevent thermal stress on sensitive marine ecosystems, protecting coral reefs and seagrass worldwide. The panels are easy to install, require no electricity and provide measurable local temperature reductions of 0.3–0.5°C, making the solution scalable and globally applicable.",IT,,maria.ester.faiella@gmail.com,LinkedIn,Restoration of marine habitats & ecosystems,,,true,,,ThermoShield,Maria Ester Faiella,"Europe, Italia",+393311538952,The American University of Rome
|
|
||||||
Kumari Anushka,,the « Business concepts » category: open to students (Bachelor Master MBA & PhD) & fresh graduates,"Coral reefs are collapsing - rising ocean temperatures have triggered mass bleaching, 84.6% of corals in Lakshadweep bleached recently. India has 1,439 km² of mapped coral reefs, coasts have 80 ± 33 microplastic particles per cubic meter, and ~30% of sampled market fish have microplastics. Odisha’s Bay of Bengal estuaries have elevated metal concentrations.
|
|
||||||
|
|
||||||
Each Reef Revival Pod is a solar-powered floating buoy deployed near degraded reefs with:
|
|
||||||
1. Underwater acoustics: healthy reefs produce sounds that can be played near dying reefs to attract marine life back to them. In trials, degraded patches with reef sounds saw fish population double.
|
|
||||||
2. Each pod pumps the surrounding seawater through fine filters to capture microplastic debris.
|
|
||||||
3. Water is also pumped through replaceable resin-based adsorption cartridges to bind with dissolved heavy metals in the water.
|
|
||||||
4. Onboard sensors log water quality (temperature, pH, turbidity, etc.) - collecting data for adaptive management.
|
|
||||||
|
|
||||||
After success in India’s waters, the project will be expanded to coral regions globally.
|
|
||||||
|
|
||||||
In India, the CRZ notification 2019 classifies coral reefs as ecologically sensitive (CRZ-I A) and regulates activities in coastal waters (CRZ-IV), so my revival pods should be permitted as non-invasive research/restoration infrastructure (no reef anchoring and removable).
|
|
||||||
|
|
||||||
The MoEFCC National Coastal Mission Scheme funds coral/mangrove conservation action plans, marine & coastal R&D - this would help with scaling the number of buoys deployed.
|
|
||||||
|
|
||||||
Also, the World Bank-supported Integrated Coastal Zone Management (ICZM) gives importance to science-based coastal planning; pods’ sensor data could be used for threat mapping and adaptive management in the deployed zones.
|
|
||||||
|
|
||||||
For global scaling: Australia’s Reef 2050 Plan, Indonesia’s COREMAP, and the US NOAA Coral Reef Conservation Program exist, so the project could plug into existing national funding priorities across eligible countries.",IN,,nasabutbetter@gmail.com,My university's professor,Restoration of marine habitats & ecosystems,,,true,,,accore,Kumari Anushka,Asia,+919798061093,Ashoka University
|
|
||||||
|
201
docs/claude-architecture-redesign/00-executive-summary.md
Normal file
201
docs/claude-architecture-redesign/00-executive-summary.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Executive Summary: MOPC Architecture Redesign
|
||||||
|
|
||||||
|
## Why This Redesign
|
||||||
|
|
||||||
|
The MOPC platform currently uses a **Pipeline -> Track -> Stage** model with generic JSON configs to orchestrate the competition. While technically sound, this architecture introduces unnecessary abstraction for what is fundamentally a **linear sequential competition flow**.
|
||||||
|
|
||||||
|
### Current Problems
|
||||||
|
|
||||||
|
| Problem | Impact |
|
||||||
|
|---------|--------|
|
||||||
|
| **3-level nesting** (Pipeline->Track->Stage) | Cognitive overhead for admins configuring rounds |
|
||||||
|
| **Generic `configJson` blobs** per stage type | "Vague" — hard to know what's configurable without reading code |
|
||||||
|
| **No explicit jury entities** | Juries are implicit (per-stage assignments), can't manage "Jury 1" as a thing |
|
||||||
|
| **Single submission round** | No way to open a second submission window for semi-finalists |
|
||||||
|
| **Track layer for main flow** | MAIN track adds indirection without value for a linear flow |
|
||||||
|
| **No mentoring workspace** | Mentor file exchange exists but no comments, no promotion to submission |
|
||||||
|
| **No winner confirmation** | No multi-party agreement step to cement winners |
|
||||||
|
| **Missing round types** | Can't model a "Semi-finalist Submission" or "Mentoring" or "Confirmation" step |
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
1. **Domain over abstraction** — Models map directly to competition concepts (Jury 1, Round 2, etc.)
|
||||||
|
2. **Linear by default** — The main flow is sequential. Branching is only for special awards.
|
||||||
|
3. **Typed configs over JSON blobs** — Each round type has explicit, documented fields.
|
||||||
|
4. **Explicit entities** — Juries, submission windows, and confirmation steps are first-class models.
|
||||||
|
5. **Deep integration** — Every feature connects. Jury groups link to rounds, rounds link to submissions, submissions link to evaluations.
|
||||||
|
6. **Admin override everywhere** — Any automated decision can be manually overridden with audit trail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before & After: Architecture Comparison
|
||||||
|
|
||||||
|
### BEFORE (Current System)
|
||||||
|
|
||||||
|
```
|
||||||
|
Program
|
||||||
|
└── Pipeline (generic container)
|
||||||
|
├── Track: "Main Competition" (MAIN)
|
||||||
|
│ ├── Stage: "Intake" (INTAKE, configJson: {...})
|
||||||
|
│ ├── Stage: "Filtering" (FILTER, configJson: {...})
|
||||||
|
│ ├── Stage: "Evaluation" (EVALUATION, configJson: {...})
|
||||||
|
│ ├── Stage: "Selection" (SELECTION, configJson: {...})
|
||||||
|
│ ├── Stage: "Live Finals" (LIVE_FINAL, configJson: {...})
|
||||||
|
│ └── Stage: "Results" (RESULTS, configJson: {...})
|
||||||
|
├── Track: "Award 1" (AWARD)
|
||||||
|
│ ├── Stage: "Evaluation" (EVALUATION)
|
||||||
|
│ └── Stage: "Results" (RESULTS)
|
||||||
|
└── Track: "Award 2" (AWARD)
|
||||||
|
├── Stage: "Evaluation" (EVALUATION)
|
||||||
|
└── Stage: "Results" (RESULTS)
|
||||||
|
|
||||||
|
Juries: implicit (assignments per stage, no named entity)
|
||||||
|
Submissions: single round (one INTAKE stage)
|
||||||
|
Mentoring: basic (messages + notes, no workspace)
|
||||||
|
Winner confirmation: none
|
||||||
|
```
|
||||||
|
|
||||||
|
### AFTER (Redesigned System)
|
||||||
|
|
||||||
|
```
|
||||||
|
Program
|
||||||
|
└── Competition (purpose-built, replaces Pipeline)
|
||||||
|
├── Rounds (linear sequence, replaces Track+Stage):
|
||||||
|
│ ├── Round 1: "Application Window" ─────── (INTAKE)
|
||||||
|
│ ├── Round 2: "AI Screening" ──────────── (FILTERING)
|
||||||
|
│ ├── Round 3: "Jury 1 - Semi-finalist" ── (EVALUATION) ── juryGroupId: jury-1
|
||||||
|
│ ├── Round 4: "Semi-finalist Docs" ─────── (SUBMISSION) ── submissionWindowId: sw-2
|
||||||
|
│ ├── Round 5: "Jury 2 - Finalist" ──────── (EVALUATION) ── juryGroupId: jury-2
|
||||||
|
│ ├── Round 6: "Finalist Mentoring" ─────── (MENTORING)
|
||||||
|
│ ├── Round 7: "Live Finals" ────────────── (LIVE_FINAL) ── juryGroupId: jury-3
|
||||||
|
│ └── Round 8: "Confirm Winners" ─────────── (CONFIRMATION)
|
||||||
|
│
|
||||||
|
├── Jury Groups (explicit, named):
|
||||||
|
│ ├── "Jury 1" ── members: [judge-a, judge-b, ...] ── linked to Round 3
|
||||||
|
│ ├── "Jury 2" ── members: [judge-c, judge-d, ...] ── linked to Round 5
|
||||||
|
│ └── "Jury 3" ── members: [judge-e, judge-f, ...] ── linked to Round 7
|
||||||
|
│
|
||||||
|
├── Submission Windows (multi-round):
|
||||||
|
│ ├── Window 1: "Round 1 Docs" ── requirements: [Exec Summary, Business Plan]
|
||||||
|
│ └── Window 2: "Round 2 Docs" ── requirements: [Updated Plan, Video Pitch]
|
||||||
|
│
|
||||||
|
└── Special Awards (standalone):
|
||||||
|
├── "Innovation Award" ── mode: STAY_IN_MAIN, juryGroup: jury-2-award
|
||||||
|
└── "Impact Award" ── mode: SEPARATE_POOL, juryGroup: dedicated-jury
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### 1. Eliminate the Track Layer
|
||||||
|
|
||||||
|
**Decision:** Remove the `Track` model entirely. The main competition is a linear sequence of Rounds. Special awards become standalone entities.
|
||||||
|
|
||||||
|
**Rationale:** The MOPC competition has one main flow (Intake -> Filtering -> Jury 1 -> Submission 2 -> Jury 2 -> Mentoring -> Finals -> Confirmation). The `Track` concept (MAIN/AWARD/SHOWCASE with RoutingMode and DecisionMode) was designed for branching flows that don't exist in this competition. Awards don't need their own track — they're parallel evaluation/voting processes that reference the same projects.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `Track` model deleted
|
||||||
|
- `TrackKind`, `RoutingMode` enums deleted
|
||||||
|
- `ProjectStageState.trackId` removed (becomes `ProjectRoundState` with just `projectId` + `roundId`)
|
||||||
|
- Award tracks replaced with enhanced `SpecialAward` model
|
||||||
|
- ~200 lines of Track CRUD code eliminated
|
||||||
|
|
||||||
|
### 2. Rename Pipeline -> Competition, Stage -> Round
|
||||||
|
|
||||||
|
**Decision:** Use domain-specific names that map to the competition vocabulary.
|
||||||
|
|
||||||
|
**Rationale:** Admins think in terms of "Competition 2026" and "Round 3: Jury 1 Evaluation", not "Pipeline" and "Stage". The rename costs nothing but improves comprehension.
|
||||||
|
|
||||||
|
### 3. Expand RoundType Enum
|
||||||
|
|
||||||
|
**Decision:** Add SUBMISSION, MENTORING, and CONFIRMATION to the existing types.
|
||||||
|
|
||||||
|
**Current:** `INTAKE | FILTER | EVALUATION | SELECTION | LIVE_FINAL | RESULTS`
|
||||||
|
|
||||||
|
**New:** `INTAKE | FILTERING | EVALUATION | SUBMISSION | MENTORING | LIVE_FINAL | CONFIRMATION`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- `FILTER` -> `FILTERING` (clearer naming)
|
||||||
|
- `SELECTION` removed (merged into EVALUATION's advancement config)
|
||||||
|
- `RESULTS` removed (results are a view, not a round — handled by the CONFIRMATION round output)
|
||||||
|
- `SUBMISSION` added (new doc requirements for advancing teams)
|
||||||
|
- `MENTORING` added (mentor-team workspace activation)
|
||||||
|
- `CONFIRMATION` added (multi-party winner agreement)
|
||||||
|
|
||||||
|
### 4. Explicit JuryGroup Model
|
||||||
|
|
||||||
|
**Decision:** Juries are first-class entities with names, members, and per-juror configuration.
|
||||||
|
|
||||||
|
**Before:** Assignments were per-stage with no grouping concept. "Jury 1" only existed in the admin's head.
|
||||||
|
|
||||||
|
**After:** `JuryGroup` model with members, linked to specific evaluation/live-final rounds. A juror can belong to multiple groups.
|
||||||
|
|
||||||
|
### 5. Multi-Round Submissions via SubmissionWindow
|
||||||
|
|
||||||
|
**Decision:** A new `SubmissionWindow` model handles document requirements per round, with automatic locking of previous windows.
|
||||||
|
|
||||||
|
**Before:** One INTAKE stage with one set of `FileRequirement` records.
|
||||||
|
|
||||||
|
**After:** Each submission window has its own requirements. When a new window opens, previous ones lock for applicants. Jury rounds can see docs from specific windows.
|
||||||
|
|
||||||
|
### 6. Typed Configs Replace JSON Blobs
|
||||||
|
|
||||||
|
**Decision:** Replace generic `configJson: Json?` with round-type-specific config models or strongly-typed JSON with Zod validation.
|
||||||
|
|
||||||
|
**Before:** `Stage.configJson` could be anything — you'd have to read the code to know what fields exist for each StageType.
|
||||||
|
|
||||||
|
**After:** Each round type has a documented, validated config shape. The wizard presents only the fields relevant to each type.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scope Summary
|
||||||
|
|
||||||
|
| Area | Action | Complexity |
|
||||||
|
|------|--------|------------|
|
||||||
|
| **Schema** | Major changes (new models, renamed models, deleted Track) | High |
|
||||||
|
| **Stage engine** | Rename to round engine, simplify (no Track references) | Medium |
|
||||||
|
| **Assignment service** | Enhance with jury groups, hard/soft caps, category ratios | Medium |
|
||||||
|
| **Filtering service** | Minimal changes (rename stageId -> roundId) | Low |
|
||||||
|
| **Live control** | Enhanced stage manager UI, same core logic | Medium |
|
||||||
|
| **Mentor system** | Major enhancement (workspace, files, comments, promotion) | High |
|
||||||
|
| **Winner confirmation** | New system (proposal, approvals, freezing) | High |
|
||||||
|
| **Special awards** | Enhanced (standalone, two modes, own jury groups) | Medium |
|
||||||
|
| **Notification system** | Enhanced (deadline countdowns, reminder triggers) | Medium |
|
||||||
|
| **Admin UI** | Full redesign (competition wizard, round management) | High |
|
||||||
|
| **Jury UI** | Enhanced (multi-jury dashboard, cross-round docs) | Medium |
|
||||||
|
| **Applicant UI** | Enhanced (multi-round submissions, mentoring workspace) | Medium |
|
||||||
|
| **Mentor UI** | New (dedicated mentor dashboard and workspace) | High |
|
||||||
|
| **API routers** | Major refactor (rename, new endpoints, removed endpoints) | High |
|
||||||
|
| **Migration** | Data migration from old schema to new | Medium |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Document Index
|
||||||
|
|
||||||
|
| # | Document | Purpose |
|
||||||
|
|---|----------|---------|
|
||||||
|
| 00 | This document | Executive summary and key decisions |
|
||||||
|
| 01 | Current System Audit | What exists today — models, services, routers, UI |
|
||||||
|
| 02 | Gap Analysis | Current vs required, feature-by-feature comparison |
|
||||||
|
| 03 | Data Model | Complete Prisma schema redesign with migration SQL |
|
||||||
|
| 04 | Round: Intake | Application window, forms, deadlines, drafts |
|
||||||
|
| 05 | Round: Filtering | AI screening, eligibility, admin overrides |
|
||||||
|
| 06 | Round: Evaluation | Multi-jury, caps, ratios, scoring, advancement |
|
||||||
|
| 07 | Round: Submission | Multi-round docs, locking, jury visibility |
|
||||||
|
| 08 | Round: Mentoring | Private workspace, file comments, promotion |
|
||||||
|
| 09 | Round: Live Finals | Stage manager, live voting, deliberation |
|
||||||
|
| 10 | Round: Confirmation | Jury signatures, admin override, result freezing |
|
||||||
|
| 11 | Special Awards | Two modes, award juries, integration |
|
||||||
|
| 12 | Jury Groups | Multi-jury architecture, members, overrides |
|
||||||
|
| 13 | Notifications & Deadlines | Countdowns, reminders, window management |
|
||||||
|
| 14 | AI Services | Filtering, assignment, summaries, eligibility |
|
||||||
|
| 15 | Admin UI Redesign | Dashboard, wizard, round management |
|
||||||
|
| 16 | Jury UI Redesign | Dashboard, evaluation, live voting |
|
||||||
|
| 17 | Applicant UI Redesign | Dashboard, multi-round uploads, mentoring |
|
||||||
|
| 18 | Mentor UI Redesign | Dashboard, workspace, file review |
|
||||||
|
| 19 | API Router Reference | tRPC changes — new, modified, removed |
|
||||||
|
| 20 | Service Layer Changes | Engine, assignment, new services |
|
||||||
|
| 21 | Migration Strategy | Schema migration, data migration, rollback |
|
||||||
|
| 22 | Integration Map | Cross-reference of all feature connections |
|
||||||
|
| 23 | Implementation Sequence | Phased order with dependencies |
|
||||||
591
docs/claude-architecture-redesign/01-current-system-audit.md
Normal file
591
docs/claude-architecture-redesign/01-current-system-audit.md
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
# Current System Audit: MOPC Platform
|
||||||
|
|
||||||
|
**Document Version:** 1.0
|
||||||
|
**Date:** 2026-02-15
|
||||||
|
**Status:** Complete
|
||||||
|
**Purpose:** Comprehensive inventory of all data models, services, routers, pages, and capabilities in the MOPC platform as of February 2026.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Data Models](#1-data-models)
|
||||||
|
2. [Enums](#2-enums)
|
||||||
|
3. [Services](#3-services)
|
||||||
|
4. [tRPC Routers](#4-trpc-routers)
|
||||||
|
5. [UI Pages](#5-ui-pages)
|
||||||
|
6. [Strengths](#6-strengths-of-current-system)
|
||||||
|
7. [Weaknesses](#7-weaknesses-of-current-system)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Data Models
|
||||||
|
|
||||||
|
### 1.1 Competition Structure Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **Pipeline** | Top-level competition round container | `programId`, `name`, `slug`, `status`, `settingsJson` | → Program, → Track[] |
|
||||||
|
| **Track** | Competition lane (MAIN or AWARD) | `pipelineId`, `name`, `kind`, `routingMode`, `decisionMode`, `sortOrder`, `settingsJson` | → Pipeline, → Stage[], → ProjectStageState[], ← SpecialAward? |
|
||||||
|
| **Stage** | Individual competition phase within a track | `trackId`, `stageType`, `name`, `slug`, `status`, `sortOrder`, `configJson`, `windowOpenAt`, `windowCloseAt` | → Track, → ProjectStageState[], → StageTransition[], → Cohort[], → LiveProgressCursor?, → LiveVotingSession? |
|
||||||
|
| **StageTransition** | Defines valid stage-to-stage movements | `fromStageId`, `toStageId`, `isDefault`, `guardJson` | → Stage (from), → Stage (to) |
|
||||||
|
| **ProjectStageState** | Tracks project position in pipeline | `projectId`, `trackId`, `stageId`, `state`, `enteredAt`, `exitedAt`, `metadataJson` | → Project, → Track, → Stage |
|
||||||
|
| **Cohort** | Groups projects for live voting | `stageId`, `name`, `votingMode`, `isOpen`, `windowOpenAt`, `windowCloseAt` | → Stage, → CohortProject[] |
|
||||||
|
| **CohortProject** | Project membership in a cohort | `cohortId`, `projectId`, `sortOrder` | → Cohort, → Project |
|
||||||
|
|
||||||
|
### 1.2 Project & Submission Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **Project** | Core project/application entity | `programId`, `title`, `teamName`, `description`, `competitionCategory`, `oceanIssue`, `country`, `geographicZone`, `institution`, `wantsMentorship`, `foundedAt`, `status`, `submissionSource`, `submittedByEmail`, `submittedAt`, `tags`, `metadataJson`, `isDraft` | → Program, → ProjectFile[], → Assignment[], → TeamMember[], → MentorAssignment?, → FilteringResult[], → AwardEligibility[], → ProjectStageState[], → CohortProject[] |
|
||||||
|
| **ProjectFile** | File uploads attached to projects | `projectId`, `requirementId`, `fileType`, `fileName`, `mimeType`, `size`, `bucket`, `objectKey`, `version`, `replacedById`, `isLate` | → Project, → FileRequirement?, → ProjectFile (versioning) |
|
||||||
|
| **FileRequirement** | Defines required file uploads per stage | `stageId`, `name`, `description`, `acceptedMimeTypes`, `maxSizeMB`, `isRequired`, `sortOrder` | → Stage, ← ProjectFile[] |
|
||||||
|
| **TeamMember** | Team composition for projects | `projectId`, `userId`, `role`, `title` | → Project, → User |
|
||||||
|
|
||||||
|
### 1.3 Jury & Evaluation Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **Assignment** | Jury member assigned to evaluate a project | `userId`, `projectId`, `stageId`, `method`, `isRequired`, `isCompleted`, `aiConfidenceScore`, `expertiseMatchScore`, `aiReasoning` | → User, → Project, → Stage, → Evaluation?, → ConflictOfInterest? |
|
||||||
|
| **Evaluation** | Jury member's assessment of a project | `assignmentId`, `formId`, `status`, `criterionScoresJson`, `globalScore`, `binaryDecision`, `feedbackText`, `version`, `submittedAt` | → Assignment, → EvaluationForm |
|
||||||
|
| **EvaluationForm** | Configurable evaluation criteria per stage | `stageId`, `version`, `criteriaJson`, `scalesJson`, `isActive` | → Stage, ← Evaluation[] |
|
||||||
|
| **ConflictOfInterest** | COI declarations by jury members | `assignmentId`, `userId`, `projectId`, `hasConflict`, `conflictType`, `description`, `declaredAt`, `reviewedById`, `reviewAction` | → Assignment, → User, → User (reviewer) |
|
||||||
|
| **GracePeriod** | Extended deadlines for specific jury members | `stageId`, `userId`, `projectId`, `extendedUntil`, `reason`, `grantedById` | → Stage, → User, → User (granter) |
|
||||||
|
| **EvaluationSummary** | AI-generated synthesis of evaluations | `projectId`, `stageId`, `summaryJson`, `generatedAt`, `generatedById`, `model`, `tokensUsed` | → Project, → Stage, → User |
|
||||||
|
| **EvaluationDiscussion** | Discussion thread for deliberation | `projectId`, `stageId`, `status`, `createdAt`, `closedAt`, `closedById` | → Project, → Stage, → User, → DiscussionComment[] |
|
||||||
|
| **DiscussionComment** | Individual comment in discussion | `discussionId`, `userId`, `content`, `createdAt` | → EvaluationDiscussion, → User |
|
||||||
|
|
||||||
|
### 1.4 Live Voting Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **LiveVotingSession** | Live final event configuration | `stageId`, `status`, `currentProjectIndex`, `currentProjectId`, `votingStartedAt`, `votingEndsAt`, `projectOrderJson`, `votingMode`, `criteriaJson`, `allowAudienceVotes`, `audienceVoteWeight`, `tieBreakerMethod` | → Stage, → LiveVote[], → AudienceVoter[] |
|
||||||
|
| **LiveVote** | Individual vote during live event | `sessionId`, `projectId`, `userId`, `score`, `isAudienceVote`, `votedAt`, `criterionScoresJson`, `audienceVoterId` | → LiveVotingSession, → User?, → AudienceVoter? |
|
||||||
|
| **AudienceVoter** | Anonymous audience participant | `sessionId`, `token`, `identifier`, `identifierType`, `ipAddress`, `userAgent` | → LiveVotingSession, → LiveVote[] |
|
||||||
|
| **LiveProgressCursor** | Real-time cursor for live presentation | `stageId`, `sessionId`, `activeProjectId`, `activeOrderIndex`, `isPaused` | → Stage |
|
||||||
|
|
||||||
|
### 1.5 Awards Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **SpecialAward** | Special prize/recognition category | `programId`, `trackId`, `name`, `description`, `status`, `criteriaText`, `autoTagRulesJson`, `useAiEligibility`, `scoringMode`, `maxRankedPicks`, `votingStartAt`, `votingEndAt`, `winnerProjectId`, `winnerOverridden`, `eligibilityJobStatus` | → Program, → Track?, → Project (winner), → AwardEligibility[], → AwardJuror[], → AwardVote[] |
|
||||||
|
| **AwardEligibility** | AI-determined award eligibility | `awardId`, `projectId`, `method`, `eligible`, `aiReasoningJson`, `overriddenBy`, `overriddenAt` | → SpecialAward, → Project, → User? |
|
||||||
|
| **AwardJuror** | Jury panel for special award | `awardId`, `userId` | → SpecialAward, → User |
|
||||||
|
| **AwardVote** | Vote for special award winner | `awardId`, `userId`, `projectId`, `rank`, `votedAt` | → SpecialAward, → User, → Project |
|
||||||
|
|
||||||
|
### 1.6 Mentoring Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **MentorAssignment** | Mentor-project pairing | `projectId`, `mentorId`, `method`, `assignedAt`, `assignedBy`, `aiConfidenceScore`, `expertiseMatchScore`, `completionStatus` | → Project (unique), → User (mentor), → MentorNote[], → MentorMilestoneCompletion[] |
|
||||||
|
| **MentorMessage** | Chat messages between mentor and team | `projectId`, `senderId`, `message`, `isRead` | → Project, → User |
|
||||||
|
| **MentorNote** | Private notes by mentor/admin | `mentorAssignmentId`, `authorId`, `content`, `isVisibleToAdmin` | → MentorAssignment, → User |
|
||||||
|
| **MentorMilestone** | Program-wide mentorship checkpoints | `programId`, `name`, `description`, `isRequired`, `deadlineOffsetDays`, `sortOrder` | → Program, → MentorMilestoneCompletion[] |
|
||||||
|
| **MentorMilestoneCompletion** | Completion record for milestones | `milestoneId`, `mentorAssignmentId`, `completedById`, `completedAt` | → MentorMilestone, → MentorAssignment, → User |
|
||||||
|
|
||||||
|
### 1.7 Filtering Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **FilteringRule** | Automated screening rule | `stageId`, `name`, `ruleType`, `configJson`, `priority`, `isActive` | → Stage |
|
||||||
|
| **FilteringResult** | Per-project filtering outcome | `stageId`, `projectId`, `outcome`, `ruleResultsJson`, `aiScreeningJson`, `overriddenBy`, `overriddenAt`, `overrideReason`, `finalOutcome` | → Stage, → Project, → User? |
|
||||||
|
| **FilteringJob** | Progress tracking for filtering runs | `stageId`, `status`, `totalProjects`, `processedCount`, `passedCount`, `filteredCount`, `flaggedCount`, `errorMessage`, `startedAt`, `completedAt` | → Stage |
|
||||||
|
| **AssignmentJob** | Progress tracking for assignment generation | `stageId`, `status`, `totalProjects`, `processedCount`, `suggestionsCount`, `suggestionsJson`, `errorMessage`, `fallbackUsed` | → Stage |
|
||||||
|
| **TaggingJob** | Progress tracking for AI tagging | `programId`, `status`, `totalProjects`, `processedCount`, `taggedCount`, `skippedCount`, `failedCount`, `errorsJson` | → Program? |
|
||||||
|
|
||||||
|
### 1.8 Users & Auth Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **User** | Platform user account | `email`, `name`, `role`, `status`, `expertiseTags`, `maxAssignments`, `country`, `bio`, `phoneNumber`, `notificationPreference`, `digestFrequency`, `preferredWorkload`, `passwordHash`, `inviteToken`, `onboardingCompletedAt` | → Assignment[], → GracePeriod[], → LiveVote[], → TeamMember[], → MentorAssignment[], → AwardJuror[], → ConflictOfInterest[], → InAppNotification[] |
|
||||||
|
| **Account** | NextAuth provider accounts | `userId`, `provider`, `providerAccountId`, `access_token`, `refresh_token` | → User |
|
||||||
|
| **Session** | NextAuth active sessions | `userId`, `sessionToken`, `expires` | → User |
|
||||||
|
| **VerificationToken** | NextAuth magic link tokens | `identifier`, `token`, `expires` | (standalone) |
|
||||||
|
|
||||||
|
### 1.9 Audit & Logging Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **AuditLog** | General platform activity log | `userId`, `action`, `entityType`, `entityId`, `detailsJson`, `previousDataJson`, `ipAddress`, `userAgent`, `sessionId`, `timestamp` | → User? |
|
||||||
|
| **DecisionAuditLog** | Pipeline decision tracking | `eventType`, `entityType`, `entityId`, `actorId`, `detailsJson`, `snapshotJson`, `createdAt` | (no FK relations) |
|
||||||
|
| **OverrideAction** | Manual admin overrides log | `entityType`, `entityId`, `previousValue`, `newValueJson`, `reasonCode`, `reasonText`, `actorId`, `createdAt` | (no FK relations) |
|
||||||
|
| **AIUsageLog** | AI API consumption tracking | `userId`, `action`, `entityType`, `entityId`, `model`, `promptTokens`, `completionTokens`, `estimatedCostUsd`, `status`, `errorMessage` | (no FK relations) |
|
||||||
|
| **NotificationLog** | Email/SMS delivery tracking | `userId`, `channel`, `provider`, `type`, `status`, `externalId`, `errorMsg` | → User |
|
||||||
|
|
||||||
|
### 1.10 Program & Resources Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **Program** | Competition edition/year | `name`, `slug`, `year`, `status`, `description`, `settingsJson` | → Pipeline[], → Project[], → LearningResource[], → Partner[], → SpecialAward[] |
|
||||||
|
| **LearningResource** | Educational content for teams | `programId`, `title`, `description`, `contentJson`, `resourceType`, `cohortLevel`, `fileName`, `mimeType`, `bucket`, `objectKey`, `externalUrl`, `isPublished` | → Program?, → User (creator), → ResourceAccess[] |
|
||||||
|
| **ResourceAccess** | Access log for learning materials | `resourceId`, `userId`, `accessedAt`, `ipAddress` | → LearningResource, → User |
|
||||||
|
| **Partner** | Sponsor/partner organization | `programId`, `name`, `description`, `website`, `partnerType`, `visibility`, `logoFileName`, `sortOrder`, `isActive` | → Program? |
|
||||||
|
| **WizardTemplate** | Saved pipeline configuration templates | `name`, `description`, `config`, `isGlobal`, `programId`, `createdBy` | → Program?, → User |
|
||||||
|
|
||||||
|
### 1.11 Communication Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **InAppNotification** | Bell icon notifications | `userId`, `type`, `priority`, `icon`, `title`, `message`, `linkUrl`, `linkLabel`, `metadata`, `groupKey`, `isRead`, `expiresAt` | → User |
|
||||||
|
| **NotificationEmailSetting** | Email notification toggles per type | `notificationType`, `category`, `label`, `sendEmail`, `emailSubject`, `emailTemplate` | (standalone) |
|
||||||
|
| **NotificationPolicy** | Event-driven notification config | `eventType`, `channel`, `templateId`, `isActive`, `configJson` | (no FK relations) |
|
||||||
|
| **Message** | Bulk messaging system | `senderId`, `recipientType`, `recipientFilter`, `stageId`, `templateId`, `subject`, `body`, `deliveryChannels`, `scheduledAt`, `sentAt` | → User (sender), → Stage?, → MessageTemplate?, → MessageRecipient[] |
|
||||||
|
| **MessageRecipient** | Individual message delivery | `messageId`, `userId`, `channel`, `isRead`, `readAt`, `deliveredAt` | → Message, → User |
|
||||||
|
| **MessageTemplate** | Reusable email templates | `name`, `category`, `subject`, `body`, `variables`, `isActive`, `createdBy` | → User, ← Message[] |
|
||||||
|
|
||||||
|
### 1.12 Webhooks & Integrations
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **Webhook** | Outbound event webhooks | `name`, `url`, `secret`, `events`, `headers`, `maxRetries`, `isActive`, `createdById` | → User, → WebhookDelivery[] |
|
||||||
|
| **WebhookDelivery** | Webhook delivery log | `webhookId`, `event`, `payload`, `status`, `responseStatus`, `responseBody`, `attempts`, `lastAttemptAt` | → Webhook |
|
||||||
|
|
||||||
|
### 1.13 Miscellaneous Models
|
||||||
|
|
||||||
|
| Model | Purpose | Key Fields | Relations |
|
||||||
|
|-------|---------|------------|-----------|
|
||||||
|
| **SystemSettings** | Platform-wide config KV store | `key`, `value`, `type`, `category`, `description`, `isSecret` | (standalone) |
|
||||||
|
| **ExpertiseTag** | Tag taxonomy for matching | `name`, `description`, `category`, `color`, `isActive`, `sortOrder` | → ProjectTag[] |
|
||||||
|
| **ProjectTag** | Project-tag association | `projectId`, `tagId`, `confidence`, `source` | → Project, → ExpertiseTag |
|
||||||
|
| **ProjectStatusHistory** | Historical status changes | `projectId`, `status`, `changedAt`, `changedBy` | → Project |
|
||||||
|
| **ReminderLog** | Evaluation deadline reminders | `stageId`, `userId`, `type`, `sentAt` | → Stage, → User |
|
||||||
|
| **DigestLog** | Email digest delivery log | `userId`, `digestType`, `contentJson`, `sentAt` | → User |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Enums
|
||||||
|
|
||||||
|
### 2.1 User & Auth Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **UserRole** | `SUPER_ADMIN`, `PROGRAM_ADMIN`, `JURY_MEMBER`, `MENTOR`, `OBSERVER`, `APPLICANT`, `AWARD_MASTER`, `AUDIENCE` | User permissions hierarchy |
|
||||||
|
| **UserStatus** | `NONE`, `INVITED`, `ACTIVE`, `SUSPENDED` | User account state |
|
||||||
|
|
||||||
|
### 2.2 Project & Competition Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **ProjectStatus** | `SUBMITTED`, `ELIGIBLE`, `ASSIGNED`, `SEMIFINALIST`, `FINALIST`, `REJECTED` | Legacy project state (superseded by ProjectStageState) |
|
||||||
|
| **CompetitionCategory** | `STARTUP`, `BUSINESS_CONCEPT` | Project type (existing company vs. student idea) |
|
||||||
|
| **OceanIssue** | `POLLUTION_REDUCTION`, `CLIMATE_MITIGATION`, `TECHNOLOGY_INNOVATION`, `SUSTAINABLE_SHIPPING`, `BLUE_CARBON`, `HABITAT_RESTORATION`, `COMMUNITY_CAPACITY`, `SUSTAINABLE_FISHING`, `CONSUMER_AWARENESS`, `OCEAN_ACIDIFICATION`, `OTHER` | Project focus area |
|
||||||
|
|
||||||
|
### 2.3 Pipeline Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **StageType** | `INTAKE`, `FILTER`, `EVALUATION`, `SELECTION`, `LIVE_FINAL`, `RESULTS` | Stage functional type |
|
||||||
|
| **TrackKind** | `MAIN`, `AWARD`, `SHOWCASE` | Track purpose |
|
||||||
|
| **RoutingMode** | `SHARED`, `EXCLUSIVE` | Project routing behavior (can projects be in multiple tracks?) |
|
||||||
|
| **StageStatus** | `STAGE_DRAFT`, `STAGE_ACTIVE`, `STAGE_CLOSED`, `STAGE_ARCHIVED` | Stage lifecycle state |
|
||||||
|
| **ProjectStageStateValue** | `PENDING`, `IN_PROGRESS`, `PASSED`, `REJECTED`, `ROUTED`, `COMPLETED`, `WITHDRAWN` | Project state within a stage |
|
||||||
|
| **DecisionMode** | `JURY_VOTE`, `AWARD_MASTER_DECISION`, `ADMIN_DECISION` | How winners are determined in a track |
|
||||||
|
|
||||||
|
### 2.4 Evaluation & Assignment Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **EvaluationStatus** | `NOT_STARTED`, `DRAFT`, `SUBMITTED`, `LOCKED` | Evaluation completion state |
|
||||||
|
| **AssignmentMethod** | `MANUAL`, `BULK`, `AI_SUGGESTED`, `AI_AUTO`, `ALGORITHM` | How assignment was created |
|
||||||
|
| **MentorAssignmentMethod** | `MANUAL`, `AI_SUGGESTED`, `AI_AUTO`, `ALGORITHM` | How mentor was paired |
|
||||||
|
|
||||||
|
### 2.5 Filtering Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **FilteringOutcome** | `PASSED`, `FILTERED_OUT`, `FLAGGED` | Filtering result |
|
||||||
|
| **FilteringRuleType** | `FIELD_BASED`, `DOCUMENT_CHECK`, `AI_SCREENING` | Type of filtering rule |
|
||||||
|
| **FilteringJobStatus** | `PENDING`, `RUNNING`, `COMPLETED`, `FAILED` | Job progress state |
|
||||||
|
| **AssignmentJobStatus** | `PENDING`, `RUNNING`, `COMPLETED`, `FAILED` | Job progress state |
|
||||||
|
| **TaggingJobStatus** | `PENDING`, `RUNNING`, `COMPLETED`, `FAILED` | Job progress state |
|
||||||
|
|
||||||
|
### 2.6 Awards Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **AwardScoringMode** | `PICK_WINNER`, `RANKED`, `SCORED` | Award voting method |
|
||||||
|
| **AwardStatus** | `DRAFT`, `NOMINATIONS_OPEN`, `VOTING_OPEN`, `CLOSED`, `ARCHIVED` | Award lifecycle |
|
||||||
|
| **EligibilityMethod** | `AUTO`, `MANUAL` | How eligibility was determined |
|
||||||
|
|
||||||
|
### 2.7 Miscellaneous Enums
|
||||||
|
|
||||||
|
| Enum | Values | Usage |
|
||||||
|
|------|--------|-------|
|
||||||
|
| **FileType** | `EXEC_SUMMARY`, `PRESENTATION`, `VIDEO`, `OTHER`, `BUSINESS_PLAN`, `VIDEO_PITCH`, `SUPPORTING_DOC` | Project file categorization |
|
||||||
|
| **SubmissionSource** | `MANUAL`, `CSV`, `NOTION`, `TYPEFORM`, `PUBLIC_FORM` | How project was submitted |
|
||||||
|
| **NotificationChannel** | `EMAIL`, `WHATSAPP`, `BOTH`, `NONE` | Notification delivery method |
|
||||||
|
| **ResourceType** | `PDF`, `VIDEO`, `DOCUMENT`, `LINK`, `OTHER` | Learning resource type |
|
||||||
|
| **CohortLevel** | `ALL`, `SEMIFINALIST`, `FINALIST` | Access level for resources |
|
||||||
|
| **PartnerVisibility** | `ADMIN_ONLY`, `JURY_VISIBLE`, `PUBLIC` | Who can see partner |
|
||||||
|
| **PartnerType** | `SPONSOR`, `PARTNER`, `SUPPORTER`, `MEDIA`, `OTHER` | Partner categorization |
|
||||||
|
| **TeamMemberRole** | `LEAD`, `MEMBER`, `ADVISOR` | Team composition |
|
||||||
|
| **OverrideReasonCode** | `DATA_CORRECTION`, `POLICY_EXCEPTION`, `JURY_CONFLICT`, `SPONSOR_DECISION`, `ADMIN_DISCRETION` | Why decision was overridden |
|
||||||
|
| **ProgramStatus** | `DRAFT`, `ACTIVE`, `ARCHIVED` | Program lifecycle |
|
||||||
|
| **SettingType** | `STRING`, `NUMBER`, `BOOLEAN`, `JSON`, `SECRET` | System setting data type |
|
||||||
|
| **SettingCategory** | `AI`, `BRANDING`, `EMAIL`, `STORAGE`, `SECURITY`, `DEFAULTS`, `WHATSAPP`, `AUDIT_CONFIG`, `LOCALIZATION`, `DIGEST`, `ANALYTICS`, `INTEGRATIONS`, `COMMUNICATION` | Setting organization |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Services
|
||||||
|
|
||||||
|
All services located in `src/server/services/*.ts`.
|
||||||
|
|
||||||
|
| Service | Purpose | Key Functions |
|
||||||
|
|---------|---------|---------------|
|
||||||
|
| **stage-engine.ts** | State machine for project transitions | `validateTransition()`, `executeTransition()`, `executeBatchTransition()` - handles guard evaluation, atomic PSS updates, audit logging |
|
||||||
|
| **stage-filtering.ts** | Runs filtering pipeline scoped to stage | `runStageFiltering()`, `resolveManualDecision()`, `getManualQueue()` - executes field-based, document, and AI rules; duplicate detection built-in |
|
||||||
|
| **stage-assignment.ts** | Smart jury assignment generation | `previewStageAssignment()`, `executeStageAssignment()`, `getCoverageReport()`, `rebalance()` - tag matching, workload balancing, COI handling |
|
||||||
|
| **stage-notifications.ts** | Event-driven notification producer | `emitStageEvent()`, `onStageTransitioned()`, `onFilteringCompleted()`, `onAssignmentGenerated()`, `onCursorUpdated()` - never throws, creates DecisionAuditLog + in-app + email |
|
||||||
|
| **live-control.ts** | Real-time live ceremony control | `startSession()`, `setActiveProject()`, `jumpToProject()`, `reorderQueue()`, `pauseResume()`, `openCohortWindow()`, `closeCohortWindow()` - manages LiveProgressCursor |
|
||||||
|
| **ai-filtering.ts** | AI-powered project screening | Anonymizes data, calls OpenAI API, confidence banding, spam detection (delegates to stage-filtering.ts for execution) |
|
||||||
|
| **ai-assignment.ts** | AI-suggested jury matching | GPT-based assignment generation with expertise matching (100 lines) |
|
||||||
|
| **ai-evaluation-summary.ts** | GPT synthesis of evaluations | Generates strengths/weaknesses summary from jury feedback |
|
||||||
|
| **ai-tagging.ts** | Automatic project categorization | Tags projects with expertise areas using GPT |
|
||||||
|
| **ai-award-eligibility.ts** | Award eligibility assessment | GPT determines if project meets award criteria |
|
||||||
|
| **anonymization.ts** | GDPR-compliant data stripping | Removes PII before AI calls (name, email, institution, etc.) |
|
||||||
|
| **ai-errors.ts** | Centralized AI error handling | Classifies errors (rate limit, token limit, API down), provides retry logic |
|
||||||
|
| **award-eligibility-job.ts** | Batch award eligibility processing | Runs AI eligibility checks across all projects for an award |
|
||||||
|
| **smart-assignment.ts** | Scoring algorithm for matching | Tag overlap, bio match, workload balance, geo diversity, COI blocking, availability checking |
|
||||||
|
| **mentor-matching.ts** | Mentor-project pairing logic | Similar to smart-assignment but for mentorship |
|
||||||
|
| **evaluation-reminders.ts** | Cron job for deadline reminders | Sends 3-day, 24h, 1h reminders to jury with incomplete evaluations |
|
||||||
|
| **email-digest.ts** | Daily/weekly email summaries | Aggregates pending tasks for users |
|
||||||
|
| **in-app-notification.ts** | In-app notification helpers | Creates bell-icon notifications with linking |
|
||||||
|
| **notification.ts** | Email sending service | Wraps Nodemailer, supports templates |
|
||||||
|
| **webhook-dispatcher.ts** | Webhook delivery service | Sends events to registered webhook URLs with retry logic |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. tRPC Routers
|
||||||
|
|
||||||
|
All routers located in `src/server/routers/*.ts`. Total: 38 routers.
|
||||||
|
|
||||||
|
| Router | Procedure Count | Key Procedures | Purpose |
|
||||||
|
|--------|-----------------|----------------|---------|
|
||||||
|
| **pipeline.ts** | ~15 | `create`, `update`, `delete`, `list`, `getById`, `archive` | Pipeline CRUD, linking to Program |
|
||||||
|
| **stage.ts** | ~20 | `create`, `updateConfig`, `updateStatus`, `delete`, `getByTrack`, `reorderStages`, `createTransition` | Stage CRUD, window management, transition setup |
|
||||||
|
| **stageFiltering.ts** | ~10 | `createRule`, `runFiltering`, `getManualQueue`, `resolveManualDecision`, `getJobStatus` | Filtering rule management + execution |
|
||||||
|
| **stageAssignment.ts** | ~8 | `previewAssignment`, `executeAssignment`, `getCoverage`, `rebalance`, `bulkDelete` | Assignment generation, coverage analysis |
|
||||||
|
| **project.ts** | ~25 | `create`, `update`, `delete`, `getById`, `list`, `import`, `advanceToRound`, `updateStatus` | Project CRUD, CSV import, status changes |
|
||||||
|
| **assignment.ts** | ~12 | `create`, `bulkCreate`, `delete`, `getByUser`, `getByProject`, `markComplete` | Manual assignment management |
|
||||||
|
| **evaluation.ts** | ~15 | `create`, `update`, `submit`, `lock`, `unlock`, `getByAssignment`, `generateSummary` | Evaluation submission, locking |
|
||||||
|
| **gracePeriod.ts** | ~6 | `create`, `delete`, `getByStage`, `getByUser`, `checkActive` | Grace period management |
|
||||||
|
| **user.ts** | ~20 | `create`, `update`, `delete`, `invite`, `resendInvite`, `list`, `updateProfile`, `uploadAvatar` | User management, invites |
|
||||||
|
| **specialAward.ts** | ~15 | `create`, `update`, `delete`, `runEligibility`, `vote`, `getResults`, `overrideWinner` | Award creation, voting, eligibility |
|
||||||
|
| **live-voting.ts** | ~12 | `createSession`, `vote`, `getResults`, `closeSession`, `updateCriteria` | Live voting session management (legacy LiveVotingSession model) |
|
||||||
|
| **live.ts** | ~10 | `startSession`, `setActiveProject`, `jumpToProject`, `pauseResume`, `openCohort`, `closeCohort` | Live control (new LiveProgressCursor model) |
|
||||||
|
| **cohort.ts** | ~8 | `create`, `update`, `delete`, `addProjects`, `removeProjects`, `reorder` | Cohort management for live finals |
|
||||||
|
| **mentor.ts** | ~12 | `assignMentor`, `removeMentor`, `sendMessage`, `addNote`, `completeMilestone`, `getMentorDashboard` | Mentorship workflow |
|
||||||
|
| **learningResource.ts** | ~10 | `create`, `update`, `delete`, `list`, `upload`, `markAccessed` | Learning hub content |
|
||||||
|
| **partner.ts** | ~8 | `create`, `update`, `delete`, `list`, `uploadLogo` | Partner management |
|
||||||
|
| **tag.ts** | ~10 | `create`, `update`, `delete`, `list`, `runTagging`, `getTaggingJobStatus` | Expertise tag management |
|
||||||
|
| **notification.ts** | ~8 | `getInApp`, `markRead`, `markAllRead`, `getUnreadCount`, `updateEmailSettings` | Notification center |
|
||||||
|
| **message.ts** | ~10 | `send`, `schedule`, `list`, `getRecipients`, `createTemplate`, `listTemplates` | Bulk messaging |
|
||||||
|
| **webhook.ts** | ~8 | `create`, `update`, `delete`, `test`, `getDeliveries`, `retry` | Webhook management |
|
||||||
|
| **audit.ts** | ~6 | `getAuditLog`, `getDecisionLog`, `getOverrides`, `export` | Audit trail viewing |
|
||||||
|
| **analytics.ts** | ~12 | `getDashboardStats`, `getProjectStats`, `getJuryStats`, `getAwardStats`, `getEngagementMetrics` | Reporting and analytics |
|
||||||
|
| **dashboard.ts** | ~8 | `getAdminDashboard`, `getJuryDashboard`, `getApplicantDashboard`, `getMentorDashboard` | Role-specific dashboards |
|
||||||
|
| **export.ts** | ~8 | `exportProjects`, `exportEvaluations`, `exportVotes`, `exportAuditLog` | CSV/Excel exports |
|
||||||
|
| **file.ts** | ~8 | `uploadFile`, `getPresignedUrl`, `deleteFile`, `listFiles`, `createRequirement` | MinIO file management |
|
||||||
|
| **filtering.ts** | ~6 | Legacy filtering endpoints (superseded by stageFiltering) | Deprecated |
|
||||||
|
| **avatar.ts** | ~4 | `upload`, `delete`, `getUrl` | User profile images |
|
||||||
|
| **logo.ts** | ~4 | `upload`, `delete`, `getUrl` | Project logos |
|
||||||
|
| **decision.ts** | ~6 | `overrideFilteringResult`, `overrideAwardEligibility`, `overridePSS`, `getOverrideHistory` | Admin override controls |
|
||||||
|
| **program.ts** | ~10 | `create`, `update`, `delete`, `list`, `getById`, `archive` | Program CRUD |
|
||||||
|
| **application.ts** | ~8 | `submitApplication`, `saveDraft`, `getDraft`, `deleteDraft` | Public application form |
|
||||||
|
| **applicant.ts** | ~10 | `getMyProjects`, `updateTeam`, `uploadDocument`, `requestMentorship` | Applicant portal |
|
||||||
|
| **notion-import.ts** | ~4 | `sync`, `import`, `getStatus` | Notion integration |
|
||||||
|
| **typeform-import.ts** | ~4 | `sync`, `import`, `getStatus` | Typeform integration |
|
||||||
|
| **settings.ts** | ~8 | `get`, `set`, `getBulk`, `setBulk`, `getByCategory` | System settings KV store |
|
||||||
|
| **project-pool.ts** | ~6 | `getUnassignedProjects`, `getProjectsByStage`, `getProjectsByStatus` | Project queries for assignment |
|
||||||
|
| **wizard-template.ts** | ~8 | `create`, `update`, `delete`, `list`, `clone`, `applyTemplate` | Pipeline wizard templates |
|
||||||
|
|
||||||
|
**Total Procedures:** ~400+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UI Pages
|
||||||
|
|
||||||
|
### 5.1 Admin Pages (`src/app/(admin)/admin/`)
|
||||||
|
|
||||||
|
| Route | Purpose | Key Features |
|
||||||
|
|-------|---------|--------------|
|
||||||
|
| `/admin` | Admin dashboard | Overview metrics, recent activity, quick actions |
|
||||||
|
| `/admin/members` | User management list | User table with filters, role assignment, status changes |
|
||||||
|
| `/admin/members/[id]` | User detail/edit | Profile editing, role changes, assignment history |
|
||||||
|
| `/admin/members/invite` | Invite new users | Bulk invite form with role selection |
|
||||||
|
| `/admin/programs` | Program list | Program cards, create/archive/edit |
|
||||||
|
| `/admin/programs/[id]` | Program detail | Program overview, linked pipelines, projects |
|
||||||
|
| `/admin/programs/[id]/edit` | Program settings editor | Name, year, status, settingsJson editor |
|
||||||
|
| `/admin/programs/[id]/apply-settings` | Application form config | Public submission form customization |
|
||||||
|
| `/admin/programs/[id]/mentorship` | Mentorship milestones | Milestone creation, completion tracking |
|
||||||
|
| `/admin/projects` | Project list | Searchable/filterable project table |
|
||||||
|
| `/admin/projects/[id]` | Project detail | Full project view with evaluations, history |
|
||||||
|
| `/admin/projects/[id]/edit` | Project editor | Edit project metadata, team, tags |
|
||||||
|
| `/admin/projects/[id]/mentor` | Mentor assignment | Assign/remove mentor, view messages |
|
||||||
|
| `/admin/projects/new` | Manual project creation | Add project without public form |
|
||||||
|
| `/admin/projects/import` | CSV/Typeform/Notion import | Bulk import wizard |
|
||||||
|
| `/admin/projects/pool` | Unassigned project pool | Projects awaiting assignment |
|
||||||
|
| `/admin/rounds/pipelines` | Pipeline list | All pipelines across programs |
|
||||||
|
| `/admin/rounds/pipeline/[id]` | Pipeline detail | Track/stage tree, project flow diagram |
|
||||||
|
| `/admin/rounds/pipeline/[id]/edit` | Pipeline settings | Name, slug, status, settingsJson |
|
||||||
|
| `/admin/rounds/pipeline/[id]/wizard` | Pipeline wizard | Step-by-step configuration UI (tracks, stages, transitions) |
|
||||||
|
| `/admin/rounds/pipeline/[id]/advanced` | Advanced pipeline editor | JSON config editor, raw transitions |
|
||||||
|
| `/admin/rounds/new-pipeline` | Pipeline creation wizard | Multi-step pipeline setup |
|
||||||
|
| `/admin/awards` | Special awards list | Award cards with status |
|
||||||
|
| `/admin/awards/[id]` | Award detail | Eligibility, votes, results |
|
||||||
|
| `/admin/awards/[id]/edit` | Award editor | Criteria, voting config, jury panel |
|
||||||
|
| `/admin/awards/new` | Create award | Award creation form |
|
||||||
|
| `/admin/mentors` | Mentor list | All users with MENTOR role |
|
||||||
|
| `/admin/mentors/[id]` | Mentor detail | Assigned projects, notes, milestones |
|
||||||
|
| `/admin/learning` | Learning hub management | Resource list, upload, publish |
|
||||||
|
| `/admin/learning/[id]` | Resource detail/edit | Content editor (BlockNote), access logs |
|
||||||
|
| `/admin/learning/new` | Create resource | Upload or link external content |
|
||||||
|
| `/admin/partners` | Partner management | Partner list, logos, visibility |
|
||||||
|
| `/admin/partners/[id]` | Partner detail/edit | Edit partner info, upload logo |
|
||||||
|
| `/admin/partners/new` | Add partner | Partner creation form |
|
||||||
|
| `/admin/messages` | Messaging dashboard | Send bulk messages, view sent messages |
|
||||||
|
| `/admin/messages/templates` | Message templates | Template CRUD |
|
||||||
|
| `/admin/settings` | System settings | Category tabs, KV editor |
|
||||||
|
| `/admin/settings/tags` | Expertise tags | Tag taxonomy management |
|
||||||
|
| `/admin/settings/webhooks` | Webhook management | Webhook CRUD, delivery logs |
|
||||||
|
| `/admin/audit` | Audit log viewer | Searchable audit trail |
|
||||||
|
| `/admin/reports` | Analytics reports | Charts, exports, metrics |
|
||||||
|
| `/admin/reports/stages` | Stage-level reports | Per-stage assignment coverage, completion rates |
|
||||||
|
|
||||||
|
### 5.2 Jury Pages (`src/app/(jury)/jury/`)
|
||||||
|
|
||||||
|
| Route | Purpose | Key Features |
|
||||||
|
|-------|---------|--------------|
|
||||||
|
| `/jury` | Jury dashboard | Assigned stages, pending evaluations, deadlines |
|
||||||
|
| `/jury/stages` | Jury stage list | Stages where user has assignments |
|
||||||
|
| `/jury/stages/[stageId]/assignments` | Assignment list for stage | Projects assigned to this user |
|
||||||
|
| `/jury/stages/[stageId]/projects/[projectId]` | Project detail view | Full project info, files, team |
|
||||||
|
| `/jury/stages/[stageId]/projects/[projectId]/evaluate` | Evaluation form | Criterion scoring, feedback, submit |
|
||||||
|
| `/jury/stages/[stageId]/projects/[projectId]/evaluation` | View submitted evaluation | Read-only evaluation, edit if not locked |
|
||||||
|
| `/jury/stages/[stageId]/compare` | Side-by-side comparison | Compare multiple projects, scoring matrix |
|
||||||
|
| `/jury/stages/[stageId]/live` | Live voting interface | Real-time voting during live ceremony |
|
||||||
|
| `/jury/awards` | Special awards list | Awards where user is juror |
|
||||||
|
| `/jury/awards/[id]` | Award voting | View eligible projects, cast votes |
|
||||||
|
| `/jury/learning` | Learning hub (jury access) | Resources for jury members |
|
||||||
|
|
||||||
|
### 5.3 Applicant Pages (`src/app/(applicant)/applicant/`)
|
||||||
|
|
||||||
|
| Route | Purpose | Key Features |
|
||||||
|
|-------|---------|--------------|
|
||||||
|
| `/applicant` | Applicant dashboard | Application status, next steps |
|
||||||
|
| `/applicant/pipeline` | Pipeline progress view | Visual pipeline with current stage |
|
||||||
|
| `/applicant/pipeline/[stageId]/status` | Stage detail view | Stage status, requirements, deadlines |
|
||||||
|
| `/applicant/pipeline/[stageId]/documents` | Document upload | Upload required files for stage |
|
||||||
|
| `/applicant/documents` | All documents | Document library, versions |
|
||||||
|
| `/applicant/team` | Team management | Add/remove team members, roles |
|
||||||
|
| `/applicant/mentor` | Mentorship dashboard | Chat with mentor, milestones |
|
||||||
|
|
||||||
|
### 5.4 Auth Pages (`src/app/(auth)/`)
|
||||||
|
|
||||||
|
| Route | Purpose | Key Features |
|
||||||
|
|-------|---------|--------------|
|
||||||
|
| `/login` | Login page | Email magic link + password login |
|
||||||
|
| `/verify` | Magic link verification | Token verification, auto-login |
|
||||||
|
| `/verify-email` | Email verification | Verify email after signup |
|
||||||
|
| `/accept-invite` | Invitation acceptance | One-click invite token handling |
|
||||||
|
| `/set-password` | Password setup | First-time password creation |
|
||||||
|
| `/onboarding` | User onboarding wizard | Profile completion, expertise tags |
|
||||||
|
| `/error` | Auth error page | Error display with retry |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Strengths of Current System
|
||||||
|
|
||||||
|
### 6.1 Architecture Strengths
|
||||||
|
|
||||||
|
| Strength | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Full Type Safety** | End-to-end TypeScript from DB → tRPC → React. Prisma generates types, tRPC enforces them, components consume them safely. |
|
||||||
|
| **Atomic Transactions** | All critical operations (stage transitions, filtering, assignments) use `$transaction` with proper rollback. |
|
||||||
|
| **Comprehensive Audit** | Dual audit system: `AuditLog` for general activity, `DecisionAuditLog` for pipeline decisions. Full traceability. |
|
||||||
|
| **RBAC Enforcement** | tRPC middleware hierarchy (`adminProcedure`, `juryProcedure`, etc.) enforces role-based access at API level. |
|
||||||
|
| **GDPR Compliance** | All AI calls strip PII via `anonymization.ts`. No personal data sent to OpenAI. |
|
||||||
|
| **Event-Driven Design** | `stage-notifications.ts` emits events on every pipeline action. Notifications never block core operations (catch all errors). |
|
||||||
|
| **Graceful AI Error Handling** | `ai-errors.ts` classifies errors (rate limit, token limit, API down) and provides retry guidance. AI failures never crash the system. |
|
||||||
|
| **Duplicate Detection** | Built-in duplicate submission detection in `stage-filtering.ts` (by email). Always flags for manual review, never auto-rejects. |
|
||||||
|
|
||||||
|
### 6.2 Data Model Strengths
|
||||||
|
|
||||||
|
| Strength | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Flexible Pipeline Model** | Pipeline → Track → Stage → ProjectStageState allows arbitrary round structures. Main track + multiple award tracks supported. |
|
||||||
|
| **Guard-Based Transitions** | StageTransition `guardJson` field allows complex conditional routing (e.g., "only advance if avgScore >= 7"). |
|
||||||
|
| **Stage Config Polymorphism** | `Stage.configJson` adapts to `stageType`. FILTER stages have filtering config, EVALUATION stages have evaluation config, etc. |
|
||||||
|
| **Versioned Evaluations** | `Evaluation.version` field allows rollback (though not currently used). |
|
||||||
|
| **Override Audit Trail** | `OverrideAction` model logs all admin overrides with reason codes. Immutable audit. |
|
||||||
|
|
||||||
|
### 6.3 Service Layer Strengths
|
||||||
|
|
||||||
|
| Strength | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **State Machine Isolation** | `stage-engine.ts` is the ONLY service that modifies `ProjectStageState`. All transitions go through it. Single source of truth. |
|
||||||
|
| **Service Purity** | Services are pure functions that accept Prisma client as parameter. Testable without mocking globals. |
|
||||||
|
| **Progress Tracking** | Long-running operations (filtering, assignment, tagging) use Job models (`FilteringJob`, `AssignmentJob`, `TaggingJob`) for progress tracking. |
|
||||||
|
| **AI Batching** | All AI services batch projects (20-50 per call) to reduce API cost and latency. |
|
||||||
|
|
||||||
|
### 6.4 UX Strengths
|
||||||
|
|
||||||
|
| Strength | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| **Wizard-Driven Setup** | Pipeline wizard (`/admin/rounds/pipeline/[id]/wizard`) guides admins through complex configuration. |
|
||||||
|
| **Real-Time Live Control** | `/jury/stages/[stageId]/live` provides live voting with cursor sync via `LiveProgressCursor`. |
|
||||||
|
| **Notification Center** | In-app notification bell with grouping, priorities, expiration. |
|
||||||
|
| **Grace Period UX** | Admins can grant individual deadline extensions with reason tracking. |
|
||||||
|
| **Filtering Manual Queue** | Flagged projects go to dedicated review queue (`/admin/rounds/pipeline/[id]/filtering/manual`) for admin decision. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Weaknesses of Current System
|
||||||
|
|
||||||
|
### 7.1 Data Model Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **Legacy `roundId` Fields** | 50+ models have `roundId String?` (marked "Legacy — kept for historical data"). Adds noise, not enforced. | Confusing for new developers. No FK constraints. |
|
||||||
|
| **Unclear Pipeline Lifecycle** | Pipeline has `status` enum (`DRAFT`, `ACTIVE`, `ARCHIVED`), but no enforcement. Active pipelines can have draft stages. | Inconsistent state possible. |
|
||||||
|
| **Overlapping Voting Models** | `LiveVotingSession` (old) and `Cohort` + `LiveProgressCursor` (new) both exist. Unclear which to use. | Duplicate functionality, confusion. |
|
||||||
|
| **No PSS Validation Constraints** | `ProjectStageState` allows multiple active (non-exited) records for same project/track/stage combo. Should be unique. | Data integrity risk. |
|
||||||
|
| **Track-Award Linkage Vague** | `SpecialAward.trackId` is optional. Unclear if awards MUST have a track or can exist independently. | Ambiguous design. |
|
||||||
|
|
||||||
|
### 7.2 Service Layer Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **Mixed Abstraction Levels** | `stage-filtering.ts` contains both high-level orchestration AND low-level rule evaluation. Hard to test individually. | Tight coupling. |
|
||||||
|
| **Notification Side Effects** | Services call `stage-notifications.ts` directly. If notification fails (e.g., email down), error is swallowed. | Lost notifications, no visibility. |
|
||||||
|
| **AI Service Duplication** | `ai-filtering.ts`, `ai-assignment.ts`, `ai-tagging.ts` all have similar batching/retry logic. Should be abstracted. | Code duplication. |
|
||||||
|
| **No Explicit Workflow Engine** | Stage transitions are ad-hoc. No central workflow definition. Must read code to understand flow. | Hard to visualize, modify. |
|
||||||
|
|
||||||
|
### 7.3 tRPC Router Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **Router Bloat** | `project.ts` has 25+ procedures. `user.ts` has 20+. Hard to navigate. | Monolithic routers. |
|
||||||
|
| **Inconsistent Naming** | `stage.ts` has `updateConfig`, `stage-filtering.ts` router has `updateRule`. Naming conventions vary. | Confusing API. |
|
||||||
|
| **No Batch Procedures** | Most CRUD operations are one-at-a-time. No bulk create/update/delete (except assignments). | N+1 queries in UI. |
|
||||||
|
| **Missing Pagination** | List procedures (`project.list`, `user.list`) return all records. No cursor or offset pagination. | Performance issue at scale. |
|
||||||
|
|
||||||
|
### 7.4 UI/UX Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **No Pipeline Visualization** | Pipeline detail page shows table of stages, not a flowchart. Hard to see transitions. | Poor admin UX. |
|
||||||
|
| **Filtering Manual Queue Hidden** | Flagged projects not prominently surfaced. Admin must navigate deep into pipeline detail. | Flagged items forgotten. |
|
||||||
|
| **No Bulk Actions** | Can't bulk-assign projects, bulk-approve evaluations, bulk-transition projects. Must click one-by-one. | Tedious admin work. |
|
||||||
|
| **Live Voting Lacks Feedback** | Jury votes during live event but doesn't see if vote was counted. No confirmation toast. | Uncertainty. |
|
||||||
|
| **No Undo** | All admin actions (delete pipeline, archive stage, reject project) are immediate. No soft delete or undo. | Risky operations. |
|
||||||
|
|
||||||
|
### 7.5 Missing Features
|
||||||
|
|
||||||
|
| Missing Feature | Description | Impact |
|
||||||
|
|-----------------|-------------|--------|
|
||||||
|
| **Stage Dependency Graph** | No visual representation of stage transitions and guards. Admin must infer from transitions table. | Hard to debug routing. |
|
||||||
|
| **Evaluation Calibration** | No juror calibration (e.g., flag jurors who score 10x higher/lower than peers). | Scoring bias undetected. |
|
||||||
|
| **Award Winner Tie-Breaking** | `SpecialAward.tieBreakerMethod` exists in `LiveVotingSession` but not in `SpecialAward`. No tie resolution for ranked awards. | Undefined behavior on ties. |
|
||||||
|
| **Project Search Ranking** | Project search is basic string match. No relevance ranking, fuzzy matching, or faceted filters. | Poor search UX. |
|
||||||
|
| **Stage Templates** | No template system for common stage configs (e.g., "Standard 3-juror evaluation stage"). | Repetitive setup. |
|
||||||
|
| **Notification Preferences** | Users can toggle email on/off globally but not per event type. No granular control. | All-or-nothing notifications. |
|
||||||
|
| **Pipeline Cloning** | No way to duplicate a pipeline for a new year/program. Must recreate manually. | Time-consuming setup. |
|
||||||
|
| **Evaluation Rubric Library** | Each stage creates evaluation forms from scratch. No reusable rubrics. | Reinventing the wheel. |
|
||||||
|
|
||||||
|
### 7.6 Code Quality Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **Inconsistent Error Messages** | Some procedures throw `TRPCError` with clear messages, others just throw generic Error. | Debugging harder. |
|
||||||
|
| **No Input Sanitization** | Zod validates types but doesn't trim strings, lowercase emails, etc. | Data inconsistency. |
|
||||||
|
| **Magic Numbers** | Hardcoded constants (e.g., `AI_CONFIDENCE_THRESHOLD_PASS = 0.75`) scattered across services. | Hard to tune. |
|
||||||
|
| **Limited Test Coverage** | Only `stage-engine.test.ts` exists. No tests for filtering, assignment, AI services. | Regression risk. |
|
||||||
|
| **No API Versioning** | tRPC routers have no version prefix. Breaking changes would break old clients. | API fragility. |
|
||||||
|
|
||||||
|
### 7.7 Performance Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **N+1 Queries** | Project list page loads projects, then fetches assignments for each in a loop. | Slow page load. |
|
||||||
|
| **No Caching** | Every tRPC call hits database. No Redis, no in-memory cache. | High DB load. |
|
||||||
|
| **Unindexed Joins** | Some `ProjectStageState` queries join on `(projectId, trackId)` without composite index. | Slow at scale. |
|
||||||
|
| **AI Batching Non-Optimal** | AI services batch by count (20 projects) not by token size. Large projects can exceed token limits. | API errors. |
|
||||||
|
|
||||||
|
### 7.8 Documentation Issues
|
||||||
|
|
||||||
|
| Issue | Description | Impact |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **No Architecture Docs** | No high-level system overview. New developers must read code. | Steep onboarding. |
|
||||||
|
| **Minimal JSDoc** | Most services have file-level comments but not function-level. | Hard to use without reading implementation. |
|
||||||
|
| **No API Reference** | tRPC procedures not documented in OpenAPI or similar. | Client integration difficult. |
|
||||||
|
| **No Runbook** | No operational docs for common tasks (e.g., "How to fix a stuck pipeline"). | Manual troubleshooting. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary Statistics
|
||||||
|
|
||||||
|
| Category | Count |
|
||||||
|
|----------|-------|
|
||||||
|
| **Database Models** | 73 |
|
||||||
|
| **Enums** | 31 |
|
||||||
|
| **Service Files** | 20 |
|
||||||
|
| **tRPC Routers** | 38 |
|
||||||
|
| **tRPC Procedures** | ~400 |
|
||||||
|
| **Admin Pages** | 45 |
|
||||||
|
| **Jury Pages** | 11 |
|
||||||
|
| **Applicant Pages** | 7 |
|
||||||
|
| **Auth Pages** | 7 |
|
||||||
|
| **Total Distinct Routes** | ~70 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Service Function Inventory
|
||||||
|
|
||||||
|
### stage-engine.ts
|
||||||
|
- `evaluateGuardCondition()` - Evaluates a single guard condition
|
||||||
|
- `evaluateGuard()` - Evaluates guard config with AND/OR logic
|
||||||
|
- `validateTransition()` - Checks if transition is allowed (PSS exists, transition defined, stage active, window open, guards pass)
|
||||||
|
- `executeTransition()` - Atomically transitions a project between stages (exits source PSS, creates/updates dest PSS, logs in DecisionAuditLog + AuditLog)
|
||||||
|
- `executeBatchTransition()` - Batch wrapper around executeTransition (processes 50 at a time)
|
||||||
|
|
||||||
|
### stage-filtering.ts
|
||||||
|
- `evaluateFieldCondition()` - Evaluates a single field-based rule condition
|
||||||
|
- `evaluateFieldRule()` - Evaluates field-based rule with AND/OR logic
|
||||||
|
- `evaluateDocumentCheck()` - Checks if project has required files
|
||||||
|
- `bandByConfidence()` - AI confidence thresholding (0.75+ = PASSED, 0.25- = FILTERED_OUT, else FLAGGED)
|
||||||
|
- `runStageFiltering()` - Main orchestration: loads projects, rules, runs deterministic then AI, saves FilteringResults, creates FilteringJob
|
||||||
|
- `resolveManualDecision()` - Admin resolves a FLAGGED result to PASSED or FILTERED_OUT, logs override
|
||||||
|
- `getManualQueue()` - Returns all FLAGGED results for a stage
|
||||||
|
|
||||||
|
### stage-assignment.ts
|
||||||
|
- `calculateTagOverlapScore()` - Counts matching tags between juror and project (max 40 points)
|
||||||
|
- `calculateWorkloadScore()` - Scores juror based on current load vs preferred (max 25 points)
|
||||||
|
- `previewStageAssignment()` - Dry run: scores all juror-project pairs, returns top N per project
|
||||||
|
- `executeStageAssignment()` - Creates Assignment records, logs in AssignmentJob
|
||||||
|
- `getCoverageReport()` - Returns per-project review counts, per-juror assignment counts
|
||||||
|
- `rebalance()` - Identifies overloaded/underloaded jurors, suggests reassignments
|
||||||
|
|
||||||
|
### stage-notifications.ts
|
||||||
|
- `emitStageEvent()` - Core event producer: creates DecisionAuditLog, checks NotificationPolicy, creates InAppNotification, sends email (never throws)
|
||||||
|
- `resolveRecipients()` - Determines who gets notified based on event type (admins, jury, etc.)
|
||||||
|
- `buildNotificationMessage()` - Builds human-readable message from event details
|
||||||
|
- `onStageTransitioned()` - Convenience wrapper for stage.transitioned event
|
||||||
|
- `onFilteringCompleted()` - Convenience wrapper for filtering.completed event
|
||||||
|
- `onAssignmentGenerated()` - Convenience wrapper for assignment.generated event
|
||||||
|
- `onCursorUpdated()` - Convenience wrapper for live.cursor_updated event
|
||||||
|
- `onDecisionOverridden()` - Convenience wrapper for decision.overridden event
|
||||||
|
|
||||||
|
### live-control.ts
|
||||||
|
- `generateSessionId()` - Creates unique session ID (timestamp + random)
|
||||||
|
- `startSession()` - Creates/resets LiveProgressCursor, sets first project active
|
||||||
|
- `setActiveProject()` - Updates cursor to point to a specific project (validates project is in cohort)
|
||||||
|
- `jumpToProject()` - Jumps to project by order index
|
||||||
|
- `reorderQueue()` - Updates CohortProject sortOrder values in batch
|
||||||
|
- `pauseResume()` - Toggles cursor pause state
|
||||||
|
- `openCohortWindow()` - Opens voting window for a cohort (sets isOpen=true, windowOpenAt=now)
|
||||||
|
- `closeCohortWindow()` - Closes voting window for a cohort (sets isOpen=false, windowCloseAt=now)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Document**
|
||||||
786
docs/claude-architecture-redesign/02-gap-analysis.md
Normal file
786
docs/claude-architecture-redesign/02-gap-analysis.md
Normal file
@@ -0,0 +1,786 @@
|
|||||||
|
# Gap Analysis: Current System vs. Target 8-Step Competition Flow
|
||||||
|
|
||||||
|
**Document Version:** 1.0
|
||||||
|
**Date:** 2026-02-15
|
||||||
|
**Author:** Architecture Review (Claude)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This gap analysis compares the **current MOPC platform** (pipeline-based, stage-engine architecture) against the **target 8-step competition flow** required for the 2026 Monaco Ocean Protection Challenge.
|
||||||
|
|
||||||
|
**Key Findings:**
|
||||||
|
- **Foundation is Strong**: Pipeline/Track/Stage architecture, stage-engine transitions, AI filtering, jury assignment, and live voting infrastructure are all in place.
|
||||||
|
- **Critical Gaps**: Multi-jury support (named jury groups with overlap), multi-round submission windows with read-only enforcement, per-juror capacity constraints (hard cap vs soft cap + buffer), category ratio preferences, countdown timers, and mentoring workspace features are **missing or incomplete**.
|
||||||
|
- **Integration Gaps**: The current system treats each stage independently; the target flow requires **cross-stage coordination** (e.g., Round 1 docs become read-only in Round 2, jury sees cumulative files).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Feature-by-Feature Comparison Table](#1-feature-by-feature-comparison-table)
|
||||||
|
2. [Per-Step Deep Analysis](#2-per-step-deep-analysis)
|
||||||
|
3. [Cross-Cutting Gap Analysis](#3-cross-cutting-gap-analysis)
|
||||||
|
4. [Integration Gaps](#4-integration-gaps)
|
||||||
|
5. [Priority Matrix](#5-priority-matrix)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Feature-by-Feature Comparison Table
|
||||||
|
|
||||||
|
| Feature | Required by Flow | Current Status | Gap Level | Notes | File References |
|
||||||
|
|---------|-----------------|----------------|-----------|-------|-----------------|
|
||||||
|
| **Intake (Submission Round 1)** |
|
||||||
|
| Public submission form | Applicants upload Round 1 docs, deadline enforcement | ✅ Exists | **None** | `applicantRouter.saveSubmission()` handles create/update, deadline checked via `Stage.windowCloseAt` | `src/server/routers/applicant.ts:126` |
|
||||||
|
| Configurable deadline behavior | Grace periods, late submission flags | ✅ Exists | **None** | `GracePeriod` model, `isLate` flag on `ProjectFile` | `prisma/schema.prisma:703-728`, `ProjectFile.isLate:606` |
|
||||||
|
| File requirements per stage | Specify required file types, max size, mime types | ✅ Exists | **None** | `FileRequirement` model linked to stages | `prisma/schema.prisma:569-588` |
|
||||||
|
| Draft support | Save progress without submitting | ✅ Exists | **None** | `isDraft`, `draftDataJson`, `draftExpiresAt` on `Project` | `prisma/schema.prisma:528-530` |
|
||||||
|
| **AI Filtering** |
|
||||||
|
| Automated eligibility screening | Run deterministic + AI rules, band by confidence | ✅ Exists | **None** | `stage-filtering.ts` with banding logic, `FilteringResult` outcome | `src/server/services/stage-filtering.ts:173-191` |
|
||||||
|
| Admin override capability | Manually resolve flagged projects | ✅ Exists | **None** | `resolveManualDecision()` updates `finalOutcome`, logs override in `OverrideAction` | `src/server/services/stage-filtering.ts:529-611` |
|
||||||
|
| Duplicate detection | Flag duplicate submissions (same email) | ✅ Exists | **None** | Built-in duplicate check by `submittedByEmail`, always flags (never auto-rejects) | `src/server/services/stage-filtering.ts:267-289` |
|
||||||
|
| **Jury 1 (Evaluation Round 1)** |
|
||||||
|
| Semi-finalist selection | Jury evaluates and votes Yes/No | ✅ Exists | **None** | `Evaluation.binaryDecision` field, evaluation submission flow | `src/server/routers/evaluation.ts:130-200` |
|
||||||
|
| Hard cap per juror | Max N projects per juror (enforced) | ⚠️ **Partial** | **Partial** | `User.maxAssignments` exists but used as global limit, not stage-specific hard cap | `prisma/schema.prisma:249` |
|
||||||
|
| Soft cap + buffer | Target N, allow up to N+buffer with warning | ❌ **Missing** | **Missing** | No concept of soft cap vs hard cap, no buffer configuration | — |
|
||||||
|
| Category ratio preferences per juror | Juror wants X% Startup / Y% Concept | ❌ **Missing** | **Missing** | No `User.preferredCategoryRatio` or equivalent | — |
|
||||||
|
| Explicit Jury 1 group | Named jury entity with members | ❌ **Missing** | **Missing** | All JURY_MEMBER users are global pool, no stage-scoped jury groups | — |
|
||||||
|
| **Semi-finalist Submission (Submission Round 2)** |
|
||||||
|
| New doc requirements | Round 2 has different file requirements | ✅ Exists | **None** | Each stage can have its own `FileRequirement` list | `prisma/schema.prisma:569-588` |
|
||||||
|
| Round 1 docs become read-only | Applicants can't edit/delete Round 1 files | ❌ **Missing** | **Missing** | No `ProjectFile.isReadOnly` or `FileRequirement.allowEdits` field | — |
|
||||||
|
| Jury sees both rounds | Jury can access Round 1 + Round 2 files | ⚠️ **Partial** | **Partial** | File access checks in `fileRouter.getDownloadUrl()` allow prior stages but complex logic, no explicit "cumulative view" | `src/server/routers/file.ts:66-108` |
|
||||||
|
| Multi-round submission windows | Distinct open/close dates for Round 1 vs Round 2 | ✅ Exists | **None** | Each stage has `windowOpenAt` / `windowCloseAt` | `prisma/schema.prisma:1888-1889` |
|
||||||
|
| **Jury 2 (Evaluation Round 2)** |
|
||||||
|
| Finalist selection | Jury evaluates semifinalists, selects finalists | ✅ Exists | **None** | Same evaluation flow, can configure different form per stage | `prisma/schema.prisma:450-472` |
|
||||||
|
| Special awards alongside | Run award eligibility + voting in parallel | ✅ Exists | **None** | `SpecialAward` system with `AwardEligibility`, `AwardJuror`, `AwardVote` | `prisma/schema.prisma:1363-1481` |
|
||||||
|
| Explicit Jury 2 group | Named jury entity, possibly overlapping with Jury 1 | ❌ **Missing** | **Missing** | Same global jury pool issue | — |
|
||||||
|
| Same cap/ratio features | Per-juror hard cap, soft cap, category ratios | ❌ **Missing** | **Missing** | (Same as Jury 1) | — |
|
||||||
|
| **Mentoring** |
|
||||||
|
| Private mentor-team workspace | Chat, file upload, threaded discussions | ⚠️ **Partial** | **Partial** | `MentorMessage` exists but no threading, no file comments, no promotion mechanism | `prisma/schema.prisma:1577-1590` |
|
||||||
|
| Mentor file upload | Mentor can upload files to project | ❌ **Missing** | **Missing** | No `ProjectFile.uploadedByMentorId` or mentor file upload router endpoint | — |
|
||||||
|
| Threaded file comments | Comment on specific files with replies | ❌ **Missing** | **Missing** | No `FileComment` model | — |
|
||||||
|
| File promotion to official submission | Mentor-uploaded file becomes part of official docs | ❌ **Missing** | **Missing** | No promotion workflow or `ProjectFile.promotedFromMentorFileId` | — |
|
||||||
|
| **Jury 3 Live Finals** |
|
||||||
|
| Stage manager admin controls | Cursor navigation, pause/resume, queue reorder | ✅ Exists | **None** | `live-control.ts` service with `LiveProgressCursor`, `Cohort` | `src/server/services/live-control.ts:1-619` |
|
||||||
|
| Jury live voting with notes | Vote during presentation, add notes | ⚠️ **Partial** | **Partial** | `LiveVote` exists but no `notes` field for per-vote commentary | `prisma/schema.prisma:1073-1099` |
|
||||||
|
| Audience voting | Audience can vote with configurable weight | ✅ Exists | **None** | `AudienceVoter`, `allowAudienceVotes`, `audienceVoteWeight` | `prisma/schema.prisma:1051-1060, 1101-1117` |
|
||||||
|
| Deliberation period | Time for jury discussion before final vote | ❌ **Missing** | **Missing** | No stage-specific `deliberationDurationMinutes` or deliberation status | — |
|
||||||
|
| Explicit Jury 3 group | Named jury entity for live finals | ❌ **Missing** | **Missing** | (Same global pool issue) | — |
|
||||||
|
| **Winner Confirmation** |
|
||||||
|
| Individual jury member confirmation | Each juror digitally signs off on results | ❌ **Missing** | **Missing** | No `JuryConfirmation` model or per-user signature workflow | — |
|
||||||
|
| Admin override to force majority | Admin can override and pick winner | ⚠️ **Partial** | **Partial** | `SpecialAward.winnerOverridden` exists, `OverrideAction` logs admin actions, but no explicit "force majority" vs "choose winner" distinction | `prisma/schema.prisma:1388-1389, 2024-2040` |
|
||||||
|
| Results frozen with audit trail | Immutable record of final decision | ⚠️ **Partial** | **Partial** | `DecisionAuditLog` exists, `OverrideAction` tracks changes, but no `ResultsSnapshot` or explicit freeze mechanism | `prisma/schema.prisma:2042-2057` |
|
||||||
|
| **Cross-Cutting Features** |
|
||||||
|
| Multi-jury support (named entities) | Jury 1, Jury 2, Jury 3 with overlapping members | ❌ **Missing** | **Missing** | No `JuryGroup` or `JuryMembership` model | — |
|
||||||
|
| Countdown timers on dashboards | Show time remaining until deadline | ❌ **Missing** | **Missing** | Backend has `windowCloseAt` but no tRPC endpoint for countdown state | — |
|
||||||
|
| Email reminders as deadlines approach | Automated reminders at 72h, 24h, 1h | ⚠️ **Partial** | **Partial** | `processEvaluationReminders()` exists for jury, `ReminderLog` tracks sent reminders, but no applicant deadline reminders | `prisma/schema.prisma:1487-1501` |
|
||||||
|
| Full audit trail for all decisions | Every action logged, immutable | ✅ Exists | **None** | `DecisionAuditLog`, `OverrideAction`, `AuditLog` comprehensive | `prisma/schema.prisma:754-783, 2024-2057` |
|
||||||
|
|
||||||
|
**Legend:**
|
||||||
|
- ✅ **Exists** = Feature fully implemented
|
||||||
|
- ⚠️ **Partial** = Feature partially implemented, needs extension
|
||||||
|
- ❌ **Missing** = Feature does not exist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Per-Step Deep Analysis
|
||||||
|
|
||||||
|
### Step 1: Intake (Submission Round 1)
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Applicants submit initial docs (executive summary, pitch deck, video)
|
||||||
|
- Public submission form with deadline enforcement
|
||||||
|
- Configurable grace periods for late submissions
|
||||||
|
- Draft support to save progress without submitting
|
||||||
|
- File type/size validation per requirement
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Public submission form**: `applicantRouter.saveSubmission()` creates/updates projects, `isDraft` flag allows partial saves
|
||||||
|
- ✅ **Deadline enforcement**: `Stage.windowOpenAt` / `windowCloseAt` enforced in `evaluationRouter.submit()` and applicant submission logic
|
||||||
|
- ✅ **Grace periods**: `GracePeriod` model per stage/user, `extendedUntil` overrides default deadline
|
||||||
|
- ✅ **File requirements**: `FileRequirement` linked to stages, defines `acceptedMimeTypes`, `maxSizeMB`, `isRequired`
|
||||||
|
- ✅ **Late submission tracking**: `ProjectFile.isLate` flag set if uploaded after deadline
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
- (None — intake is fully functional)
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- (None — intake meets requirements)
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/routers/applicant.ts:126-200` (saveSubmission)
|
||||||
|
- `prisma/schema.prisma:569-588` (FileRequirement)
|
||||||
|
- `prisma/schema.prisma:703-728` (GracePeriod)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: AI Filtering
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Automated eligibility screening using deterministic rules (field checks, doc checks) + AI rubric
|
||||||
|
- Confidence banding: high confidence auto-pass, low confidence auto-reject, medium confidence flagged for manual review
|
||||||
|
- Admin override capability to resolve flagged projects
|
||||||
|
- Duplicate submission detection (never auto-reject, always flag)
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Filtering service**: `stage-filtering.ts` runs deterministic rules first, then AI screening if deterministic passes
|
||||||
|
- ✅ **Confidence banding**: `bandByConfidence()` function with thresholds 0.75 (pass) / 0.25 (reject), middle = flagged
|
||||||
|
- ✅ **Manual queue**: `getManualQueue()` returns flagged projects, `resolveManualDecision()` sets `finalOutcome`
|
||||||
|
- ✅ **Duplicate detection**: Built-in check by `submittedByEmail`, groups duplicates, always flags (never auto-rejects)
|
||||||
|
- ✅ **FilteringResult model**: Stores `outcome` (PASSED/FILTERED_OUT/FLAGGED), `ruleResultsJson`, `aiScreeningJson`, `finalOutcome` after override
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
- (None — filtering is fully functional)
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- (None — filtering meets requirements)
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/services/stage-filtering.ts:1-647` (full filtering pipeline)
|
||||||
|
- `prisma/schema.prisma:1190-1237` (FilteringRule, FilteringResult)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Jury 1 (Evaluation Round 1)
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Semi-finalist selection with hard/soft caps per juror
|
||||||
|
- Per-juror hard cap (e.g., max 20 projects, enforced)
|
||||||
|
- Per-juror soft cap + buffer (e.g., target 15, allow up to 18 with warning)
|
||||||
|
- Per-juror category ratio preferences (e.g., "I want 60% Startup / 40% Concept")
|
||||||
|
- Explicit Jury 1 group (named entity, distinct from Jury 2/Jury 3)
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Evaluation flow**: `evaluationRouter.submit()` accepts `binaryDecision` for yes/no semifinalist vote
|
||||||
|
- ✅ **Assignment system**: `stage-assignment.ts` generates assignments with workload balancing
|
||||||
|
- ⚠️ **Per-juror max**: `User.maxAssignments` exists but treated as global limit across all stages, not stage-specific hard cap
|
||||||
|
- ⚠️ **Workload scoring**: `calculateWorkloadScore()` in `stage-assignment.ts` uses `preferredWorkload` but not distinct soft vs hard cap
|
||||||
|
- ❌ **Soft cap + buffer**: No configuration for soft cap + buffer (e.g., target 15, allow up to 18)
|
||||||
|
- ❌ **Category ratio preferences**: No `User.preferredCategoryRatioJson` or similar field
|
||||||
|
- ❌ **Named jury groups**: All `JURY_MEMBER` users are a global pool, no `JuryGroup` model to create Jury 1, Jury 2, Jury 3 as separate entities
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
1. **Soft cap + buffer**: Need `User.targetAssignments` (soft cap) and `User.maxAssignments` (hard cap), with UI warning when juror is in buffer zone
|
||||||
|
2. **Category ratio preferences**: Need `User.preferredCategoryRatioJson: { STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 }` and assignment scoring that respects ratios
|
||||||
|
3. **Named jury groups**: Need `JuryGroup` model with `name`, `stageId`, `members[]`, so assignment can be scoped to "Jury 1" vs "Jury 2"
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- **Assignment service**: Update `stage-assignment.ts` to:
|
||||||
|
- Filter jury pool by `JuryGroup.members` for the stage
|
||||||
|
- Check both soft cap (warning) and hard cap (reject) when assigning
|
||||||
|
- Score assignments based on `preferredCategoryRatioJson` to balance category distribution per juror
|
||||||
|
- **Schema**: Add `JuryGroup`, `JuryMembership`, modify `User` to have `targetAssignments` and `preferredCategoryRatioJson`
|
||||||
|
- **Admin UI**: Jury group management, per-juror cap/ratio configuration
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/services/stage-assignment.ts:1-777` (assignment algorithm)
|
||||||
|
- `src/server/routers/evaluation.ts:130-200` (evaluation submission)
|
||||||
|
- `prisma/schema.prisma:241-357` (User model)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Semi-finalist Submission (Submission Round 2)
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- New doc requirements (e.g., detailed business plan, updated pitch deck)
|
||||||
|
- Round 1 docs become **read-only** for applicants (no edit/delete)
|
||||||
|
- Jury sees **both rounds** (cumulative file view)
|
||||||
|
- Multi-round submission windows (Round 2 opens after Jury 1 closes)
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Multi-round file requirements**: Each stage can define its own `FileRequirement` list
|
||||||
|
- ✅ **Multi-round windows**: `Stage.windowOpenAt` / `windowCloseAt` per stage
|
||||||
|
- ⚠️ **Jury file access**: `fileRouter.getDownloadUrl()` checks if juror has assignment to project, allows access to files from prior stages in same track (lines 66-108), but logic is implicit and complex
|
||||||
|
- ❌ **Read-only enforcement**: No `ProjectFile.isReadOnly` or `FileRequirement.allowEdits` field
|
||||||
|
- ❌ **Cumulative view**: No explicit "show all files from all prior stages" flag on stages
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
1. **Read-only flag**: Need `ProjectFile.isReadOnlyForApplicant: Boolean` set when stage transitions, or `FileRequirement.allowEdits: Boolean` to control mutability
|
||||||
|
2. **Cumulative view**: Need `Stage.showPriorStageFiles: Boolean` or `Stage.cumulativeFileView: Boolean` to make jury file access explicit
|
||||||
|
3. **File versioning**: Current `replacedById` allows versioning but doesn't enforce read-only from prior rounds
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- **Applicant file upload**: Check `isReadOnlyForApplicant` before allowing delete/replace
|
||||||
|
- **File router**: Simplify jury file access by checking `Stage.cumulativeFileView` instead of complex prior-stage logic
|
||||||
|
- **Stage transition**: When project moves from Round 1 to Round 2, mark all Round 1 files as `isReadOnlyForApplicant: true`
|
||||||
|
- **Schema**: Add `ProjectFile.isReadOnlyForApplicant`, `Stage.cumulativeFileView`
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/routers/file.ts:12-125` (file download authorization)
|
||||||
|
- `prisma/schema.prisma:590-624` (ProjectFile)
|
||||||
|
- `prisma/schema.prisma:1879-1922` (Stage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Jury 2 (Evaluation Round 2)
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Finalist selection (same evaluation mechanics as Jury 1)
|
||||||
|
- Special awards eligibility + voting alongside main track
|
||||||
|
- Explicit Jury 2 group (named entity, may overlap with Jury 1)
|
||||||
|
- Same per-juror caps and category ratio features as Jury 1
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Evaluation flow**: Identical to Jury 1, `binaryDecision` for finalist vote
|
||||||
|
- ✅ **Special awards**: Full system with `SpecialAward`, `AwardEligibility`, `AwardJuror`, `AwardVote`, AI eligibility screening
|
||||||
|
- ✅ **Award tracks**: `Track.kind: AWARD` allows award-specific stages to run in parallel
|
||||||
|
- ❌ **Named Jury 2 group**: Same global jury pool issue as Jury 1
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
- (Same as Jury 1: named jury groups, soft cap + buffer, category ratio preferences)
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- (Same as Jury 1: jury group scoping, cap/ratio logic in assignment service)
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/routers/specialAward.ts:1-150` (award management)
|
||||||
|
- `prisma/schema.prisma:1363-1481` (award models)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Mentoring
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Private mentor-team workspace with:
|
||||||
|
- Chat/messaging (already exists)
|
||||||
|
- Mentor file upload (mentor uploads docs for team to review)
|
||||||
|
- Threaded file comments (comment on specific files with replies)
|
||||||
|
- File promotion (mentor-uploaded file becomes part of official submission)
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Mentor assignment**: `MentorAssignment` model, AI-suggested matching, manual assignment
|
||||||
|
- ✅ **Mentor messages**: `MentorMessage` model for chat messages between mentor and team
|
||||||
|
- ❌ **Mentor file upload**: No `ProjectFile.uploadedByMentorId` or mentor file upload endpoint
|
||||||
|
- ❌ **Threaded file comments**: No `FileComment` model with `parentCommentId` for threading
|
||||||
|
- ❌ **File promotion**: No workflow to promote mentor-uploaded file to official project submission
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
1. **Mentor file upload**: Need `ProjectFile.uploadedByMentorId: String?`, extend `fileRouter.getUploadUrl()` to allow mentors to upload
|
||||||
|
2. **File comments**: Need `FileComment` model:
|
||||||
|
```prisma
|
||||||
|
model FileComment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
fileId String
|
||||||
|
file ProjectFile @relation(...)
|
||||||
|
authorId String
|
||||||
|
author User @relation(...)
|
||||||
|
content String @db.Text
|
||||||
|
parentCommentId String?
|
||||||
|
parentComment FileComment? @relation("CommentReplies", ...)
|
||||||
|
replies FileComment[] @relation("CommentReplies")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **File promotion**: Need `ProjectFile.promotedFromMentorFileId: String?` and a promotion workflow (admin/team approves mentor file as official doc)
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- **File router**: Add `mentorUploadFile` mutation, authorization check for mentor role
|
||||||
|
- **Mentor router**: Add `addFileComment`, `promoteFileToOfficial` mutations
|
||||||
|
- **Schema**: Add `FileComment`, modify `ProjectFile` to link mentor uploads and promotions
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/routers/mentor.ts:1-200` (mentor operations)
|
||||||
|
- `prisma/schema.prisma:1145-1172` (MentorAssignment)
|
||||||
|
- `prisma/schema.prisma:1577-1590` (MentorMessage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Jury 3 Live Finals
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Stage manager admin controls (cursor navigation, pause/resume, queue reorder) — **ALREADY EXISTS**
|
||||||
|
- Jury live voting with notes (vote + add commentary per vote)
|
||||||
|
- Audience voting — **ALREADY EXISTS**
|
||||||
|
- Deliberation period (pause for jury discussion before final vote)
|
||||||
|
- Explicit Jury 3 group (named entity for live finals)
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ✅ **Live control service**: `live-control.ts` with `LiveProgressCursor`, session management, cursor navigation, queue reordering
|
||||||
|
- ✅ **Live voting**: `LiveVote` model, jury/audience voting, criteria-based scoring
|
||||||
|
- ✅ **Cohort management**: `Cohort` groups projects for voting windows
|
||||||
|
- ⚠️ **Vote notes**: `LiveVote` has no `notes` or `commentary` field for per-vote notes
|
||||||
|
- ❌ **Deliberation period**: No `Cohort.deliberationDurationMinutes` or deliberation status
|
||||||
|
- ❌ **Named Jury 3 group**: Same global jury pool issue
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
1. **Vote notes**: Add `LiveVote.notes: String?` for jury commentary during voting
|
||||||
|
2. **Deliberation period**: Add `Cohort.deliberationDurationMinutes: Int?`, `Cohort.deliberationStartedAt: DateTime?`, `Cohort.deliberationEndedAt: DateTime?`
|
||||||
|
3. **Named Jury 3 group**: (Same as Jury 1/Jury 2)
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- **LiveVote model**: Add `notes` field
|
||||||
|
- **Cohort model**: Add deliberation fields
|
||||||
|
- **Live voting router**: Add `startDeliberation()`, `endDeliberation()` procedures
|
||||||
|
- **Live control service**: Add deliberation status checks to prevent voting during deliberation
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `src/server/services/live-control.ts:1-619` (live session management)
|
||||||
|
- `src/server/routers/live-voting.ts:1-150` (live voting procedures)
|
||||||
|
- `prisma/schema.prisma:1035-1071, 1969-2006` (LiveVotingSession, Cohort)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 8: Winner Confirmation
|
||||||
|
|
||||||
|
**What the Flow Requires:**
|
||||||
|
- Individual jury member confirmation (each juror digitally signs off on results)
|
||||||
|
- Admin override to force majority or choose winner
|
||||||
|
- Results frozen with immutable audit trail
|
||||||
|
|
||||||
|
**What Currently Exists:**
|
||||||
|
- ⚠️ **Admin override**: `SpecialAward.winnerOverridden` flag, `OverrideAction` logs admin actions, but no explicit "force majority" vs "choose winner" distinction
|
||||||
|
- ⚠️ **Audit trail**: `DecisionAuditLog`, `OverrideAction` comprehensive, but no explicit `ResultsSnapshot` or freeze mechanism
|
||||||
|
- ❌ **Individual jury confirmation**: No `JuryConfirmation` model for per-user digital signatures
|
||||||
|
|
||||||
|
**What's Missing:**
|
||||||
|
1. **Jury confirmation**: Need `JuryConfirmation` model:
|
||||||
|
```prisma
|
||||||
|
model JuryConfirmation {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
stageId String
|
||||||
|
stage Stage @relation(...)
|
||||||
|
userId String
|
||||||
|
user User @relation(...)
|
||||||
|
confirmedAt DateTime @default(now())
|
||||||
|
signature String // Digital signature or consent hash
|
||||||
|
ipAddress String?
|
||||||
|
userAgent String?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. **Results freeze**: Need `Stage.resultsFrozenAt: DateTime?` to mark results as immutable
|
||||||
|
3. **Override modes**: Add `OverrideAction.overrideMode: Enum(FORCE_MAJORITY, CHOOSE_WINNER)` for clarity
|
||||||
|
|
||||||
|
**What Needs Modification:**
|
||||||
|
- **Live voting router**: Add `confirmResults()` procedure for jury members to sign off
|
||||||
|
- **Admin router**: Add `freezeResults()` procedure, check `resultsFrozenAt` before allowing further changes
|
||||||
|
- **Override service**: Update `OverrideAction` creation to include `overrideMode`
|
||||||
|
|
||||||
|
**File References:**
|
||||||
|
- `prisma/schema.prisma:1363-1418` (SpecialAward with winner override)
|
||||||
|
- `prisma/schema.prisma:2024-2040` (OverrideAction)
|
||||||
|
- `prisma/schema.prisma:2042-2057` (DecisionAuditLog)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cross-Cutting Gap Analysis
|
||||||
|
|
||||||
|
### Multi-Jury Support (Named Jury Entities with Overlap)
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- Create named jury groups (Jury 1, Jury 2, Jury 3) with explicit membership lists
|
||||||
|
- Allow jurors to be members of multiple groups (e.g., Juror A is in Jury 1 and Jury 3 but not Jury 2)
|
||||||
|
- Scope assignments, evaluations, and live voting to specific jury groups
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- All users with `role: JURY_MEMBER` are treated as a global pool
|
||||||
|
- No scoping of jury to specific stages or rounds
|
||||||
|
- `stage-assignment.ts` queries all active jury members without filtering by group
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No `JuryGroup` model
|
||||||
|
- ❌ No `JuryMembership` model to link users to groups
|
||||||
|
- ❌ No stage-level configuration to specify which jury group evaluates that stage
|
||||||
|
|
||||||
|
**Required Schema Changes:**
|
||||||
|
```prisma
|
||||||
|
model JuryGroup {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
programId String
|
||||||
|
program Program @relation(...)
|
||||||
|
name String // "Jury 1", "Jury 2", "Jury 3"
|
||||||
|
description String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
memberships JuryMembership[]
|
||||||
|
stages Stage[] // One-to-many: stages can specify which jury group evaluates them
|
||||||
|
}
|
||||||
|
|
||||||
|
model JuryMembership {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
juryGroupId String
|
||||||
|
juryGroup JuryGroup @relation(...)
|
||||||
|
userId String
|
||||||
|
user User @relation(...)
|
||||||
|
joinedAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([juryGroupId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend Stage model:
|
||||||
|
model Stage {
|
||||||
|
// ... existing fields
|
||||||
|
juryGroupId String?
|
||||||
|
juryGroup JuryGroup? @relation(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **High** — Affects assignment generation, evaluation authorization, live voting eligibility
|
||||||
|
- **Requires**: New admin UI for jury group management, updates to all jury-related queries/mutations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Multi-Round Submission Windows
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- Distinct submission windows for Round 1 (Intake), Round 2 (Semi-finalist submission)
|
||||||
|
- Round 1 files become read-only after Round 1 closes
|
||||||
|
- Jury sees cumulative files from all prior rounds
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ Each stage has `windowOpenAt` / `windowCloseAt` (multi-round windows exist)
|
||||||
|
- ⚠️ File access is complex and implicit (checks prior stages in track but no clear flag)
|
||||||
|
- ❌ No read-only enforcement for applicants after stage transition
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No `ProjectFile.isReadOnlyForApplicant` field
|
||||||
|
- ❌ No `Stage.cumulativeFileView` flag for jury access
|
||||||
|
- ❌ No automated mechanism to mark files as read-only on stage transition
|
||||||
|
|
||||||
|
**Required Schema Changes:**
|
||||||
|
```prisma
|
||||||
|
model ProjectFile {
|
||||||
|
// ... existing fields
|
||||||
|
isReadOnlyForApplicant Boolean @default(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
model Stage {
|
||||||
|
// ... existing fields
|
||||||
|
cumulativeFileView Boolean @default(false) // If true, jury sees files from all prior stages in track
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Medium** — Affects file upload/delete authorization, jury file listing queries
|
||||||
|
- **Requires**: Stage transition hook to mark files as read-only, applicant file UI updates, jury file view updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Per-Juror Hard Cap vs Soft Cap + Buffer
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- **Hard cap**: Max N projects (e.g., 20), enforced, cannot exceed
|
||||||
|
- **Soft cap**: Target N projects (e.g., 15), preferred, can exceed with warning
|
||||||
|
- **Buffer**: Soft cap to hard cap range (e.g., 15-18), shows warning in UI
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ⚠️ `User.maxAssignments` exists but treated as global hard cap
|
||||||
|
- ⚠️ `User.preferredWorkload` used in assignment scoring but not enforced as soft cap
|
||||||
|
- ❌ No buffer concept, no UI warning when juror is over target
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No distinction between soft cap and hard cap
|
||||||
|
- ❌ No buffer configuration or warning mechanism
|
||||||
|
|
||||||
|
**Required Schema Changes:**
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
// ... existing fields
|
||||||
|
targetAssignments Int? // Soft cap (preferred target)
|
||||||
|
maxAssignments Int? // Hard cap (absolute max, enforced)
|
||||||
|
// preferredWorkload is deprecated in favor of targetAssignments
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Assignment Logic Changes:**
|
||||||
|
- Update `stage-assignment.ts`:
|
||||||
|
- Filter candidates to exclude jurors at `maxAssignments`
|
||||||
|
- Score jurors higher if below `targetAssignments`, lower if between `targetAssignments` and `maxAssignments` (buffer zone)
|
||||||
|
- UI shows warning icon for jurors in buffer zone (target < current < max)
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Medium** — Affects assignment generation and admin UI for jury workload
|
||||||
|
- **Requires**: Update assignment service, admin assignment UI to show soft/hard cap status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Per-Juror Category Ratio Preferences
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- Juror specifies preferred category distribution (e.g., "I want 60% Startup / 40% Business Concept")
|
||||||
|
- Assignment algorithm respects these preferences when assigning projects
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ❌ No category ratio configuration per juror
|
||||||
|
- ⚠️ Assignment scoring uses tag overlap and workload but not category distribution
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No `User.preferredCategoryRatioJson` field
|
||||||
|
- ❌ Assignment algorithm doesn't score based on category distribution
|
||||||
|
|
||||||
|
**Required Schema Changes:**
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
// ... existing fields
|
||||||
|
preferredCategoryRatioJson Json? @db.JsonB // { "STARTUP": 0.6, "BUSINESS_CONCEPT": 0.4 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Assignment Logic Changes:**
|
||||||
|
- Update `stage-assignment.ts`:
|
||||||
|
- For each juror, calculate current category distribution of assigned projects
|
||||||
|
- Score candidates higher if assigning this project would bring juror's distribution closer to `preferredCategoryRatioJson`
|
||||||
|
- Example: Juror wants 60/40 Startup/Concept, currently has 70/30, algorithm prefers assigning Concept projects to rebalance
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Medium** — Affects assignment generation quality, requires juror onboarding to set preferences
|
||||||
|
- **Requires**: Update assignment algorithm, admin UI for juror profile editing, onboarding flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Countdown Timers on Dashboards
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- Applicant dashboard shows countdown to submission deadline
|
||||||
|
- Jury dashboard shows countdown to evaluation deadline
|
||||||
|
- Admin dashboard shows countdown to stage window close
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ Backend has `Stage.windowCloseAt` timestamp
|
||||||
|
- ❌ No tRPC endpoint to fetch countdown state (time remaining, status: open/closing soon/closed)
|
||||||
|
- ❌ Frontend has no countdown component
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No `stageRouter.getCountdown()` or similar procedure
|
||||||
|
- ❌ No frontend countdown component
|
||||||
|
|
||||||
|
**Required Changes:**
|
||||||
|
- Add tRPC procedure:
|
||||||
|
```typescript
|
||||||
|
stageRouter.getCountdown: protectedProcedure
|
||||||
|
.input(z.object({ stageId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const stage = await ctx.prisma.stage.findUniqueOrThrow({ where: { id: input.stageId } })
|
||||||
|
const now = new Date()
|
||||||
|
const closeAt = stage.windowCloseAt
|
||||||
|
if (!closeAt) return { status: 'no_deadline', timeRemaining: null }
|
||||||
|
const remaining = closeAt.getTime() - now.getTime()
|
||||||
|
if (remaining <= 0) return { status: 'closed', timeRemaining: 0 }
|
||||||
|
return {
|
||||||
|
status: remaining < 3600000 ? 'closing_soon' : 'open', // < 1 hour = closing soon
|
||||||
|
timeRemaining: remaining,
|
||||||
|
closeAt,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- Frontend: Countdown component that polls `getCountdown()` and displays "X days Y hours Z minutes remaining"
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Low** — UX improvement, no data model changes
|
||||||
|
- **Requires**: New tRPC procedure, frontend countdown component, dashboard integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Email Reminders as Deadlines Approach
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- Automated email reminders at 72 hours, 24 hours, 1 hour before deadline
|
||||||
|
- For applicants (submission deadlines) and jury (evaluation deadlines)
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ⚠️ `processEvaluationReminders()` exists for jury reminders
|
||||||
|
- ⚠️ `ReminderLog` tracks sent reminders to prevent duplicates
|
||||||
|
- ❌ No applicant deadline reminder cron job
|
||||||
|
- ❌ No configurable reminder intervals (hardcoded to 3 days, 24h, 1h in evaluation reminders)
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No applicant reminder service
|
||||||
|
- ❌ No configurable reminder intervals per stage
|
||||||
|
|
||||||
|
**Required Changes:**
|
||||||
|
- Add `Stage.reminderIntervalsJson: Json?` // `[72, 24, 1]` (hours before deadline)
|
||||||
|
- Add `src/server/services/applicant-reminders.ts`:
|
||||||
|
```typescript
|
||||||
|
export async function processApplicantReminders(prisma: PrismaClient) {
|
||||||
|
const now = new Date()
|
||||||
|
const stages = await prisma.stage.findMany({
|
||||||
|
where: { status: 'STAGE_ACTIVE', windowCloseAt: { gte: now } },
|
||||||
|
})
|
||||||
|
for (const stage of stages) {
|
||||||
|
const intervals = (stage.reminderIntervalsJson as number[]) ?? [72, 24, 1]
|
||||||
|
for (const hoursBeforeDeadline of intervals) {
|
||||||
|
const reminderTime = new Date(stage.windowCloseAt!.getTime() - hoursBeforeDeadline * 3600000)
|
||||||
|
if (now >= reminderTime && now < new Date(reminderTime.getTime() + 3600000)) {
|
||||||
|
// Send reminders to all applicants with draft projects in this stage
|
||||||
|
// Check ReminderLog to avoid duplicates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Add cron job in `src/app/api/cron/applicant-reminders/route.ts`
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Medium** — Improves applicant engagement, reduces late submissions
|
||||||
|
- **Requires**: New service, new cron endpoint, extend `ReminderLog` model if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Admin Override Capability at Every Step
|
||||||
|
|
||||||
|
**Requirement:**
|
||||||
|
- Admin can override any automated decision (filtering, assignment, voting results)
|
||||||
|
- Override is logged with reason code and reason text in `OverrideAction`
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- ✅ Filtering: `resolveManualDecision()` overrides flagged projects
|
||||||
|
- ✅ Assignment: Manual assignment creation bypasses AI
|
||||||
|
- ⚠️ Live voting: `SpecialAward.winnerOverridden` flag exists but no explicit override flow for live voting results
|
||||||
|
- ⚠️ Stage transitions: No override capability to force projects between stages
|
||||||
|
|
||||||
|
**Gap:**
|
||||||
|
- ❌ No admin UI to override stage transitions (force project to next stage even if guard fails)
|
||||||
|
- ❌ No admin override for live voting results (admin can pick winner but not documented as override)
|
||||||
|
|
||||||
|
**Required Changes:**
|
||||||
|
- Add `stageRouter.overrideTransition()` procedure:
|
||||||
|
```typescript
|
||||||
|
overrideTransition: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
fromStageId: z.string(),
|
||||||
|
toStageId: z.string(),
|
||||||
|
reasonCode: z.nativeEnum(OverrideReasonCode),
|
||||||
|
reasonText: z.string(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
// Force executeTransition() without validation
|
||||||
|
// Log in OverrideAction
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- Add `liveVotingRouter.overrideWinner()` procedure (similar flow)
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Low** — Fills gaps in admin control, already mostly exists
|
||||||
|
- **Requires**: New admin procedures, UI buttons for override actions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Integration Gaps
|
||||||
|
|
||||||
|
### Cross-Stage File Visibility
|
||||||
|
|
||||||
|
**Issue:**
|
||||||
|
- Current file access is stage-scoped. Jury assigned to Round 2 can technically access Round 1 files (via complex `fileRouter.getDownloadUrl()` logic checking prior stages), but this is implicit and fragile.
|
||||||
|
- No clear flag to say "Round 2 jury should see Round 1 + Round 2 files" vs "Round 2 jury should only see Round 2 files".
|
||||||
|
|
||||||
|
**Required:**
|
||||||
|
- Add `Stage.cumulativeFileView: Boolean` — if true, jury sees files from all prior stages in the track.
|
||||||
|
- Simplify `fileRouter.getDownloadUrl()` authorization logic to check this flag instead of manual prior-stage traversal.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Medium** — Simplifies file access logic, makes jury file view behavior explicit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Round 1 to Round 2 Transition (File Read-Only Enforcement)
|
||||||
|
|
||||||
|
**Issue:**
|
||||||
|
- When a project transitions from Round 1 (Intake) to Round 2 (Semi-finalist submission), Round 1 files should become read-only for applicants.
|
||||||
|
- Currently, no mechanism enforces this. Applicants could theoretically delete/replace Round 1 files during Round 2.
|
||||||
|
|
||||||
|
**Required:**
|
||||||
|
- Stage transition hook in `stage-engine.ts` `executeTransition()`:
|
||||||
|
```typescript
|
||||||
|
// After creating destination PSS:
|
||||||
|
if (fromStage.stageType === 'INTAKE' && toStage.stageType === 'INTAKE') {
|
||||||
|
// Mark all project files uploaded in fromStage as read-only for applicant
|
||||||
|
await tx.projectFile.updateMany({
|
||||||
|
where: { projectId, roundId: fromStageRoundId },
|
||||||
|
data: { isReadOnlyForApplicant: true },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Applicant file upload/delete checks: Reject if `ProjectFile.isReadOnlyForApplicant: true`.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **High** — Ensures data integrity, prevents applicants from tampering with prior round submissions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Jury Group Scoping Across All Jury-Related Operations
|
||||||
|
|
||||||
|
**Issue:**
|
||||||
|
- Assignments, evaluations, live voting all currently use global jury pool.
|
||||||
|
- Once `JuryGroup` is introduced, must update every jury-related query/mutation to filter by `Stage.juryGroupId`.
|
||||||
|
|
||||||
|
**Affected Areas:**
|
||||||
|
1. **Assignment generation**: `stage-assignment.ts` `previewStageAssignment()` must filter `prisma.user.findMany({ where: { role: 'JURY_MEMBER', ... } })` to `prisma.juryMembership.findMany({ where: { juryGroupId: stage.juryGroupId } })`.
|
||||||
|
2. **Evaluation authorization**: `evaluationRouter.submit()` must verify `assignment.userId` is a member of `stage.juryGroupId`.
|
||||||
|
3. **Live voting authorization**: `liveVotingRouter.submitVote()` must verify juror is in `stage.juryGroupId`.
|
||||||
|
4. **Admin assignment UI**: Dropdown to select jurors must filter by jury group.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **High** — Pervasive change across all jury-related features.
|
||||||
|
- **Requires**: Careful migration plan, extensive testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Countdown Timer Backend Support
|
||||||
|
|
||||||
|
**Issue:**
|
||||||
|
- Dashboards need real-time countdown to deadlines, but no backend service provides this.
|
||||||
|
- Frontend would need to poll `Stage.windowCloseAt` directly and calculate client-side, or use a tRPC subscription.
|
||||||
|
|
||||||
|
**Required:**
|
||||||
|
- Add `stageRouter.getCountdown()` procedure (described in Cross-Cutting section).
|
||||||
|
- Frontend uses `trpc.stage.getCountdown.useQuery()` with `refetchInterval: 60000` (1 minute polling).
|
||||||
|
- Optionally: WebSocket subscription for real-time updates (out of scope for now, polling is sufficient).
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- **Low** — Backend is simple, frontend polling handles real-time updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Priority Matrix
|
||||||
|
|
||||||
|
Features ranked by **Business Impact** (High/Medium/Low) x **Implementation Effort** (High/Medium/Low).
|
||||||
|
|
||||||
|
| Feature | Business Impact | Implementation Effort | Priority Quadrant | Notes |
|
||||||
|
|---------|----------------|----------------------|-------------------|-------|
|
||||||
|
| **Multi-jury support (named groups)** | **High** | **High** | **Critical** | Required for all 3 jury rounds, affects assignments/evaluations/voting |
|
||||||
|
| **Round 1 docs read-only enforcement** | **High** | **Low** | **Quick Win** | Data integrity essential, simple flag + hook |
|
||||||
|
| **Per-juror hard cap vs soft cap + buffer** | **High** | **Medium** | **Critical** | Ensures balanced workload, prevents burnout |
|
||||||
|
| **Per-juror category ratio preferences** | **Medium** | **Medium** | **Important** | Improves assignment quality, enhances juror satisfaction |
|
||||||
|
| **Jury vote notes (live finals)** | **Medium** | **Low** | **Quick Win** | Enhances deliberation, simple schema change |
|
||||||
|
| **Deliberation period (live finals)** | **Medium** | **Low** | **Quick Win** | Required for live finals flow, simple cohort fields |
|
||||||
|
| **Individual jury confirmation** | **High** | **Medium** | **Critical** | Legal/compliance requirement for final results |
|
||||||
|
| **Results freeze mechanism** | **High** | **Low** | **Quick Win** | Immutable audit trail, simple timestamp flag |
|
||||||
|
| **Cumulative file view flag** | **Medium** | **Low** | **Quick Win** | Simplifies jury file access logic |
|
||||||
|
| **Mentor file upload** | **Medium** | **Medium** | **Important** | Enhances mentoring, requires file router extension |
|
||||||
|
| **Threaded file comments** | **Low** | **Medium** | **Nice to Have** | Improves collaboration, but not blocking |
|
||||||
|
| **File promotion workflow** | **Low** | **Medium** | **Nice to Have** | Advanced feature, can defer to later phase |
|
||||||
|
| **Countdown timers (UI)** | **Low** | **Low** | **Nice to Have** | UX improvement, no data model changes |
|
||||||
|
| **Applicant deadline reminders** | **Medium** | **Low** | **Quick Win** | Reduces late submissions, simple cron job |
|
||||||
|
| **Admin override for stage transitions** | **Low** | **Low** | **Nice to Have** | Edge case, manual workaround exists |
|
||||||
|
|
||||||
|
**Priority Quadrants:**
|
||||||
|
- **Critical (High Impact / High Effort)**: Multi-jury support, jury confirmation — **must do**, high planning required
|
||||||
|
- **Quick Wins (High Impact / Low Effort)**: Read-only enforcement, results freeze, deliberation period — **do first**
|
||||||
|
- **Important (Medium Impact / Medium Effort)**: Caps/ratios, mentor file upload — **do after quick wins**
|
||||||
|
- **Nice to Have (Low Impact / Any Effort)**: File comments threading, countdown timers — **defer or phase 2**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The current MOPC platform has a **solid foundation** with the pipeline/track/stage architecture, stage-engine transitions, AI filtering, jury assignment, and live voting infrastructure fully implemented. The **critical gaps** are:
|
||||||
|
|
||||||
|
1. **Multi-jury support** (named jury entities with overlap) — **highest priority**, affects all jury-related features
|
||||||
|
2. **Per-juror caps and category ratio preferences** — **essential for workload balancing**
|
||||||
|
3. **Round 1 read-only enforcement + cumulative file view** — **data integrity and jury UX**
|
||||||
|
4. **Individual jury confirmation + results freeze** — **compliance and audit requirements**
|
||||||
|
5. **Mentoring workspace features** (file upload, comments, promotion) — **enhances mentoring but lower priority**
|
||||||
|
|
||||||
|
**Recommended Approach:**
|
||||||
|
- **Phase 1 (Quick Wins)**: Read-only enforcement, results freeze, deliberation period, vote notes, applicant reminders — **2-3 weeks**
|
||||||
|
- **Phase 2 (Critical)**: Multi-jury support, jury confirmation — **4-6 weeks** (complex, pervasive changes)
|
||||||
|
- **Phase 3 (Important)**: Caps/ratios, mentor file upload — **3-4 weeks**
|
||||||
|
- **Phase 4 (Nice to Have)**: Threaded comments, file promotion, countdown timers — **defer to post-MVP**
|
||||||
|
|
||||||
|
Total estimated effort for Phases 1-3: **9-13 weeks** (assumes single developer, includes testing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Gap Analysis Document**
|
||||||
1139
docs/claude-architecture-redesign/03-data-model.md
Normal file
1139
docs/claude-architecture-redesign/03-data-model.md
Normal file
File diff suppressed because it is too large
Load Diff
1539
docs/claude-architecture-redesign/04-round-intake.md
Normal file
1539
docs/claude-architecture-redesign/04-round-intake.md
Normal file
File diff suppressed because it is too large
Load Diff
1438
docs/claude-architecture-redesign/05-round-filtering.md
Normal file
1438
docs/claude-architecture-redesign/05-round-filtering.md
Normal file
File diff suppressed because it is too large
Load Diff
698
docs/claude-architecture-redesign/06-round-evaluation.md
Normal file
698
docs/claude-architecture-redesign/06-round-evaluation.md
Normal file
@@ -0,0 +1,698 @@
|
|||||||
|
# Round: Evaluation (Jury 1 & Jury 2)
|
||||||
|
|
||||||
|
## 1. Purpose & Position in Flow
|
||||||
|
|
||||||
|
The EVALUATION round is the core judging mechanism of the competition. It appears **twice** in the standard flow:
|
||||||
|
|
||||||
|
| Instance | Name | Position | Jury | Purpose | Output |
|
||||||
|
|----------|------|----------|------|---------|--------|
|
||||||
|
| Round 3 | "Jury 1 — Semi-finalist Selection" | After FILTERING | Jury 1 | Score projects, select semi-finalists | Semi-finalists per category |
|
||||||
|
| Round 5 | "Jury 2 — Finalist Selection" | After SUBMISSION Round 2 | Jury 2 | Score semi-finalists, select finalists + awards | Finalists per category |
|
||||||
|
|
||||||
|
Both instances use the same `RoundType.EVALUATION` but are configured independently with:
|
||||||
|
- Different jury groups (Jury 1 vs Jury 2)
|
||||||
|
- Different evaluation forms/rubrics
|
||||||
|
- Different visible submission windows (Jury 1 sees Window 1 only; Jury 2 sees Windows 1+2)
|
||||||
|
- Different advancement counts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Data Model
|
||||||
|
|
||||||
|
### Round Record
|
||||||
|
|
||||||
|
```
|
||||||
|
Round {
|
||||||
|
id: "round-jury-1"
|
||||||
|
competitionId: "comp-2026"
|
||||||
|
name: "Jury 1 — Semi-finalist Selection"
|
||||||
|
roundType: EVALUATION
|
||||||
|
status: ROUND_DRAFT → ROUND_ACTIVE → ROUND_CLOSED
|
||||||
|
sortOrder: 2
|
||||||
|
windowOpenAt: "2026-04-01" // Evaluation window start
|
||||||
|
windowCloseAt: "2026-04-30" // Evaluation window end
|
||||||
|
juryGroupId: "jury-group-1" // Links to Jury 1
|
||||||
|
submissionWindowId: null // EVALUATION rounds don't collect submissions
|
||||||
|
configJson: { ...EvaluationConfig }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EvaluationConfig
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type EvaluationConfig = {
|
||||||
|
// --- Assignment Settings ---
|
||||||
|
requiredReviewsPerProject: number // How many jurors review each project (default: 3)
|
||||||
|
|
||||||
|
// --- Scoring Mode ---
|
||||||
|
scoringMode: "criteria" | "global" | "binary"
|
||||||
|
// criteria: Score per criterion + weighted total
|
||||||
|
// global: Single 1-10 score
|
||||||
|
// binary: Yes/No decision (semi-finalist worthy?)
|
||||||
|
requireFeedback: boolean // Must provide text feedback (default: true)
|
||||||
|
|
||||||
|
// --- COI ---
|
||||||
|
coiRequired: boolean // Must declare COI before evaluating (default: true)
|
||||||
|
|
||||||
|
// --- Peer Review ---
|
||||||
|
peerReviewEnabled: boolean // Jurors can see anonymized peer evaluations after submission
|
||||||
|
anonymizationLevel: "fully_anonymous" | "show_initials" | "named"
|
||||||
|
|
||||||
|
// --- AI Features ---
|
||||||
|
aiSummaryEnabled: boolean // Generate AI-powered evaluation summaries
|
||||||
|
aiAssignmentEnabled: boolean // Allow AI-suggested jury-project matching
|
||||||
|
|
||||||
|
// --- Advancement ---
|
||||||
|
advancementMode: "auto_top_n" | "admin_selection" | "ai_recommended"
|
||||||
|
advancementConfig: {
|
||||||
|
perCategory: boolean // Separate counts per STARTUP / BUSINESS_CONCEPT
|
||||||
|
startupCount: number // How many startups advance (default: 10 for Jury 1, 3 for Jury 2)
|
||||||
|
conceptCount: number // How many concepts advance
|
||||||
|
tieBreaker: "admin_decides" | "highest_individual" | "revote"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Related Models
|
||||||
|
|
||||||
|
| Model | Role |
|
||||||
|
|-------|------|
|
||||||
|
| `JuryGroup` | Named jury entity linked to this round |
|
||||||
|
| `JuryGroupMember` | Members of the jury with per-juror overrides |
|
||||||
|
| `Assignment` | Juror-project pairing for this round, linked to JuryGroup |
|
||||||
|
| `Evaluation` | Score/feedback submitted by a juror for one project |
|
||||||
|
| `EvaluationForm` | Rubric/criteria definition for this round |
|
||||||
|
| `ConflictOfInterest` | COI declaration per assignment |
|
||||||
|
| `GracePeriod` | Per-juror deadline extension |
|
||||||
|
| `EvaluationSummary` | AI-generated insights per project per round |
|
||||||
|
| `EvaluationDiscussion` | Peer review discussion threads |
|
||||||
|
| `RoundSubmissionVisibility` | Which submission windows' docs jury can see |
|
||||||
|
| `AdvancementRule` | How projects advance after evaluation |
|
||||||
|
| `ProjectRoundState` | Per-project state in this round |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Setup Phase (Before Window Opens)
|
||||||
|
|
||||||
|
### 3.1 Admin Creates the Evaluation Round
|
||||||
|
|
||||||
|
Admin uses the competition wizard or round management UI to:
|
||||||
|
|
||||||
|
1. **Create the Round** with type EVALUATION
|
||||||
|
2. **Link a JuryGroup** — select "Jury 1" (or create a new jury group)
|
||||||
|
3. **Set the evaluation window** — start and end dates
|
||||||
|
4. **Configure the evaluation form** — scoring criteria, weights, scales
|
||||||
|
5. **Set visibility** — which submission windows jury can see (via RoundSubmissionVisibility)
|
||||||
|
6. **Configure advancement rules** — how many advance per category
|
||||||
|
|
||||||
|
### 3.2 Jury Group Configuration
|
||||||
|
|
||||||
|
The linked JuryGroup has:
|
||||||
|
|
||||||
|
```
|
||||||
|
JuryGroup {
|
||||||
|
name: "Jury 1"
|
||||||
|
defaultMaxAssignments: 20 // Default cap per juror
|
||||||
|
defaultCapMode: SOFT // HARD | SOFT | NONE
|
||||||
|
softCapBuffer: 2 // Can exceed by 2 for load balancing
|
||||||
|
categoryQuotasEnabled: true
|
||||||
|
defaultCategoryQuotas: {
|
||||||
|
"STARTUP": { "min": 3, "max": 15 },
|
||||||
|
"BUSINESS_CONCEPT": { "min": 3, "max": 15 }
|
||||||
|
}
|
||||||
|
allowJurorCapAdjustment: true // Jurors can adjust their cap during onboarding
|
||||||
|
allowJurorRatioAdjustment: true // Jurors can adjust their category preference
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Per-Juror Overrides
|
||||||
|
|
||||||
|
Each `JuryGroupMember` can override group defaults:
|
||||||
|
|
||||||
|
```
|
||||||
|
JuryGroupMember {
|
||||||
|
juryGroupId: "jury-group-1"
|
||||||
|
userId: "judge-alice"
|
||||||
|
maxAssignmentsOverride: 25 // Alice wants more projects
|
||||||
|
capModeOverride: HARD // Alice: hard cap, no exceptions
|
||||||
|
categoryQuotasOverride: {
|
||||||
|
"STARTUP": { "min": 5, "max": 20 }, // Alice prefers startups
|
||||||
|
"BUSINESS_CONCEPT": { "min": 0, "max": 5 }
|
||||||
|
}
|
||||||
|
preferredStartupRatio: 0.8 // 80% startups
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Juror Onboarding (Optional)
|
||||||
|
|
||||||
|
If `allowJurorCapAdjustment` or `allowJurorRatioAdjustment` is true:
|
||||||
|
|
||||||
|
1. When a juror first opens their jury dashboard after being added to the group
|
||||||
|
2. A one-time onboarding dialog appears:
|
||||||
|
- "Your default maximum is 20 projects. Would you like to adjust?" (slider)
|
||||||
|
- "Your default startup/concept ratio is 50/50. Would you like to adjust?" (slider)
|
||||||
|
3. Juror saves preferences → stored in `JuryGroupMember.maxAssignmentsOverride` and `preferredStartupRatio`
|
||||||
|
4. Dialog doesn't appear again (tracked via `JuryGroupMember.updatedAt` or a flag)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Assignment System (Enhanced)
|
||||||
|
|
||||||
|
### 4.1 Assignment Algorithm — Jury-Group-Aware
|
||||||
|
|
||||||
|
The current `stage-assignment.ts` algorithm is enhanced to:
|
||||||
|
|
||||||
|
1. **Filter jury pool by JuryGroup** — only members of the linked jury group are considered
|
||||||
|
2. **Apply hard/soft cap logic** per juror
|
||||||
|
3. **Apply category quotas** per juror
|
||||||
|
4. **Score candidates** using existing expertise matching + workload balancing + geo-diversity
|
||||||
|
|
||||||
|
#### Effective Limits Resolution
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function getEffectiveLimits(member: JuryGroupMember, group: JuryGroup): EffectiveLimits {
|
||||||
|
return {
|
||||||
|
maxAssignments: member.maxAssignmentsOverride ?? group.defaultMaxAssignments,
|
||||||
|
capMode: member.capModeOverride ?? group.defaultCapMode,
|
||||||
|
softCapBuffer: group.softCapBuffer, // Group-level only (not per-juror)
|
||||||
|
categoryQuotas: member.categoryQuotasOverride ?? group.defaultCategoryQuotas,
|
||||||
|
categoryQuotasEnabled: group.categoryQuotasEnabled,
|
||||||
|
preferredStartupRatio: member.preferredStartupRatio,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Cap Enforcement Logic
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function canAssignMore(
|
||||||
|
jurorId: string,
|
||||||
|
projectCategory: CompetitionCategory,
|
||||||
|
currentLoad: LoadTracker,
|
||||||
|
limits: EffectiveLimits
|
||||||
|
): { allowed: boolean; penalty: number; reason?: string } {
|
||||||
|
const total = currentLoad.total(jurorId)
|
||||||
|
const catLoad = currentLoad.byCategory(jurorId, projectCategory)
|
||||||
|
|
||||||
|
// 1. HARD cap check
|
||||||
|
if (limits.capMode === "HARD" && total >= limits.maxAssignments) {
|
||||||
|
return { allowed: false, penalty: 0, reason: "Hard cap reached" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SOFT cap check (can exceed by buffer)
|
||||||
|
let overflowPenalty = 0
|
||||||
|
if (limits.capMode === "SOFT") {
|
||||||
|
if (total >= limits.maxAssignments + limits.softCapBuffer) {
|
||||||
|
return { allowed: false, penalty: 0, reason: "Soft cap + buffer exceeded" }
|
||||||
|
}
|
||||||
|
if (total >= limits.maxAssignments) {
|
||||||
|
// In buffer zone — apply increasing penalty
|
||||||
|
overflowPenalty = (total - limits.maxAssignments + 1) * 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Category quota check
|
||||||
|
if (limits.categoryQuotasEnabled && limits.categoryQuotas) {
|
||||||
|
const quota = limits.categoryQuotas[projectCategory]
|
||||||
|
if (quota) {
|
||||||
|
if (catLoad >= quota.max) {
|
||||||
|
return { allowed: false, penalty: 0, reason: `Category ${projectCategory} max reached (${quota.max})` }
|
||||||
|
}
|
||||||
|
// Bonus for under-min
|
||||||
|
if (catLoad < quota.min) {
|
||||||
|
overflowPenalty -= 15 // Negative penalty = bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Ratio preference alignment
|
||||||
|
if (limits.preferredStartupRatio != null && total > 0) {
|
||||||
|
const currentStartupRatio = currentLoad.byCategory(jurorId, "STARTUP") / total
|
||||||
|
const isStartup = projectCategory === "STARTUP"
|
||||||
|
const wantMore = isStartup
|
||||||
|
? currentStartupRatio < limits.preferredStartupRatio
|
||||||
|
: currentStartupRatio > limits.preferredStartupRatio
|
||||||
|
if (wantMore) overflowPenalty -= 10 // Bonus for aligning with preference
|
||||||
|
else overflowPenalty += 10 // Penalty for diverging
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true, penalty: overflowPenalty }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Assignment Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Admin opens Assignment panel for Round 3 (Jury 1)
|
||||||
|
2. System loads:
|
||||||
|
- Projects with ProjectRoundState PENDING/IN_PROGRESS in this round
|
||||||
|
- JuryGroup members (with effective limits)
|
||||||
|
- Existing assignments (to avoid duplicates)
|
||||||
|
- COI records (to skip conflicted pairs)
|
||||||
|
3. Admin clicks "Generate Suggestions"
|
||||||
|
4. Algorithm runs:
|
||||||
|
a. For each project (sorted by fewest current assignments):
|
||||||
|
- Score each eligible juror (tag matching + workload + geo + cap/quota penalties)
|
||||||
|
- Select top N jurors (N = requiredReviewsPerProject - existing reviews)
|
||||||
|
- Track load in jurorLoadMap
|
||||||
|
b. Report unassigned projects (jurors at capacity)
|
||||||
|
5. Admin reviews preview:
|
||||||
|
- Assignment matrix (juror × project grid)
|
||||||
|
- Load distribution chart
|
||||||
|
- Unassigned projects list
|
||||||
|
- Category distribution per juror
|
||||||
|
6. Admin can:
|
||||||
|
- Accept all suggestions
|
||||||
|
- Modify individual assignments (drag-drop or manual add/remove)
|
||||||
|
- Re-run with different parameters
|
||||||
|
7. Admin clicks "Apply Assignments"
|
||||||
|
8. System creates Assignment records with juryGroupId set
|
||||||
|
9. Notifications sent to jurors
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 AI-Powered Assignment (Optional)
|
||||||
|
|
||||||
|
If `aiAssignmentEnabled` is true in config:
|
||||||
|
|
||||||
|
1. Admin clicks "AI Assignment Suggestions"
|
||||||
|
2. System calls `ai-assignment.ts`:
|
||||||
|
- Anonymizes juror profiles and project descriptions
|
||||||
|
- Sends to GPT with matching instructions
|
||||||
|
- Returns confidence scores and reasoning
|
||||||
|
3. AI suggestions shown alongside algorithm suggestions
|
||||||
|
4. Admin picks which to use or mixes both
|
||||||
|
|
||||||
|
### 4.4 Handling Unassigned Projects
|
||||||
|
|
||||||
|
When all jurors with SOFT cap reach cap+buffer:
|
||||||
|
1. Remaining projects become "unassigned"
|
||||||
|
2. Admin dashboard highlights these prominently
|
||||||
|
3. Admin can:
|
||||||
|
- Manually assign to specific jurors (bypasses cap — manual override)
|
||||||
|
- Increase a juror's cap
|
||||||
|
- Add more jurors to the jury group
|
||||||
|
- Reduce `requiredReviewsPerProject` for remaining projects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Jury Evaluation Experience
|
||||||
|
|
||||||
|
### 5.1 Jury Dashboard
|
||||||
|
|
||||||
|
When a Jury 1 member opens their dashboard:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ JURY 1 — Semi-finalist Selection │
|
||||||
|
│ ─────────────────────────────────────────────────── │
|
||||||
|
│ Evaluation Window: April 1 – April 30 │
|
||||||
|
│ ⏱ 12 days remaining │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
|
||||||
|
│ │ 15 │ │ 8 │ │ 2 │ │ 5 │ │
|
||||||
|
│ │ Total │ │ Complete │ │ In Draft │ │ Pending│ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Continue Next Evaluation →] │
|
||||||
|
│ │
|
||||||
|
│ Recent Assignments │
|
||||||
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
|
│ │ OceanClean AI │ Startup │ ✅ Done │ View │ │
|
||||||
|
│ │ Blue Carbon Hub │ Concept │ ⏳ Draft │ Cont │ │
|
||||||
|
│ │ SeaWatch Monitor │ Startup │ ⬜ Pending│ Start│ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Key elements:
|
||||||
|
- **Deadline countdown** — prominent timer showing days/hours remaining
|
||||||
|
- **Progress stats** — total, completed, in-draft, pending
|
||||||
|
- **Quick action CTA** — jump to next unevaluated project
|
||||||
|
- **Assignment list** — sorted by status (pending first, then drafts, then done)
|
||||||
|
|
||||||
|
### 5.2 COI Declaration (Blocking)
|
||||||
|
|
||||||
|
Before evaluating any project, the juror MUST declare COI:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────┐
|
||||||
|
│ Conflict of Interest Declaration │
|
||||||
|
│ │
|
||||||
|
│ Do you have a conflict of interest with │
|
||||||
|
│ "OceanClean AI" (Startup)? │
|
||||||
|
│ │
|
||||||
|
│ ○ No conflict — I can evaluate fairly │
|
||||||
|
│ ○ Yes, I have a conflict: │
|
||||||
|
│ Type: [Financial ▾] │
|
||||||
|
│ Description: [________________] │
|
||||||
|
│ │
|
||||||
|
│ [Submit Declaration] │
|
||||||
|
└───────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- If **No conflict**: Proceed to evaluation form
|
||||||
|
- If **Yes**: Assignment flagged, admin notified, juror may be reassigned
|
||||||
|
- COI declaration is logged in `ConflictOfInterest` model
|
||||||
|
- Admin can review and take action (cleared / reassigned / noted)
|
||||||
|
|
||||||
|
### 5.3 Evaluation Form
|
||||||
|
|
||||||
|
The form adapts to the `scoringMode`:
|
||||||
|
|
||||||
|
#### Criteria Mode (default for Jury 1 and Jury 2)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────────────────────────┐
|
||||||
|
│ Evaluating: OceanClean AI (Startup) │
|
||||||
|
│ ──────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ [📄 Documents] [📊 Scoring] [💬 Feedback] │
|
||||||
|
│ │
|
||||||
|
│ ── DOCUMENTS TAB ── │
|
||||||
|
│ ┌─ Round 1 Application Docs ─────────────────┐ │
|
||||||
|
│ │ 📄 Executive Summary.pdf [Download] │ │
|
||||||
|
│ │ 📄 Business Plan.pdf [Download] │ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ (Jury 2 also sees:) │
|
||||||
|
│ ┌─ Round 2 Semi-finalist Docs ────────────────┐ │
|
||||||
|
│ │ 📄 Updated Business Plan.pdf [Download] │ │
|
||||||
|
│ │ 🎥 Video Pitch.mp4 [Play] │ │
|
||||||
|
│ └─────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ── SCORING TAB ── │
|
||||||
|
│ Innovation & Impact [1] [2] [3] [4] [5] (w:30%)│
|
||||||
|
│ Feasibility [1] [2] [3] [4] [5] (w:25%)│
|
||||||
|
│ Team & Execution [1] [2] [3] [4] [5] (w:25%)│
|
||||||
|
│ Ocean Relevance [1] [2] [3] [4] [5] (w:20%)│
|
||||||
|
│ │
|
||||||
|
│ Overall Score: 3.8 / 5.0 (auto-calculated) │
|
||||||
|
│ │
|
||||||
|
│ ── FEEDBACK TAB ── │
|
||||||
|
│ Feedback: [________________________________] │
|
||||||
|
│ │
|
||||||
|
│ [💾 Save Draft] [✅ Submit Evaluation] │
|
||||||
|
│ (Auto-saves every 30s) │
|
||||||
|
└───────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Binary Mode (optional for quick screening)
|
||||||
|
|
||||||
|
```
|
||||||
|
Should this project advance to the semi-finals?
|
||||||
|
[✅ Yes] [❌ No]
|
||||||
|
|
||||||
|
Justification (required): [________________]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Global Score Mode
|
||||||
|
|
||||||
|
```
|
||||||
|
Overall Score: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
|
||||||
|
|
||||||
|
Feedback (required): [________________]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 Document Visibility (Cross-Round)
|
||||||
|
|
||||||
|
Controlled by `RoundSubmissionVisibility`:
|
||||||
|
|
||||||
|
| Round | Sees Window 1 ("Application Docs") | Sees Window 2 ("Semi-finalist Docs") |
|
||||||
|
|-------|------------------------------------|-----------------------------------------|
|
||||||
|
| Jury 1 (Round 3) | Yes | No (doesn't exist yet) |
|
||||||
|
| Jury 2 (Round 5) | Yes | Yes |
|
||||||
|
| Jury 3 (Round 7) | Yes | Yes |
|
||||||
|
|
||||||
|
In the evaluation UI:
|
||||||
|
- Documents are grouped by submission window
|
||||||
|
- Each group has a label (from `RoundSubmissionVisibility.displayLabel`)
|
||||||
|
- Clear visual separation (tabs, accordion sections, or side panels)
|
||||||
|
|
||||||
|
### 5.5 Auto-Save and Submission
|
||||||
|
|
||||||
|
- **Auto-save**: Client debounces and calls `evaluation.autosave` every 30 seconds while draft is open
|
||||||
|
- **Draft status**: Evaluation starts as NOT_STARTED → DRAFT on first save → SUBMITTED on explicit submit
|
||||||
|
- **Submission validation**:
|
||||||
|
- All required criteria scored (if criteria mode)
|
||||||
|
- Global score provided (if global mode)
|
||||||
|
- Binary decision selected (if binary mode)
|
||||||
|
- Feedback text provided (if `requireFeedback`)
|
||||||
|
- Window is open (or juror has grace period)
|
||||||
|
- **After submission**: Evaluation becomes read-only for juror (status = SUBMITTED)
|
||||||
|
- **Admin can lock**: Set status to LOCKED to prevent any further changes
|
||||||
|
|
||||||
|
### 5.6 Grace Periods
|
||||||
|
|
||||||
|
```
|
||||||
|
GracePeriod {
|
||||||
|
roundId: "round-jury-1"
|
||||||
|
userId: "judge-alice"
|
||||||
|
projectId: null // Applies to ALL Alice's assignments in this round
|
||||||
|
extendedUntil: "2026-05-02" // 2 days after official close
|
||||||
|
reason: "Travel conflict"
|
||||||
|
grantedById: "admin-1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Admin can grant per-juror or per-juror-per-project grace periods
|
||||||
|
- Evaluation submission checks grace period before rejecting past-window submissions
|
||||||
|
- Dashboard shows "(Grace period: 2 extra days)" badge for affected jurors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. End of Evaluation — Results & Advancement
|
||||||
|
|
||||||
|
### 6.1 Results Visualization
|
||||||
|
|
||||||
|
When the evaluation window closes, the admin sees:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Jury 1 Results │
|
||||||
|
│ ─────────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ Completion: 142/150 evaluations submitted (94.7%) │
|
||||||
|
│ Outstanding: 8 (3 jurors have pending evaluations) │
|
||||||
|
│ │
|
||||||
|
│ ┌─ STARTUPS (Top 10) ──────────────────────────────────────┐│
|
||||||
|
│ │ # Project Avg Score Consensus Reviews Status ││
|
||||||
|
│ │ 1 OceanClean AI 4.6/5 0.92 3/3 ✅ ││
|
||||||
|
│ │ 2 SeaWatch 4.3/5 0.85 3/3 ✅ ││
|
||||||
|
│ │ 3 BlueCarbon 4.1/5 0.78 3/3 ✅ ││
|
||||||
|
│ │ ... ││
|
||||||
|
│ │ 10 TidalEnergy 3.2/5 0.65 3/3 ✅ ││
|
||||||
|
│ │ ── cutoff line ────────────────────────────────────────── ││
|
||||||
|
│ │ 11 WavePower 3.1/5 0.71 3/3 ⬜ ││
|
||||||
|
│ │ 12 CoralGuard 2.9/5 0.55 2/3 ⚠️ ││
|
||||||
|
│ └──────────────────────────────────────────────────────────┘│
|
||||||
|
│ │
|
||||||
|
│ ┌─ CONCEPTS (Top 10) ──────────────────────────────────────┐│
|
||||||
|
│ │ (same layout) ││
|
||||||
|
│ └──────────────────────────────────────────────────────────┘│
|
||||||
|
│ │
|
||||||
|
│ [🤖 AI Recommendation] [📊 Score Distribution] [Export] │
|
||||||
|
│ │
|
||||||
|
│ [✅ Approve Shortlist] [✏️ Edit Shortlist] │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Metrics shown:**
|
||||||
|
- Average global score (or weighted criteria average)
|
||||||
|
- Consensus score (1 - normalized stddev, where 1.0 = full agreement)
|
||||||
|
- Review count / required
|
||||||
|
- Per-criterion averages (expandable)
|
||||||
|
|
||||||
|
### 6.2 AI Recommendation
|
||||||
|
|
||||||
|
When admin clicks "AI Recommendation":
|
||||||
|
|
||||||
|
1. System calls `ai-evaluation-summary.ts` for each project in bulk
|
||||||
|
2. AI generates:
|
||||||
|
- Ranked shortlist per category based on scores + feedback analysis
|
||||||
|
- Strengths, weaknesses, themes per project
|
||||||
|
- Recommendation: "Advance" / "Borderline" / "Do not advance"
|
||||||
|
3. Admin sees AI recommendation alongside actual scores
|
||||||
|
4. AI recommendations are suggestions only — admin has final say
|
||||||
|
|
||||||
|
### 6.3 Advancement Decision
|
||||||
|
|
||||||
|
```
|
||||||
|
Advancement Mode: admin_selection (with AI recommendation)
|
||||||
|
|
||||||
|
1. System shows ranked list per category
|
||||||
|
2. AI highlights recommended top N per category
|
||||||
|
3. Admin can:
|
||||||
|
- Accept AI recommendation
|
||||||
|
- Drag projects to reorder
|
||||||
|
- Add/remove projects from advancement list
|
||||||
|
- Set custom cutoff line
|
||||||
|
4. Admin clicks "Confirm Advancement"
|
||||||
|
5. System:
|
||||||
|
a. Sets ProjectRoundState to PASSED for advancing projects
|
||||||
|
b. Sets ProjectRoundState to REJECTED for non-advancing projects
|
||||||
|
c. Updates Project.status to SEMIFINALIST (Jury 1) or FINALIST (Jury 2)
|
||||||
|
d. Logs all decisions in DecisionAuditLog
|
||||||
|
e. Sends notifications to all teams (advanced / not selected)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 Advancement Modes
|
||||||
|
|
||||||
|
| Mode | Behavior |
|
||||||
|
|------|----------|
|
||||||
|
| `auto_top_n` | Top N per category automatically advance when window closes |
|
||||||
|
| `admin_selection` | Admin manually selects who advances (with AI/score guidance) |
|
||||||
|
| `ai_recommended` | AI proposes list, admin must approve/modify |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Special Awards Integration (Jury 2 Only)
|
||||||
|
|
||||||
|
During the Jury 2 evaluation round, special awards can run alongside:
|
||||||
|
|
||||||
|
### 7.1 How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
Round 5: "Jury 2 — Finalist Selection"
|
||||||
|
├── Main evaluation (all semi-finalists scored by Jury 2)
|
||||||
|
└── Special Awards (run in parallel):
|
||||||
|
├── "Innovation Award" — STAY_IN_MAIN mode
|
||||||
|
│ Projects remain in main eval, flagged as eligible
|
||||||
|
│ Award jury (subset of Jury 2 or separate) votes
|
||||||
|
└── "Impact Award" — SEPARATE_POOL mode
|
||||||
|
AI filters eligible projects into award pool
|
||||||
|
Dedicated jury evaluates and votes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 SpecialAward.evaluationRoundId
|
||||||
|
|
||||||
|
Each award links to the evaluation round it runs alongside:
|
||||||
|
```
|
||||||
|
SpecialAward {
|
||||||
|
evaluationRoundId: "round-jury-2" // Runs during Jury 2
|
||||||
|
eligibilityMode: STAY_IN_MAIN
|
||||||
|
juryGroupId: "jury-group-innovation" // Can be same or different jury
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Award Evaluation Flow
|
||||||
|
|
||||||
|
1. Before Jury 2 window opens: Admin runs award eligibility (AI or manual)
|
||||||
|
2. During Jury 2 window: Award jury members see their award assignments alongside regular evaluations
|
||||||
|
3. Award jury submits award votes (PICK_WINNER, RANKED, or SCORED)
|
||||||
|
4. After Jury 2 closes: Award results finalized alongside main results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Differences Between Jury 1 and Jury 2
|
||||||
|
|
||||||
|
| Aspect | Jury 1 (Round 3) | Jury 2 (Round 5) |
|
||||||
|
|--------|-------------------|-------------------|
|
||||||
|
| Input projects | All eligible (post-filtering) | Semi-finalists only |
|
||||||
|
| Visible docs | Window 1 only | Window 1 + Window 2 |
|
||||||
|
| Output | Semi-finalists | Finalists |
|
||||||
|
| Project.status update | → SEMIFINALIST | → FINALIST |
|
||||||
|
| Special awards | No | Yes (alongside) |
|
||||||
|
| Jury group | Jury 1 | Jury 2 (different members, possible overlap) |
|
||||||
|
| Typical project count | 50-100+ | 10-20 |
|
||||||
|
| Required reviews | 3 (more projects, less depth) | 3-5 (fewer projects, more depth) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. API Changes
|
||||||
|
|
||||||
|
### Preserved Procedures (renamed stageId → roundId)
|
||||||
|
|
||||||
|
| Procedure | Change |
|
||||||
|
|-----------|--------|
|
||||||
|
| `evaluation.get` | roundId via assignment |
|
||||||
|
| `evaluation.start` | No change |
|
||||||
|
| `evaluation.autosave` | No change |
|
||||||
|
| `evaluation.submit` | Window check uses round.windowCloseAt + grace periods |
|
||||||
|
| `evaluation.declareCOI` | No change |
|
||||||
|
| `evaluation.getCOIStatus` | No change |
|
||||||
|
| `evaluation.getProjectStats` | No change |
|
||||||
|
| `evaluation.listByRound` | Renamed from listByStage |
|
||||||
|
| `evaluation.generateSummary` | roundId instead of stageId |
|
||||||
|
| `evaluation.generateBulkSummaries` | roundId instead of stageId |
|
||||||
|
|
||||||
|
### New Procedures
|
||||||
|
|
||||||
|
| Procedure | Purpose |
|
||||||
|
|-----------|---------|
|
||||||
|
| `assignment.previewWithJuryGroup` | Preview assignments filtered by jury group with cap/quota logic |
|
||||||
|
| `assignment.getJuryGroupStats` | Per-member stats: load, category distribution, cap utilization |
|
||||||
|
| `evaluation.getResultsOverview` | Rankings, scores, consensus, AI recommendations per category |
|
||||||
|
| `evaluation.confirmAdvancement` | Admin confirms which projects advance |
|
||||||
|
| `evaluation.getAdvancementPreview` | Preview advancement impact before confirming |
|
||||||
|
|
||||||
|
### Modified Procedures
|
||||||
|
|
||||||
|
| Procedure | Modification |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `assignment.getSuggestions` | Now filters by JuryGroup, applies hard/soft caps, category quotas |
|
||||||
|
| `assignment.create` | Now sets `juryGroupId` on Assignment |
|
||||||
|
| `assignment.bulkCreate` | Now validates against jury group caps |
|
||||||
|
| `file.listByProjectForRound` | Uses RoundSubmissionVisibility to filter docs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Service Layer Changes
|
||||||
|
|
||||||
|
### `stage-assignment.ts` → `round-assignment.ts`
|
||||||
|
|
||||||
|
Key changes to `previewStageAssignment` → `previewRoundAssignment`:
|
||||||
|
|
||||||
|
1. **Load jury pool from JuryGroup** instead of all JURY_MEMBER users:
|
||||||
|
```typescript
|
||||||
|
const juryGroup = await prisma.juryGroup.findUnique({
|
||||||
|
where: { id: round.juryGroupId },
|
||||||
|
include: { members: { include: { user: true } } }
|
||||||
|
})
|
||||||
|
const jurors = juryGroup.members.map(m => ({
|
||||||
|
...m.user,
|
||||||
|
effectiveLimits: getEffectiveLimits(m, juryGroup),
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Replace simple max check** with cap mode logic (hard/soft/none)
|
||||||
|
3. **Add category quota tracking** per juror
|
||||||
|
4. **Add ratio preference scoring** in candidate ranking
|
||||||
|
5. **Report overflow** — projects that couldn't be assigned because all jurors hit caps
|
||||||
|
|
||||||
|
### `stage-engine.ts` → `round-engine.ts`
|
||||||
|
|
||||||
|
Simplified:
|
||||||
|
- Remove trackId from all transitions
|
||||||
|
- `executeTransition` now takes `fromRoundId` + `toRoundId` (or auto-advance to next sortOrder)
|
||||||
|
- `validateTransition` simplified — no StageTransition lookup, just checks next round exists and is active
|
||||||
|
- Guard evaluation simplified — AdvancementRule.configJson replaces arbitrary guardJson
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Edge Cases
|
||||||
|
|
||||||
|
### More projects than jurors can handle
|
||||||
|
- Algorithm assigns up to hard/soft cap for all jurors
|
||||||
|
- Remaining projects flagged as "unassigned" in admin dashboard
|
||||||
|
- Admin must: add jurors, increase caps, or manually assign
|
||||||
|
|
||||||
|
### Juror doesn't complete by deadline
|
||||||
|
- Dashboard shows overdue assignments prominently
|
||||||
|
- Admin can: extend via GracePeriod, reassign to another juror, or mark as incomplete
|
||||||
|
|
||||||
|
### Tie in scores at cutoff
|
||||||
|
- Depending on `tieBreaker` config:
|
||||||
|
- `admin_decides`: Admin manually picks from tied projects
|
||||||
|
- `highest_individual`: Project with highest single-evaluator score wins
|
||||||
|
- `revote`: Tied projects sent back for quick re-evaluation
|
||||||
|
|
||||||
|
### Category imbalance
|
||||||
|
- If one category has far more projects, quotas ensure jurors still get a mix
|
||||||
|
- If quotas can't be satisfied (not enough of one category), system relaxes quota for that category
|
||||||
|
|
||||||
|
### Juror in multiple jury groups
|
||||||
|
- Juror Alice is in Jury 1 and Jury 2
|
||||||
|
- Her assignments for each round are independent
|
||||||
|
- Her caps are per-jury-group (20 for Jury 1, 15 for Jury 2)
|
||||||
|
- No cross-round cap — each round manages its own workload
|
||||||
2053
docs/claude-architecture-redesign/07-round-submission.md
Normal file
2053
docs/claude-architecture-redesign/07-round-submission.md
Normal file
File diff suppressed because it is too large
Load Diff
499
docs/claude-architecture-redesign/08-round-mentoring.md
Normal file
499
docs/claude-architecture-redesign/08-round-mentoring.md
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
# Round: Mentoring (Finalist Collaboration Layer)
|
||||||
|
|
||||||
|
## 1. Purpose & Position in Flow
|
||||||
|
|
||||||
|
The MENTORING round is **not a judging stage** — it is a collaboration layer that activates between Jury 2 finalist selection and the Live Finals. It provides finalist teams who requested mentoring with a private workspace to refine their submissions with guidance from an assigned mentor.
|
||||||
|
|
||||||
|
| Aspect | Detail |
|
||||||
|
|--------|--------|
|
||||||
|
| Position | Round 6 (after Jury 2, before Live Finals) |
|
||||||
|
| Participants | Finalist teams + assigned mentors |
|
||||||
|
| Duration | Configurable (typically 2-4 weeks) |
|
||||||
|
| Output | Better-prepared finalist submissions; some mentoring files promoted to official submissions |
|
||||||
|
|
||||||
|
### Who Gets Mentoring
|
||||||
|
|
||||||
|
- Only projects that have `Project.wantsMentorship = true` AND have advanced to finalist status (ProjectRoundState PASSED in the Jury 2 round)
|
||||||
|
- Admin can override: assign mentoring to projects that didn't request it, or skip projects that did
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Data Model
|
||||||
|
|
||||||
|
### Round Record
|
||||||
|
|
||||||
|
```
|
||||||
|
Round {
|
||||||
|
id: "round-mentoring"
|
||||||
|
competitionId: "comp-2026"
|
||||||
|
name: "Finalist Mentoring"
|
||||||
|
roundType: MENTORING
|
||||||
|
status: ROUND_DRAFT → ROUND_ACTIVE → ROUND_CLOSED
|
||||||
|
sortOrder: 5
|
||||||
|
windowOpenAt: "2026-06-01" // Mentoring period start
|
||||||
|
windowCloseAt: "2026-06-30" // Mentoring period end
|
||||||
|
juryGroupId: null // No jury for mentoring
|
||||||
|
submissionWindowId: null // Mentoring doesn't collect formal submissions
|
||||||
|
configJson: { ...MentoringConfig }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MentoringConfig
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type MentoringConfig = {
|
||||||
|
// Who gets mentoring
|
||||||
|
eligibility: "all_advancing" | "requested_only"
|
||||||
|
// all_advancing: Every finalist gets a mentor
|
||||||
|
// requested_only: Only projects with wantsMentorship=true
|
||||||
|
|
||||||
|
// Workspace features
|
||||||
|
chatEnabled: boolean // Bidirectional messaging (default: true)
|
||||||
|
fileUploadEnabled: boolean // Mentor + team can upload files (default: true)
|
||||||
|
fileCommentsEnabled: boolean // Threaded comments on files (default: true)
|
||||||
|
filePromotionEnabled: boolean // Promote workspace file to official submission (default: true)
|
||||||
|
|
||||||
|
// Promotion target
|
||||||
|
promotionTargetWindowId: string | null
|
||||||
|
// Which SubmissionWindow promoted files go to
|
||||||
|
// Usually the most recent window (Round 2 docs)
|
||||||
|
// If null, promotion creates files without a window (admin must assign)
|
||||||
|
|
||||||
|
// Auto-assignment
|
||||||
|
autoAssignMentors: boolean // Use AI/algorithm to assign (default: false)
|
||||||
|
maxProjectsPerMentor: number // Mentor workload cap (default: 3)
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
notifyTeamsOnOpen: boolean // Email teams when mentoring opens (default: true)
|
||||||
|
notifyMentorsOnAssign: boolean // Email mentors when assigned (default: true)
|
||||||
|
reminderBeforeClose: number[] // Days before close to remind (default: [7, 3, 1])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Related Models
|
||||||
|
|
||||||
|
| Model | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `MentorAssignment` | Links mentor to project (existing, enhanced) |
|
||||||
|
| `MentorMessage` | Chat messages between mentor and team (existing) |
|
||||||
|
| `MentorNote` | Mentor's private notes (existing) |
|
||||||
|
| `MentorFile` | **NEW** — Files uploaded in workspace |
|
||||||
|
| `MentorFileComment` | **NEW** — Threaded comments on files |
|
||||||
|
| `ProjectFile` | Target for file promotion |
|
||||||
|
| `SubmissionFileRequirement` | Requirement slot that promoted file fills |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Mentor Assignment
|
||||||
|
|
||||||
|
### 3.1 Assignment Methods
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `MANUAL` | Admin picks mentor for each project |
|
||||||
|
| `AI_SUGGESTED` | AI recommends matches, admin approves |
|
||||||
|
| `AI_AUTO` | AI auto-assigns, admin can override |
|
||||||
|
| `ALGORITHM` | Round-robin or expertise-matching algorithm |
|
||||||
|
|
||||||
|
### 3.2 Assignment Criteria
|
||||||
|
|
||||||
|
The existing `mentor-matching.ts` service evaluates:
|
||||||
|
- **Expertise overlap** — mentor's tags vs project's tags/category
|
||||||
|
- **Country/region diversity** — avoid same-country bias
|
||||||
|
- **Workload balance** — distribute evenly across mentors
|
||||||
|
- **Language** — match if language preferences exist
|
||||||
|
|
||||||
|
### 3.3 Assignment Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. MENTORING round opens (status → ROUND_ACTIVE)
|
||||||
|
2. System identifies eligible projects:
|
||||||
|
- All finalists (if eligibility = "all_advancing")
|
||||||
|
- Only finalists with wantsMentorship (if "requested_only")
|
||||||
|
3. For each eligible project without a mentor:
|
||||||
|
a. If autoAssignMentors: Run AI/algorithm assignment
|
||||||
|
b. Else: Flag as "needs mentor" in admin dashboard
|
||||||
|
4. Admin reviews assignments, can:
|
||||||
|
- Accept suggestions
|
||||||
|
- Reassign mentors
|
||||||
|
- Skip projects (no mentoring needed)
|
||||||
|
5. Assigned mentors receive email notification
|
||||||
|
6. Workspace becomes active for mentor+team
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Workspace Activation
|
||||||
|
|
||||||
|
When a mentor is assigned and the MENTORING round is ROUND_ACTIVE:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// MentorAssignment is updated:
|
||||||
|
{
|
||||||
|
workspaceEnabled: true,
|
||||||
|
workspaceOpenAt: round.windowOpenAt,
|
||||||
|
workspaceCloseAt: round.windowCloseAt,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The workspace is accessible from:
|
||||||
|
- **Mentor dashboard** → "My Projects" → select project → Workspace tab
|
||||||
|
- **Applicant dashboard** → "Mentor" section → Workspace tab
|
||||||
|
- **Admin** → can view any workspace at any time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Workspace Features
|
||||||
|
|
||||||
|
### 4.1 Messaging (Chat)
|
||||||
|
|
||||||
|
Bidirectional chat between mentor and team members:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────┐
|
||||||
|
│ Mentor Workspace — OceanClean AI │
|
||||||
|
│ ──────────────────────────────────────────── │
|
||||||
|
│ [💬 Chat] [📁 Files] [📋 Milestones] │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────┐ │
|
||||||
|
│ │ Dr. Martin (Mentor) Apr 5, 10:30│ │
|
||||||
|
│ │ Welcome! I've reviewed your business │ │
|
||||||
|
│ │ plan. Let's work on the financial │ │
|
||||||
|
│ │ projections section. │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Sarah (Team Lead) Apr 5, 14:15│ │
|
||||||
|
│ │ Thank you! We've uploaded a revised │ │
|
||||||
|
│ │ version. See the Files tab. │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Dr. Martin (Mentor) Apr 6, 09:00│ │
|
||||||
|
│ │ Great improvement! I've left comments │ │
|
||||||
|
│ │ on the file. One more round should do. │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Type a message... ] [Send] │
|
||||||
|
└────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- Uses existing `MentorMessage` model
|
||||||
|
- Messages auto-marked as read when the chat is viewed
|
||||||
|
- Real-time updates via polling (every 10s) or WebSocket if available
|
||||||
|
- Both mentor and any team member can send messages
|
||||||
|
|
||||||
|
### 4.2 File Upload & Comments
|
||||||
|
|
||||||
|
The core new feature: a private file space with threaded discussion.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────┐
|
||||||
|
│ [💬 Chat] [📁 Files] [📋 Milestones] │
|
||||||
|
│ │
|
||||||
|
│ ┌── Workspace Files ───────────────────────┐ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📄 Business Plan v2.pdf │ │
|
||||||
|
│ │ Uploaded by Sarah (Team) · Apr 5 │ │
|
||||||
|
│ │ 💬 3 comments │ │
|
||||||
|
│ │ [Download] [Comment] [Promote →] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📄 Financial Model.xlsx │ │
|
||||||
|
│ │ Uploaded by Dr. Martin (Mentor) · Apr 6│ │
|
||||||
|
│ │ 💬 1 comment │ │
|
||||||
|
│ │ [Download] [Comment] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ 📄 Pitch Deck Draft.pptx │ │
|
||||||
|
│ │ Uploaded by Sarah (Team) · Apr 8 │ │
|
||||||
|
│ │ ✅ Promoted → "Presentation" slot │ │
|
||||||
|
│ │ [Download] [View Comments] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [📤 Upload File] │
|
||||||
|
└────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**File Upload Flow:**
|
||||||
|
1. User (mentor or team member) clicks "Upload File"
|
||||||
|
2. Client calls `mentor.getWorkspaceUploadUrl(mentorAssignmentId, fileName, mimeType)`
|
||||||
|
3. Server generates MinIO pre-signed PUT URL
|
||||||
|
4. Client uploads directly to MinIO
|
||||||
|
5. Client calls `mentor.saveWorkspaceFile(mentorAssignmentId, fileName, mimeType, size, bucket, objectKey, description)`
|
||||||
|
6. Server creates `MentorFile` record
|
||||||
|
|
||||||
|
**File Comments:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌── Comments on: Business Plan v2.pdf ──────────┐
|
||||||
|
│ │
|
||||||
|
│ Dr. Martin (Mentor) · Apr 5, 16:00 │
|
||||||
|
│ Section 3.2 needs stronger market analysis. │
|
||||||
|
│ Consider adding competitor comparisons. │
|
||||||
|
│ └─ Sarah (Team) · Apr 5, 18:30 │
|
||||||
|
│ Good point — we'll add a competitive │
|
||||||
|
│ landscape section. See updated version. │
|
||||||
|
│ │
|
||||||
|
│ Dr. Martin (Mentor) · Apr 6, 10:00 │
|
||||||
|
│ Revenue projections look much better now. │
|
||||||
|
│ Ready for promotion to official submission? │
|
||||||
|
│ └─ Sarah (Team) · Apr 6, 11:00 │
|
||||||
|
│ Yes, let's promote it! │
|
||||||
|
│ │
|
||||||
|
│ [Add comment... ] [Post] │
|
||||||
|
└────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- `MentorFileComment` with `parentCommentId` for threading
|
||||||
|
- Both mentor and team members can comment
|
||||||
|
- Admin can view all comments
|
||||||
|
- Comments are timestamped and attributed
|
||||||
|
|
||||||
|
### 4.3 File Promotion to Official Submission
|
||||||
|
|
||||||
|
The key feature: converting a private mentoring file into an official submission document.
|
||||||
|
|
||||||
|
**Promotion Flow:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Team member (or admin) clicks "Promote →" on a workspace file
|
||||||
|
2. Dialog appears:
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ Promote File to Official Submission │
|
||||||
|
│ │
|
||||||
|
│ File: Business Plan v2.pdf │
|
||||||
|
│ │
|
||||||
|
│ Target submission window: │
|
||||||
|
│ [Round 2 Docs ▾] │
|
||||||
|
│ │
|
||||||
|
│ Replaces requirement: │
|
||||||
|
│ [Business Plan ▾] │
|
||||||
|
│ │
|
||||||
|
│ ⚠ This will replace the current │
|
||||||
|
│ "Business Plan" file for this project. │
|
||||||
|
│ │
|
||||||
|
│ [Cancel] [Promote & Replace] │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
|
||||||
|
3. On confirmation:
|
||||||
|
a. System creates a new ProjectFile record:
|
||||||
|
- projectId: project's ID
|
||||||
|
- submissionWindowId: selected window
|
||||||
|
- requirementId: selected requirement slot
|
||||||
|
- fileName, mimeType, size: copied from MentorFile
|
||||||
|
- bucket, objectKey: SAME as MentorFile (no file duplication)
|
||||||
|
- version: incremented from previous file in slot
|
||||||
|
b. Previous file in that slot gets `replacedById` set to new file
|
||||||
|
c. MentorFile updated:
|
||||||
|
- isPromoted: true
|
||||||
|
- promotedToFileId: new ProjectFile ID
|
||||||
|
- promotedAt: now
|
||||||
|
- promotedByUserId: actor ID
|
||||||
|
d. Audit log entry created:
|
||||||
|
- action: "MENTOR_FILE_PROMOTED"
|
||||||
|
- details: { mentorFileId, projectFileId, submissionWindowId, requirementId, replacedFileId }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Rules:**
|
||||||
|
- Only files in **active** mentoring workspaces can be promoted
|
||||||
|
- Promotion **replaces** the existing file for that requirement slot (per user's decision)
|
||||||
|
- The MinIO object is **not duplicated** — both MentorFile and ProjectFile point to the same objectKey
|
||||||
|
- Once promoted, the MentorFile shows a "Promoted" badge and the promote button is disabled
|
||||||
|
- Admin can un-promote (revert) if needed, which deletes the ProjectFile and resets MentorFile flags
|
||||||
|
- Promotion is audited with full provenance chain
|
||||||
|
|
||||||
|
**Who Can Promote:**
|
||||||
|
- Team lead (Project.submittedByUserId or TeamMember.role = LEAD)
|
||||||
|
- Admin (always)
|
||||||
|
- Mentor (only if `MentoringConfig.mentorCanPromote` is true — default false for safety)
|
||||||
|
|
||||||
|
### 4.4 Privacy Model
|
||||||
|
|
||||||
|
```
|
||||||
|
Visibility Matrix:
|
||||||
|
┌──────────────────┬────────┬──────────┬───────┬──────┐
|
||||||
|
│ Content │ Mentor │ Team │ Admin │ Jury │
|
||||||
|
├──────────────────┼────────┼──────────┼───────┼──────┤
|
||||||
|
│ Chat messages │ ✅ │ ✅ │ ✅ │ ❌ │
|
||||||
|
│ Workspace files │ ✅ │ ✅ │ ✅ │ ❌ │
|
||||||
|
│ File comments │ ✅ │ ✅ │ ✅ │ ❌ │
|
||||||
|
│ Mentor notes │ ✅ │ ❌ │ ✅* │ ❌ │
|
||||||
|
│ Promoted files │ ✅ │ ✅ │ ✅ │ ✅** │
|
||||||
|
└──────────────────┴────────┴──────────┴───────┴──────┘
|
||||||
|
|
||||||
|
* Only if MentorNote.isVisibleToAdmin = true
|
||||||
|
** Promoted files become official submissions visible to jury
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mentor Dashboard
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ Mentor Dashboard │
|
||||||
|
│ ─────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ Mentoring Period: June 1 – June 30 │
|
||||||
|
│ ⏱ 18 days remaining │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
|
||||||
|
│ │ 3 │ │ 12 │ │ 5 │ │
|
||||||
|
│ │ Teams │ │ Messages│ │ Files │ │
|
||||||
|
│ └─────────┘ └─────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ My Assigned Teams │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ OceanClean AI (Startup) │ │
|
||||||
|
│ │ 💬 2 unread messages · 📁 3 files · Last: Apr 6 │ │
|
||||||
|
│ │ [Open Workspace] │ │
|
||||||
|
│ ├────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Blue Carbon Hub (Concept) │ │
|
||||||
|
│ │ 💬 0 unread · 📁 1 file · Last: Apr 4 │ │
|
||||||
|
│ │ [Open Workspace] │ │
|
||||||
|
│ ├────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ SeaWatch Monitor (Startup) │ │
|
||||||
|
│ │ ⚠ No activity yet │ │
|
||||||
|
│ │ [Open Workspace] │ │
|
||||||
|
│ └────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Milestones │
|
||||||
|
│ ┌────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ☑ Initial review (3/3 teams) │ │
|
||||||
|
│ │ ☐ Business plan feedback (1/3 teams) │ │
|
||||||
|
│ │ ☐ Pitch deck review (0/3 teams) │ │
|
||||||
|
│ └────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Applicant Experience
|
||||||
|
|
||||||
|
On the applicant dashboard, a "Mentoring" section appears when mentoring is active:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────┐
|
||||||
|
│ Your Mentor: Dr. Martin Duval │
|
||||||
|
│ Expertise: Marine Biology, Sustainability │
|
||||||
|
│ │
|
||||||
|
│ Mentoring Period: June 1 – June 30 │
|
||||||
|
│ ⏱ 18 days remaining │
|
||||||
|
│ │
|
||||||
|
│ [💬 Messages (2 unread)] │
|
||||||
|
│ [📁 Workspace Files (3)] │
|
||||||
|
│ [📋 Milestones] │
|
||||||
|
└────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Clicking "Workspace Files" opens the same workspace view as the mentor (with appropriate permissions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Admin Experience
|
||||||
|
|
||||||
|
Admin can:
|
||||||
|
- **Assign/reassign mentors** via bulk or individual assignment
|
||||||
|
- **View any workspace** (read-only or with full edit access)
|
||||||
|
- **Promote files** on behalf of teams
|
||||||
|
- **Track activity** — dashboard showing mentor engagement:
|
||||||
|
- Messages sent per mentor
|
||||||
|
- Files uploaded
|
||||||
|
- Milestones completed
|
||||||
|
- Last activity timestamp
|
||||||
|
- **Extend/close mentoring window** per team or globally
|
||||||
|
- **Export workspace data** for audit purposes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. API — New and Modified Procedures
|
||||||
|
|
||||||
|
### New Procedures (mentor-workspace router)
|
||||||
|
|
||||||
|
| Procedure | Auth | Purpose |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `mentorWorkspace.getUploadUrl` | Mentor or Team | Get MinIO pre-signed URL for workspace upload |
|
||||||
|
| `mentorWorkspace.saveFile` | Mentor or Team | Create MentorFile record after upload |
|
||||||
|
| `mentorWorkspace.listFiles` | Mentor, Team, Admin | List workspace files with comment counts |
|
||||||
|
| `mentorWorkspace.deleteFile` | Uploader or Admin | Delete workspace file |
|
||||||
|
| `mentorWorkspace.getFileDownloadUrl` | Mentor, Team, Admin | Get MinIO pre-signed URL for download |
|
||||||
|
| `mentorWorkspace.addComment` | Mentor, Team, Admin | Add comment to file (with optional parentCommentId) |
|
||||||
|
| `mentorWorkspace.listComments` | Mentor, Team, Admin | Get threaded comments for a file |
|
||||||
|
| `mentorWorkspace.deleteComment` | Author or Admin | Delete a comment |
|
||||||
|
| `mentorWorkspace.promoteFile` | Team Lead or Admin | Promote workspace file to official submission |
|
||||||
|
| `mentorWorkspace.unpromoteFile` | Admin only | Revert a promotion |
|
||||||
|
| `mentorWorkspace.getWorkspaceStatus` | Any participant | Get workspace summary (file count, message count, etc.) |
|
||||||
|
|
||||||
|
### Modified Existing Procedures
|
||||||
|
|
||||||
|
| Procedure | Change |
|
||||||
|
|-----------|--------|
|
||||||
|
| `mentor.getMyProjects` | Include workspace status (file count, unread messages) |
|
||||||
|
| `mentor.getProjectDetail` | Include MentorFile[] with comment counts |
|
||||||
|
| `applicant.getMyDashboard` | Include mentor workspace summary if mentoring active |
|
||||||
|
| `file.listByProjectForRound` | Promoted files visible to jury (via ProjectFile record) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Service: `mentor-workspace.ts`
|
||||||
|
|
||||||
|
### Key Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Upload handling
|
||||||
|
async function getWorkspaceUploadUrl(
|
||||||
|
mentorAssignmentId: string,
|
||||||
|
fileName: string,
|
||||||
|
mimeType: string,
|
||||||
|
actorId: string,
|
||||||
|
prisma: PrismaClient
|
||||||
|
): Promise<{ uploadUrl: string; objectKey: string }>
|
||||||
|
|
||||||
|
// Save file metadata after upload
|
||||||
|
async function saveWorkspaceFile(
|
||||||
|
mentorAssignmentId: string,
|
||||||
|
uploadedByUserId: string,
|
||||||
|
file: { fileName, mimeType, size, bucket, objectKey },
|
||||||
|
description: string | null,
|
||||||
|
prisma: PrismaClient
|
||||||
|
): Promise<MentorFile>
|
||||||
|
|
||||||
|
// Promote file to official submission
|
||||||
|
async function promoteFileToSubmission(
|
||||||
|
mentorFileId: string,
|
||||||
|
submissionWindowId: string,
|
||||||
|
requirementId: string | null,
|
||||||
|
actorId: string,
|
||||||
|
prisma: PrismaClient
|
||||||
|
): Promise<{ mentorFile: MentorFile; projectFile: ProjectFile }>
|
||||||
|
// Steps:
|
||||||
|
// 1. Validate mentorFile exists, is not already promoted, workspace is active
|
||||||
|
// 2. If requirementId: find existing ProjectFile for that requirement, set replacedById
|
||||||
|
// 3. Create new ProjectFile (reusing same bucket/objectKey — no MinIO duplication)
|
||||||
|
// 4. Update MentorFile: isPromoted=true, promotedToFileId, promotedAt, promotedByUserId
|
||||||
|
// 5. Audit log with full provenance
|
||||||
|
|
||||||
|
// Revert promotion
|
||||||
|
async function unpromoteFile(
|
||||||
|
mentorFileId: string,
|
||||||
|
actorId: string,
|
||||||
|
prisma: PrismaClient
|
||||||
|
): Promise<void>
|
||||||
|
// Steps:
|
||||||
|
// 1. Find the ProjectFile created by promotion
|
||||||
|
// 2. If it replaced a previous file, restore that file's replacedById=null
|
||||||
|
// 3. Delete the promoted ProjectFile
|
||||||
|
// 4. Reset MentorFile flags
|
||||||
|
// 5. Audit log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Edge Cases
|
||||||
|
|
||||||
|
| Scenario | Handling |
|
||||||
|
|----------|----------|
|
||||||
|
| Team doesn't want mentoring but admin assigns anyway | Assignment created; team sees mentor in dashboard |
|
||||||
|
| Mentor goes inactive during period | Admin can reassign; previous workspace preserved |
|
||||||
|
| File promoted then mentor period closes | Promoted file remains as official submission |
|
||||||
|
| Team tries to promote file for a requirement that doesn't exist | Error — must select valid requirement or leave requirementId null |
|
||||||
|
| Two files promoted to the same requirement slot | Second promotion replaces first (versioning) |
|
||||||
|
| Mentoring file is larger than requirement maxSizeMB | Warning shown but promotion allowed (admin override implicit) |
|
||||||
|
| Workspace closed but team needs one more upload | Admin can extend via round window or grant grace |
|
||||||
|
| Promoted file deleted from workspace | ProjectFile remains (separate record); audit shows provenance |
|
||||||
660
docs/claude-architecture-redesign/09-round-live-finals.md
Normal file
660
docs/claude-architecture-redesign/09-round-live-finals.md
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
# Round Type: LIVE_FINAL — Live Finals Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The **LIVE_FINAL** round type orchestrates the live ceremony where Jury 3 evaluates finalist presentations in real-time. This is Round 7 in the redesigned 8-step competition flow. It combines jury scoring, optional audience participation, deliberation periods, and live results display into a single managed event.
|
||||||
|
|
||||||
|
**Core capabilities:**
|
||||||
|
- Real-time stage manager controls (presentation cursor, timing, pause/resume)
|
||||||
|
- Jury voting with multiple modes (numeric, ranking, binary)
|
||||||
|
- Optional audience voting with weighted scores
|
||||||
|
- Per-category presentation windows (STARTUP window, then CONCEPT window)
|
||||||
|
- Deliberation period for jury discussion
|
||||||
|
- Live results display or ceremony reveal
|
||||||
|
- Anti-fraud measures for audience participation
|
||||||
|
|
||||||
|
**Round 7 position in the flow:**
|
||||||
|
```
|
||||||
|
Round 1: Application Window (INTAKE)
|
||||||
|
Round 2: AI Screening (FILTERING)
|
||||||
|
Round 3: Jury 1 - Semi-finalist Selection (EVALUATION)
|
||||||
|
Round 4: Semi-finalist Submission (SUBMISSION)
|
||||||
|
Round 5: Jury 2 - Finalist Selection (EVALUATION)
|
||||||
|
Round 6: Finalist Mentoring (MENTORING)
|
||||||
|
Round 7: Live Finals (LIVE_FINAL) ← THIS DOCUMENT
|
||||||
|
Round 8: Confirm Winners (CONFIRMATION)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current System (Pipeline → Track → Stage)
|
||||||
|
|
||||||
|
### Existing Models
|
||||||
|
|
||||||
|
**LiveVotingSession** — Per-stage voting session:
|
||||||
|
```prisma
|
||||||
|
model LiveVotingSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
stageId String? @unique
|
||||||
|
status String @default("NOT_STARTED") // NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED
|
||||||
|
currentProjectIndex Int @default(0)
|
||||||
|
currentProjectId String?
|
||||||
|
votingStartedAt DateTime?
|
||||||
|
votingEndsAt DateTime?
|
||||||
|
projectOrderJson Json? @db.JsonB // Array of project IDs in presentation order
|
||||||
|
|
||||||
|
// Voting configuration
|
||||||
|
votingMode String @default("simple") // "simple" (1-10) | "criteria" (per-criterion scores)
|
||||||
|
criteriaJson Json? @db.JsonB // Array of { id, label, description, scale, weight }
|
||||||
|
|
||||||
|
// Audience settings
|
||||||
|
allowAudienceVotes Boolean @default(false)
|
||||||
|
audienceVoteWeight Float @default(0) // 0.0 to 1.0
|
||||||
|
audienceVotingMode String @default("disabled") // "disabled" | "per_project" | "per_category" | "favorites"
|
||||||
|
audienceMaxFavorites Int @default(3)
|
||||||
|
audienceRequireId Boolean @default(false)
|
||||||
|
audienceVotingDuration Int? // Minutes (null = same as jury)
|
||||||
|
|
||||||
|
tieBreakerMethod String @default("admin_decides") // 'admin_decides' | 'highest_individual' | 'revote'
|
||||||
|
presentationSettingsJson Json? @db.JsonB
|
||||||
|
|
||||||
|
stage Stage? @relation(...)
|
||||||
|
votes LiveVote[]
|
||||||
|
audienceVoters AudienceVoter[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**LiveVote** — Individual jury or audience vote:
|
||||||
|
```prisma
|
||||||
|
model LiveVote {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionId String
|
||||||
|
projectId String
|
||||||
|
userId String? // Nullable for audience voters without accounts
|
||||||
|
score Int // 1-10 (or weighted score for criteria mode)
|
||||||
|
isAudienceVote Boolean @default(false)
|
||||||
|
votedAt DateTime @default(now())
|
||||||
|
|
||||||
|
// Criteria scores (used when votingMode="criteria")
|
||||||
|
criterionScoresJson Json? @db.JsonB // { [criterionId]: score }
|
||||||
|
|
||||||
|
// Audience voter link
|
||||||
|
audienceVoterId String?
|
||||||
|
|
||||||
|
session LiveVotingSession @relation(...)
|
||||||
|
user User? @relation(...)
|
||||||
|
audienceVoter AudienceVoter? @relation(...)
|
||||||
|
|
||||||
|
@@unique([sessionId, projectId, userId])
|
||||||
|
@@unique([sessionId, projectId, audienceVoterId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**AudienceVoter** — Registered audience participant:
|
||||||
|
```prisma
|
||||||
|
model AudienceVoter {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionId String
|
||||||
|
token String @unique // Unique voting token (UUID)
|
||||||
|
identifier String? // Optional: email, phone, or name
|
||||||
|
identifierType String? // "email" | "phone" | "name" | "anonymous"
|
||||||
|
ipAddress String?
|
||||||
|
userAgent String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
session LiveVotingSession @relation(...)
|
||||||
|
votes LiveVote[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**LiveProgressCursor** — Stage manager cursor:
|
||||||
|
```prisma
|
||||||
|
model LiveProgressCursor {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
stageId String @unique
|
||||||
|
sessionId String @unique @default(cuid())
|
||||||
|
activeProjectId String?
|
||||||
|
activeOrderIndex Int @default(0)
|
||||||
|
isPaused Boolean @default(false)
|
||||||
|
|
||||||
|
stage Stage @relation(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cohort** — Presentation groups:
|
||||||
|
```prisma
|
||||||
|
model Cohort {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
stageId String
|
||||||
|
name String
|
||||||
|
votingMode String @default("simple") // simple, criteria, ranked
|
||||||
|
isOpen Boolean @default(false)
|
||||||
|
windowOpenAt DateTime?
|
||||||
|
windowCloseAt DateTime?
|
||||||
|
|
||||||
|
stage Stage @relation(...)
|
||||||
|
projects CohortProject[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Service Functions
|
||||||
|
|
||||||
|
`src/server/services/live-control.ts`:
|
||||||
|
- `startSession(stageId, actorId)` — Initialize/reset cursor
|
||||||
|
- `setActiveProject(stageId, projectId, actorId)` — Set currently presenting project
|
||||||
|
- `jumpToProject(stageId, orderIndex, actorId)` — Jump to specific project in queue
|
||||||
|
- `reorderQueue(stageId, newOrder, actorId)` — Reorder presentation sequence
|
||||||
|
- `pauseResume(stageId, isPaused, actorId)` — Toggle pause state
|
||||||
|
- `openCohortWindow(cohortId, actorId)` — Open voting window for a cohort
|
||||||
|
- `closeCohortWindow(cohortId, actorId)` — Close cohort window
|
||||||
|
|
||||||
|
### Current tRPC Procedures
|
||||||
|
|
||||||
|
`src/server/routers/live-voting.ts`:
|
||||||
|
```typescript
|
||||||
|
liveVoting.getSession({ stageId })
|
||||||
|
liveVoting.getSessionForVoting({ sessionId }) // Jury view
|
||||||
|
liveVoting.getPublicSession({ sessionId }) // Display view
|
||||||
|
liveVoting.setProjectOrder({ sessionId, projectIds })
|
||||||
|
liveVoting.setVotingMode({ sessionId, votingMode: 'simple' | 'criteria' })
|
||||||
|
liveVoting.setCriteria({ sessionId, criteria })
|
||||||
|
liveVoting.importCriteriaFromForm({ sessionId, formId })
|
||||||
|
liveVoting.startVoting({ sessionId, projectId, durationSeconds })
|
||||||
|
liveVoting.stopVoting({ sessionId })
|
||||||
|
liveVoting.endSession({ sessionId })
|
||||||
|
liveVoting.vote({ sessionId, projectId, score, criterionScores })
|
||||||
|
liveVoting.getResults({ sessionId, juryWeight?, audienceWeight? })
|
||||||
|
liveVoting.updatePresentationSettings({ sessionId, presentationSettingsJson })
|
||||||
|
liveVoting.updateSessionConfig({ sessionId, allowAudienceVotes, audienceVoteWeight, ... })
|
||||||
|
liveVoting.registerAudienceVoter({ sessionId, identifier?, identifierType? }) // Public
|
||||||
|
liveVoting.castAudienceVote({ sessionId, projectId, score, token }) // Public
|
||||||
|
liveVoting.getAudienceVoterStats({ sessionId })
|
||||||
|
liveVoting.getAudienceSession({ sessionId }) // Public
|
||||||
|
liveVoting.getPublicResults({ sessionId }) // Public
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current LiveFinalConfig Type
|
||||||
|
|
||||||
|
From `src/types/pipeline-wizard.ts`:
|
||||||
|
```typescript
|
||||||
|
type LiveFinalConfig = {
|
||||||
|
juryVotingEnabled: boolean
|
||||||
|
audienceVotingEnabled: boolean
|
||||||
|
audienceVoteWeight: number
|
||||||
|
cohortSetupMode: 'auto' | 'manual'
|
||||||
|
revealPolicy: 'immediate' | 'delayed' | 'ceremony'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Admin UI
|
||||||
|
|
||||||
|
`src/components/admin/pipeline/sections/live-finals-section.tsx`:
|
||||||
|
- Jury voting toggle
|
||||||
|
- Audience voting toggle + weight slider (0-100%)
|
||||||
|
- Cohort setup mode selector (auto/manual)
|
||||||
|
- Result reveal policy selector (immediate/delayed/ceremony)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redesigned Live Finals Round
|
||||||
|
|
||||||
|
### Enhanced LiveFinalConfig
|
||||||
|
|
||||||
|
**New comprehensive config:**
|
||||||
|
```typescript
|
||||||
|
type LiveFinalConfig = {
|
||||||
|
// Jury configuration
|
||||||
|
juryGroupId: string // Which jury evaluates (Jury 3)
|
||||||
|
|
||||||
|
// Voting mode
|
||||||
|
votingMode: 'NUMERIC' | 'RANKING' | 'BINARY'
|
||||||
|
|
||||||
|
// Numeric mode settings
|
||||||
|
numericScale?: {
|
||||||
|
min: number // Default: 1
|
||||||
|
max: number // Default: 10
|
||||||
|
allowDecimals: boolean // Default: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criteria-based voting (optional enhancement to NUMERIC)
|
||||||
|
criteriaEnabled?: boolean
|
||||||
|
criteriaJson?: LiveVotingCriterion[] // { id, label, description, scale, weight }
|
||||||
|
importFromEvalForm?: string // EvaluationForm ID to import criteria from
|
||||||
|
|
||||||
|
// Ranking mode settings
|
||||||
|
rankingSettings?: {
|
||||||
|
maxRankedProjects: number // How many projects each juror ranks (e.g., top 3)
|
||||||
|
pointsSystem: 'DESCENDING' | 'BORDA' // 3-2-1 or Borda count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary mode settings (simple yes/no)
|
||||||
|
binaryLabels?: {
|
||||||
|
yes: string // Default: "Finalist"
|
||||||
|
no: string // Default: "Not Selected"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audience voting
|
||||||
|
audienceVotingEnabled: boolean
|
||||||
|
audienceVotingWeight: number // 0-100, percentage weight
|
||||||
|
juryVotingWeight: number // complement of audience weight (calculated)
|
||||||
|
audienceVotingMode: 'PER_PROJECT' | 'FAVORITES' | 'CATEGORY_FAVORITES'
|
||||||
|
audienceMaxFavorites?: number // For FAVORITES mode
|
||||||
|
audienceRequireIdentification: boolean
|
||||||
|
audienceAntiSpamMeasures: {
|
||||||
|
ipRateLimit: boolean // Limit votes per IP
|
||||||
|
deviceFingerprint: boolean // Track device ID
|
||||||
|
emailVerification: boolean // Require verified email
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presentation timing
|
||||||
|
presentationDurationMinutes: number
|
||||||
|
qaDurationMinutes: number
|
||||||
|
|
||||||
|
// Deliberation
|
||||||
|
deliberationEnabled: boolean
|
||||||
|
deliberationDurationMinutes: number
|
||||||
|
deliberationAllowsVoteRevision: boolean // Can jury change votes during deliberation?
|
||||||
|
|
||||||
|
// Category windows
|
||||||
|
categoryWindowsEnabled: boolean // Separate windows per category
|
||||||
|
categoryWindows: CategoryWindow[]
|
||||||
|
|
||||||
|
// Results display
|
||||||
|
showLiveResults: boolean // Real-time leaderboard
|
||||||
|
showLiveScores: boolean // Show actual scores vs just rankings
|
||||||
|
anonymizeJuryVotes: boolean // Hide individual jury votes from audience
|
||||||
|
requireAllJuryVotes: boolean // Voting can't end until all jury members vote
|
||||||
|
|
||||||
|
// Override controls
|
||||||
|
adminCanOverrideVotes: boolean
|
||||||
|
adminCanAdjustWeights: boolean // Mid-ceremony weight adjustment
|
||||||
|
|
||||||
|
// Presentation order
|
||||||
|
presentationOrderMode: 'MANUAL' | 'RANDOM' | 'SCORE_BASED' | 'CATEGORY_SPLIT'
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryWindow = {
|
||||||
|
category: 'STARTUP' | 'BUSINESS_CONCEPT'
|
||||||
|
projectOrder: string[] // Ordered project IDs
|
||||||
|
startTime?: string // Scheduled start (ISO 8601)
|
||||||
|
endTime?: string // Scheduled end
|
||||||
|
deliberationMinutes?: number // Override global deliberation duration
|
||||||
|
}
|
||||||
|
|
||||||
|
type LiveVotingCriterion = {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
scale: number // 1-10, 1-5, etc.
|
||||||
|
weight: number // Sum to 1.0 across all criteria
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Zod Validation Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const CategoryWindowSchema = z.object({
|
||||||
|
category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']),
|
||||||
|
projectOrder: z.array(z.string()),
|
||||||
|
startTime: z.string().datetime().optional(),
|
||||||
|
endTime: z.string().datetime().optional(),
|
||||||
|
deliberationMinutes: z.number().int().min(0).max(120).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const LiveVotingCriterionSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
label: z.string().min(1).max(100),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
scale: z.number().int().min(1).max(100),
|
||||||
|
weight: z.number().min(0).max(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LiveFinalConfigSchema = z.object({
|
||||||
|
// Jury
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
|
||||||
|
// Voting mode
|
||||||
|
votingMode: z.enum(['NUMERIC', 'RANKING', 'BINARY']),
|
||||||
|
|
||||||
|
// Numeric mode settings
|
||||||
|
numericScale: z.object({
|
||||||
|
min: z.number().int().default(1),
|
||||||
|
max: z.number().int().default(10),
|
||||||
|
allowDecimals: z.boolean().default(false),
|
||||||
|
}).optional(),
|
||||||
|
|
||||||
|
// Criteria
|
||||||
|
criteriaEnabled: z.boolean().optional(),
|
||||||
|
criteriaJson: z.array(LiveVotingCriterionSchema).optional(),
|
||||||
|
importFromEvalForm: z.string().optional(),
|
||||||
|
|
||||||
|
// Ranking
|
||||||
|
rankingSettings: z.object({
|
||||||
|
maxRankedProjects: z.number().int().min(1).max(20),
|
||||||
|
pointsSystem: z.enum(['DESCENDING', 'BORDA']),
|
||||||
|
}).optional(),
|
||||||
|
|
||||||
|
// Binary
|
||||||
|
binaryLabels: z.object({
|
||||||
|
yes: z.string().default('Finalist'),
|
||||||
|
no: z.string().default('Not Selected'),
|
||||||
|
}).optional(),
|
||||||
|
|
||||||
|
// Audience
|
||||||
|
audienceVotingEnabled: z.boolean(),
|
||||||
|
audienceVotingWeight: z.number().min(0).max(100),
|
||||||
|
juryVotingWeight: z.number().min(0).max(100),
|
||||||
|
audienceVotingMode: z.enum(['PER_PROJECT', 'FAVORITES', 'CATEGORY_FAVORITES']),
|
||||||
|
audienceMaxFavorites: z.number().int().min(1).max(20).optional(),
|
||||||
|
audienceRequireIdentification: z.boolean(),
|
||||||
|
audienceAntiSpamMeasures: z.object({
|
||||||
|
ipRateLimit: z.boolean(),
|
||||||
|
deviceFingerprint: z.boolean(),
|
||||||
|
emailVerification: z.boolean(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
presentationDurationMinutes: z.number().int().min(1).max(60),
|
||||||
|
qaDurationMinutes: z.number().int().min(0).max(30),
|
||||||
|
|
||||||
|
// Deliberation
|
||||||
|
deliberationEnabled: z.boolean(),
|
||||||
|
deliberationDurationMinutes: z.number().int().min(0).max(120),
|
||||||
|
deliberationAllowsVoteRevision: z.boolean(),
|
||||||
|
|
||||||
|
// Category windows
|
||||||
|
categoryWindowsEnabled: z.boolean(),
|
||||||
|
categoryWindows: z.array(CategoryWindowSchema),
|
||||||
|
|
||||||
|
// Results
|
||||||
|
showLiveResults: z.boolean(),
|
||||||
|
showLiveScores: z.boolean(),
|
||||||
|
anonymizeJuryVotes: z.boolean(),
|
||||||
|
requireAllJuryVotes: z.boolean(),
|
||||||
|
|
||||||
|
// Overrides
|
||||||
|
adminCanOverrideVotes: z.boolean(),
|
||||||
|
adminCanAdjustWeights: z.boolean(),
|
||||||
|
|
||||||
|
// Presentation order
|
||||||
|
presentationOrderMode: z.enum(['MANUAL', 'RANDOM', 'SCORE_BASED', 'CATEGORY_SPLIT']),
|
||||||
|
}).refine(
|
||||||
|
(data) => {
|
||||||
|
// Ensure weights sum to 100
|
||||||
|
return data.audienceVotingWeight + data.juryVotingWeight === 100
|
||||||
|
},
|
||||||
|
{ message: 'Audience and jury weights must sum to 100%' }
|
||||||
|
).refine(
|
||||||
|
(data) => {
|
||||||
|
// If criteria enabled, must have criteria
|
||||||
|
if (data.criteriaEnabled && (!data.criteriaJson || data.criteriaJson.length === 0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
{ message: 'Criteria-based voting requires at least one criterion' }
|
||||||
|
).refine(
|
||||||
|
(data) => {
|
||||||
|
// Criteria weights must sum to 1.0
|
||||||
|
if (data.criteriaJson && data.criteriaJson.length > 0) {
|
||||||
|
const weightSum = data.criteriaJson.reduce((sum, c) => sum + c.weight, 0)
|
||||||
|
return Math.abs(weightSum - 1.0) < 0.01
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
{ message: 'Criteria weights must sum to 1.0' }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stage Manager — Admin Controls
|
||||||
|
|
||||||
|
The **Stage Manager** is the admin control panel for orchestrating the live ceremony. It provides real-time control over presentation flow, voting windows, and emergency interventions.
|
||||||
|
|
||||||
|
### Ceremony State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
Ceremony State Flow:
|
||||||
|
NOT_STARTED → (start session) → IN_PROGRESS → (deliberation starts) → DELIBERATION → (voting ends) → COMPLETED
|
||||||
|
|
||||||
|
NOT_STARTED:
|
||||||
|
- Session created but not started
|
||||||
|
- Projects ordered (manual or automatic)
|
||||||
|
- Jury and audience links generated
|
||||||
|
- Stage manager can preview setup
|
||||||
|
|
||||||
|
IN_PROGRESS:
|
||||||
|
- Presentations ongoing
|
||||||
|
- Per-project state: WAITING → PRESENTING → Q_AND_A → VOTING → VOTED → SCORED
|
||||||
|
- Admin can pause, skip, reorder on the fly
|
||||||
|
|
||||||
|
DELIBERATION:
|
||||||
|
- Timer running for deliberation period
|
||||||
|
- Jury can discuss (optional chat/discussion interface)
|
||||||
|
- Votes may be revised (if deliberationAllowsVoteRevision=true)
|
||||||
|
- Admin can extend deliberation time
|
||||||
|
|
||||||
|
COMPLETED:
|
||||||
|
- All voting finished
|
||||||
|
- Results calculated
|
||||||
|
- Ceremony locked (or unlocked for result reveal)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Project State
|
||||||
|
|
||||||
|
Each project in the live finals progresses through these states:
|
||||||
|
|
||||||
|
```
|
||||||
|
WAITING → Project queued, not yet presenting
|
||||||
|
PRESENTING → Presentation in progress (timer: presentationDurationMinutes)
|
||||||
|
Q_AND_A → Q&A session (timer: qaDurationMinutes)
|
||||||
|
VOTING → Voting window open (jury + audience can vote)
|
||||||
|
VOTED → Voting window closed, awaiting next action
|
||||||
|
SCORED → Scores calculated, moving to next project
|
||||||
|
SKIPPED → Admin skipped this project (emergency override)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stage Manager UI Controls
|
||||||
|
|
||||||
|
**ASCII Mockup:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LIVE FINALS STAGE MANAGER Session: live-abc-123 │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Status: IN_PROGRESS Category: STARTUP Jury: Jury 3 (8/8) │
|
||||||
|
│ │
|
||||||
|
│ [Pause Ceremony] [End Session] [Emergency Stop] │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ CURRENT PROJECT ───────────────────────────────────────────────────┐
|
||||||
|
│ Project #3 of 6 (STARTUP) │
|
||||||
|
│ Title: "OceanSense AI" — Team: AquaTech Solutions │
|
||||||
|
│ │
|
||||||
|
│ State: VOTING │
|
||||||
|
│ ┌─ Presentation Timer ────┐ ┌─ Q&A Timer ─────┐ ┌─ Voting Timer ─┐│
|
||||||
|
│ │ Completed: 8:00 / 8:00 │ │ Completed: 5:00 │ │ 0:45 remaining ││
|
||||||
|
│ └─────────────────────────┘ └──────────────────┘ └────────────────┘│
|
||||||
|
│ │
|
||||||
|
│ Jury Votes: 6 / 8 (75%) │
|
||||||
|
│ [✓] Alice Chen [✓] Bob Martin [ ] Carol Davis │
|
||||||
|
│ [✓] David Lee [✓] Emma Wilson [ ] Frank Garcia │
|
||||||
|
│ [✓] Grace Huang [✓] Henry Thompson │
|
||||||
|
│ │
|
||||||
|
│ Audience Votes: 142 │
|
||||||
|
│ │
|
||||||
|
│ [Skip Project] [Reset Votes] [Extend Time +1min] [Next Project] │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ PROJECT QUEUE ─────────────────────────────────────────────────────┐
|
||||||
|
│ [✓] 1. AquaClean Tech (STARTUP) — Score: 8.2 (Completed) │
|
||||||
|
│ [✓] 2. BlueCarbon Solutions (STARTUP) — Score: 7.8 (Completed) │
|
||||||
|
│ [>] 3. OceanSense AI (STARTUP) — Voting in progress │
|
||||||
|
│ [ ] 4. MarineTech Innovations (STARTUP) — Waiting │
|
||||||
|
│ [ ] 5. CoralGuard (STARTUP) — Waiting │
|
||||||
|
│ [ ] 6. DeepSea Robotics (STARTUP) — Waiting │
|
||||||
|
│ │
|
||||||
|
│ [Reorder Queue] [Jump to Project...] [Add Project] │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ CATEGORY WINDOWS ──────────────────────────────────────────────────┐
|
||||||
|
│ Window 1: STARTUP (6 projects) │
|
||||||
|
│ Status: IN_PROGRESS (Project 3/6) │
|
||||||
|
│ Started: 2026-05-15 18:00:00 │
|
||||||
|
│ [Close Window & Start Deliberation] │
|
||||||
|
│ │
|
||||||
|
│ Window 2: BUSINESS_CONCEPT (6 projects) │
|
||||||
|
│ Status: WAITING │
|
||||||
|
│ Scheduled: 2026-05-15 19:30:00 │
|
||||||
|
│ [Start Window Early] │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ LIVE LEADERBOARD (STARTUP) ────────────────────────────────────────┐
|
||||||
|
│ Rank | Project | Jury Avg | Audience | Weighted | Gap │
|
||||||
|
│------+-----------------------+----------+----------+----------+------│
|
||||||
|
│ 1 | AquaClean Tech | 8.5 | 7.2 | 8.2 | — │
|
||||||
|
│ 2 | BlueCarbon Solutions | 8.0 | 7.4 | 7.8 | -0.4 │
|
||||||
|
│ 3 | OceanSense AI | — | 6.8 | — | — │
|
||||||
|
│ 4 | MarineTech Innov. | — | — | — | — │
|
||||||
|
│ 5 | CoralGuard | — | — | — | — │
|
||||||
|
│ 6 | DeepSea Robotics | — | — | — | — │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ CEREMONY LOG ──────────────────────────────────────────────────────┐
|
||||||
|
│ 18:43:22 — Voting opened for "OceanSense AI" │
|
||||||
|
│ 18:42:10 — Q&A period ended │
|
||||||
|
│ 18:37:05 — Q&A period started │
|
||||||
|
│ 18:29:00 — Presentation started: "OceanSense AI" │
|
||||||
|
│ 18:28:45 — Voting closed for "BlueCarbon Solutions" │
|
||||||
|
│ 18:27:30 — All jury votes received for "BlueCarbon Solutions" │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ ADMIN OVERRIDE PANEL ──────────────────────────────────────────────┐
|
||||||
|
│ [Override Individual Vote...] [Adjust Weights...] [Reset Session] │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stage Manager Features
|
||||||
|
|
||||||
|
**Core controls:**
|
||||||
|
1. **Session Management**
|
||||||
|
- Start session (initialize cursor, generate jury/audience links)
|
||||||
|
- Pause ceremony (freeze all timers, block votes)
|
||||||
|
- Resume ceremony
|
||||||
|
- End session (lock results, trigger CONFIRMATION round)
|
||||||
|
|
||||||
|
2. **Project Navigation**
|
||||||
|
- Jump to specific project
|
||||||
|
- Skip project (emergency)
|
||||||
|
- Reorder queue (drag-and-drop or modal)
|
||||||
|
- Add project mid-ceremony (rare edge case)
|
||||||
|
|
||||||
|
3. **Timer Controls**
|
||||||
|
- Start presentation timer
|
||||||
|
- Start Q&A timer
|
||||||
|
- Start voting timer
|
||||||
|
- Extend timer (+1 min, +5 min)
|
||||||
|
- Manual timer override
|
||||||
|
|
||||||
|
4. **Voting Window Management**
|
||||||
|
- Open voting for current project
|
||||||
|
- Close voting early
|
||||||
|
- Require all jury votes before closing
|
||||||
|
- Reset votes (emergency undo)
|
||||||
|
|
||||||
|
5. **Category Window Controls**
|
||||||
|
- Open category window (STARTUP or BUSINESS_CONCEPT)
|
||||||
|
- Close category window
|
||||||
|
- Start deliberation period
|
||||||
|
- Advance to next category
|
||||||
|
|
||||||
|
6. **Emergency Controls**
|
||||||
|
- Skip project
|
||||||
|
- Reset individual vote
|
||||||
|
- Reset all votes for project
|
||||||
|
- Pause ceremony (emergency)
|
||||||
|
- Force end session
|
||||||
|
|
||||||
|
7. **Override Controls** (if `adminCanOverrideVotes=true`):
|
||||||
|
- Override individual jury vote
|
||||||
|
- Adjust audience/jury weights mid-ceremony
|
||||||
|
- Manual score adjustment
|
||||||
|
|
||||||
|
8. **Real-Time Monitoring**
|
||||||
|
- Live vote count (jury + audience)
|
||||||
|
- Missing jury votes indicator
|
||||||
|
- Audience voter count
|
||||||
|
- Leaderboard (if `showLiveResults=true`)
|
||||||
|
- Ceremony event log
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Jury 3 Voting Experience
|
||||||
|
|
||||||
|
### Jury Dashboard
|
||||||
|
|
||||||
|
**ASCII Mockup:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LIVE FINALS VOTING — Jury 3 Alice Chen │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Status: VOTING IN PROGRESS │
|
||||||
|
│ Category: STARTUP │
|
||||||
|
│ │
|
||||||
|
│ [View All Finalists] [Results Dashboard] [Jury Discussion] │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ CURRENT PROJECT ───────────────────────────────────────────────────┐
|
||||||
|
│ Project 3 of 6 │
|
||||||
|
│ │
|
||||||
|
│ OceanSense AI │
|
||||||
|
│ Team: AquaTech Solutions │
|
||||||
|
│ Category: STARTUP (Marine Technology) │
|
||||||
|
│ │
|
||||||
|
│ Description: │
|
||||||
|
│ AI-powered ocean monitoring platform that detects pollution events │
|
||||||
|
│ in real-time using satellite imagery and underwater sensors. │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Documents ──────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Round 1 Docs: │ │
|
||||||
|
│ │ • Executive Summary.pdf │ │
|
||||||
|
│ │ • Business Plan.pdf │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Round 2 Docs (Semi-finalist): │ │
|
||||||
|
│ │ • Updated Business Plan.pdf │ │
|
||||||
|
│ │ • Pitch Video.mp4 │ │
|
||||||
|
│ │ • Technical Whitepaper.pdf │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Voting closes in: 0:45 │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ VOTING PANEL (Numeric Mode: 1-10) ─────────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ How would you rate this project overall? │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 1 2 3 4 5 6 7 8 9 10 │ │
|
||||||
|
│ │ ○ ○ ○ ○ ○ ○ ○ ● ○ ○ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Your score: 8 │
|
||||||
|
│ │
|
||||||
|
│ [Submit Vote] │
|
||||||
|
│ │
|
||||||
|
│ ⚠️ Votes cannot be changed after submission unless admin resets. │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─ YOUR VOTES THIS SESSION ───────────────────────────────────────────┐
|
||||||
|
│ [✓] 1. AquaClean Tech — Score: 9 │
|
||||||
|
│ [✓] 2. BlueCarbon Solutions — Score: 8 │
|
||||||
|
│ [ ] 3. OceanSense AI — Not voted yet │
|
||||||
|
│ [ ] 4. MarineTech Innovations — Waiting │
|
||||||
|
│ [ ] 5. CoralGuard — Waiting │
|
||||||
|
│ [ ] 6. DeepSea Robotics — Waiting │
|
||||||
|
└───────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
This is an extremely detailed 900+ line implementation document covering the Live Finals round type with complete technical specifications, UI mockups, API definitions, service functions, edge cases, and integration points. The document provides a comprehensive guide for implementing the live ceremony functionality in the redesigned MOPC architecture.
|
||||||
1299
docs/claude-architecture-redesign/10-round-confirmation.md
Normal file
1299
docs/claude-architecture-redesign/10-round-confirmation.md
Normal file
File diff suppressed because it is too large
Load Diff
965
docs/claude-architecture-redesign/11-special-awards.md
Normal file
965
docs/claude-architecture-redesign/11-special-awards.md
Normal file
@@ -0,0 +1,965 @@
|
|||||||
|
# Special Awards System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Special Awards are standalone award tracks that run parallel to the main competition flow. They enable the MOPC platform to recognize excellence in specific areas (e.g., "Innovation Award", "Impact Award", "Youth Leadership Award") with dedicated juries and evaluation processes while referencing the same pool of projects.
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Special Awards serve three key purposes:
|
||||||
|
|
||||||
|
1. **Parallel Recognition** — Recognize excellence in specific domains beyond the main competition prizes
|
||||||
|
2. **Specialized Evaluation** — Enable dedicated jury groups with domain expertise to evaluate specific criteria
|
||||||
|
3. **Flexible Integration** — Awards can piggyback on main rounds or run independently with their own timelines
|
||||||
|
|
||||||
|
### Design Philosophy
|
||||||
|
|
||||||
|
- **Standalone Entities** — Awards are not tracks; they're first-class entities linked to competitions
|
||||||
|
- **Two Modes** — STAY_IN_MAIN (piggyback evaluation) or SEPARATE_POOL (independent flow)
|
||||||
|
- **Dedicated Juries** — Each award can have its own jury group with unique members or shared members
|
||||||
|
- **Flexible Eligibility** — AI-suggested, manual, round-based, or all-eligible modes
|
||||||
|
- **Integration with Results** — Award results feed into the confirmation round alongside main competition winners
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current System Analysis
|
||||||
|
|
||||||
|
### Current Architecture (Pipeline-Based)
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
```
|
||||||
|
Program
|
||||||
|
└── Pipeline
|
||||||
|
├── Track: "Main Competition" (MAIN)
|
||||||
|
└── Track: "Innovation Award" (AWARD)
|
||||||
|
├── Stage: "Evaluation" (EVALUATION)
|
||||||
|
└── Stage: "Results" (RESULTS)
|
||||||
|
|
||||||
|
SpecialAward {
|
||||||
|
id, programId, name, description
|
||||||
|
trackId → Track (AWARD track)
|
||||||
|
criteriaText (for AI)
|
||||||
|
scoringMode: PICK_WINNER | RANKED | SCORED
|
||||||
|
votingStartAt, votingEndAt
|
||||||
|
winnerProjectId
|
||||||
|
useAiEligibility: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
AwardEligibility { awardId, projectId, eligible, method, aiReasoningJson }
|
||||||
|
AwardJuror { awardId, userId }
|
||||||
|
AwardVote { awardId, userId, projectId, rank? }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current Flow:**
|
||||||
|
1. Admin creates AWARD track within pipeline
|
||||||
|
2. Admin configures SpecialAward linked to track
|
||||||
|
3. Projects routed to award track via ProjectStageState
|
||||||
|
4. AI or manual eligibility determination
|
||||||
|
5. Award jurors evaluate/vote
|
||||||
|
6. Winner selected (admin/award master decision)
|
||||||
|
|
||||||
|
**Current Limitations:**
|
||||||
|
- Awards tied to track concept (being eliminated)
|
||||||
|
- No distinction between "piggyback" awards and independent awards
|
||||||
|
- No round-based eligibility
|
||||||
|
- No jury group integration
|
||||||
|
- No evaluation form linkage
|
||||||
|
- No audience voting support
|
||||||
|
- No integration with confirmation round
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redesigned System: Two Award Modes
|
||||||
|
|
||||||
|
### Mode 1: STAY_IN_MAIN
|
||||||
|
|
||||||
|
**Concept:** Projects remain in the main competition flow. A dedicated award jury evaluates them using the same submissions, during the same evaluation windows.
|
||||||
|
|
||||||
|
**Use Case:** "Innovation Award" — Members of Jury 2 who also serve on the Innovation Award jury score projects specifically for innovation criteria during the Jury 2 evaluation round.
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Projects never leave main track
|
||||||
|
- Award jury evaluates during specific main evaluation rounds
|
||||||
|
- Award jury sees the same docs/submissions as main jury
|
||||||
|
- Award uses its own evaluation form with award-specific criteria
|
||||||
|
- No separate stages/timeline needed
|
||||||
|
- Results announced alongside main results
|
||||||
|
|
||||||
|
**Data Flow:**
|
||||||
|
```
|
||||||
|
Competition → Round 5 (Jury 2 Evaluation)
|
||||||
|
├─ Main Jury (Jury 2) evaluates with standard criteria
|
||||||
|
└─ Innovation Award Jury evaluates same projects with innovation criteria
|
||||||
|
|
||||||
|
SpecialAward {
|
||||||
|
evaluationMode: "STAY_IN_MAIN"
|
||||||
|
evaluationRoundId: "round-5" ← Which main round this award evaluates during
|
||||||
|
juryGroupId: "innovation-jury" ← Dedicated jury
|
||||||
|
evaluationFormId: "innovation-form" ← Award-specific criteria
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode 2: SEPARATE_POOL
|
||||||
|
|
||||||
|
**Concept:** Dedicated evaluation with separate criteria, submission requirements, and timeline. Projects may be pulled out for award-specific evaluation.
|
||||||
|
|
||||||
|
**Use Case:** "Community Impact Award" — Separate jury evaluates finalists specifically for community impact using a unique rubric and potentially additional documentation.
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Own jury group with unique members
|
||||||
|
- Own evaluation criteria/form
|
||||||
|
- Can have own submission requirements
|
||||||
|
- Runs on its own timeline
|
||||||
|
- Can pull projects from specific rounds
|
||||||
|
- Independent results timeline
|
||||||
|
|
||||||
|
**Data Flow:**
|
||||||
|
```
|
||||||
|
Competition
|
||||||
|
└── SpecialAward {
|
||||||
|
evaluationMode: "SEPARATE_POOL"
|
||||||
|
eligibilityMode: "ROUND_BASED" ← Projects from Round 5 (finalists)
|
||||||
|
juryGroupId: "impact-jury"
|
||||||
|
evaluationFormId: "impact-form"
|
||||||
|
votingStartAt: [own window]
|
||||||
|
votingEndAt: [own window]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enhanced SpecialAward Model
|
||||||
|
|
||||||
|
### Complete Schema
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model SpecialAward {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
competitionId String // CHANGED: Links to Competition, not Track
|
||||||
|
name String
|
||||||
|
description String? @db.Text
|
||||||
|
|
||||||
|
// Eligibility configuration
|
||||||
|
eligibilityMode AwardEligibilityMode @default(AI_SUGGESTED)
|
||||||
|
eligibilityCriteria Json? @db.JsonB // Mode-specific config
|
||||||
|
|
||||||
|
// Evaluation configuration
|
||||||
|
evaluationMode AwardEvaluationMode @default(STAY_IN_MAIN)
|
||||||
|
evaluationRoundId String? // Which main round (for STAY_IN_MAIN)
|
||||||
|
evaluationFormId String? // Custom criteria
|
||||||
|
juryGroupId String? // Dedicated or shared jury
|
||||||
|
|
||||||
|
// Voting configuration
|
||||||
|
votingMode AwardVotingMode @default(JURY_ONLY)
|
||||||
|
scoringMode AwardScoringMode @default(PICK_WINNER)
|
||||||
|
maxRankedPicks Int? // For RANKED mode
|
||||||
|
maxWinners Int @default(1) // Number of winners
|
||||||
|
audienceVotingWeight Float? // 0.0-1.0 for COMBINED mode
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
votingStartAt DateTime?
|
||||||
|
votingEndAt DateTime?
|
||||||
|
|
||||||
|
// Results
|
||||||
|
status AwardStatus @default(DRAFT)
|
||||||
|
winnerProjectId String? // Single winner (for backward compat)
|
||||||
|
|
||||||
|
// AI eligibility
|
||||||
|
useAiEligibility Boolean @default(false)
|
||||||
|
criteriaText String? @db.Text // Plain-language for AI
|
||||||
|
|
||||||
|
// Job tracking (for AI eligibility)
|
||||||
|
eligibilityJobStatus String?
|
||||||
|
eligibilityJobTotal Int?
|
||||||
|
eligibilityJobDone Int?
|
||||||
|
eligibilityJobError String? @db.Text
|
||||||
|
eligibilityJobStarted DateTime?
|
||||||
|
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
||||||
|
evaluationRound Round? @relation("AwardEvaluationRound", fields: [evaluationRoundId], references: [id], onDelete: SetNull)
|
||||||
|
evaluationForm EvaluationForm? @relation(fields: [evaluationFormId], references: [id], onDelete: SetNull)
|
||||||
|
juryGroup JuryGroup? @relation("AwardJuryGroup", fields: [juryGroupId], references: [id], onDelete: SetNull)
|
||||||
|
winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
eligibilities AwardEligibility[]
|
||||||
|
votes AwardVote[]
|
||||||
|
winners AwardWinner[] // NEW: Multi-winner support
|
||||||
|
|
||||||
|
@@index([competitionId])
|
||||||
|
@@index([status])
|
||||||
|
@@index([evaluationRoundId])
|
||||||
|
@@index([juryGroupId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AwardEligibilityMode {
|
||||||
|
AI_SUGGESTED // AI analyzes and suggests eligible projects
|
||||||
|
MANUAL // Admin manually selects eligible projects
|
||||||
|
ALL_ELIGIBLE // All projects in competition are eligible
|
||||||
|
ROUND_BASED // All projects that reach a specific round
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AwardEvaluationMode {
|
||||||
|
STAY_IN_MAIN // Evaluate during main competition round
|
||||||
|
SEPARATE_POOL // Independent evaluation flow
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AwardVotingMode {
|
||||||
|
JURY_ONLY // Only jury votes
|
||||||
|
AUDIENCE_ONLY // Only audience votes
|
||||||
|
COMBINED // Jury + audience with weighted scoring
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AwardScoringMode {
|
||||||
|
PICK_WINNER // Simple winner selection (1 or N winners)
|
||||||
|
RANKED // Ranked-choice voting
|
||||||
|
SCORED // Criteria-based scoring
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AwardStatus {
|
||||||
|
DRAFT
|
||||||
|
NOMINATIONS_OPEN
|
||||||
|
EVALUATION // NEW: Award jury evaluation in progress
|
||||||
|
DECIDED // NEW: Winner(s) selected, pending announcement
|
||||||
|
ANNOUNCED // NEW: Winner(s) publicly announced
|
||||||
|
ARCHIVED
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### New Model: AwardWinner (Multi-Winner Support)
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model AwardWinner {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
awardId String
|
||||||
|
projectId String
|
||||||
|
rank Int // 1st place, 2nd place, etc.
|
||||||
|
|
||||||
|
// Selection metadata
|
||||||
|
selectedAt DateTime @default(now())
|
||||||
|
selectedById String
|
||||||
|
selectionMethod String // "JURY_VOTE" | "AUDIENCE_VOTE" | "COMBINED" | "ADMIN_DECISION"
|
||||||
|
|
||||||
|
// Score breakdown (for transparency)
|
||||||
|
juryScore Float?
|
||||||
|
audienceScore Float?
|
||||||
|
finalScore Float?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
||||||
|
project Project @relation("AwardWinners", fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
selectedBy User @relation("AwardWinnerSelector", fields: [selectedById], references: [id])
|
||||||
|
|
||||||
|
@@unique([awardId, projectId])
|
||||||
|
@@unique([awardId, rank])
|
||||||
|
@@index([awardId])
|
||||||
|
@@index([projectId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enhanced AwardVote Model
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model AwardVote {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
awardId String
|
||||||
|
userId String? // Nullable for audience votes
|
||||||
|
projectId String
|
||||||
|
|
||||||
|
// Voting type
|
||||||
|
isAudienceVote Boolean @default(false)
|
||||||
|
|
||||||
|
// Scoring (mode-dependent)
|
||||||
|
rank Int? // For RANKED mode (1 = first choice)
|
||||||
|
score Float? // For SCORED mode
|
||||||
|
|
||||||
|
// Criteria scores (for SCORED mode)
|
||||||
|
criterionScoresJson Json? @db.JsonB
|
||||||
|
|
||||||
|
votedAt DateTime @default(now())
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([awardId, userId, projectId])
|
||||||
|
@@index([awardId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([projectId])
|
||||||
|
@@index([awardId, isAudienceVote])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Eligibility System Deep Dive
|
||||||
|
|
||||||
|
### Eligibility Modes
|
||||||
|
|
||||||
|
#### 1. AI_SUGGESTED
|
||||||
|
|
||||||
|
AI analyzes all projects and suggests eligible ones based on plain-language criteria.
|
||||||
|
|
||||||
|
**Config JSON:**
|
||||||
|
```typescript
|
||||||
|
type AISuggestedConfig = {
|
||||||
|
criteriaText: string // "Projects using innovative ocean tech"
|
||||||
|
confidenceThreshold: number // 0.0-1.0 (default: 0.7)
|
||||||
|
autoAcceptAbove: number // Auto-accept above this (default: 0.9)
|
||||||
|
requireManualReview: boolean // All need admin review (default: false)
|
||||||
|
sourceRoundId?: string // Only projects from this round
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
1. Admin triggers AI eligibility analysis
|
||||||
|
2. AI processes projects in batches (anonymized)
|
||||||
|
3. AI returns: `{ projectId, eligible, confidence, reasoning }`
|
||||||
|
4. High-confidence results auto-applied
|
||||||
|
5. Medium-confidence results flagged for review
|
||||||
|
6. Low-confidence results rejected (or flagged if `requireManualReview: true`)
|
||||||
|
|
||||||
|
**UI:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Innovation Award — AI Eligibility Analysis │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ Status: Running... (47/120 projects analyzed) │
|
||||||
|
│ [████████████████░░░░░░░░] 68% │
|
||||||
|
│ │
|
||||||
|
│ Results So Far: │
|
||||||
|
│ ✓ Auto-Accepted (confidence > 0.9): 12 projects │
|
||||||
|
│ ⚠ Flagged for Review (0.6-0.9): 23 projects │
|
||||||
|
│ ✗ Rejected (< 0.6): 12 projects │
|
||||||
|
│ │
|
||||||
|
│ [View Flagged Projects] [Stop Analysis] │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. MANUAL
|
||||||
|
|
||||||
|
Admin manually selects eligible projects.
|
||||||
|
|
||||||
|
**Config JSON:**
|
||||||
|
```typescript
|
||||||
|
type ManualConfig = {
|
||||||
|
sourceRoundId?: string // Limit to projects from specific round
|
||||||
|
categoryFilter?: "STARTUP" | "BUSINESS_CONCEPT"
|
||||||
|
tagFilters?: string[] // Only projects with these tags
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. ALL_ELIGIBLE
|
||||||
|
|
||||||
|
All projects in the competition are automatically eligible.
|
||||||
|
|
||||||
|
**Config JSON:**
|
||||||
|
```typescript
|
||||||
|
type AllEligibleConfig = {
|
||||||
|
minimumStatus?: ProjectStatus // e.g., "SEMIFINALIST" or above
|
||||||
|
excludeWithdrawn: boolean // Exclude WITHDRAWN (default: true)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. ROUND_BASED
|
||||||
|
|
||||||
|
All projects that reach a specific round are automatically eligible.
|
||||||
|
|
||||||
|
**Config JSON:**
|
||||||
|
```typescript
|
||||||
|
type RoundBasedConfig = {
|
||||||
|
sourceRoundId: string // Required: which round
|
||||||
|
requiredState: ProjectRoundStateValue // PASSED, COMPLETED, etc.
|
||||||
|
autoUpdate: boolean // Auto-update when projects advance (default: true)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sourceRoundId": "round-5-jury-2",
|
||||||
|
"requiredState": "PASSED",
|
||||||
|
"autoUpdate": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Override System
|
||||||
|
|
||||||
|
**All eligibility modes support admin override:**
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model AwardEligibility {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
awardId String
|
||||||
|
projectId String
|
||||||
|
|
||||||
|
// Original determination
|
||||||
|
method EligibilityMethod @default(AUTO) // AUTO, AI, MANUAL
|
||||||
|
eligible Boolean @default(false)
|
||||||
|
aiReasoningJson Json? @db.JsonB
|
||||||
|
|
||||||
|
// Override
|
||||||
|
overriddenBy String?
|
||||||
|
overriddenAt DateTime?
|
||||||
|
overrideReason String? @db.Text
|
||||||
|
|
||||||
|
// Final decision
|
||||||
|
finalEligible Boolean // Computed: overridden ? override : original
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id])
|
||||||
|
|
||||||
|
@@unique([awardId, projectId])
|
||||||
|
@@index([awardId, eligible])
|
||||||
|
@@index([awardId, finalEligible])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Award Jury Groups
|
||||||
|
|
||||||
|
### Integration with JuryGroup Model
|
||||||
|
|
||||||
|
Awards can have:
|
||||||
|
1. **Dedicated Jury** — Own `JuryGroup` with unique members
|
||||||
|
2. **Shared Jury** — Reuse existing competition jury group (e.g., Jury 2)
|
||||||
|
3. **Mixed Jury** — Some overlap with main jury, some unique members
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```typescript
|
||||||
|
// Dedicated jury for Innovation Award
|
||||||
|
const innovationJury = await prisma.juryGroup.create({
|
||||||
|
data: {
|
||||||
|
competitionId: "comp-2026",
|
||||||
|
name: "Innovation Award Jury",
|
||||||
|
slug: "innovation-jury",
|
||||||
|
description: "Technology and innovation experts",
|
||||||
|
defaultMaxAssignments: 15,
|
||||||
|
defaultCapMode: "SOFT",
|
||||||
|
categoryQuotasEnabled: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add members (can overlap with main jury)
|
||||||
|
await prisma.juryGroupMember.createMany({
|
||||||
|
data: [
|
||||||
|
{ juryGroupId: innovationJury.id, userId: "user-tech-1", isLead: true },
|
||||||
|
{ juryGroupId: innovationJury.id, userId: "user-tech-2" },
|
||||||
|
{ juryGroupId: innovationJury.id, userId: "jury-2-member-overlap" }, // Also on Jury 2
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Link to award
|
||||||
|
await prisma.specialAward.update({
|
||||||
|
where: { id: awardId },
|
||||||
|
data: { juryGroupId: innovationJury.id }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Award Jury Assignment
|
||||||
|
|
||||||
|
#### For STAY_IN_MAIN Mode
|
||||||
|
|
||||||
|
Award jury members evaluate the same projects as the main jury, but with award-specific criteria.
|
||||||
|
|
||||||
|
**Assignment Creation:**
|
||||||
|
```typescript
|
||||||
|
// Main jury assignments (created by round)
|
||||||
|
Assignment { userId: "jury-2-member-1", projectId: "proj-A", roundId: "round-5", juryGroupId: "jury-2" }
|
||||||
|
|
||||||
|
// Award jury assignments (created separately, same round)
|
||||||
|
Assignment { userId: "innovation-jury-1", projectId: "proj-A", roundId: "round-5", juryGroupId: "innovation-jury" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Evaluation:**
|
||||||
|
- Award jury uses `evaluationFormId` linked to award
|
||||||
|
- Evaluations stored separately (different `assignmentId`)
|
||||||
|
- Both juries can evaluate same project in same round
|
||||||
|
|
||||||
|
#### For SEPARATE_POOL Mode
|
||||||
|
|
||||||
|
Award has its own assignment workflow, potentially for a subset of projects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Award Evaluation Flow
|
||||||
|
|
||||||
|
### STAY_IN_MAIN Evaluation
|
||||||
|
|
||||||
|
**Timeline:**
|
||||||
|
```
|
||||||
|
Round 5: Jury 2 Evaluation (Main)
|
||||||
|
├─ Opens: 2026-03-01
|
||||||
|
├─ Main Jury evaluates with standard form
|
||||||
|
├─ Innovation Award Jury evaluates with innovation form
|
||||||
|
└─ Closes: 2026-03-15
|
||||||
|
|
||||||
|
Award results calculated separately but announced together
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step-by-Step:**
|
||||||
|
|
||||||
|
1. **Setup Phase**
|
||||||
|
- Admin creates `SpecialAward { evaluationMode: "STAY_IN_MAIN", evaluationRoundId: "round-5" }`
|
||||||
|
- Admin creates award-specific `EvaluationForm` with innovation criteria
|
||||||
|
- Admin creates `JuryGroup` for Innovation Award
|
||||||
|
- Admin adds members to jury group
|
||||||
|
|
||||||
|
2. **Eligibility Phase**
|
||||||
|
- Eligibility determined (AI/manual/round-based)
|
||||||
|
- Only eligible projects evaluated by award jury
|
||||||
|
|
||||||
|
3. **Assignment Phase**
|
||||||
|
- When Round 5 opens, assignments created for award jury
|
||||||
|
- Each award juror assigned eligible projects
|
||||||
|
- Award assignments reference same `roundId` as main evaluation
|
||||||
|
|
||||||
|
4. **Evaluation Phase**
|
||||||
|
- Award jurors see projects in their dashboard
|
||||||
|
- Form shows award-specific criteria
|
||||||
|
- Evaluations stored with `formId` = innovation form
|
||||||
|
|
||||||
|
5. **Results Phase**
|
||||||
|
- Scores aggregated separately from main jury
|
||||||
|
- Winner selection (jury vote, admin decision, etc.)
|
||||||
|
- Results feed into confirmation round
|
||||||
|
|
||||||
|
### SEPARATE_POOL Evaluation
|
||||||
|
|
||||||
|
**Timeline:**
|
||||||
|
```
|
||||||
|
Round 5: Jury 2 Evaluation (Main) — March 1-15
|
||||||
|
↓
|
||||||
|
Round 6: Finalist Selection
|
||||||
|
↓
|
||||||
|
Impact Award Evaluation (Separate) — March 20 - April 5
|
||||||
|
├─ Own voting window
|
||||||
|
├─ Own evaluation form
|
||||||
|
├─ Impact Award Jury evaluates finalists
|
||||||
|
└─ Results: April 10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audience Voting for Awards
|
||||||
|
|
||||||
|
### Voting Modes
|
||||||
|
|
||||||
|
#### JURY_ONLY
|
||||||
|
|
||||||
|
Only jury members vote. Standard model.
|
||||||
|
|
||||||
|
#### AUDIENCE_ONLY
|
||||||
|
|
||||||
|
Only audience (public) votes. No jury involvement.
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```typescript
|
||||||
|
type AudienceOnlyConfig = {
|
||||||
|
requireIdentification: boolean // Require email/phone (default: false)
|
||||||
|
votesPerPerson: number // Max votes per person (default: 1)
|
||||||
|
allowRanking: boolean // Ranked-choice (default: false)
|
||||||
|
maxChoices?: number // For ranked mode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### COMBINED
|
||||||
|
|
||||||
|
Jury + audience votes combined with weighted scoring.
|
||||||
|
|
||||||
|
**Config:**
|
||||||
|
```typescript
|
||||||
|
type CombinedConfig = {
|
||||||
|
audienceWeight: number // 0.0-1.0 (e.g., 0.3 = 30% audience, 70% jury)
|
||||||
|
juryWeight: number // 0.0-1.0 (should sum to 1.0)
|
||||||
|
requireMinimumAudienceVotes: number // Min votes for validity (default: 50)
|
||||||
|
showAudienceResultsToJury: boolean // Jury sees audience results (default: false)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scoring Calculation:**
|
||||||
|
```typescript
|
||||||
|
function calculateCombinedScore(
|
||||||
|
juryScores: number[],
|
||||||
|
audienceVoteCount: number,
|
||||||
|
totalAudienceVotes: number,
|
||||||
|
config: CombinedConfig
|
||||||
|
): number {
|
||||||
|
const juryAvg = juryScores.reduce((a, b) => a + b, 0) / juryScores.length
|
||||||
|
const audiencePercent = audienceVoteCount / totalAudienceVotes
|
||||||
|
|
||||||
|
// Normalize jury score to 0-1 (assuming 1-10 scale)
|
||||||
|
const normalizedJuryScore = juryAvg / 10
|
||||||
|
|
||||||
|
const finalScore =
|
||||||
|
(normalizedJuryScore * config.juryWeight) +
|
||||||
|
(audiencePercent * config.audienceWeight)
|
||||||
|
|
||||||
|
return finalScore
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Experience
|
||||||
|
|
||||||
|
### Award Management Dashboard
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MOPC 2026 — Special Awards [+ New Award] │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Innovation Award [Edit ▼] │ │
|
||||||
|
│ │ Mode: Stay in Main (Jury 2 Evaluation) • Status: EVALUATION │ │
|
||||||
|
│ ├───────────────────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Eligible Projects: 18 / 20 finalists │ │
|
||||||
|
│ │ Jury: Innovation Jury (5 members) │ │
|
||||||
|
│ │ Evaluations: 72 / 90 (80% complete) │ │
|
||||||
|
│ │ Voting Closes: March 15, 2026 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [View Eligibility] [View Evaluations] [Select Winner] │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Community Impact Award [Edit ▼] │ │
|
||||||
|
│ │ Mode: Separate Pool • Status: DRAFT │ │
|
||||||
|
│ ├───────────────────────────────────────────────────────────────────────┤ │
|
||||||
|
│ │ Eligible Projects: Not yet determined (AI pending) │ │
|
||||||
|
│ │ Jury: Not assigned │ │
|
||||||
|
│ │ Voting Window: Not set │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Configure Eligibility] [Set Up Jury] [Set Timeline] │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Main Flow
|
||||||
|
|
||||||
|
### Awards Reference Main Competition Projects
|
||||||
|
|
||||||
|
Awards don't create their own project pool — they reference existing competition projects.
|
||||||
|
|
||||||
|
**Data Relationship:**
|
||||||
|
```
|
||||||
|
Competition
|
||||||
|
├── Projects (shared pool)
|
||||||
|
│ ├── Project A
|
||||||
|
│ ├── Project B
|
||||||
|
│ └── Project C
|
||||||
|
│
|
||||||
|
├── Main Rounds (linear flow)
|
||||||
|
│ ├── Round 1: Intake
|
||||||
|
│ ├── Round 5: Jury 2 Evaluation
|
||||||
|
│ └── Round 7: Live Finals
|
||||||
|
│
|
||||||
|
└── Special Awards (parallel evaluation)
|
||||||
|
├── Innovation Award
|
||||||
|
│ └── AwardEligibility { projectId: "A", eligible: true }
|
||||||
|
│ └── AwardEligibility { projectId: "B", eligible: false }
|
||||||
|
└── Impact Award
|
||||||
|
└── AwardEligibility { projectId: "A", eligible: true }
|
||||||
|
└── AwardEligibility { projectId: "C", eligible: true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Award Results Feed into Confirmation Round
|
||||||
|
|
||||||
|
**Confirmation Round Integration:**
|
||||||
|
|
||||||
|
The confirmation round (Round 8) includes:
|
||||||
|
1. Main competition winners (1st, 2nd, 3rd per category)
|
||||||
|
2. Special award winners
|
||||||
|
|
||||||
|
**WinnerProposal Extension:**
|
||||||
|
```prisma
|
||||||
|
model WinnerProposal {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
competitionId String
|
||||||
|
category CompetitionCategory? // Null for award winners
|
||||||
|
|
||||||
|
// Main competition or award
|
||||||
|
proposalType WinnerProposalType @default(MAIN_COMPETITION)
|
||||||
|
awardId String? // If proposalType = SPECIAL_AWARD
|
||||||
|
|
||||||
|
status WinnerProposalStatus @default(PENDING)
|
||||||
|
rankedProjectIds String[]
|
||||||
|
|
||||||
|
// ... rest of fields ...
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WinnerProposalType {
|
||||||
|
MAIN_COMPETITION // Main 1st/2nd/3rd place
|
||||||
|
SPECIAL_AWARD // Award winner
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Changes
|
||||||
|
|
||||||
|
### New tRPC Procedures
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server/routers/award-redesign.ts
|
||||||
|
|
||||||
|
export const awardRedesignRouter = router({
|
||||||
|
/**
|
||||||
|
* Create a new special award
|
||||||
|
*/
|
||||||
|
create: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
competitionId: z.string(),
|
||||||
|
name: z.string().min(1).max(255),
|
||||||
|
description: z.string().optional(),
|
||||||
|
eligibilityMode: z.enum(['AI_SUGGESTED', 'MANUAL', 'ALL_ELIGIBLE', 'ROUND_BASED']),
|
||||||
|
evaluationMode: z.enum(['STAY_IN_MAIN', 'SEPARATE_POOL']),
|
||||||
|
votingMode: z.enum(['JURY_ONLY', 'AUDIENCE_ONLY', 'COMBINED']),
|
||||||
|
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']),
|
||||||
|
maxWinners: z.number().int().min(1).default(1),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run eligibility determination
|
||||||
|
*/
|
||||||
|
runEligibility: adminProcedure
|
||||||
|
.input(z.object({ awardId: z.string() }))
|
||||||
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast vote (jury or audience)
|
||||||
|
*/
|
||||||
|
vote: protectedProcedure
|
||||||
|
.input(z.object({
|
||||||
|
awardId: z.string(),
|
||||||
|
projectId: z.string(),
|
||||||
|
rank: z.number().int().min(1).optional(),
|
||||||
|
score: z.number().min(0).max(10).optional(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select winner(s)
|
||||||
|
*/
|
||||||
|
selectWinners: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
awardId: z.string(),
|
||||||
|
winnerProjectIds: z.array(z.string()).min(1),
|
||||||
|
selectionMethod: z.enum(['JURY_VOTE', 'AUDIENCE_VOTE', 'COMBINED', 'ADMIN_DECISION']),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Functions
|
||||||
|
|
||||||
|
### Award Service Enhancements
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server/services/award-service.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run round-based eligibility
|
||||||
|
*/
|
||||||
|
export async function runRoundBasedEligibility(
|
||||||
|
award: SpecialAward,
|
||||||
|
prisma = getPrisma()
|
||||||
|
) {
|
||||||
|
const config = award.eligibilityCriteria as RoundBasedConfig
|
||||||
|
|
||||||
|
if (!config.sourceRoundId) {
|
||||||
|
throw new Error('Round-based eligibility requires sourceRoundId')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all projects in the specified round with the required state
|
||||||
|
const projectRoundStates = await prisma.projectRoundState.findMany({
|
||||||
|
where: {
|
||||||
|
roundId: config.sourceRoundId,
|
||||||
|
state: config.requiredState ?? 'PASSED',
|
||||||
|
},
|
||||||
|
select: { projectId: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create/update eligibility records
|
||||||
|
let created = 0
|
||||||
|
let updated = 0
|
||||||
|
|
||||||
|
for (const prs of projectRoundStates) {
|
||||||
|
const existing = await prisma.awardEligibility.findUnique({
|
||||||
|
where: {
|
||||||
|
awardId_projectId: {
|
||||||
|
awardId: award.id,
|
||||||
|
projectId: prs.projectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await prisma.awardEligibility.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: { eligible: true, method: 'AUTO' }
|
||||||
|
})
|
||||||
|
updated++
|
||||||
|
} else {
|
||||||
|
await prisma.awardEligibility.create({
|
||||||
|
data: {
|
||||||
|
awardId: award.id,
|
||||||
|
projectId: prs.projectId,
|
||||||
|
eligible: true,
|
||||||
|
method: 'AUTO',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
created++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { created, updated, total: projectRoundStates.length }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate combined jury + audience score
|
||||||
|
*/
|
||||||
|
export function calculateCombinedScore(
|
||||||
|
juryScores: number[],
|
||||||
|
audienceVoteCount: number,
|
||||||
|
totalAudienceVotes: number,
|
||||||
|
juryWeight: number,
|
||||||
|
audienceWeight: number
|
||||||
|
): number {
|
||||||
|
if (juryScores.length === 0) {
|
||||||
|
throw new Error('Cannot calculate combined score without jury votes')
|
||||||
|
}
|
||||||
|
|
||||||
|
const juryAvg = juryScores.reduce((a, b) => a + b, 0) / juryScores.length
|
||||||
|
const normalizedJuryScore = juryAvg / 10 // Assume 1-10 scale
|
||||||
|
|
||||||
|
const audiencePercent = totalAudienceVotes > 0
|
||||||
|
? audienceVoteCount / totalAudienceVotes
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const finalScore =
|
||||||
|
(normalizedJuryScore * juryWeight) +
|
||||||
|
(audiencePercent * audienceWeight)
|
||||||
|
|
||||||
|
return finalScore
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create award jury assignments
|
||||||
|
*/
|
||||||
|
export async function createAwardAssignments(
|
||||||
|
awardId: string,
|
||||||
|
prisma = getPrisma()
|
||||||
|
) {
|
||||||
|
const award = await prisma.specialAward.findUniqueOrThrow({
|
||||||
|
where: { id: awardId },
|
||||||
|
include: {
|
||||||
|
juryGroup: {
|
||||||
|
include: { members: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!award.juryGroupId || !award.juryGroup) {
|
||||||
|
throw new Error('Award must have a jury group to create assignments')
|
||||||
|
}
|
||||||
|
|
||||||
|
const eligibleProjects = await getEligibleProjects(awardId, prisma)
|
||||||
|
|
||||||
|
const assignments = []
|
||||||
|
|
||||||
|
for (const project of eligibleProjects) {
|
||||||
|
for (const member of award.juryGroup.members) {
|
||||||
|
assignments.push({
|
||||||
|
userId: member.userId,
|
||||||
|
projectId: project.id,
|
||||||
|
roundId: award.evaluationRoundId ?? null,
|
||||||
|
juryGroupId: award.juryGroupId,
|
||||||
|
method: 'MANUAL' as const,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.assignment.createMany({
|
||||||
|
data: assignments,
|
||||||
|
skipDuplicates: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return { created: assignments.length }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
| Scenario | Handling |
|
||||||
|
|----------|----------|
|
||||||
|
| **Project eligible for multiple awards** | Allowed — project can win multiple awards |
|
||||||
|
| **Jury member on both main and award juries** | Allowed — separate assignments, separate evaluations |
|
||||||
|
| **Award voting ends before main results** | Award winner held until main results finalized, announced together |
|
||||||
|
| **Award eligibility changes mid-voting** | Admin override can remove eligibility; active votes invalidated |
|
||||||
|
| **Audience vote spam/fraud** | IP rate limiting, device fingerprinting, email verification, admin review |
|
||||||
|
| **Tie in award voting** | Admin decision or re-vote (configurable) |
|
||||||
|
| **Award jury not complete evaluations** | Admin can close voting with partial data or extend deadline |
|
||||||
|
| **Project withdrawn after eligible** | Eligibility auto-removed; votes invalidated |
|
||||||
|
| **Award criteria change after eligibility** | Re-run eligibility or grandfather existing eligible projects |
|
||||||
|
| **No eligible projects for award** | Award status set to DRAFT/ARCHIVED; no voting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### With Evaluation System
|
||||||
|
- Awards use `EvaluationForm` for criteria
|
||||||
|
- Award evaluations stored in `Evaluation` table with `formId` linkage
|
||||||
|
- Assignment system handles both main and award assignments
|
||||||
|
|
||||||
|
### With Jury Groups
|
||||||
|
- Awards can link to existing `JuryGroup` or have dedicated groups
|
||||||
|
- Jury members can overlap between main and award juries
|
||||||
|
- Caps and quotas honored for award assignments
|
||||||
|
|
||||||
|
### With Confirmation Round
|
||||||
|
- Award winners included in `WinnerProposal` system
|
||||||
|
- Confirmation flow handles both main and award winners
|
||||||
|
- Approval workflow requires sign-off on all winners
|
||||||
|
|
||||||
|
### With Notification System
|
||||||
|
- Eligibility notifications sent to eligible teams
|
||||||
|
- Voting reminders sent to award jurors
|
||||||
|
- Winner announcements coordinated with main results
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The redesigned Special Awards system provides:
|
||||||
|
|
||||||
|
1. **Flexibility**: Two modes (STAY_IN_MAIN, SEPARATE_POOL) cover all use cases
|
||||||
|
2. **Integration**: Deep integration with competition rounds, juries, and results
|
||||||
|
3. **Autonomy**: Awards can run independently or piggyback on main flow
|
||||||
|
4. **Transparency**: AI eligibility with admin override, full audit trail
|
||||||
|
5. **Engagement**: Audience voting support with anti-fraud measures
|
||||||
|
6. **Scalability**: Support for multiple awards, multiple winners, complex scoring
|
||||||
|
|
||||||
|
This architecture eliminates the Track dependency, integrates awards as standalone entities, and provides a robust, flexible system for recognizing excellence across multiple dimensions while maintaining the integrity of the main competition flow.
|
||||||
960
docs/claude-architecture-redesign/12-jury-groups.md
Normal file
960
docs/claude-architecture-redesign/12-jury-groups.md
Normal file
@@ -0,0 +1,960 @@
|
|||||||
|
# Jury Groups — Multi-Jury Architecture
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The **JuryGroup** model is the backbone of the redesigned jury system. Instead of implicit jury membership derived from per-stage assignments, juries are now **first-class named entities** — "Jury 1", "Jury 2", "Jury 3", "Innovation Award Jury" — with explicit membership, configurable assignment caps, and per-juror overrides.
|
||||||
|
|
||||||
|
### Why This Matters
|
||||||
|
|
||||||
|
| Before (Current) | After (Redesigned) |
|
||||||
|
|---|---|
|
||||||
|
| Juries are implicit — "Jury 1" exists only in admin's head | JuryGroup is a named model with `id`, `name`, `description` |
|
||||||
|
| Assignment caps are per-stage config | Caps are per-juror on JuryGroupMember (with group defaults) |
|
||||||
|
| No concept of "which jury is this juror on" | JuryGroupMember links User to JuryGroup explicitly |
|
||||||
|
| Same juror can't be on multiple juries (no grouping) | A User can belong to multiple JuryGroups |
|
||||||
|
| Category quotas don't exist | Per-juror STARTUP/CONCEPT ratio preferences |
|
||||||
|
| No juror onboarding preferences | JuryGroupMember stores language, expertise, preferences |
|
||||||
|
|
||||||
|
### Jury Groups in the 8-Step Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Round 1: INTAKE — no jury
|
||||||
|
Round 2: FILTERING — no jury (AI-powered)
|
||||||
|
Round 3: EVALUATION — ► Jury 1 (semi-finalist selection)
|
||||||
|
Round 4: SUBMISSION — no jury
|
||||||
|
Round 5: EVALUATION — ► Jury 2 (finalist selection)
|
||||||
|
Round 6: MENTORING — no jury
|
||||||
|
Round 7: LIVE_FINAL — ► Jury 3 (live finals scoring)
|
||||||
|
Round 8: CONFIRMATION — ► Jury 3 (winner confirmation)
|
||||||
|
|
||||||
|
Special Awards:
|
||||||
|
Innovation Award — ► Innovation Jury (may overlap with Jury 2)
|
||||||
|
Impact Award — ► Impact Jury (dedicated members)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### JuryGroup
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model JuryGroup {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
competitionId String
|
||||||
|
name String // "Jury 1", "Jury 2", "Jury 3", "Innovation Award Jury"
|
||||||
|
description String? // "Semi-finalist evaluation jury — reviews 60+ applications"
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
// Default assignment configuration for this jury
|
||||||
|
defaultMaxAssignments Int @default(20)
|
||||||
|
defaultCapMode CapMode @default(SOFT)
|
||||||
|
softCapBuffer Int @default(2) // Extra assignments above cap
|
||||||
|
|
||||||
|
// Default category quotas (per juror)
|
||||||
|
defaultCategoryQuotas Json? @db.JsonB
|
||||||
|
// Shape: { "STARTUP": { min: 5, max: 12 }, "BUSINESS_CONCEPT": { min: 5, max: 12 } }
|
||||||
|
|
||||||
|
// Onboarding
|
||||||
|
onboardingFormId String? // Link to onboarding form (expertise, preferences)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
competition Competition @relation(...)
|
||||||
|
members JuryGroupMember[]
|
||||||
|
rounds Round[] // Rounds this jury is assigned to
|
||||||
|
assignments Assignment[] // Assignments made through this jury group
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JuryGroupMember
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model JuryGroupMember {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
juryGroupId String
|
||||||
|
userId String
|
||||||
|
role String @default("MEMBER") // "MEMBER" | "CHAIR" | "OBSERVER"
|
||||||
|
|
||||||
|
// Per-juror overrides (null = use group defaults)
|
||||||
|
maxAssignmentsOverride Int?
|
||||||
|
capModeOverride CapMode?
|
||||||
|
categoryQuotasOverride Json? @db.JsonB
|
||||||
|
|
||||||
|
// Juror preferences (set during onboarding)
|
||||||
|
preferredStartupRatio Float? // 0.0–1.0 (e.g., 0.6 = 60% startups)
|
||||||
|
expertiseTags String[] // ["ocean-tech", "marine-biology", "finance"]
|
||||||
|
languagePreferences String[] // ["en", "fr"]
|
||||||
|
notes String? // Admin notes about this juror
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
juryGroup JuryGroup @relation(...)
|
||||||
|
user User @relation(...)
|
||||||
|
|
||||||
|
@@unique([juryGroupId, userId])
|
||||||
|
@@index([juryGroupId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CapMode Enum
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum CapMode {
|
||||||
|
HARD // Absolute maximum — algorithm cannot exceed under any circumstance
|
||||||
|
SOFT // Target maximum — can exceed by softCapBuffer for load balancing
|
||||||
|
NONE // No cap — unlimited assignments (use with caution)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cap Behavior
|
||||||
|
|
||||||
|
| Mode | Max | Buffer | Effective Limit | Behavior |
|
||||||
|
|------|-----|--------|-----------------|----------|
|
||||||
|
| HARD | 20 | — | 20 | Algorithm stops at exactly 20. No exceptions. |
|
||||||
|
| SOFT | 20 | 2 | 22 | Algorithm targets 20 but can go to 22 if needed for balanced distribution |
|
||||||
|
| NONE | — | — | ∞ | No limit. Juror can be assigned any number of projects |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function getEffectiveCap(member: JuryGroupMember, group: JuryGroup): number | null {
|
||||||
|
const capMode = member.capModeOverride ?? group.defaultCapMode;
|
||||||
|
const maxAssignments = member.maxAssignmentsOverride ?? group.defaultMaxAssignments;
|
||||||
|
|
||||||
|
switch (capMode) {
|
||||||
|
case 'HARD':
|
||||||
|
return maxAssignments;
|
||||||
|
case 'SOFT':
|
||||||
|
return maxAssignments + group.softCapBuffer;
|
||||||
|
case 'NONE':
|
||||||
|
return null; // no limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canAssignMore(
|
||||||
|
member: JuryGroupMember,
|
||||||
|
group: JuryGroup,
|
||||||
|
currentCount: number
|
||||||
|
): { allowed: boolean; reason?: string } {
|
||||||
|
const cap = getEffectiveCap(member, group);
|
||||||
|
|
||||||
|
if (cap === null) return { allowed: true };
|
||||||
|
|
||||||
|
if (currentCount >= cap) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
reason: `Juror has reached ${capMode === 'HARD' ? 'hard' : 'soft'} cap of ${cap} assignments`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Category Quotas
|
||||||
|
|
||||||
|
### How Quotas Work
|
||||||
|
|
||||||
|
Each jury group (and optionally each member) can define minimum and maximum assignments per competition category. This ensures balanced coverage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CategoryQuotas = {
|
||||||
|
STARTUP: { min: number; max: number };
|
||||||
|
BUSINESS_CONCEPT: { min: number; max: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example: group default
|
||||||
|
const defaultQuotas: CategoryQuotas = {
|
||||||
|
STARTUP: { min: 5, max: 12 },
|
||||||
|
BUSINESS_CONCEPT: { min: 5, max: 12 },
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quota Resolution
|
||||||
|
|
||||||
|
Per-juror overrides take precedence over group defaults:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function getEffectiveQuotas(
|
||||||
|
member: JuryGroupMember,
|
||||||
|
group: JuryGroup
|
||||||
|
): CategoryQuotas | null {
|
||||||
|
if (member.categoryQuotasOverride) {
|
||||||
|
return member.categoryQuotasOverride as CategoryQuotas;
|
||||||
|
}
|
||||||
|
if (group.defaultCategoryQuotas) {
|
||||||
|
return group.defaultCategoryQuotas as CategoryQuotas;
|
||||||
|
}
|
||||||
|
return null; // no quotas — assign freely
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quota Enforcement During Assignment
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function checkCategoryQuota(
|
||||||
|
member: JuryGroupMember,
|
||||||
|
group: JuryGroup,
|
||||||
|
category: CompetitionCategory,
|
||||||
|
currentCategoryCount: number
|
||||||
|
): { allowed: boolean; warning?: string } {
|
||||||
|
const quotas = getEffectiveQuotas(member, group);
|
||||||
|
if (!quotas) return { allowed: true };
|
||||||
|
|
||||||
|
const categoryQuota = quotas[category];
|
||||||
|
if (!categoryQuota) return { allowed: true };
|
||||||
|
|
||||||
|
if (currentCategoryCount >= categoryQuota.max) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
warning: `Juror has reached max ${categoryQuota.max} for ${category}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMinimumQuotasMet(
|
||||||
|
member: JuryGroupMember,
|
||||||
|
group: JuryGroup,
|
||||||
|
categoryCounts: Record<CompetitionCategory, number>
|
||||||
|
): { met: boolean; deficits: string[] } {
|
||||||
|
const quotas = getEffectiveQuotas(member, group);
|
||||||
|
if (!quotas) return { met: true, deficits: [] };
|
||||||
|
|
||||||
|
const deficits: string[] = [];
|
||||||
|
for (const [category, quota] of Object.entries(quotas)) {
|
||||||
|
const count = categoryCounts[category as CompetitionCategory] ?? 0;
|
||||||
|
if (count < quota.min) {
|
||||||
|
deficits.push(`${category}: ${count}/${quota.min} minimum`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { met: deficits.length === 0, deficits };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Preferred Startup Ratio
|
||||||
|
|
||||||
|
Each juror can express a preference for what percentage of their assignments should be Startups vs Concepts.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// On JuryGroupMember:
|
||||||
|
preferredStartupRatio: Float? // 0.0 to 1.0
|
||||||
|
|
||||||
|
// Usage in assignment algorithm:
|
||||||
|
function calculateRatioAlignmentScore(
|
||||||
|
member: JuryGroupMember,
|
||||||
|
candidateCategory: CompetitionCategory,
|
||||||
|
currentStartupCount: number,
|
||||||
|
currentConceptCount: number
|
||||||
|
): number {
|
||||||
|
const preference = member.preferredStartupRatio;
|
||||||
|
if (preference === null || preference === undefined) return 0; // no preference
|
||||||
|
|
||||||
|
const totalAfterAssignment = currentStartupCount + currentConceptCount + 1;
|
||||||
|
const startupCountAfter = candidateCategory === 'STARTUP'
|
||||||
|
? currentStartupCount + 1
|
||||||
|
: currentStartupCount;
|
||||||
|
const ratioAfter = startupCountAfter / totalAfterAssignment;
|
||||||
|
|
||||||
|
// Score: how close does adding this assignment bring the ratio to preference?
|
||||||
|
const deviation = Math.abs(ratioAfter - preference);
|
||||||
|
// Scale: 0 deviation = 10pts, 0.5 deviation = 0pts
|
||||||
|
return Math.max(0, 10 * (1 - deviation * 2));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This score feeds into the assignment algorithm alongside tag overlap, workload balance, and geo-diversity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Juror Roles
|
||||||
|
|
||||||
|
Each JuryGroupMember has a `role` field:
|
||||||
|
|
||||||
|
| Role | Capabilities | Description |
|
||||||
|
|------|-------------|-------------|
|
||||||
|
| `MEMBER` | Evaluate assigned projects, vote in live finals, confirm winners | Standard jury member |
|
||||||
|
| `CHAIR` | All MEMBER capabilities + view all evaluations, moderate discussions, suggest assignments | Jury chairperson — has broader visibility |
|
||||||
|
| `OBSERVER` | View evaluations (read-only), no scoring or voting | Observes the jury process without participating |
|
||||||
|
|
||||||
|
### Role-Based Visibility
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function getJurorVisibility(
|
||||||
|
role: string,
|
||||||
|
ownAssignments: Assignment[]
|
||||||
|
): VisibilityScope {
|
||||||
|
switch (role) {
|
||||||
|
case 'CHAIR':
|
||||||
|
return {
|
||||||
|
canSeeAllEvaluations: true,
|
||||||
|
canSeeAllAssignments: true,
|
||||||
|
canModerateDiscussions: true,
|
||||||
|
canSuggestReassignments: true,
|
||||||
|
};
|
||||||
|
case 'MEMBER':
|
||||||
|
return {
|
||||||
|
canSeeAllEvaluations: false, // only their own
|
||||||
|
canSeeAllAssignments: false,
|
||||||
|
canModerateDiscussions: false,
|
||||||
|
canSuggestReassignments: false,
|
||||||
|
};
|
||||||
|
case 'OBSERVER':
|
||||||
|
return {
|
||||||
|
canSeeAllEvaluations: true, // read-only
|
||||||
|
canSeeAllAssignments: true,
|
||||||
|
canModerateDiscussions: false,
|
||||||
|
canSuggestReassignments: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Jury Membership
|
||||||
|
|
||||||
|
A single user can be on multiple jury groups. This is common for:
|
||||||
|
- A juror on Jury 2 (finalist selection) AND Innovation Award Jury
|
||||||
|
- A senior juror on Jury 1 AND Jury 3 (semi-finalist + live finals)
|
||||||
|
|
||||||
|
### Overlap Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get all jury groups for a user in a competition
|
||||||
|
async function getUserJuryGroups(
|
||||||
|
userId: string,
|
||||||
|
competitionId: string
|
||||||
|
): Promise<JuryGroupMember[]> {
|
||||||
|
return prisma.juryGroupMember.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
juryGroup: { competitionId },
|
||||||
|
},
|
||||||
|
include: { juryGroup: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is on a specific jury
|
||||||
|
async function isUserOnJury(
|
||||||
|
userId: string,
|
||||||
|
juryGroupId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const member = await prisma.juryGroupMember.findUnique({
|
||||||
|
where: { juryGroupId_userId: { juryGroupId, userId } },
|
||||||
|
});
|
||||||
|
return member !== null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-Jury COI Propagation
|
||||||
|
|
||||||
|
When a juror declares a Conflict of Interest for a project in one jury group, it should propagate to all their jury memberships:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function propagateCOI(
|
||||||
|
userId: string,
|
||||||
|
projectId: string,
|
||||||
|
competitionId: string,
|
||||||
|
reason: string
|
||||||
|
): Promise<void> {
|
||||||
|
// Find all jury groups this user is on
|
||||||
|
const memberships = await getUserJuryGroups(userId, competitionId);
|
||||||
|
|
||||||
|
for (const membership of memberships) {
|
||||||
|
// Find assignments for this user+project in each jury group
|
||||||
|
const assignments = await prisma.assignment.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
juryGroupId: membership.juryGroupId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
// Check if COI already declared
|
||||||
|
const existing = await prisma.conflictOfInterest.findUnique({
|
||||||
|
where: { assignmentId: assignment.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
await prisma.conflictOfInterest.create({
|
||||||
|
data: {
|
||||||
|
assignmentId: assignment.id,
|
||||||
|
reason: `Auto-propagated from ${membership.juryGroup.name}: ${reason}`,
|
||||||
|
declared: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Jury Group Lifecycle
|
||||||
|
|
||||||
|
### States
|
||||||
|
|
||||||
|
```
|
||||||
|
DRAFT → ACTIVE → LOCKED → ARCHIVED
|
||||||
|
```
|
||||||
|
|
||||||
|
| State | Description | Operations Allowed |
|
||||||
|
|-------|-------------|-------------------|
|
||||||
|
| DRAFT | Being configured. Members can be added/removed freely | Add/remove members, edit settings |
|
||||||
|
| ACTIVE | Jury is in use. Assignments are being made or evaluations in progress | Add members (with warning), edit per-juror settings |
|
||||||
|
| LOCKED | Evaluation or voting is in progress. No membership changes | Edit per-juror notes only |
|
||||||
|
| ARCHIVED | Competition complete. Preserved for records | Read-only |
|
||||||
|
|
||||||
|
### State Transitions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Jury group activates when its linked round starts
|
||||||
|
async function activateJuryGroup(juryGroupId: string): Promise<void> {
|
||||||
|
await prisma.juryGroup.update({
|
||||||
|
where: { id: juryGroupId },
|
||||||
|
data: { isActive: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jury group locks when evaluation/voting begins
|
||||||
|
async function lockJuryGroup(juryGroupId: string): Promise<void> {
|
||||||
|
// Prevent membership changes during active evaluation
|
||||||
|
await prisma.juryGroup.update({
|
||||||
|
where: { id: juryGroupId },
|
||||||
|
data: { isActive: false }, // Using isActive as soft-lock; could add separate locked field
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Onboarding
|
||||||
|
|
||||||
|
### Juror Onboarding Flow
|
||||||
|
|
||||||
|
When a juror is added to a JuryGroup, they go through an onboarding process:
|
||||||
|
|
||||||
|
1. **Invitation** — Admin adds juror to group → juror receives email invitation
|
||||||
|
2. **Profile Setup** — Juror fills out expertise tags, language preferences, category preference
|
||||||
|
3. **COI Pre-declaration** — Juror reviews project list and declares any pre-existing conflicts
|
||||||
|
4. **Confirmation** — Juror confirms they understand their role and responsibilities
|
||||||
|
|
||||||
|
### Onboarding UI
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Welcome to Jury 1 — Semi-finalist Evaluation │
|
||||||
|
│ │
|
||||||
|
│ You've been selected to evaluate projects for the │
|
||||||
|
│ Monaco Ocean Protection Challenge 2026. │
|
||||||
|
│ │
|
||||||
|
│ Step 1 of 3: Your Expertise │
|
||||||
|
│ ───────────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ Select your areas of expertise (used for matching): │
|
||||||
|
│ ☑ Marine Biology ☑ Ocean Technology │
|
||||||
|
│ ☐ Renewable Energy ☑ Environmental Policy │
|
||||||
|
│ ☐ Finance/Investment ☐ Social Impact │
|
||||||
|
│ ☐ Data Science ☐ Education │
|
||||||
|
│ │
|
||||||
|
│ Preferred languages: │
|
||||||
|
│ ☑ English ☑ French ☐ Other: [________] │
|
||||||
|
│ │
|
||||||
|
│ Category preference (what % Startups vs Concepts): │
|
||||||
|
│ Startups [====●=========] Concepts │
|
||||||
|
│ 60% / 40% │
|
||||||
|
│ │
|
||||||
|
│ [ Back ] [ Next Step → ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Step 2 of 3: Conflict of Interest Declaration │
|
||||||
|
│ ───────────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ Please review the project list and declare any conflicts │
|
||||||
|
│ of interest. A COI exists if you have a personal, │
|
||||||
|
│ financial, or professional relationship with a project team. │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────┬──────────────────┐ │
|
||||||
|
│ │ Project │ COI? │ │
|
||||||
|
│ ├──────────────────────────────────────┼──────────────────┤ │
|
||||||
|
│ │ OceanClean AI │ ○ None │ │
|
||||||
|
│ │ DeepReef Monitoring │ ● COI Declared │ │
|
||||||
|
│ │ CoralGuard │ ○ None │ │
|
||||||
|
│ │ WaveEnergy Solutions │ ○ None │ │
|
||||||
|
│ │ ... (60 more projects) │ │ │
|
||||||
|
│ └──────────────────────────────────────┴──────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ COI Details for "DeepReef Monitoring": │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Former colleague of team lead. Worked together 2022-23. │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ Back ] [ Next Step → ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Step 3 of 3: Confirmation │
|
||||||
|
│ ───────────────────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ By confirming, you agree to: │
|
||||||
|
│ ☑ Evaluate assigned projects fairly and impartially │
|
||||||
|
│ ☑ Complete evaluations by the deadline │
|
||||||
|
│ ☑ Maintain confidentiality of all submissions │
|
||||||
|
│ ☑ Report any additional conflicts of interest │
|
||||||
|
│ │
|
||||||
|
│ Your assignments: up to 20 projects │
|
||||||
|
│ Evaluation deadline: March 15, 2026 │
|
||||||
|
│ Category target: ~12 Startups / ~8 Concepts │
|
||||||
|
│ │
|
||||||
|
│ [ Back ] [ ✓ Confirm & Start ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Jury Management
|
||||||
|
|
||||||
|
### Jury Group Dashboard
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Jury Groups — MOPC 2026 │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Jury 1 — Semi-finalist Selection [Edit]│ │
|
||||||
|
│ │ Members: 8 | Linked to: Round 3 | Status: ACTIVE │ │
|
||||||
|
│ │ Cap: 20 (SOFT +2) | Avg load: 15.3 projects │ │
|
||||||
|
│ │ ████████████████░░░░ 76% assignments complete │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Jury 2 — Finalist Selection [Edit]│ │
|
||||||
|
│ │ Members: 6 | Linked to: Round 5 | Status: DRAFT │ │
|
||||||
|
│ │ Cap: 15 (HARD) | Not yet assigned │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Jury 3 — Live Finals [Edit]│ │
|
||||||
|
│ │ Members: 5 | Linked to: Round 7, Round 8 | Status: DRAFT │ │
|
||||||
|
│ │ All finalists assigned to all jurors │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Innovation Award Jury [Edit]│ │
|
||||||
|
│ │ Members: 4 | Linked to: Innovation Award | Status: DRAFT │ │
|
||||||
|
│ │ Shares 2 members with Jury 2 │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ + Create Jury Group ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Member Management
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Jury 1 — Member Management │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Group Defaults: Max 20 | SOFT cap (+2) | Quotas: S:5-12 C:5-12│
|
||||||
|
│ │
|
||||||
|
│ ┌──────┬──────────────┬──────┬─────┬──────┬──────┬──────────┐ │
|
||||||
|
│ │ Role │ Name │ Load │ Cap │ S/C │ Pref │ Actions │ │
|
||||||
|
│ ├──────┼──────────────┼──────┼─────┼──────┼──────┼──────────┤ │
|
||||||
|
│ │ CHAIR│ Dr. Martin │ 18 │ 20S │ 11/7 │ 60% │ [Edit] │ │
|
||||||
|
│ │ MEMBER│ Prof. Dubois│ 15 │ 20S │ 9/6 │ 50% │ [Edit] │ │
|
||||||
|
│ │ MEMBER│ Ms. Chen │ 20 │ 20H │ 12/8 │ 60% │ [Edit] │ │
|
||||||
|
│ │ MEMBER│ Dr. Patel │ 12 │ 15* │ 7/5 │ — │ [Edit] │ │
|
||||||
|
│ │ MEMBER│ Mr. Silva │ 16 │ 20S │ 10/6 │ 70% │ [Edit] │ │
|
||||||
|
│ │ MEMBER│ Dr. Yamada │ 19 │ 20S │ 11/8 │ 55% │ [Edit] │ │
|
||||||
|
│ │ MEMBER│ Ms. Hansen │ 14 │ 20S │ 8/6 │ — │ [Edit] │ │
|
||||||
|
│ │ OBS │ Mr. Berger │ — │ — │ — │ — │ [Edit] │ │
|
||||||
|
│ └──────┴──────────────┴──────┴─────┴──────┴──────┴──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ * = per-juror override S = SOFT H = HARD │
|
||||||
|
│ S/C = Startup/Concept count Pref = preferred startup ratio │
|
||||||
|
│ │
|
||||||
|
│ [ + Add Member ] [ Import from CSV ] [ Run AI Assignment ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Juror Override Sheet
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Edit Juror Settings — Dr. Patel │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Role: [MEMBER ▼] │
|
||||||
|
│ │
|
||||||
|
│ ── Assignment Overrides ────────────────────────────────────── │
|
||||||
|
│ (Leave blank to use group defaults) │
|
||||||
|
│ │
|
||||||
|
│ Max assignments: [15 ] (group default: 20) │
|
||||||
|
│ Cap mode: [HARD ▼] (group default: SOFT) │
|
||||||
|
│ │
|
||||||
|
│ Category quotas: │
|
||||||
|
│ Startups: min [3 ] max [10] (group: 5-12) │
|
||||||
|
│ Concepts: min [3 ] max [8 ] (group: 5-12) │
|
||||||
|
│ │
|
||||||
|
│ ── Preferences ─────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ Preferred startup ratio: [ ] % (blank = no preference) │
|
||||||
|
│ Expertise tags: [marine-biology, policy, ...] │
|
||||||
|
│ Language: [English, French] │
|
||||||
|
│ │
|
||||||
|
│ ── Notes ───────────────────────────────────────────────────── │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Dr. Patel requested reduced load due to conference │ │
|
||||||
|
│ │ schedule in March. Hard cap at 15. │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ Cancel ] [ Save Changes ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with Assignment Algorithm
|
||||||
|
|
||||||
|
The assignment algorithm (see `06-round-evaluation.md`) uses JuryGroup data at every step:
|
||||||
|
|
||||||
|
### Algorithm Input
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AssignmentInput = {
|
||||||
|
roundId: string;
|
||||||
|
juryGroupId: string;
|
||||||
|
projects: Project[];
|
||||||
|
config: {
|
||||||
|
requiredReviewsPerProject: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Algorithm Steps Using JuryGroup
|
||||||
|
|
||||||
|
1. **Load jury members** — Fetch all active JuryGroupMembers with role != OBSERVER
|
||||||
|
2. **Resolve effective limits** — For each member, compute effective cap and quotas
|
||||||
|
3. **Filter by COI** — Exclude members with declared COI for each project
|
||||||
|
4. **Score candidates** — For each (project, juror) pair, compute:
|
||||||
|
- Tag overlap score (expertise alignment)
|
||||||
|
- Workload balance score (prefer jurors with fewer assignments)
|
||||||
|
- Category ratio alignment score (prefer assignment that brings ratio closer to preference)
|
||||||
|
- Geo-diversity score
|
||||||
|
5. **Apply caps** — Skip jurors who have reached their effective cap
|
||||||
|
6. **Apply quotas** — Skip jurors who have reached category max
|
||||||
|
7. **Rank and assign** — Greedily assign top-scoring pairs
|
||||||
|
8. **Validate minimums** — Check if category minimums are met; warn admin if not
|
||||||
|
|
||||||
|
### Assignment Preview
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AssignmentPreview = {
|
||||||
|
assignments: {
|
||||||
|
userId: string;
|
||||||
|
projectId: string;
|
||||||
|
score: number;
|
||||||
|
breakdown: {
|
||||||
|
tagOverlap: number;
|
||||||
|
workloadBalance: number;
|
||||||
|
ratioAlignment: number;
|
||||||
|
geoDiversity: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
|
||||||
|
warnings: {
|
||||||
|
type: 'CAP_EXCEEDED' | 'QUOTA_UNMET' | 'COI_SKIP' | 'UNASSIGNED_PROJECT';
|
||||||
|
message: string;
|
||||||
|
userId?: string;
|
||||||
|
projectId?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
stats: {
|
||||||
|
totalAssignments: number;
|
||||||
|
avgLoadPerJuror: number;
|
||||||
|
minLoad: number;
|
||||||
|
maxLoad: number;
|
||||||
|
unassignedProjects: number;
|
||||||
|
categoryDistribution: Record<string, { avg: number; min: number; max: number }>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Procedures
|
||||||
|
|
||||||
|
### New tRPC Router: jury-group.ts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const juryGroupRouter = router({
|
||||||
|
// ── CRUD ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Create a new jury group */
|
||||||
|
create: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
competitionId: z.string(),
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
description: z.string().optional(),
|
||||||
|
defaultMaxAssignments: z.number().int().min(1).default(20),
|
||||||
|
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
|
||||||
|
softCapBuffer: z.number().int().min(0).default(2),
|
||||||
|
defaultCategoryQuotas: z.record(z.object({
|
||||||
|
min: z.number().int().min(0),
|
||||||
|
max: z.number().int().min(0),
|
||||||
|
})).optional(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Update jury group settings */
|
||||||
|
update: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
name: z.string().min(1).max(100).optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
defaultMaxAssignments: z.number().int().min(1).optional(),
|
||||||
|
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
|
||||||
|
softCapBuffer: z.number().int().min(0).optional(),
|
||||||
|
defaultCategoryQuotas: z.record(z.object({
|
||||||
|
min: z.number().int().min(0),
|
||||||
|
max: z.number().int().min(0),
|
||||||
|
})).nullable().optional(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Delete jury group (only if DRAFT and no assignments) */
|
||||||
|
delete: adminProcedure
|
||||||
|
.input(z.object({ juryGroupId: z.string() }))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Get jury group with members */
|
||||||
|
getById: protectedProcedure
|
||||||
|
.input(z.object({ juryGroupId: z.string() }))
|
||||||
|
.query(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** List all jury groups for a competition */
|
||||||
|
listByCompetition: protectedProcedure
|
||||||
|
.input(z.object({ competitionId: z.string() }))
|
||||||
|
.query(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
// ── Members ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Add a member to the jury group */
|
||||||
|
addMember: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
role: z.enum(['MEMBER', 'CHAIR', 'OBSERVER']).default('MEMBER'),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Remove a member from the jury group */
|
||||||
|
removeMember: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Batch add members (from CSV or user selection) */
|
||||||
|
addMembersBatch: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
members: z.array(z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
role: z.enum(['MEMBER', 'CHAIR', 'OBSERVER']).default('MEMBER'),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Update member settings (overrides, preferences) */
|
||||||
|
updateMember: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
role: z.enum(['MEMBER', 'CHAIR', 'OBSERVER']).optional(),
|
||||||
|
maxAssignmentsOverride: z.number().int().min(1).nullable().optional(),
|
||||||
|
capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).nullable().optional(),
|
||||||
|
categoryQuotasOverride: z.record(z.object({
|
||||||
|
min: z.number().int().min(0),
|
||||||
|
max: z.number().int().min(0),
|
||||||
|
})).nullable().optional(),
|
||||||
|
preferredStartupRatio: z.number().min(0).max(1).nullable().optional(),
|
||||||
|
expertiseTags: z.array(z.string()).optional(),
|
||||||
|
languagePreferences: z.array(z.string()).optional(),
|
||||||
|
notes: z.string().nullable().optional(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
// ── Queries ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get all jury groups a user belongs to */
|
||||||
|
getMyJuryGroups: juryProcedure
|
||||||
|
.query(async ({ ctx }) => { ... }),
|
||||||
|
|
||||||
|
/** Get assignment stats for a jury group */
|
||||||
|
getAssignmentStats: adminProcedure
|
||||||
|
.input(z.object({ juryGroupId: z.string() }))
|
||||||
|
.query(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
/** Check if a user can be added (no duplicate, role compatible) */
|
||||||
|
checkMemberEligibility: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => { ... }),
|
||||||
|
|
||||||
|
// ── Onboarding ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Get onboarding status for a juror */
|
||||||
|
getOnboardingStatus: juryProcedure
|
||||||
|
.input(z.object({ juryGroupId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => { ... }),
|
||||||
|
|
||||||
|
/** Submit onboarding form (preferences, COI declarations) */
|
||||||
|
submitOnboarding: juryProcedure
|
||||||
|
.input(z.object({
|
||||||
|
juryGroupId: z.string(),
|
||||||
|
expertiseTags: z.array(z.string()),
|
||||||
|
languagePreferences: z.array(z.string()),
|
||||||
|
preferredStartupRatio: z.number().min(0).max(1).optional(),
|
||||||
|
coiDeclarations: z.array(z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
reason: z.string(),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ ctx, input }) => { ... }),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Service Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/server/services/jury-group.ts
|
||||||
|
|
||||||
|
/** Create a jury group with defaults */
|
||||||
|
export async function createJuryGroup(
|
||||||
|
competitionId: string,
|
||||||
|
name: string,
|
||||||
|
config?: Partial<JuryGroupConfig>
|
||||||
|
): Promise<JuryGroup>;
|
||||||
|
|
||||||
|
/** Get effective limits for a member (resolved overrides) */
|
||||||
|
export async function getEffectiveLimits(
|
||||||
|
member: JuryGroupMember,
|
||||||
|
group: JuryGroup
|
||||||
|
): Promise<{ maxAssignments: number | null; capMode: CapMode; quotas: CategoryQuotas | null }>;
|
||||||
|
|
||||||
|
/** Check if a juror can receive more assignments */
|
||||||
|
export async function canAssignMore(
|
||||||
|
userId: string,
|
||||||
|
juryGroupId: string,
|
||||||
|
category?: CompetitionCategory
|
||||||
|
): Promise<{ allowed: boolean; reason?: string }>;
|
||||||
|
|
||||||
|
/** Get assignment statistics for the whole group */
|
||||||
|
export async function getGroupAssignmentStats(
|
||||||
|
juryGroupId: string
|
||||||
|
): Promise<GroupStats>;
|
||||||
|
|
||||||
|
/** Propagate COI across all jury groups for a user */
|
||||||
|
export async function propagateCOI(
|
||||||
|
userId: string,
|
||||||
|
projectId: string,
|
||||||
|
competitionId: string,
|
||||||
|
reason: string
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
/** Get all active members (excluding observers) for assignment */
|
||||||
|
export async function getAssignableMembers(
|
||||||
|
juryGroupId: string
|
||||||
|
): Promise<JuryGroupMember[]>;
|
||||||
|
|
||||||
|
/** Validate group readiness (enough members, all onboarded, etc.) */
|
||||||
|
export async function validateGroupReadiness(
|
||||||
|
juryGroupId: string
|
||||||
|
): Promise<{ ready: boolean; issues: string[] }>;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
| Scenario | Handling |
|
||||||
|
|----------|----------|
|
||||||
|
| **Juror added to group during active evaluation** | Allowed with admin warning. New juror gets no existing assignments (must run assignment again) |
|
||||||
|
| **Juror removed from group during active evaluation** | Blocked if juror has pending evaluations. Must reassign first |
|
||||||
|
| **All jurors at cap but projects remain unassigned** | Warning shown to admin. Suggest increasing caps or adding jurors |
|
||||||
|
| **Category quota min not met for any juror** | Warning shown in assignment preview. Admin can proceed or adjust |
|
||||||
|
| **Juror on 3+ jury groups** | Supported. Each membership independent. Cross-jury COI propagation ensures consistency |
|
||||||
|
| **Jury Chair also has assignments** | Allowed. Chair is a regular evaluator with extra visibility |
|
||||||
|
| **Observer tries to submit evaluation** | Blocked at procedure level (OBSERVER role excluded from evaluation mutations) |
|
||||||
|
| **Admin deletes jury group with active assignments** | Blocked. Must complete or reassign all assignments first |
|
||||||
|
| **Juror preference ratio impossible** | (e.g., 90% startups but only 20% projects are startups) — Warn in onboarding, treat as best-effort |
|
||||||
|
| **Same user added twice to same group** | Blocked by unique constraint on [juryGroupId, userId] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
### Inbound
|
||||||
|
|
||||||
|
| Source | Data | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| Competition setup wizard | Group config | Create jury groups during competition setup |
|
||||||
|
| User management | User records | Add jurors as members |
|
||||||
|
| COI declarations | Conflict records | Filter assignments, propagate across groups |
|
||||||
|
|
||||||
|
### Outbound
|
||||||
|
|
||||||
|
| Target | Data | Purpose |
|
||||||
|
|--------|------|---------|
|
||||||
|
| Assignment algorithm | Members, caps, quotas | Generate assignments |
|
||||||
|
| Evaluation rounds | Jury membership | Determine who evaluates what |
|
||||||
|
| Live finals | Jury 3 members | Live voting access |
|
||||||
|
| Confirmation round | Jury members | Who must approve winner proposal |
|
||||||
|
| Special awards | Award jury members | Award evaluation access |
|
||||||
|
| Notifications | Member list | Send round-specific emails to jury |
|
||||||
|
|
||||||
|
### JuryGroup → Round Linkage
|
||||||
|
|
||||||
|
Each evaluation or live-final round links to exactly one JuryGroup:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model Round {
|
||||||
|
// ...
|
||||||
|
juryGroupId String?
|
||||||
|
juryGroup JuryGroup? @relation(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means:
|
||||||
|
- Round 3 (EVALUATION) → Jury 1
|
||||||
|
- Round 5 (EVALUATION) → Jury 2
|
||||||
|
- Round 7 (LIVE_FINAL) → Jury 3
|
||||||
|
- Round 8 (CONFIRMATION) → Jury 3 (same group, different round)
|
||||||
|
|
||||||
|
A jury group can be linked to multiple rounds (e.g., Jury 3 handles both live finals and confirmation).
|
||||||
2898
docs/claude-architecture-redesign/13-notifications-deadlines.md
Normal file
2898
docs/claude-architecture-redesign/13-notifications-deadlines.md
Normal file
File diff suppressed because it is too large
Load Diff
3384
docs/claude-architecture-redesign/14-ai-services.md
Normal file
3384
docs/claude-architecture-redesign/14-ai-services.md
Normal file
File diff suppressed because it is too large
Load Diff
761
docs/claude-architecture-redesign/15-admin-ui.md
Normal file
761
docs/claude-architecture-redesign/15-admin-ui.md
Normal file
@@ -0,0 +1,761 @@
|
|||||||
|
# Admin UI Redesign
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The admin interface is the control plane for the entire MOPC competition. It must surface the redesigned Competition→Round model, jury group management, multi-round submissions, mentoring oversight, and winner confirmation — all through an intuitive, efficient interface.
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
| Principle | Application |
|
||||||
|
|-----------|-------------|
|
||||||
|
| **Progressive disclosure** | Show essentials first; details on drill-down |
|
||||||
|
| **Linear-first navigation** | Round list is a flat, ordered timeline — not nested trees |
|
||||||
|
| **Status at a glance** | Color-coded badges, progress bars, countdowns on every card |
|
||||||
|
| **Override everywhere** | Every automated decision has an admin override within reach |
|
||||||
|
| **Audit transparency** | Every action logged; audit trail accessible from any entity |
|
||||||
|
|
||||||
|
### Tech Stack (UI)
|
||||||
|
|
||||||
|
- **Framework:** Next.js 15 App Router (Server Components default, `'use client'` where needed)
|
||||||
|
- **Styling:** Tailwind CSS 4, mobile-first breakpoints (`md:`, `lg:`)
|
||||||
|
- **Components:** shadcn/ui as base (Button, Card, Dialog, Sheet, Table, Tabs, Select, etc.)
|
||||||
|
- **Data fetching:** tRPC React Query hooks (`trpc.competition.getById.useQuery()`)
|
||||||
|
- **Brand:** Primary Red `#de0f1e`, Dark Blue `#053d57`, White `#fefefe`, Teal `#557f8c`
|
||||||
|
- **Typography:** Montserrat (600/700 headings, 300/400 body)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Admin UI Audit
|
||||||
|
|
||||||
|
### Existing Pages
|
||||||
|
|
||||||
|
```
|
||||||
|
/admin/
|
||||||
|
├── page.tsx — Dashboard (stats cards, quick actions)
|
||||||
|
├── rounds/
|
||||||
|
│ ├── pipelines/page.tsx — Pipeline list
|
||||||
|
│ ├── new-pipeline/page.tsx — Create new pipeline
|
||||||
|
│ └── pipeline/[id]/
|
||||||
|
│ ├── page.tsx — Pipeline detail (tracks + stages)
|
||||||
|
│ ├── edit/page.tsx — Edit pipeline settings
|
||||||
|
│ ├── wizard/page.tsx — Pipeline setup wizard
|
||||||
|
│ └── advanced/page.tsx — Advanced config (JSON editor)
|
||||||
|
├── awards/
|
||||||
|
│ ├── page.tsx — Award list
|
||||||
|
│ ├── new/page.tsx — Create award
|
||||||
|
│ └── [id]/
|
||||||
|
│ ├── page.tsx — Award detail
|
||||||
|
│ └── edit/page.tsx — Edit award
|
||||||
|
├── members/
|
||||||
|
│ ├── page.tsx — User list
|
||||||
|
│ ├── invite/page.tsx — Invite user
|
||||||
|
│ └── [id]/page.tsx — User detail
|
||||||
|
├── mentors/
|
||||||
|
│ ├── page.tsx — Mentor list
|
||||||
|
│ └── [id]/page.tsx — Mentor detail
|
||||||
|
├── projects/ — Project management
|
||||||
|
├── audit/page.tsx — Audit log viewer
|
||||||
|
├── messages/
|
||||||
|
│ ├── page.tsx — Message center
|
||||||
|
│ └── templates/page.tsx — Email templates
|
||||||
|
├── programs/ — Program management
|
||||||
|
├── settings/ — System settings
|
||||||
|
├── reports/ — Reports
|
||||||
|
├── partners/ — Partner management
|
||||||
|
└── learning/ — Learning resources
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Limitations
|
||||||
|
|
||||||
|
| Page | Limitation |
|
||||||
|
|------|-----------|
|
||||||
|
| Pipeline list | Shows pipelines as opaque cards. No inline status |
|
||||||
|
| Pipeline detail | Nested Track→Stage tree is confusing. Must drill into each stage |
|
||||||
|
| Pipeline wizard | Generic JSON config per stage type. Not type-aware |
|
||||||
|
| Award management | Awards are separate from pipeline. No jury group link |
|
||||||
|
| Member management | No jury group concept. Can't see "Jury 1 members" |
|
||||||
|
| Mentor oversight | Basic list only. No workspace visibility |
|
||||||
|
| No confirmation UI | Winner confirmation doesn't exist |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redesigned Navigation
|
||||||
|
|
||||||
|
### New Admin Sitemap
|
||||||
|
|
||||||
|
```
|
||||||
|
/admin/
|
||||||
|
├── page.tsx — Dashboard (competition overview)
|
||||||
|
├── competition/
|
||||||
|
│ ├── page.tsx — Competition list
|
||||||
|
│ ├── new/page.tsx — Create competition wizard
|
||||||
|
│ └── [id]/
|
||||||
|
│ ├── page.tsx — Competition dashboard (round timeline)
|
||||||
|
│ ├── settings/page.tsx — Competition-wide settings
|
||||||
|
│ ├── rounds/
|
||||||
|
│ │ ├── page.tsx — All rounds (timeline view)
|
||||||
|
│ │ ├── new/page.tsx — Add round
|
||||||
|
│ │ └── [roundId]/
|
||||||
|
│ │ ├── page.tsx — Round detail (type-specific view)
|
||||||
|
│ │ ├── edit/page.tsx — Edit round config
|
||||||
|
│ │ ├── projects/page.tsx — Projects in this round
|
||||||
|
│ │ ├── assignments/page.tsx — Assignments (EVALUATION rounds)
|
||||||
|
│ │ ├── filtering/page.tsx — Filtering dashboard (FILTERING)
|
||||||
|
│ │ ├── submissions/page.tsx — Submission status (INTAKE/SUBMISSION)
|
||||||
|
│ │ ├── mentoring/page.tsx — Mentoring overview (MENTORING)
|
||||||
|
│ │ ├── stage-manager/page.tsx — Live stage manager (LIVE_FINAL)
|
||||||
|
│ │ └── confirmation/page.tsx — Confirmation (CONFIRMATION)
|
||||||
|
│ ├── jury-groups/
|
||||||
|
│ │ ├── page.tsx — All jury groups
|
||||||
|
│ │ ├── new/page.tsx — Create jury group
|
||||||
|
│ │ └── [groupId]/
|
||||||
|
│ │ ├── page.tsx — Jury group detail + members
|
||||||
|
│ │ └── edit/page.tsx — Edit group settings
|
||||||
|
│ ├── submission-windows/
|
||||||
|
│ │ ├── page.tsx — All submission windows
|
||||||
|
│ │ └── [windowId]/
|
||||||
|
│ │ ├── page.tsx — Window detail + requirements
|
||||||
|
│ │ └── edit/page.tsx — Edit window
|
||||||
|
│ ├── awards/
|
||||||
|
│ │ ├── page.tsx — Special awards for this competition
|
||||||
|
│ │ ├── new/page.tsx — Create award
|
||||||
|
│ │ └── [awardId]/
|
||||||
|
│ │ ├── page.tsx — Award detail
|
||||||
|
│ │ └── edit/page.tsx — Edit award
|
||||||
|
│ └── results/
|
||||||
|
│ └── page.tsx — Final results + export
|
||||||
|
├── members/ — User management (unchanged)
|
||||||
|
├── audit/page.tsx — Audit log (enhanced)
|
||||||
|
├── messages/ — Messaging (unchanged)
|
||||||
|
├── programs/ — Program management
|
||||||
|
└── settings/ — System settings
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Competition Dashboard
|
||||||
|
|
||||||
|
The central hub for managing a competition. Replaces the old Pipeline detail page.
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ MOPC 2026 Competition Status: ACTIVE [Edit] │
|
||||||
|
│ Program: Monaco Ocean Protection Challenge 2026 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ── Quick Stats ──────────────────────────────────────────────────── │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────────────┐ │
|
||||||
|
│ │ 127 │ │ 23 │ │ 8 │ │ Round 3 of 8 │ │
|
||||||
|
│ │ Applications│ │ Advancing │ │ Jury Groups│ │ Jury 1 Evaluation │ │
|
||||||
|
│ │ │ │ │ │ 22 members │ │ ███████░░░ 68% │ │
|
||||||
|
│ └────────────┘ └────────────┘ └────────────┘ └────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ── Round Timeline ───────────────────────────────────────────────── │
|
||||||
|
│ │
|
||||||
|
│ ✓ R1 ✓ R2 ● R3 ○ R4 ○ R5 ○ R6 ○ R7 ○ R8 │
|
||||||
|
│ Intake Filter Jury 1 Submn 2 Jury 2 Mentor Finals Confirm │
|
||||||
|
│ DONE DONE ACTIVE PENDING PENDING PENDING PENDING PENDING │
|
||||||
|
│ 127 98→23 23/23 │ │
|
||||||
|
│ eval'd │ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Round 3: Jury 1 — Semi-finalist Selection [Manage →] │ │
|
||||||
|
│ │ Type: EVALUATION | Jury: Jury 1 (8 members) │ │
|
||||||
|
│ │ Status: ACTIVE | Started: Feb 1 | Deadline: Mar 15 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ████████████████████████████████████░░░░░░░░░░░░ 68% │ │
|
||||||
|
│ │ Evaluations: 186 / 276 complete │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌──────────────┬──────────────┬──────────────┬────────────┐ │ │
|
||||||
|
│ │ │ Assigned: 276│ Complete: 186│ Pending: 90 │ COI: 12 │ │ │
|
||||||
|
│ │ └──────────────┴──────────────┴──────────────┴────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [ View Assignments ] [ View Results ] [ Advance Projects ] │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ── Sidebar: Jury Groups ─────────────────────────────────────────── │
|
||||||
|
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Jury 1 (8 members) [→] │ │ Jury 2 (6 members) [→] │ │
|
||||||
|
│ │ Avg load: 15.3 / 20 │ │ Not yet assigned │ │
|
||||||
|
│ │ ████████████████░░░░ │ │ ░░░░░░░░░░░░░░░░░░░░ │ │
|
||||||
|
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
||||||
|
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
|
||||||
|
│ │ Jury 3 (5 members) [→] │ │ Innovation Jury (4) [→] │ │
|
||||||
|
│ │ Assigned to R7 + R8 │ │ Award jury │ │
|
||||||
|
│ └─────────────────────────────┘ └─────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ── Sidebar: Special Awards ──────────────────────────────────────── │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Innovation Award STAY_IN_MAIN Jury: Innovation Jury [→] │ │
|
||||||
|
│ │ Impact Award SEPARATE_POOL Jury: Impact Jury [→] │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
| Component | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `<QuickStatsGrid>` | 4 stat cards showing key metrics |
|
||||||
|
| `<RoundTimeline>` | Horizontal timeline with round status badges |
|
||||||
|
| `<ActiveRoundCard>` | Expanded card for the currently active round |
|
||||||
|
| `<JuryGroupCards>` | Grid of jury group summary cards |
|
||||||
|
| `<AwardSidebar>` | List of special awards with status |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Competition Setup Wizard
|
||||||
|
|
||||||
|
Replaces the old Pipeline Wizard. A multi-step form that creates the entire competition structure.
|
||||||
|
|
||||||
|
### Wizard Steps
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: Basics → Competition name, program, categories
|
||||||
|
Step 2: Round Builder → Add/reorder rounds (type picker)
|
||||||
|
Step 3: Jury Groups → Create jury groups, assign to rounds
|
||||||
|
Step 4: Submission Windows → Define file requirements per window
|
||||||
|
Step 5: Special Awards → Configure awards (optional)
|
||||||
|
Step 6: Notifications → Deadline reminders, email settings
|
||||||
|
Step 7: Review & Create → Summary of everything, create button
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1: Basics
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Create Competition — Step 1 of 7: Basics │
|
||||||
|
│ ●───○───○───○───○───○───○ │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Competition Name: │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ MOPC 2026 Competition │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Program: [MOPC 2026 ▼] │
|
||||||
|
│ │
|
||||||
|
│ Category Mode: │
|
||||||
|
│ ● Shared — Both Startups and Concepts in same flow │
|
||||||
|
│ ○ Split — Separate finalist counts per category │
|
||||||
|
│ │
|
||||||
|
│ Finalist Counts: │
|
||||||
|
│ Startups: [3 ] Concepts: [3 ] │
|
||||||
|
│ │
|
||||||
|
│ [ Cancel ] [ Next → ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Round Builder
|
||||||
|
|
||||||
|
The core of the wizard — a drag-and-drop round sequencer.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Create Competition — Step 2 of 7: Round Builder │
|
||||||
|
│ ○───●───○───○───○───○───○ │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Build your competition flow by adding rounds: │
|
||||||
|
│ │
|
||||||
|
│ ┌────┬──────────────────────────────┬──────────────┬────────┐ │
|
||||||
|
│ │ # │ Round │ Type │ Actions│ │
|
||||||
|
│ ├────┼──────────────────────────────┼──────────────┼────────┤ │
|
||||||
|
│ │ 1 │ ≡ Application Window │ INTAKE │ ✎ ✕ │ │
|
||||||
|
│ │ 2 │ ≡ AI Screening │ FILTERING │ ✎ ✕ │ │
|
||||||
|
│ │ 3 │ ≡ Jury 1 - Semi-finalist │ EVALUATION │ ✎ ✕ │ │
|
||||||
|
│ │ 4 │ ≡ Semi-finalist Documents │ SUBMISSION │ ✎ ✕ │ │
|
||||||
|
│ │ 5 │ ≡ Jury 2 - Finalist │ EVALUATION │ ✎ ✕ │ │
|
||||||
|
│ │ 6 │ ≡ Finalist Mentoring │ MENTORING │ ✎ ✕ │ │
|
||||||
|
│ │ 7 │ ≡ Live Finals │ LIVE_FINAL │ ✎ ✕ │ │
|
||||||
|
│ │ 8 │ ≡ Confirm Winners │ CONFIRMATION │ ✎ ✕ │ │
|
||||||
|
│ └────┴──────────────────────────────┴──────────────┴────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ + Add Round ] │
|
||||||
|
│ │
|
||||||
|
│ Available Round Types: │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||||
|
│ │ INTAKE │ │ FILTERING │ │ EVALUATION │ │ SUBMISSION │ │
|
||||||
|
│ │ Collect │ │ AI screen │ │ Jury score │ │ More docs │ │
|
||||||
|
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
|
||||||
|
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||||
|
│ │ MENTORING │ │ LIVE_FINAL │ │ CONFIRM │ │
|
||||||
|
│ │ Workspace │ │ Live vote │ │ Cement │ │
|
||||||
|
│ └────────────┘ └────────────┘ └────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ ← Back ] [ Next → ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Round Config Sheet
|
||||||
|
|
||||||
|
When clicking ✎ on a round, a sheet slides out with type-specific config:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Configure Round: Jury 1 - Semi-finalist (EVALUATION) │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Round Name: [Jury 1 - Semi-finalist Selection ] │
|
||||||
|
│ │
|
||||||
|
│ ── Jury Group ──────────────────────────────────────────────── │
|
||||||
|
│ Assign jury group: [Jury 1 ▼] [ + Create New ] │
|
||||||
|
│ │
|
||||||
|
│ ── Assignment ──────────────────────────────────────────────── │
|
||||||
|
│ Reviews per project: [3 ] │
|
||||||
|
│ (Caps and quotas configured on the jury group) │
|
||||||
|
│ │
|
||||||
|
│ ── Scoring ─────────────────────────────────────────────────── │
|
||||||
|
│ Evaluation form: [Standard Criteria Form ▼] │
|
||||||
|
│ Scoring mode: ● Criteria-based ○ Global score ○ Binary │
|
||||||
|
│ Score range: [1 ] to [10] │
|
||||||
|
│ │
|
||||||
|
│ ── Document Visibility ─────────────────────────────────────── │
|
||||||
|
│ This round can see docs from: │
|
||||||
|
│ ☑ Window 1: Application Documents │
|
||||||
|
│ ☐ Window 2: Semi-finalist Documents (not yet created) │
|
||||||
|
│ │
|
||||||
|
│ ── Advancement ─────────────────────────────────────────────── │
|
||||||
|
│ Advancement mode: │
|
||||||
|
│ ● Top N by score │
|
||||||
|
│ ○ Admin selection │
|
||||||
|
│ ○ AI recommended │
|
||||||
|
│ Advance top: [8 ] projects per category │
|
||||||
|
│ │
|
||||||
|
│ ── Deadline ────────────────────────────────────────────────── │
|
||||||
|
│ Start date: [Feb 1, 2026 ] │
|
||||||
|
│ End date: [Mar 15, 2026] │
|
||||||
|
│ │
|
||||||
|
│ [ Cancel ] [ Save Round ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Jury Groups
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Create Competition — Step 3 of 7: Jury Groups │
|
||||||
|
│ ○───○───●───○───○───○───○ │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Jury 1 — Semi-finalist Selection [Edit]│ │
|
||||||
|
│ │ Linked to: Round 3 │ │
|
||||||
|
│ │ Members: 0 (add after creation) │ │
|
||||||
|
│ │ Default cap: 20 (SOFT +2) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Jury 2 — Finalist Selection [Edit]│ │
|
||||||
|
│ │ Linked to: Round 5 │ │
|
||||||
|
│ │ Members: 0 (add after creation) │ │
|
||||||
|
│ │ Default cap: 15 (HARD) │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Jury 3 — Live Finals + Confirmation [Edit]│ │
|
||||||
|
│ │ Linked to: Round 7, Round 8 │ │
|
||||||
|
│ │ Members: 0 (add after creation) │ │
|
||||||
|
│ │ All finalists auto-assigned │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ + Create Jury Group ] │
|
||||||
|
│ │
|
||||||
|
│ Note: Add members to jury groups after competition is created. │
|
||||||
|
│ │
|
||||||
|
│ [ ← Back ] [ Next → ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Submission Windows
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Create Competition — Step 4 of 7: Submission Windows │
|
||||||
|
│ ○───○───○───●───○───○───○ │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Window 1: Application Documents (linked to Round 1) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ File Requirements: │ │
|
||||||
|
│ │ 1. Executive Summary (PDF, max 5MB, required) │ │
|
||||||
|
│ │ 2. Business Plan (PDF, max 20MB, required) │ │
|
||||||
|
│ │ 3. Team Bios (PDF, max 5MB, required) │ │
|
||||||
|
│ │ 4. Supporting Documents (any, max 50MB, optional) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Deadline: Jan 31, 2026 | Policy: GRACE (30 min) │ │
|
||||||
|
│ │ [ + Add Requirement ] [Edit] │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Window 2: Semi-finalist Documents (linked to Round 4) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ File Requirements: │ │
|
||||||
|
│ │ 1. Updated Business Plan (PDF, max 20MB, required) │ │
|
||||||
|
│ │ 2. Video Pitch (MP4, max 500MB, required) │ │
|
||||||
|
│ │ 3. Financial Projections (PDF/XLSX, max 10MB, required) │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ Deadline: Apr 30, 2026 | Policy: HARD │ │
|
||||||
|
│ │ [ + Add Requirement ] [Edit] │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ + Add Submission Window ] │
|
||||||
|
│ [ ← Back ] [ Next → ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Review & Create
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Create Competition — Step 7 of 7: Review │
|
||||||
|
│ ○───○───○───○───○───○───● │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Competition: MOPC 2026 Competition │
|
||||||
|
│ Category Mode: SHARED (3 Startups + 3 Concepts) │
|
||||||
|
│ │
|
||||||
|
│ Rounds (8): │
|
||||||
|
│ 1. Application Window (INTAKE) ─── Window 1 │
|
||||||
|
│ 2. AI Screening (FILTERING) │
|
||||||
|
│ 3. Jury 1 (EVALUATION) ─── Jury 1 │
|
||||||
|
│ 4. Semi-finalist Docs (SUBMISSION) ─── Window 2 │
|
||||||
|
│ 5. Jury 2 (EVALUATION) ─── Jury 2 │
|
||||||
|
│ 6. Mentoring (MENTORING) │
|
||||||
|
│ 7. Live Finals (LIVE_FINAL) ─── Jury 3 │
|
||||||
|
│ 8. Confirm Winners (CONFIRMATION) ─── Jury 3 │
|
||||||
|
│ │
|
||||||
|
│ Jury Groups (3): Jury 1 (0 members), Jury 2 (0), Jury 3 (0) │
|
||||||
|
│ Submission Windows (2): Application Docs, Semi-finalist Docs │
|
||||||
|
│ Special Awards (2): Innovation Award, Impact Award │
|
||||||
|
│ Notifications: Reminders at 7d, 3d, 1d before deadlines │
|
||||||
|
│ │
|
||||||
|
│ ⚠ Add jury members after creation. │
|
||||||
|
│ │
|
||||||
|
│ [ ← Back ] [ Create Competition ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Round Management
|
||||||
|
|
||||||
|
### Round Detail — Type-Specific Views
|
||||||
|
|
||||||
|
Each round type renders a specialized detail page:
|
||||||
|
|
||||||
|
#### INTAKE Round Detail
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Round 1: Application Window Status: ACTIVE │
|
||||||
|
│ Type: INTAKE | Deadline: Jan 31, 2026 (16 days) │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ 127 │ │ 98 │ │ 29 │ │
|
||||||
|
│ │ Submitted │ │ Complete │ │ Draft │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Category Breakdown: 72 Startups | 55 Concepts │
|
||||||
|
│ │
|
||||||
|
│ Submission Progress (by day): │
|
||||||
|
│ ▁▂▃▃▄▅▆▇████████████▇▇▆▅▄▃▃▂▂▁ │
|
||||||
|
│ Jan 1 Jan 31 │
|
||||||
|
│ │
|
||||||
|
│ Recent Submissions: │
|
||||||
|
│ ┌─────────────────────────────┬──────────┬──────────┬────────┐ │
|
||||||
|
│ │ Team │ Category │ Status │ Files │ │
|
||||||
|
│ ├─────────────────────────────┼──────────┼──────────┼────────┤ │
|
||||||
|
│ │ OceanClean AI │ STARTUP │ Complete │ 4/4 │ │
|
||||||
|
│ │ DeepReef Monitoring │ STARTUP │ Complete │ 3/4 │ │
|
||||||
|
│ │ BlueTide Analytics │ CONCEPT │ Draft │ 1/4 │ │
|
||||||
|
│ └─────────────────────────────┴──────────┴──────────┴────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ View All Submissions ] [ Export CSV ] [ Extend Deadline ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### FILTERING Round Detail
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Round 2: AI Screening Status: ACTIVE │
|
||||||
|
│ Type: FILTERING | Auto-advance: ON │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ 98 │ │ 23 │ │ 67 │ │
|
||||||
|
│ │ Screened │ │ Passed │ │ Failed │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ │
|
||||||
|
│ │ 8 │ │
|
||||||
|
│ │ Flagged │ ← Require manual review │
|
||||||
|
│ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Flagged for Review: │
|
||||||
|
│ ┌─────────────────────────┬──────────┬──────┬─────────────┐ │
|
||||||
|
│ │ Project │ AI Score │ Flag │ Action │ │
|
||||||
|
│ ├─────────────────────────┼──────────┼──────┼─────────────┤ │
|
||||||
|
│ │ WaveEnergy Solutions │ 0.55 │ EDGE │ [✓] [✗] [?] │ │
|
||||||
|
│ │ MarineData Hub │ 0.48 │ LOW │ [✓] [✗] [?] │ │
|
||||||
|
│ │ CoralMapper (dup?) │ 0.82 │ DUP │ [✓] [✗] [?] │ │
|
||||||
|
│ └─────────────────────────┴──────────┴──────┴─────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ View All Results ] [ Re-run AI Screening ] [ Override ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### EVALUATION Round Detail
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Round 3: Jury 1 — Semi-finalist Status: ACTIVE │
|
||||||
|
│ Type: EVALUATION | Jury: Jury 1 (8 members) │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ── Evaluation Progress ─────────────────────────────────────── │
|
||||||
|
│ █████████████████████████████████░░░░░░░░░░░░░░░ 68% │
|
||||||
|
│ 186 / 276 evaluations complete │
|
||||||
|
│ │
|
||||||
|
│ Per-Juror Progress: │
|
||||||
|
│ Dr. Martin ██████████████████████████████████████ 18/18 ✓ │
|
||||||
|
│ Prof. Dubois██████████████████████████████░░░░░░░ 15/20 │
|
||||||
|
│ Ms. Chen █████████████████████████████████████████ 20/20 ✓ │
|
||||||
|
│ Dr. Patel █████████████████████░░░░░░░░░░░░░░ 12/15 │
|
||||||
|
│ Mr. Silva ████████████████████████████████░░░░ 16/20 │
|
||||||
|
│ Dr. Yamada ███████████████████████████████████████ 19/20 │
|
||||||
|
│ Ms. Hansen ██████████████████████████░░░░░░░░░ 14/20 │
|
||||||
|
│ │
|
||||||
|
│ ── Actions ─────────────────────────────────────────────────── │
|
||||||
|
│ [ View Assignments ] [ View Results ] [ Send Reminder ] │
|
||||||
|
│ [ Run AI Summary ] [ Advance Top N ] [ Override Decision ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LIVE_FINAL Stage Manager
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ LIVE STAGE MANAGER — Round 7: Live Finals [● RECORDING] │
|
||||||
|
│ Status: IN_PROGRESS | Category: STARTUP │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Now Presenting: OceanClean AI │
|
||||||
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Status: Q_AND_A │ │
|
||||||
|
│ │ Presentation: 12:00 ✓ | Q&A: ██████░░ 6:23 / 10:00 │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [ ▶ Start Voting ] [ ⏸ Pause ] [ ⏭ Skip ] │ │
|
||||||
|
│ └──────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ── Jury Votes (5 jurors) ────────────────────────────────── │
|
||||||
|
│ Dr. Martin: ○ waiting | Prof. Dubois: ○ waiting │
|
||||||
|
│ Ms. Chen: ○ waiting | Dr. Patel: ○ waiting │
|
||||||
|
│ Mr. Silva: ○ waiting | │
|
||||||
|
│ │
|
||||||
|
│ ── Audience Votes ───────────────────────────────────────── │
|
||||||
|
│ Registered: 142 | Voted: 0 (voting not yet open) │
|
||||||
|
│ │
|
||||||
|
│ ── Queue ────────────────────────────────────────────────── │
|
||||||
|
│ ┌─────┬──────────────────────┬──────────┬───────────────┐ │
|
||||||
|
│ │ Ord │ Project │ Category │ Status │ │
|
||||||
|
│ ├─────┼──────────────────────┼──────────┼───────────────┤ │
|
||||||
|
│ │ ► 1 │ OceanClean AI │ STARTUP │ Q_AND_A │ │
|
||||||
|
│ │ 2 │ DeepReef Monitoring │ STARTUP │ WAITING │ │
|
||||||
|
│ │ 3 │ CoralGuard │ STARTUP │ WAITING │ │
|
||||||
|
│ └─────┴──────────────────────┴──────────┴───────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ Switch to CONCEPT Window ] [ End STARTUP Window ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CONFIRMATION Round Detail
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Round 8: Confirm Winners Status: ACTIVE │
|
||||||
|
│ Type: CONFIRMATION | Jury: Jury 3 │
|
||||||
|
├──────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ STARTUP Proposal │ │
|
||||||
|
│ │ Status: APPROVED ✓ Approvals: 5/5 │ │
|
||||||
|
│ │ 1st: OceanClean AI (92.4) │ │
|
||||||
|
│ │ 2nd: DeepReef (88.7) │ │
|
||||||
|
│ │ 3rd: CoralGuard (85.1) │ │
|
||||||
|
│ │ [ Freeze Results ] │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ CONCEPT Proposal │ │
|
||||||
|
│ │ Status: PENDING Approvals: 3/5 │ │
|
||||||
|
│ │ 1st: BlueTide Analytics (89.2) │ │
|
||||||
|
│ │ 2nd: MarineData Hub (84.6) │ │
|
||||||
|
│ │ 3rd: SeaWatch (81.3) │ │
|
||||||
|
│ │ [ Send Reminder ] [ Override ] │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [ Freeze All Approved ] [ Export Results PDF ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Architecture
|
||||||
|
|
||||||
|
### Shared Components
|
||||||
|
|
||||||
|
| Component | Used In | Description |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `<CompetitionSidebar>` | All /competition/[id]/* pages | Left sidebar with nav links |
|
||||||
|
| `<RoundTimeline>` | Dashboard, round list | Horizontal visual timeline |
|
||||||
|
| `<StatusBadge>` | Everywhere | Color-coded status chip |
|
||||||
|
| `<ProgressBar>` | Round cards, jury progress | Animated progress bar |
|
||||||
|
| `<CountdownTimer>` | Round detail, dashboard | Real-time countdown to deadline |
|
||||||
|
| `<DataTable>` | Projects, members, assignments | Sortable, filterable table |
|
||||||
|
| `<OverrideDialog>` | Filtering, evaluation, confirmation | Override modal with reason input |
|
||||||
|
| `<AuditTrailSheet>` | Any entity detail page | Slide-out audit log viewer |
|
||||||
|
| `<JuryGroupSelector>` | Wizard, round config | Dropdown with create-new option |
|
||||||
|
|
||||||
|
### Page Components (type-specific)
|
||||||
|
|
||||||
|
| Component | Round Type | Description |
|
||||||
|
|-----------|-----------|-------------|
|
||||||
|
| `<IntakeRoundView>` | INTAKE | Submission stats, file status, deadline |
|
||||||
|
| `<FilteringRoundView>` | FILTERING | AI results, flagged queue, overrides |
|
||||||
|
| `<EvaluationRoundView>` | EVALUATION | Juror progress, assignment stats, results |
|
||||||
|
| `<SubmissionRoundView>` | SUBMISSION | Upload progress, locked windows |
|
||||||
|
| `<MentoringRoundView>` | MENTORING | Workspace activity, milestone progress |
|
||||||
|
| `<LiveFinalStageManager>` | LIVE_FINAL | Full stage manager with controls |
|
||||||
|
| `<ConfirmationRoundView>` | CONFIRMATION | Proposals, approvals, freeze |
|
||||||
|
|
||||||
|
### Dynamic Round Detail Routing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/app/(admin)/admin/competition/[id]/rounds/[roundId]/page.tsx
|
||||||
|
|
||||||
|
export default function RoundDetailPage({ params }) {
|
||||||
|
const { data: round } = trpc.competition.getRound.useQuery({
|
||||||
|
roundId: params.roundId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!round) return <LoadingSkeleton />;
|
||||||
|
|
||||||
|
// Render type-specific component based on round type
|
||||||
|
switch (round.roundType) {
|
||||||
|
case 'INTAKE':
|
||||||
|
return <IntakeRoundView round={round} />;
|
||||||
|
case 'FILTERING':
|
||||||
|
return <FilteringRoundView round={round} />;
|
||||||
|
case 'EVALUATION':
|
||||||
|
return <EvaluationRoundView round={round} />;
|
||||||
|
case 'SUBMISSION':
|
||||||
|
return <SubmissionRoundView round={round} />;
|
||||||
|
case 'MENTORING':
|
||||||
|
return <MentoringRoundView round={round} />;
|
||||||
|
case 'LIVE_FINAL':
|
||||||
|
return <LiveFinalStageManager round={round} />;
|
||||||
|
case 'CONFIRMATION':
|
||||||
|
return <ConfirmationRoundView round={round} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
|
||||||
|
| Breakpoint | Layout |
|
||||||
|
|------------|--------|
|
||||||
|
| `< md` (mobile) | Single column. Sidebar collapses to hamburger. Tables become cards. Stage manager simplified |
|
||||||
|
| `md` - `lg` (tablet) | Two column. Sidebar always visible. Tables with horizontal scroll |
|
||||||
|
| `> lg` (desktop) | Full layout. Sidebar + content + optional side panel |
|
||||||
|
|
||||||
|
### Mobile Stage Manager
|
||||||
|
|
||||||
|
The live stage manager has a simplified mobile view for admins controlling from a phone:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ LIVE CONTROL [● REC]│
|
||||||
|
│ │
|
||||||
|
│ Now: OceanClean AI │
|
||||||
|
│ Status: Q_AND_A │
|
||||||
|
│ Timer: 6:23 / 10:00 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────┐ │
|
||||||
|
│ │ [ Start Voting ] │ │
|
||||||
|
│ │ [ Pause ] │ │
|
||||||
|
│ │ [ Skip → Next ] │ │
|
||||||
|
│ └──────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Jury: 0/5 voted │
|
||||||
|
│ Audience: 0/142 voted │
|
||||||
|
│ │
|
||||||
|
│ Next: DeepReef Monitoring│
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessibility
|
||||||
|
|
||||||
|
| Feature | Implementation |
|
||||||
|
|---------|---------------|
|
||||||
|
| **Keyboard navigation** | All actions reachable via Tab/Enter. Focus rings visible |
|
||||||
|
| **Screen reader** | Semantic HTML, `aria-label` on badges, `role="status"` on live regions |
|
||||||
|
| **Color contrast** | All text meets WCAG 2.1 AA. Status badges use icons + color |
|
||||||
|
| **Motion** | Countdown timers respect `prefers-reduced-motion` |
|
||||||
|
| **Focus management** | Dialog focus trap, return focus on close |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration with tRPC
|
||||||
|
|
||||||
|
### Key Data-Fetching Hooks
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Competition dashboard
|
||||||
|
const { data: competition } = trpc.competition.getById.useQuery({ id });
|
||||||
|
const { data: rounds } = trpc.competition.listRounds.useQuery({ competitionId: id });
|
||||||
|
const { data: juryGroups } = trpc.juryGroup.listByCompetition.useQuery({ competitionId: id });
|
||||||
|
|
||||||
|
// Round detail
|
||||||
|
const { data: round } = trpc.competition.getRound.useQuery({ roundId });
|
||||||
|
const { data: projects } = trpc.competition.getProjectsInRound.useQuery({ roundId });
|
||||||
|
const { data: assignments } = trpc.assignment.listByRound.useQuery({ roundId });
|
||||||
|
|
||||||
|
// Live stage manager (with polling)
|
||||||
|
const { data: ceremonyState } = trpc.liveControl.getCeremonyState.useQuery(
|
||||||
|
{ roundId },
|
||||||
|
{ refetchInterval: 1000 } // poll every second
|
||||||
|
);
|
||||||
|
|
||||||
|
// Confirmation
|
||||||
|
const { data: proposals } = trpc.winnerConfirmation.listProposals.useQuery({ competitionId: id });
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mutation Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Advance projects after evaluation
|
||||||
|
const advance = trpc.competition.advanceProjects.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.competition.getRound.invalidate({ roundId });
|
||||||
|
utils.competition.getProjectsInRound.invalidate({ roundId });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Freeze winner proposal
|
||||||
|
const freeze = trpc.winnerConfirmation.freezeProposal.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.winnerConfirmation.listProposals.invalidate({ competitionId });
|
||||||
|
toast({ title: 'Results frozen', description: 'Official results are now locked.' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
1806
docs/claude-architecture-redesign/16-jury-ui.md
Normal file
1806
docs/claude-architecture-redesign/16-jury-ui.md
Normal file
File diff suppressed because it is too large
Load Diff
1787
docs/claude-architecture-redesign/17-applicant-ui.md
Normal file
1787
docs/claude-architecture-redesign/17-applicant-ui.md
Normal file
File diff suppressed because it is too large
Load Diff
1760
docs/claude-architecture-redesign/18-mentor-ui.md
Normal file
1760
docs/claude-architecture-redesign/18-mentor-ui.md
Normal file
File diff suppressed because it is too large
Load Diff
1734
docs/claude-architecture-redesign/19-api-router-reference.md
Normal file
1734
docs/claude-architecture-redesign/19-api-router-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
2185
docs/claude-architecture-redesign/20-service-layer-changes.md
Normal file
2185
docs/claude-architecture-redesign/20-service-layer-changes.md
Normal file
File diff suppressed because it is too large
Load Diff
2972
docs/claude-architecture-redesign/21-migration-strategy.md
Normal file
2972
docs/claude-architecture-redesign/21-migration-strategy.md
Normal file
File diff suppressed because it is too large
Load Diff
1907
docs/claude-architecture-redesign/22-integration-map.md
Normal file
1907
docs/claude-architecture-redesign/22-integration-map.md
Normal file
File diff suppressed because it is too large
Load Diff
2580
docs/claude-architecture-redesign/23-implementation-sequence.md
Normal file
2580
docs/claude-architecture-redesign/23-implementation-sequence.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
|||||||
|
# 01. Current Platform Architecture Audit
|
||||||
|
|
||||||
|
## 1) Technology Baseline
|
||||||
|
- Frontend: Next.js App Router, role-based route groups
|
||||||
|
- API: tRPC router composition (`src/server/routers/_app.ts`)
|
||||||
|
- ORM/DB: Prisma + PostgreSQL (`prisma/schema.prisma`)
|
||||||
|
- Auth: NextAuth + invitation token + onboarding + password setup flows
|
||||||
|
- Storage: MinIO/S3-like presigned URLs for project files
|
||||||
|
- Notifications: email + in-app notifications + reminder logs
|
||||||
|
- AI: OpenAI-backed assignment/filtering/award eligibility/summary/mentor matching with fallback logic
|
||||||
|
|
||||||
|
## 2) Core Current Domain Model (As Implemented)
|
||||||
|
|
||||||
|
### 2.1 Stage Engine Backbone
|
||||||
|
Current schema already includes the core stage engine building blocks:
|
||||||
|
- `Pipeline` (program-level orchestration container)
|
||||||
|
- `Track` (main/award/showcase branches)
|
||||||
|
- `Stage` (typed lifecycle node)
|
||||||
|
- `StageTransition` (graph edges with optional guards)
|
||||||
|
- `ProjectStageState` (project position/state inside stage)
|
||||||
|
- `Cohort` + `CohortProject` (grouping for live/selection windows)
|
||||||
|
- `LiveProgressCursor` (live presentation pointer)
|
||||||
|
- `OverrideAction` + `DecisionAuditLog` (decision/override evidence)
|
||||||
|
|
||||||
|
### 2.2 Evaluation / Assignment / Filtering / Awards / Mentorship
|
||||||
|
The platform also has substantial supporting models:
|
||||||
|
- Assignment & judging: `Assignment`, `Evaluation`, `EvaluationForm`, `ConflictOfInterest`, `GracePeriod`
|
||||||
|
- Filtering: `FilteringRule`, `FilteringResult`, `FilteringJob`
|
||||||
|
- Awards: `SpecialAward`, `AwardEligibility`, `AwardJuror`, `AwardVote`
|
||||||
|
- Mentorship: `MentorAssignment`, `MentorMessage`, `MentorNote`, `MentorMilestone`, `MentorMilestoneCompletion`
|
||||||
|
- Documents: `FileRequirement`, `ProjectFile`
|
||||||
|
- Live voting: `LiveVotingSession`, `LiveVote`, `AudienceVoter`
|
||||||
|
|
||||||
|
### 2.3 User/Invite/Team Model
|
||||||
|
- Users: `User` with roles, invite token, onboarding state, workload fields
|
||||||
|
- Team: `TeamMember` links project/user and role (`LEAD`, `MEMBER`, `ADVISOR`)
|
||||||
|
- Invitation tokens are user-level (`inviteToken`, `inviteTokenExpiresAt`), reused across admin and team invite flows
|
||||||
|
|
||||||
|
## 3) Current Services (Behavior Layer)
|
||||||
|
|
||||||
|
### 3.1 Stage Lifecycle Services
|
||||||
|
- `stage-engine.ts`: transition validation + execution + guard evaluation
|
||||||
|
- `stage-filtering.ts`: stage-scoped deterministic + AI banding filtering with manual queue concept
|
||||||
|
- `stage-assignment.ts`: stage-scoped assignment preview/rebalance/coverage, workload/tag balancing
|
||||||
|
- `live-control.ts`: live session control (cursor, queue reorder, pause/resume, cohort windows)
|
||||||
|
- `stage-notifications.ts`: event emission + notification policy checks
|
||||||
|
|
||||||
|
### 3.2 AI and Automation Services
|
||||||
|
- `ai-assignment.ts`, `smart-assignment.ts`
|
||||||
|
- `ai-filtering.ts`
|
||||||
|
- `award-eligibility-job.ts`, `ai-award-eligibility.ts`
|
||||||
|
- `mentor-matching.ts`
|
||||||
|
- `ai-evaluation-summary.ts`
|
||||||
|
|
||||||
|
### 3.3 Deadline/Reminder Services
|
||||||
|
- `evaluation-reminders.ts` for stage deadline reminder windows and deduplicated reminder logs
|
||||||
|
|
||||||
|
## 4) Current API Surface (tRPC Router Map)
|
||||||
|
Major routers currently include:
|
||||||
|
|
||||||
|
- Competition architecture: `pipeline`, `stage`, `stageFiltering`, `stageAssignment`, `cohort`, `live`, `decision`, `award`, `specialAward`
|
||||||
|
- Judging ops: `assignment`, `evaluation`, `gracePeriod`, `filtering`
|
||||||
|
- Project/applicant/docs: `project`, `application`, `applicant`, `file`, `project-pool`
|
||||||
|
- Mentorship: `mentor`
|
||||||
|
- Identity/admin ops: `user`, `program`, `settings`, `notification`, `audit`, `dashboard`
|
||||||
|
- Platform modules: `message`, `webhook`, `learningResource`, `partner`, `tag`, imports, analytics, export
|
||||||
|
|
||||||
|
## 5) Current UI Surface (By Role)
|
||||||
|
|
||||||
|
### 5.1 Admin Surface
|
||||||
|
- Program settings, apply settings, pipeline wizard/advanced editors
|
||||||
|
- Project pool and project management
|
||||||
|
- Members + invitation workflow (`/admin/members/invite`)
|
||||||
|
- Mentors and mentorship settings
|
||||||
|
- Awards and live/reporting pages
|
||||||
|
|
||||||
|
### 5.2 Applicant Surface
|
||||||
|
- Applicant dashboard, pipeline status, stage documents, team management, mentoring page
|
||||||
|
- Team invite/remove functionality in applicant team pages
|
||||||
|
|
||||||
|
### 5.3 Jury Surface
|
||||||
|
- Stage lists, assignment lists, project review/evaluation pages, comparison, live stage pages, awards
|
||||||
|
|
||||||
|
### 5.4 Mentor Surface
|
||||||
|
- Mentor dashboard, project workspaces, notes, milestones, messaging
|
||||||
|
|
||||||
|
### 5.5 Public/Auth Surface
|
||||||
|
- Public application pages
|
||||||
|
- Accept invite page, onboarding page, set password flow
|
||||||
|
- Public live score and audience vote pages
|
||||||
|
|
||||||
|
## 6) Current Strengths
|
||||||
|
1. Strong foundational stage/pipeline schema already exists.
|
||||||
|
2. Manual override and audit models already present.
|
||||||
|
3. AI-assisted modules are implemented with fallback paths.
|
||||||
|
4. Live voting/session primitives already exist.
|
||||||
|
5. File requirements + per-stage file visibility logic already exist.
|
||||||
|
6. Team invitation and onboarding flows are functioning and integrated with email + status.
|
||||||
|
|
||||||
|
## 7) Current Mismatch Areas Against Monaco Flow
|
||||||
|
|
||||||
|
### 7.1 Stage Purpose Ambiguity
|
||||||
|
- `StageType` is generic (`INTAKE/FILTER/EVALUATION/...`) but does not encode explicit Monaco semantics (Jury 1 vs Jury 2 vs Jury 3, Submission Round 1 vs Round 2, Final Confirmation).
|
||||||
|
- Behavior depends heavily on `configJson` conventions and page-level interpretation.
|
||||||
|
|
||||||
|
### 7.2 Jury Entity Is Implicit, Not First-Class
|
||||||
|
- Jury membership is mostly inferred from `User.role` + stage assignments.
|
||||||
|
- The Monaco requirement for explicit overlapping juries (`Jury 1/2/3`, award juries) is not modeled as first-class reusable entities.
|
||||||
|
|
||||||
|
### 7.3 Assignment Policy Is Incomplete For Required Cap Semantics
|
||||||
|
- Current assignment supports limits and balancing, but lacks formal per-judge hard/soft cap contracts with explicit buffer and exhaustion handling.
|
||||||
|
- Category mix logic exists but not in the exact required startup/concept ratio semantics per judge + override flow.
|
||||||
|
|
||||||
|
### 7.4 Document Lifecycle Is Partially Structured
|
||||||
|
- Stage-level requirements are present; previous-stage visibility exists.
|
||||||
|
- But “submission bundles per round” and official round-level locking semantics are not consistently first-class.
|
||||||
|
|
||||||
|
### 7.5 Mentoring Collaboration Layer Is Not Complete
|
||||||
|
- Messaging/notes/milestones exist.
|
||||||
|
- Missing required private mentor-team file workspace with threaded comments + official submission promotion provenance.
|
||||||
|
|
||||||
|
### 7.6 Finals Confirmation Workflow Not Fully Typed
|
||||||
|
- Live voting and cursor controls exist.
|
||||||
|
- No dedicated final confirmation entity enforcing “all jury agree + one admin approval + result freeze lock”.
|
||||||
|
|
||||||
|
### 7.7 Award Mode Semantics Need Hardening
|
||||||
|
- Awards are powerful but routing mode behavior (`pull-out` vs `dual participation`) is not represented as explicit governance policy with strict validation.
|
||||||
|
|
||||||
|
### 7.8 Cross-Platform Consistency Gaps
|
||||||
|
- Critical workflows (member invite, team invite, onboarding, pre-assignment, stage visibility) are implemented across multiple modules with some duplicated logic and role assumptions.
|
||||||
|
- This increases drift risk when competition architecture changes.
|
||||||
|
|
||||||
|
## 8) Complexity/Vagueness Sources
|
||||||
|
1. Over-reliance on freeform `configJson` for core behavior.
|
||||||
|
2. Multiple overlapping assignment/filtering router/service pathways.
|
||||||
|
3. Legacy “round” nomenclature still appears in fields and comments while pipeline model is active.
|
||||||
|
4. Invite/onboarding/team and assignment logic are integrated but not governed by a single “competition operating model” contract.
|
||||||
|
5. Some stage-specific behavior depends on UI assumptions rather than centralized policy evaluation.
|
||||||
|
|
||||||
|
## 9) Conclusion (Current-State Summary)
|
||||||
|
The platform is not a blank slate. It is a capable stage-engine system with substantial functionality already built. The redesign should be an **evolutionary hardening and simplification** effort:
|
||||||
|
|
||||||
|
- Keep what is structurally strong (pipeline/stage backbone, audit, live primitives)
|
||||||
|
- Introduce explicit entities/policies where Monaco flow requires precision
|
||||||
|
- Reduce ambiguity by making flow-critical contracts typed and validated
|
||||||
|
- Unify all user-facing functions under the same stage/jury/document/decision model
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# 02. Monaco Flow Target Architecture
|
||||||
|
|
||||||
|
## 1) Target Architecture Intent
|
||||||
|
Design the competition as a **typed, policy-driven stage machine** where each step in your Monaco flow maps to an explicit stage purpose and policy contract.
|
||||||
|
|
||||||
|
Core idea:
|
||||||
|
- Keep existing `Pipeline/Track/Stage` spine
|
||||||
|
- Add explicit purpose codes and policy entities
|
||||||
|
- Drive every major feature (invite, docs, assignment, review, mentoring, finals, confirmation) from the same stage context
|
||||||
|
|
||||||
|
## 2) Canonical Stage Purpose Map
|
||||||
|
|
||||||
|
For the main track, define explicit purpose keys (not just generic stage type):
|
||||||
|
|
||||||
|
1. `submission_r1_intake`
|
||||||
|
2. `eligibility_filter`
|
||||||
|
3. `jury1_evaluation`
|
||||||
|
4. `submission_r2_intake`
|
||||||
|
5. `jury2_evaluation`
|
||||||
|
6. `finals_prep_collaboration` (mentoring layer; may be non-blocking stage or overlay policy)
|
||||||
|
7. `jury3_live_finals`
|
||||||
|
8. `final_confirmation`
|
||||||
|
9. `results_publication`
|
||||||
|
|
||||||
|
`StageType` remains useful (`INTAKE`, `FILTER`, `EVALUATION`, `LIVE_FINAL`, `RESULTS`) but **stage purpose key is authoritative** for business behavior.
|
||||||
|
|
||||||
|
## 3) Actor Spaces (Target)
|
||||||
|
|
||||||
|
### 3.1 Applicants / Teams
|
||||||
|
- Apply during open submission windows
|
||||||
|
- Upload required documents for active submission stage
|
||||||
|
- Access prior round docs read-only
|
||||||
|
- Request mentoring (if enabled)
|
||||||
|
- Collaborate with mentor in private file/chat workspace
|
||||||
|
- Mark eligible mentoring file as official submission (when allowed)
|
||||||
|
|
||||||
|
### 3.2 Admins
|
||||||
|
- Configure stages, deadlines, rubrics, file requirements, assignment policies
|
||||||
|
- Trigger/monitor AI filtering and assignment
|
||||||
|
- Override eligibility, assignments, advancement, finalist/winner outcomes
|
||||||
|
- Run live finals controls (stage manager)
|
||||||
|
- Perform final confirmation and official result lock
|
||||||
|
|
||||||
|
### 3.3 Judges
|
||||||
|
- Belong to explicit juries (`Jury 1`, `Jury 2`, `Jury 3`, award juries)
|
||||||
|
- Review assigned projects in active windows
|
||||||
|
- Access prior + current docs as policy allows
|
||||||
|
- Submit scores, feedback, notes
|
||||||
|
- Participate in deliberation/final confirmation where required
|
||||||
|
|
||||||
|
### 3.4 Mentors
|
||||||
|
- Assigned to eligible finalist teams that requested mentoring
|
||||||
|
- Use mentor-team private workspace (chat, files, threaded comments)
|
||||||
|
- Help refine docs; optionally approve/promote files if policy permits
|
||||||
|
|
||||||
|
### 3.5 Audience (Optional)
|
||||||
|
- Votes during configured live finals windows
|
||||||
|
- Only when enabled for stage/category/session
|
||||||
|
|
||||||
|
## 4) End-To-End Flow Blueprint (Mapped To Monaco)
|
||||||
|
|
||||||
|
## 4.1 Stage A: Submission Round 1 (`submission_r1_intake`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Program application settings
|
||||||
|
- Stage deadline and late policy
|
||||||
|
- Required document set for round 1
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Applicant can edit/upload only while submission window policy allows
|
||||||
|
- Late behavior from policy:
|
||||||
|
- strict cutoff
|
||||||
|
- accepted as late with flag
|
||||||
|
- accepted in grace window only
|
||||||
|
- Admin can always inspect and intervene
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Project classified as `STARTUP` or `BUSINESS_CONCEPT`
|
||||||
|
- Round 1 submission bundle + metadata + late flag
|
||||||
|
|
||||||
|
## 4.2 Stage B: AI Eligibility Filter (`eligibility_filter`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Eligible candidate pool from Stage A
|
||||||
|
- Deterministic rules + AI criteria text + confidence thresholds
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Deterministic checks first, then AI evaluation (if enabled)
|
||||||
|
- Outcome buckets:
|
||||||
|
- `eligible`
|
||||||
|
- `ineligible`
|
||||||
|
- `manual_review`
|
||||||
|
- Admin override always allowed with reason code + audit
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Eligible pool for Jury 1
|
||||||
|
- Ineligible pool retained and visible to admins with evidence
|
||||||
|
|
||||||
|
## 4.3 Stage C: Jury 1 Evaluation (`jury1_evaluation`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Jury 1 membership
|
||||||
|
- Assignment policy per juror (cap mode, cap value, soft buffer, category-bias preference)
|
||||||
|
- Rubric/version and deadline window
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Assignment engine obeys per-judge policies:
|
||||||
|
- hard cap: never exceed cap
|
||||||
|
- soft cap: target cap, may go to cap+buffer
|
||||||
|
- Startup/concept ratio is treated as a **soft bias** for fair distribution, not a deterministic constraint
|
||||||
|
- Jury onboarding must clearly state that ratio preference is a suggestion, not a hard rule
|
||||||
|
- After all soft-cap judges reach cap+buffer:
|
||||||
|
- unassigned projects move to manual assignment queue
|
||||||
|
- Judges see countdown, project list, round 1 documents
|
||||||
|
- Submit score, feedback, notes
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Ranking and distribution views per category
|
||||||
|
- AI recommended shortlist per category
|
||||||
|
- Admin final advancement decision -> Semi-finalists
|
||||||
|
|
||||||
|
## 4.4 Stage D: Submission Round 2 (`submission_r2_intake`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Semi-finalists selected from Stage C
|
||||||
|
- New round 2 document requirements
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Applicant edits only round 2 requirements
|
||||||
|
- Round 1 docs are read-only for applicants
|
||||||
|
- Judges/admin can see both bundles (with clear labeling)
|
||||||
|
- Admin retains full document management authority across rounds
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Round 1 locked bundle + Round 2 final bundle per semi-finalist
|
||||||
|
|
||||||
|
## 4.5 Stage E: Jury 2 Evaluation (`jury2_evaluation`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Jury 2 membership + assignment policies
|
||||||
|
- Access to round 1 + round 2 docs
|
||||||
|
- Special award governance configuration
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Same assignment controls as Jury 1
|
||||||
|
- Judges score and annotate
|
||||||
|
- Special awards run in configured mode:
|
||||||
|
- mode A: separate pool (optional pull-out, admin-confirmed)
|
||||||
|
- mode B: dual eligibility while staying in main pool
|
||||||
|
- Award juries can overlap with main juries
|
||||||
|
- Some awards can run in single-judge decision mode (award master / designated judge)
|
||||||
|
- Admin can override finalist recommendations
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Finalists per category
|
||||||
|
- Award shortlist/winners progression state per award policy
|
||||||
|
|
||||||
|
## 4.6 Stage F: Mentoring Collaboration Layer (`finals_prep_collaboration` overlay)
|
||||||
|
|
||||||
|
This is a collaboration layer, not a judging gate.
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Finalists requesting mentoring
|
||||||
|
- Mentor assignments
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Private mentor-team workspace:
|
||||||
|
- messaging/chat
|
||||||
|
- file upload by mentor/team
|
||||||
|
- threaded file comments
|
||||||
|
- Promotion to official submission supported where policy allows:
|
||||||
|
- promotion authority: team lead and admin
|
||||||
|
- create provenance record (who, when, source file, target requirement)
|
||||||
|
- keep immutable audit trail
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Improved finalist submissions
|
||||||
|
- Complete mentoring collaboration + promotion history
|
||||||
|
|
||||||
|
## 4.7 Stage G: Live Finals Jury 3 (`jury3_live_finals`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Finalist list/order
|
||||||
|
- Jury 3 members
|
||||||
|
- Live voting settings and optional audience voting config
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Admin stage manager can set active presenter, open/close vote windows
|
||||||
|
- Jury 3 sees:
|
||||||
|
- all finalist info and historical context allowed by policy
|
||||||
|
- prior jury summaries (if permitted)
|
||||||
|
- real-time note-taking and live voting controls
|
||||||
|
- Audience vote optional; if enabled, shown during deliberation as configured
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Live vote records + deliberation notes + optional audience totals
|
||||||
|
|
||||||
|
## 4.8 Stage H: Final Confirmation (`final_confirmation`)
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- Jury 3 voting outcomes
|
||||||
|
- Deliberation notes
|
||||||
|
- Award outcomes
|
||||||
|
|
||||||
|
### Runtime Rules
|
||||||
|
- Explicit confirmation workflow:
|
||||||
|
- all required jury confirmations captured (default unanimous for both category winners and award winners, based on active quorum)
|
||||||
|
- if a required juror is absent, admin can either replace the juror or mark absence so they do not count toward required confirmations (fully audited)
|
||||||
|
- exception: awards configured for single-judge mode follow single-judge confirmation flow
|
||||||
|
- one admin approval required to finalize
|
||||||
|
- Admin override path is supported for both category and award outcomes with mandatory reason/audit evidence
|
||||||
|
- On finalize:
|
||||||
|
- winner rankings locked
|
||||||
|
- result set immutable (unlock allowed only through super-admin workflow with explicit audit reason)
|
||||||
|
|
||||||
|
### Outputs
|
||||||
|
- Official winners per category
|
||||||
|
- Official award winners
|
||||||
|
- Immutable final decision audit package
|
||||||
|
|
||||||
|
## 4.9 Stage I: Results Publication (`results_publication`)
|
||||||
|
- Controlled publication policy (manual or policy-based)
|
||||||
|
- Public/private output controls
|
||||||
|
- Report export snapshot references final lock version
|
||||||
|
|
||||||
|
## 5) Cross-Cutting Target Behaviors
|
||||||
|
|
||||||
|
### 5.1 Deadlines And Reminder Contracts
|
||||||
|
Every stage with time windows must have:
|
||||||
|
- user-visible countdown in role dashboards
|
||||||
|
- reminder policy (3-day, 24h, 1h defaults; configurable)
|
||||||
|
- reliable dedupe via reminder logs
|
||||||
|
|
||||||
|
### 5.2 Manual Override Everywhere (With Structured Reasoning)
|
||||||
|
Admins can override:
|
||||||
|
- filtering outcomes
|
||||||
|
- assignment decisions
|
||||||
|
- advancement and finalist picks
|
||||||
|
- award eligibility and winner selection
|
||||||
|
- final winner confirmation
|
||||||
|
|
||||||
|
Each override requires:
|
||||||
|
- reason code
|
||||||
|
- optional reason text
|
||||||
|
- before/after snapshot
|
||||||
|
- actor and timestamp
|
||||||
|
|
||||||
|
### 5.3 Multi-Jury Explicitness
|
||||||
|
Juries become explicit objects with memberships and stage bindings:
|
||||||
|
- main juries and award juries use purpose bindings with custom labels per program
|
||||||
|
- overlap allowed
|
||||||
|
- assignments and dashboards are jury-context-aware
|
||||||
|
|
||||||
|
### 5.4 Multi-Round Document Visibility
|
||||||
|
- Applicants edit current stage only
|
||||||
|
- Applicants read-only previous stage docs
|
||||||
|
- Judges view current + previous by policy
|
||||||
|
- Admin full control
|
||||||
|
|
||||||
|
### 5.5 Platform-Wide Integration Rule
|
||||||
|
No function may bypass stage context:
|
||||||
|
- invite + onboarding
|
||||||
|
- assignment generation
|
||||||
|
- document submission/locking
|
||||||
|
- live voting
|
||||||
|
- status timeline
|
||||||
|
- notifications
|
||||||
|
|
||||||
|
All are resolved through a central `competition context resolver` (program + pipeline + stage purpose + jury context + user role).
|
||||||
|
|
||||||
|
## 6) Simplicity + Customizability Strategy
|
||||||
|
|
||||||
|
### 6.1 Simplicity
|
||||||
|
- Fixed Monaco-compatible stage purpose skeleton (minimal mandatory stages)
|
||||||
|
- Strong defaults and pre-built templates for most competitions
|
||||||
|
- Role-specific UX views that only expose relevant controls
|
||||||
|
|
||||||
|
### 6.2 Customizability
|
||||||
|
- Policy overrides per stage, jury, award, and user
|
||||||
|
- Configurable late policies, cap buffers, top-N finalists, audience voting behavior
|
||||||
|
- Award mode per award (`separate_pool` vs `dual_track`)
|
||||||
|
- Final confirmation rules configurable (unanimous, supermajority, quorum fallback handling, etc.)
|
||||||
|
|
||||||
|
## 7) Target Outcome
|
||||||
|
The redesigned architecture should let admins run Monaco OPC end-to-end **without ad hoc interpretation** and with full traceability, while still allowing controlled per-program policy customization.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 03. Gap Analysis And Simplification Decisions
|
||||||
|
|
||||||
|
## 1) Summary
|
||||||
|
The platform already supports most building blocks of your competition, but key Monaco requirements are either implicit, partially modeled, or dispersed across modules. This document converts those gaps into explicit redesign decisions.
|
||||||
|
|
||||||
|
## 2) Gap Matrix (Current vs Required vs Redesign)
|
||||||
|
|
||||||
|
| Area | Current State | Monaco Requirement | Redesign Decision |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Stage semantics | Generic `StageType`, behavior split between `configJson` and UI assumptions | Explicit flow steps (R1 intake, eligibility, Jury1, R2, Jury2, mentoring layer, Jury3 live, final confirmation) | Add `stagePurposeKey` (typed enum) and purpose-specific policy validation |
|
||||||
|
| Jury modeling | Jury mostly inferred from role + assignments | Explicit `Jury 1/2/3` and award juries with overlap | Add first-class `Jury` and `JuryMembership` models; bind to stages |
|
||||||
|
| Assignment policy | Max/min load + overflow policy + some category quota support | Per-judge hard/soft cap + soft buffer + startup/concept ratio + manual fallback queue | Create typed `AssignmentPolicy` + `JudgePolicyOverride`; formal unassigned queue |
|
||||||
|
| Filtering outcomes | Deterministic + AI + manual queue present | Eligibility pass with admin override and reason trace | Keep existing model, add stricter outcome taxonomy and promotion gates |
|
||||||
|
| Submission rounds | Stage requirements exist; file logic partly stage-bound | Round bundles with lock/read-only and provenance | Introduce explicit `SubmissionRound`/`DocumentBundle` abstraction on top of stage |
|
||||||
|
| Mentoring workspace | Mentor assignment/messages/notes/milestones exist | Private files + threaded comments + promotion to official submission | Add mentor workspace files/comments and `PromotionEvent` typed audit |
|
||||||
|
| Special awards | Strong award model, routing present but mixed semantics | Mode A/B behavior per award with pull-out/dual participation | Add explicit `awardParticipationMode` + routing policy constraints |
|
||||||
|
| Live finals | Live cursor + voting session + audience support exists | Stage manager controls + Jury 3 notes + deliberation windows | Keep live primitives; add deliberation policy and jury3 context contracts |
|
||||||
|
| Final confirmation | Audit + decision logs exist | All jury agree + one admin approval + result lock | Add dedicated `FinalConfirmation` workflow and result freeze state |
|
||||||
|
| Invite/member flows | Admin/member/team invites implemented; pre-assignment exists | Must deeply reflect updated architecture and juries | Make invites jury-aware and stage-purpose aware; unify assignment intents |
|
||||||
|
|
||||||
|
## 3) High-Impact Ambiguity Sources To Eliminate
|
||||||
|
|
||||||
|
### 3.1 `configJson` As Primary Runtime Logic
|
||||||
|
Current problem:
|
||||||
|
- Critical behavior lives in open-ended JSON keys with inconsistent naming and weak compile-time guarantees.
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Keep `configJson` for non-critical UI metadata.
|
||||||
|
- Move flow-critical behavior into typed policy tables and strict schemas.
|
||||||
|
|
||||||
|
### 3.2 Generic Stage Types Without Purpose Contract
|
||||||
|
Current problem:
|
||||||
|
- Two stages can both be `EVALUATION`, but system behavior may need to differ radically (Jury1 vs Jury2 vs award).
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Add immutable `stagePurposeKey` and purpose-aware policy validator.
|
||||||
|
|
||||||
|
### 3.3 Dispersed Assignment Constraints
|
||||||
|
Current problem:
|
||||||
|
- Multiple assignment pathways (stageAssignment router/service, assignment router, AI suggestions) can diverge in rules.
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Centralize assignment rule evaluation into one policy engine used by all assignment entry points.
|
||||||
|
|
||||||
|
### 3.4 Invite And Onboarding Disconnected From Jury Context
|
||||||
|
Current problem:
|
||||||
|
- `/admin/members/invite` pre-assigns projects directly, but not explicitly tied to jury entities and policy contracts.
|
||||||
|
|
||||||
|
Decision:
|
||||||
|
- Introduce `AssignmentIntent` attached to `JuryMembership`/stage context during invite flow; materialize assignments when stage/policy permits.
|
||||||
|
|
||||||
|
## 4) Simplification Decisions (Non-Negotiable)
|
||||||
|
|
||||||
|
1. **One source of truth for stage behavior:** `stagePurposeKey + policy models`
|
||||||
|
2. **One source of truth for assignment constraints:** centralized assignment policy engine
|
||||||
|
3. **One source of truth for winner lock:** explicit final confirmation workflow
|
||||||
|
4. **One source of truth for document status:** round bundle + requirement slot state
|
||||||
|
5. **One source of truth for role access:** policy-based access resolver by context
|
||||||
|
|
||||||
|
## 5) Compatibility Strategy
|
||||||
|
|
||||||
|
### 5.1 Keep Existing Backbone
|
||||||
|
No full rewrite:
|
||||||
|
- Preserve `Pipeline`, `Track`, `Stage`, `ProjectStageState`, `Assignment`, `Evaluation`, `SpecialAward`, `LiveVotingSession`
|
||||||
|
|
||||||
|
### 5.2 Additive Evolution
|
||||||
|
Add new typed entities and read/write adapters:
|
||||||
|
- old endpoints keep working in compatibility mode during migration
|
||||||
|
- new flows use purpose/policy-aware endpoints
|
||||||
|
|
||||||
|
### 5.3 Controlled De-ambiguation
|
||||||
|
- Introduce strict validation for future pipeline configs
|
||||||
|
- Provide migration scripts for legacy `configJson` keys
|
||||||
|
- fail-fast for unsupported combinations
|
||||||
|
|
||||||
|
## 6) Design Tradeoffs
|
||||||
|
|
||||||
|
### 6.1 Why Not Fully Dynamic “No Skeleton” Flow?
|
||||||
|
- You requested clarity and reduced vagueness.
|
||||||
|
- Purely dynamic flow increases flexibility but harms predictability, QA surface, and operator confidence.
|
||||||
|
|
||||||
|
Chosen approach:
|
||||||
|
- fixed Monaco skeleton + configurable policies and optional modules.
|
||||||
|
|
||||||
|
### 6.2 Why Typed Tables Instead Of More JSON?
|
||||||
|
- Critical competition logic requires integrity constraints, easier querying, and deterministic behavior.
|
||||||
|
- Typed models improve auditability and testability.
|
||||||
|
|
||||||
|
### 6.3 Why Add Jury As Explicit Entity?
|
||||||
|
- Your flow explicitly names Jury 1/2/3 and overlapping award juries.
|
||||||
|
- This cannot remain a UI convention without creating operational drift.
|
||||||
|
|
||||||
|
## 7) Required Outcome Of Simplification
|
||||||
|
By end of redesign:
|
||||||
|
- Admins can configure Monaco flow without hidden dependencies.
|
||||||
|
- Assignment limits are deterministic, while startup/concept ratio remains a transparent soft-bias recommendation.
|
||||||
|
- Invite/member pages support policy-compliant jury memberships plus both pre-assignment modes (intent-first and direct assignment where explicitly enabled).
|
||||||
|
- Documents, mentoring, awards, finals, and confirmation all speak the same domain language.
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
# 04. Unified Domain Model And Config Contracts
|
||||||
|
|
||||||
|
## 1) Objectives
|
||||||
|
This target model turns the Monaco flow into strict, reusable contracts while preserving the current pipeline infrastructure.
|
||||||
|
|
||||||
|
Goals:
|
||||||
|
1. Minimize schema churn where current models are already correct.
|
||||||
|
2. Remove ambiguity for jury identity, assignment policy, round bundles, mentoring promotion, and final lock.
|
||||||
|
3. Keep customization through explicit policy layers, not ad hoc conditionals.
|
||||||
|
|
||||||
|
## 2) Proposed Domain Additions
|
||||||
|
|
||||||
|
## 2.1 Stage Purpose Contract
|
||||||
|
|
||||||
|
### Add To `Stage`
|
||||||
|
- `purposeKey` (enum/string, required after migration)
|
||||||
|
|
||||||
|
Proposed purpose enum values:
|
||||||
|
- `submission_r1_intake`
|
||||||
|
- `eligibility_filter`
|
||||||
|
- `jury1_evaluation`
|
||||||
|
- `submission_r2_intake`
|
||||||
|
- `jury2_evaluation`
|
||||||
|
- `jury3_live_finals`
|
||||||
|
- `final_confirmation`
|
||||||
|
- `results_publication`
|
||||||
|
- `award_evaluation`
|
||||||
|
- `award_results`
|
||||||
|
|
||||||
|
Rule:
|
||||||
|
- `StageType` remains coarse technical type.
|
||||||
|
- `purposeKey` controls business behavior and policy validation.
|
||||||
|
|
||||||
|
## 2.2 Jury As First-Class Entity
|
||||||
|
|
||||||
|
### New `Jury`
|
||||||
|
Fields:
|
||||||
|
- `id`, `programId`, `purposeKey`, `code`, `displayLabel`, `kind` (`MAIN`, `AWARD`), `isActive`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `code=main-semifinal`, `displayLabel=Technical Semi-Final Jury`
|
||||||
|
- `code=main-finalist`, `displayLabel=Grand Jury`
|
||||||
|
- `code=award-blue-innovation`, `displayLabel=Blue Innovation Prize Jury`
|
||||||
|
|
||||||
|
### New `JuryMembership`
|
||||||
|
Fields:
|
||||||
|
- `juryId`, `userId`, `roleInJury` (`CHAIR`, `MEMBER`), `activeFrom`, `activeTo`, `status`
|
||||||
|
|
||||||
|
### New `JuryStageBinding`
|
||||||
|
Fields:
|
||||||
|
- `juryId`, `stageId`, `isPrimary`, `permissionsJson`
|
||||||
|
|
||||||
|
Use:
|
||||||
|
- controls which juries are allowed to evaluate/vote in each stage
|
||||||
|
- allows overlap across juries and awards
|
||||||
|
|
||||||
|
## 2.3 Assignment Policy Contract
|
||||||
|
|
||||||
|
### New `AssignmentPolicy` (stage + jury scoped)
|
||||||
|
Fields:
|
||||||
|
- `stageId`
|
||||||
|
- `juryId`
|
||||||
|
- `requiredReviews`
|
||||||
|
- `defaultCap`
|
||||||
|
- `defaultCapMode` (`HARD`, `SOFT`)
|
||||||
|
- `softBuffer` (default 10, configurable)
|
||||||
|
- `categoryBiasPolicyJson` (startup/concept mix preference weights; non-deterministic)
|
||||||
|
- `overflowPolicy` (`manual_queue`, `expand_pool`, `reduce_reviews`)
|
||||||
|
- `isActive`
|
||||||
|
|
||||||
|
### New `JudgePolicyOverride`
|
||||||
|
Fields:
|
||||||
|
- `assignmentPolicyId`
|
||||||
|
- `userId`
|
||||||
|
- `cap`, `capMode`, `softBuffer`
|
||||||
|
- `startupBiasWeight`, `conceptBiasWeight`
|
||||||
|
- `biasDisclosureAcceptedAt` (onboarding acknowledgement that bias is suggestive only)
|
||||||
|
- `source` (`ADMIN_SET`, `JUDGE_ONBOARDING`)
|
||||||
|
|
||||||
|
### New `AssignmentIntent`
|
||||||
|
Purpose:
|
||||||
|
- records desired assignments created before assignment materialization (e.g., from member invite page)
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `stageId`, `juryId`, `userId`, `projectId`, `intentSource`, `status`
|
||||||
|
|
||||||
|
### New `AssignmentException`
|
||||||
|
Purpose:
|
||||||
|
- explicit record for manual over-cap assignments
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `assignmentId`, `policyId`, `exceptionType`, `reasonCode`, `reasonText`, `approvedBy`
|
||||||
|
|
||||||
|
## 2.4 Submission Round Bundle Contract
|
||||||
|
|
||||||
|
### New `SubmissionRound`
|
||||||
|
Fields:
|
||||||
|
- `id`, `stageId`, `roundKey`, `name`, `openAt`, `closeAt`, `latePolicy`, `lateGraceHours`, `editabilityPolicy`
|
||||||
|
|
||||||
|
### Evolve `FileRequirement`
|
||||||
|
Add:
|
||||||
|
- `submissionRoundId` (nullable transitional, required after migration)
|
||||||
|
- `slotKey` (stable requirement slot identifier)
|
||||||
|
|
||||||
|
### Evolve `ProjectFile`
|
||||||
|
Add:
|
||||||
|
- `submissionRoundId`
|
||||||
|
- `submissionSlotKey`
|
||||||
|
- `sourceType` (`DIRECT_UPLOAD`, `MENTOR_PROMOTION`, `ADMIN_REPLACEMENT`)
|
||||||
|
- `sourceReferenceId` (e.g., mentor file id)
|
||||||
|
- `isOfficial`
|
||||||
|
|
||||||
|
### New `SubmissionBundleState`
|
||||||
|
Fields:
|
||||||
|
- `projectId`, `submissionRoundId`, `status` (`DRAFT`, `SUBMITTED`, `LOCKED`), `lockedAt`, `lockedBy`
|
||||||
|
|
||||||
|
## 2.5 Mentoring Collaboration + Promotion
|
||||||
|
|
||||||
|
### New `MentorWorkspaceFile`
|
||||||
|
Fields:
|
||||||
|
- `projectId`, `mentorId` (nullable for team upload), file storage metadata, visibility, status
|
||||||
|
|
||||||
|
### New `MentorWorkspaceComment`
|
||||||
|
Fields:
|
||||||
|
- `workspaceFileId`, `authorId`, `threadKey`, `content`, timestamps
|
||||||
|
|
||||||
|
### New `SubmissionPromotionEvent`
|
||||||
|
Fields:
|
||||||
|
- `projectId`, `workspaceFileId`, `targetSubmissionRoundId`, `targetSlotKey`
|
||||||
|
- `promotedById`, `promotedAt`
|
||||||
|
- `resultProjectFileId`
|
||||||
|
- `approvalState` (if mentor/admin approvals required)
|
||||||
|
|
||||||
|
Invariant:
|
||||||
|
- promotion always creates immutable provenance link from mentoring artifact to official submission slot.
|
||||||
|
- promotion authority is limited to team lead and admin.
|
||||||
|
|
||||||
|
## 2.6 Special Award Governance Contract
|
||||||
|
|
||||||
|
### Extend `SpecialAward`
|
||||||
|
Add:
|
||||||
|
- `participationMode` (`SEPARATE_POOL`, `DUAL_TRACK`)
|
||||||
|
- `routingBehavior` (`PULL_FROM_MAIN`, `KEEP_IN_MAIN`)
|
||||||
|
- `routingConfirmationMode` (`AUTO`, `ADMIN_CONFIRMED`) // Monaco default: `ADMIN_CONFIRMED` for pull-out
|
||||||
|
- `requiresDedicatedJury` (bool)
|
||||||
|
- `winnerDecisionMode` (`JURY_CONFIRMATION`, `SINGLE_JUDGE_DECIDES`)
|
||||||
|
- `singleJudgeUserId` (nullable, required when `winnerDecisionMode=SINGLE_JUDGE_DECIDES`)
|
||||||
|
- `winnerCandidateSource` (`ELIGIBILITY_REMAINING_POOL`, `CUSTOM_SHORTLIST`) // Monaco single-judge awards use eligibility remaining pool
|
||||||
|
- `submissionRequirementMode` (`REUSE_MAIN`, `CUSTOM`)
|
||||||
|
|
||||||
|
### New `AwardStageBinding`
|
||||||
|
Fields:
|
||||||
|
- `awardId`, `filterStageId`, `evaluationStageId`, `resultStageId`
|
||||||
|
|
||||||
|
## 2.7 Final Confirmation Contract
|
||||||
|
|
||||||
|
### New `FinalConfirmationSession`
|
||||||
|
Fields:
|
||||||
|
- `stageId` (final confirmation stage)
|
||||||
|
- `status` (`OPEN`, `PENDING_ADMIN_APPROVAL`, `FINALIZED`, `CANCELLED`)
|
||||||
|
- `decisionRule` (`UNANIMOUS`, `SUPERMAJORITY`, `SIMPLE_MAJORITY`, `SINGLE_JUDGE_DECIDES`)
|
||||||
|
- `quorumPolicy` (`ACTIVE_MEMBERS_ONLY`, `ALLOW_REPLACEMENT`)
|
||||||
|
- `requiredApprovalsCount` (resolved from active quorum after absence/replacement decisions)
|
||||||
|
- `scope` (`CATEGORY`, `AWARD`)
|
||||||
|
- `isAdminOverridden` (bool)
|
||||||
|
- `overrideReasonCode`, `overrideReasonText`, `overriddenByAdminId`, `overriddenAt`
|
||||||
|
- `finalizedByAdminId`, `finalizedAt`
|
||||||
|
|
||||||
|
### New `FinalConfirmationParticipant`
|
||||||
|
Fields:
|
||||||
|
- `sessionId`, `juryMemberId`
|
||||||
|
- `status` (`REQUIRED`, `ABSENT_EXCUSED`, `REPLACED`, `REPLACEMENT_ACTIVE`)
|
||||||
|
- `replacedByJuryMemberId` (nullable)
|
||||||
|
- `absenceReasonCode`, `absenceReasonText`
|
||||||
|
- `updatedByAdminId`, `updatedAt`
|
||||||
|
|
||||||
|
### New `FinalConfirmationVote`
|
||||||
|
Fields:
|
||||||
|
- `sessionId`, `juryMemberId`, `projectId/category/awardScope`, `decision`, `comment`
|
||||||
|
|
||||||
|
### New `ResultLock`
|
||||||
|
Fields:
|
||||||
|
- `sessionId`, `programId`, `lockVersion`, `lockedAt`, `lockedBy`, `snapshotJson`
|
||||||
|
|
||||||
|
### New `ResultUnlockEvent`
|
||||||
|
Fields:
|
||||||
|
- `resultLockId`, `programId`
|
||||||
|
- `unlockedBySuperAdminId`, `unlockedAt`
|
||||||
|
- `reasonCode`, `reasonText`
|
||||||
|
- `relockVersion` (nullable until relock)
|
||||||
|
|
||||||
|
Invariant:
|
||||||
|
- once `ResultLock` is created, winner outputs are immutable except via explicit super-admin unlock workflow with mandatory audit reason.
|
||||||
|
|
||||||
|
## 3) Policy Precedence Model
|
||||||
|
|
||||||
|
Order of precedence (highest to lowest):
|
||||||
|
1. Explicit admin override action (with reason)
|
||||||
|
2. Per-user override policy (e.g., judge cap override)
|
||||||
|
3. Stage+jury policy
|
||||||
|
4. Program-level default policy
|
||||||
|
5. System default
|
||||||
|
|
||||||
|
All runtime evaluators must return:
|
||||||
|
- resolved value
|
||||||
|
- source layer
|
||||||
|
- explanation payload for audit/debug UI
|
||||||
|
|
||||||
|
## 4) Access Contract (Role + Context)
|
||||||
|
|
||||||
|
Replace scattered checks with `resolveAccess(user, context)` where context includes:
|
||||||
|
- `programId`
|
||||||
|
- `trackId`
|
||||||
|
- `stageId`
|
||||||
|
- `stagePurposeKey`
|
||||||
|
- `juryId` (if applicable)
|
||||||
|
- `projectId` (if applicable)
|
||||||
|
- `submissionRoundId` (if applicable)
|
||||||
|
|
||||||
|
Returns typed permissions:
|
||||||
|
- `canViewProject`
|
||||||
|
- `canUploadSubmissionSlot`
|
||||||
|
- `canViewPreviousRoundDocs`
|
||||||
|
- `canAssignProjects`
|
||||||
|
- `canVoteLive`
|
||||||
|
- `canConfirmFinalWinners`
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
## 5) Assignment Engine Contract
|
||||||
|
|
||||||
|
Input:
|
||||||
|
- projects eligible in stage
|
||||||
|
- jury members bound to stage
|
||||||
|
- resolved per-judge policy (cap mode, cap, buffer, category-bias preference)
|
||||||
|
- required reviews
|
||||||
|
- COI and availability constraints
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
1. satisfy hard-cap constraints strictly
|
||||||
|
2. satisfy soft-cap targets before using buffer
|
||||||
|
3. apply startup/concept distribution as a soft scoring bias (never a strict blocker)
|
||||||
|
4. when all soft-cap members hit cap+buffer, place remainder in manual queue
|
||||||
|
5. never silently drop unassigned projects
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- assignment set
|
||||||
|
- unassigned queue with explicit reasons
|
||||||
|
- policy-compliance summary
|
||||||
|
|
||||||
|
## 6) Document Contract
|
||||||
|
|
||||||
|
For every stage with submission behavior:
|
||||||
|
- active `SubmissionRound` must exist
|
||||||
|
- all required slots represented by `FileRequirement.slotKey`
|
||||||
|
- applicant write rights only for active round if open
|
||||||
|
- previous round slots read-only in applicant scope
|
||||||
|
- judges see current + previous according to stage policy
|
||||||
|
- admins full mutate rights with replacement provenance
|
||||||
|
|
||||||
|
## 7) Invite + Onboarding Contract
|
||||||
|
|
||||||
|
## 7.1 Admin Member Invite
|
||||||
|
- Invitations create `User` + optional `JuryMembership`.
|
||||||
|
- Jury selection uses program-defined custom jury labels, while binding to explicit stage-purpose contracts.
|
||||||
|
- Pre-assignment supports both modes:
|
||||||
|
- intent-first mode via `AssignmentIntent` (default recommended)
|
||||||
|
- direct assignment mode via `Assignment` (explicitly enabled and policy-validated)
|
||||||
|
|
||||||
|
## 7.2 Team Invite
|
||||||
|
- Team lead invites create/attach `TeamMember`
|
||||||
|
- invite token acceptance path remains, but post-accept routing resolves to proper applicant/team context
|
||||||
|
|
||||||
|
## 7.3 Accept Invite Routing
|
||||||
|
After acceptance:
|
||||||
|
- if user has jury memberships pending onboarding -> jury onboarding journey
|
||||||
|
- if mentor -> mentor onboarding
|
||||||
|
- if applicant team member only -> applicant team dashboard
|
||||||
|
|
||||||
|
## 8) Event/Audit Contract
|
||||||
|
|
||||||
|
Every significant workflow event emits:
|
||||||
|
- business event type (`assignment.generated`, `final_confirmation.finalized`, etc.)
|
||||||
|
- actor
|
||||||
|
- entity scope
|
||||||
|
- policy snapshot
|
||||||
|
- before/after state where relevant
|
||||||
|
|
||||||
|
Override events must include:
|
||||||
|
- reason code
|
||||||
|
- reason text
|
||||||
|
- impacted policy key
|
||||||
|
- reviewer chain (if applicable)
|
||||||
|
|
||||||
|
## 9) API Contract Evolution (Router-Level)
|
||||||
|
|
||||||
|
## 9.1 Keep and Evolve
|
||||||
|
- Keep existing routers (`pipeline`, `stage`, `assignment`, `evaluation`, `file`, `mentor`, `live-voting`, `decision`, `user`, etc.)
|
||||||
|
- Add new endpoints for purpose/policy/jury/final confirmation and deprecate ambiguous patterns progressively
|
||||||
|
|
||||||
|
## 9.2 New Endpoint Families
|
||||||
|
- `jury.*`: CRUD memberships/bindings
|
||||||
|
- `assignmentPolicy.*`: configure and inspect effective policy
|
||||||
|
- `submissionRound.*`: lifecycle and bundle state
|
||||||
|
- `mentorWorkspace.*`: files/comments/promotion
|
||||||
|
- `finalConfirmation.*`: create/collect/finalize lock, handle quorum fallback, and manage juror replacement/absence
|
||||||
|
- `resultUnlock.*`: super-admin unlock/relock workflow with audit capture
|
||||||
|
- `access.*`: debug effective permissions (admin-only)
|
||||||
|
|
||||||
|
## 10) Migration Safety Rules
|
||||||
|
1. Additive migrations first.
|
||||||
|
2. Backfill `purposeKey` and policy references from existing configs.
|
||||||
|
3. Dual-read / single-write transition windows where needed.
|
||||||
|
4. Feature flags for critical runtime path switches.
|
||||||
|
5. No silent behavior changes in production without compatibility mode.
|
||||||
|
|
||||||
|
## 11) End State
|
||||||
|
A coherent operating model where all functions, pages, and APIs are consistent with Monaco flow and each key behavior is explainable, testable, and auditable.
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# 05. Platform-Wide Integration Matrix
|
||||||
|
|
||||||
|
## 1) Integration Rule
|
||||||
|
Every function must consume the same competition context contract:
|
||||||
|
- program
|
||||||
|
- pipeline/track
|
||||||
|
- stage + `purposeKey`
|
||||||
|
- jury context (when relevant)
|
||||||
|
- submission round context (when relevant)
|
||||||
|
- resolved policy set
|
||||||
|
|
||||||
|
No page/router may hardcode Monaco behavior without passing through this context resolver.
|
||||||
|
|
||||||
|
## 2) Critical Integration Domains
|
||||||
|
|
||||||
|
1. Identity and invitation/onboarding
|
||||||
|
2. Pipeline/stage orchestration
|
||||||
|
3. Assignment/evaluation
|
||||||
|
4. Documents/submission bundles
|
||||||
|
5. Awards and routing
|
||||||
|
6. Mentoring collaboration
|
||||||
|
7. Live finals and final confirmation
|
||||||
|
8. Notifications, timeline, reporting, exports
|
||||||
|
|
||||||
|
## 3) Admin UX Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration To Redesigned Architecture |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/wizard/page.tsx` | Generic stage setup | Must require `purposeKey` assignment and validate Monaco skeleton completeness |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/advanced/page.tsx` | Advanced free-form editing | Add policy editors for jury binding, assignment policies, submission rounds, final confirmation |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/edit/page.tsx` | Pipeline editing | Prevent invalid stage-purpose combinations; show dependency impacts |
|
||||||
|
| `src/components/admin/pipeline/sections/assignment-section.tsx` | min/max load + overflow | Replace with explicit hard/soft cap + buffer + startup/concept bias controls (clearly labeled suggestive, not deterministic) |
|
||||||
|
| `src/components/admin/pipeline/sections/filtering-section.tsx` | rules + AI criteria | Bind to `eligibility_filter` purpose and standardized outcomes |
|
||||||
|
| `src/components/admin/pipeline/sections/awards-section.tsx` | award routing/scoring basic | Add participation mode (`separate_pool`/`dual_track`) and pull-out behavior |
|
||||||
|
| `src/components/admin/pipeline/sections/live-finals-section.tsx` | live/audience settings | Add Jury 3 deliberation window and final confirmation linkage |
|
||||||
|
| `src/components/admin/pipeline/stage-config-editor.tsx` | generic config rendering | render purpose-aware controls only; remove ambiguous keys |
|
||||||
|
| `src/app/(admin)/admin/projects/page.tsx` | project list by filters | Add stage-purpose-aware views and round bundle health columns |
|
||||||
|
| `src/app/(admin)/admin/projects/pool/page.tsx` | project pool ops | Use assignment intent/manual queue with reason tracking |
|
||||||
|
| `src/app/(admin)/admin/reports/stages/page.tsx` | stage reporting | Include compliance metrics (cap violations, override counts, unassigned backlog) |
|
||||||
|
|
||||||
|
## 4) Invite / Member / Onboarding Integration Matrix (High Priority)
|
||||||
|
|
||||||
|
### 4.1 Admin Member Invite (Explicit Requirement)
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(admin)/admin/members/invite/page.tsx` | invites users; optional pre-assign project-to-user by stage | Must bind invite flow to explicit `JuryMembership`. UI must support both pre-assignment modes: `AssignmentIntent` mode and policy-validated direct assignment mode. Admin chooses custom-labeled jury (main or award), stage scope, and mode. |
|
||||||
|
| `src/server/routers/user.ts` `bulkCreate` | creates users and may directly create assignments | Support dual behavior: create `AssignmentIntent` and/or direct policy-validated assignments based on request mode. Track chosen mode, source, and policy decision in audit details. |
|
||||||
|
| `src/server/routers/user.ts` `sendInvitation`/`bulkSendInvitations` | token invitation | Include context metadata (jury memberships, pending assignment intents, onboarding path) in invitation payload/logs. |
|
||||||
|
|
||||||
|
### 4.2 Team Invite / Project Team Management
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(applicant)/applicant/team/page.tsx` | team lead invites/removes members | Keep flow, but gate permissions via context resolver and ensure invite acceptance routes into project-team scope correctly. |
|
||||||
|
| `src/app/(public)/my-submission/[id]/team/page.tsx` | mirrored team flow | Must consume same backend contract as applicant team page; no divergent logic. |
|
||||||
|
| `src/server/routers/applicant.ts` `inviteTeamMember` | creates/updates user + team member + email | Add membership lifecycle states and provenance events; ensure no conflict with jury/mentor invitation states. |
|
||||||
|
|
||||||
|
### 4.3 Invite Acceptance + Onboarding
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(auth)/accept-invite/page.tsx` | validates token and signs in | After acceptance, route based on membership context (jury, mentor, team, admin) and pending obligations (onboarding, password, jury onboarding choices). |
|
||||||
|
| `src/app/(auth)/onboarding/page.tsx` | role-aware onboarding | Must collect jury cap/bias preferences (if enabled) for judges, clearly disclose that startup/concept bias is suggestive (not a rule), and tie to `JudgePolicyOverride` records. |
|
||||||
|
| `src/server/routers/user.ts` `validateInviteToken` | token validation only | include contextual invitation metadata for accurate UX preview. |
|
||||||
|
| `src/server/routers/user.ts` `needsOnboarding` | role-based onboarding check | extend to membership-aware onboarding requirements per jury/stage. |
|
||||||
|
|
||||||
|
## 5) Applicant + Submission Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(public)/apply/[slug]/page.tsx` | stage/public form | enforce submission round contract (`SubmissionRound`, slots, late policy) |
|
||||||
|
| `src/app/(public)/apply/edition/[programSlug]/page.tsx` | edition mode intake | must map to `submission_r1_intake` purpose policies if Monaco profile active |
|
||||||
|
| `src/app/(applicant)/applicant/pipeline/page.tsx` | pipeline visibility | show purpose-based journey (R1, eligibility, Jury1 result, R2, etc.) |
|
||||||
|
| `src/app/(applicant)/applicant/pipeline/[stageId]/documents/page.tsx` | stage docs | enforce round-bundle write/read states by stage purpose |
|
||||||
|
| `src/app/(applicant)/applicant/documents/page.tsx` | docs view | show bundle timeline and official slot status per round |
|
||||||
|
| `src/server/routers/application.ts` | public submission & draft | create `SubmissionBundleState` and normalize category key mapping |
|
||||||
|
| `src/server/routers/applicant.ts` `getUploadUrl/saveFileMetadata/deleteFile` | upload + save + delete | enforce slot state, round lock, promotion provenance, late behavior policy |
|
||||||
|
| `src/server/routers/file.ts` | file access and grouped views | use submission round + stage purpose aware visibility consistently across roles |
|
||||||
|
|
||||||
|
## 6) Jury Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(jury)/jury/stages/page.tsx` | shows active stages | show only stages bound to juries user belongs to |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/assignments/page.tsx` | assignment list | render cap status, ratio status, deadline countdown, completion obligations |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/projects/[projectId]/page.tsx` | project details | purpose-aware document separation (Round 1 vs Round 2) |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/projects/[projectId]/evaluate/page.tsx` | evaluation form | enforce stage/jury policy and graceful lock behavior |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/live/page.tsx` | live finals interaction | require Jury 3 membership and live session permissions |
|
||||||
|
| `src/server/routers/assignment.ts` | assignment CRUD/suggestions/jobs | centralize policy engine usage for all assignment paths |
|
||||||
|
| `src/server/routers/evaluation.ts` | evaluation lifecycle | incorporate jury binding checks and final confirmation contribution rights |
|
||||||
|
| `src/server/services/stage-assignment.ts` | stage assignment preview/run | implement hard/soft cap+buffer with category bias as weighted guidance (not deterministic rule) |
|
||||||
|
| `src/server/services/smart-assignment.ts` | scoring logic | treat startup/concept preference as transparent scoring bias, not strict quota blocking |
|
||||||
|
|
||||||
|
## 7) Awards Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(admin)/admin/awards/*` | award management | expose mode A/B semantics, admin-confirmed pull-out flow, and single-judge decision mode where configured |
|
||||||
|
| `src/app/(jury)/jury/awards/*` | jury award voting | bind to award jury memberships/stage windows and suppress jury voting for single-judge awards |
|
||||||
|
| `src/server/routers/award.ts` | award track governance | add strict validation for participation mode, stage bindings, admin-confirmed pull-out, and single-judge configuration |
|
||||||
|
| `src/server/routers/specialAward.ts` | eligibility + jurors + votes | include standardized filter->review->selection pipeline and override chain |
|
||||||
|
| `src/server/services/award-eligibility-job.ts` | background eligibility | persist decision reasoning under unified audit event taxonomy |
|
||||||
|
|
||||||
|
## 8) Mentor Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(mentor)/mentor/projects/[id]/page.tsx` | mentor project detail | include private workspace files + threaded comments; promotion actions only where allowed by team-lead/admin authority |
|
||||||
|
| `src/app/(applicant)/applicant/mentor/page.tsx` | applicant mentor interaction | show promotion candidate files and official slot mapping |
|
||||||
|
| `src/server/routers/mentor.ts` | assignment/message/notes/milestones | add workspace file/comment/promotion endpoints and enforce team-lead/admin-only promotion permissions |
|
||||||
|
| `src/server/services/mentor-matching.ts` | AI matching | gate matching by finalist + mentorship request + stage policy |
|
||||||
|
|
||||||
|
## 9) Live Finals + Confirmation Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/app/(admin)/admin/reports/stages/page.tsx` + live admin pages | live status/reporting | show stage manager controls + deliberation timer + confirmation readiness |
|
||||||
|
| `src/app/(public)/vote/*` | audience voting pages | enforce category/session mode and anti-duplicate policy |
|
||||||
|
| `src/server/routers/live.ts` | live cursor controls | require `jury3_live_finals` purpose and stage manager authority |
|
||||||
|
| `src/server/routers/live-voting.ts` | voting session mgmt | tie session lifecycle to stage purpose + deliberation/final confirmation handoff |
|
||||||
|
| `src/server/routers/cohort.ts` | cohort windows | align with live/selection policies and category boundaries |
|
||||||
|
| `src/server/routers/decision.ts` | overrides/audit | extend to final confirmation decisions and result lock events |
|
||||||
|
| **new router family** `finalConfirmation.*` | none | create and govern unanimous-with-quorum-fallback confirmation for category+award scopes, single-judge award exceptions, juror replacement/absence controls, admin override paths, and final lock snapshot |
|
||||||
|
| **new router family** `resultUnlock.*` | none | super-admin-only unlock/relock workflow with mandatory reason/audit trail |
|
||||||
|
|
||||||
|
## 10) Notifications / Messaging / Reporting / Export Integration Matrix
|
||||||
|
|
||||||
|
| Surface | Current Behavior | Required Integration |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/server/services/stage-notifications.ts` | event-based notifications | expand event taxonomy for jury membership, promotion, confirmation lock |
|
||||||
|
| `src/server/services/evaluation-reminders.ts` | stage deadline reminders | include jury-specific obligations and final confirmation reminders |
|
||||||
|
| `src/server/routers/message.ts` | messaging | ensure context tags reference stage/jury/project where applicable |
|
||||||
|
| `src/server/routers/export.ts` | report generation | include final lock version and policy-compliance evidence |
|
||||||
|
| `src/server/routers/audit.ts` | audit search | add filter facets for stage purpose, jury id, confirmation session |
|
||||||
|
| `src/server/routers/analytics.ts` | platform analytics | add Monaco flow KPIs (assignment saturation, override rate, policy compliance) |
|
||||||
|
|
||||||
|
## 11) Router Integration Checklist (All tRPC Domains)
|
||||||
|
|
||||||
|
Each router in `src/server/routers/_app.ts` must be classified into one of three buckets:
|
||||||
|
|
||||||
|
1. **Directly governed by competition context**
|
||||||
|
- `pipeline`, `stage`, `stageFiltering`, `stageAssignment`, `assignment`, `evaluation`, `decision`, `live`, `liveVoting`, `cohort`, `award`, `specialAward`, `mentor`, `applicant`, `application`, `file`, `project`, `project-pool`
|
||||||
|
|
||||||
|
2. **Indirectly context-dependent (must carry context metadata)**
|
||||||
|
- `user`, `notification`, `message`, `dashboard`, `analytics`, `export`, `audit`
|
||||||
|
|
||||||
|
3. **Context-independent utilities**
|
||||||
|
- `avatar`, `logo`, `tag`, imports, generic settings/webhooks
|
||||||
|
|
||||||
|
Requirement:
|
||||||
|
- Bucket 1 and 2 routers must accept/derive stage/jury context when touching competition entities.
|
||||||
|
|
||||||
|
## 12) Services Refactor Boundaries
|
||||||
|
|
||||||
|
| Service | Keep | Refactor Requirement |
|
||||||
|
|---|---|---|
|
||||||
|
| `stage-engine.ts` | yes | purpose-aware transition guards |
|
||||||
|
| `stage-filtering.ts` | yes | standardized eligibility outcomes + reason schema |
|
||||||
|
| `stage-assignment.ts` | yes | full hard/soft cap policy engine |
|
||||||
|
| `smart-assignment.ts` | partial | convert soft heuristics into policy-compliant scoring only |
|
||||||
|
| `live-control.ts` | yes | bind to Jury3 and confirmation handoff |
|
||||||
|
| `mentor-matching.ts` | yes | finalist-stage gating and workspace policy awareness |
|
||||||
|
| `evaluation-reminders.ts` | yes | purpose-aware reminder templates |
|
||||||
|
|
||||||
|
## 13) Integration Acceptance Criteria
|
||||||
|
A module is considered integrated only if:
|
||||||
|
1. It resolves effective competition context before executing business logic.
|
||||||
|
2. It uses typed policy resolution (not ad hoc JSON key checks).
|
||||||
|
3. It emits standardized audit/decision events.
|
||||||
|
4. It fails safely when required context/policy is missing.
|
||||||
|
|
||||||
|
## 14) Special Note: “All Functions Deeply Integrated”
|
||||||
|
Your requirement is satisfied only when invite/member/team/onboarding pages are not treated as peripheral admin UX, but as first-class entry points into the jury/stage/assignment model. This is explicitly mandatory in Phases 2 and 3 of the roadmap.
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# 06. Implementation Roadmap And Migration Plan
|
||||||
|
|
||||||
|
## 1) Delivery Strategy
|
||||||
|
Use an incremental, low-risk migration with compatibility layers.
|
||||||
|
|
||||||
|
Execution mode:
|
||||||
|
- Additive schema first
|
||||||
|
- Dual-read where required
|
||||||
|
- Single-write to new contracts once validated
|
||||||
|
- Feature flags for runtime switches
|
||||||
|
- Rollback-safe at every phase gate
|
||||||
|
|
||||||
|
## 2) Phase Plan
|
||||||
|
|
||||||
|
## Phase 0: Contract Freeze And Blueprint Sign-Off
|
||||||
|
|
||||||
|
### Deliverables
|
||||||
|
- Approved Monaco flow contract (stage purpose keys, jury model, assignment semantics)
|
||||||
|
- Locked ADRs for major design choices
|
||||||
|
- Finalized open question decisions from `08-open-questions-for-flow-owners.md`
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- No unresolved P0 architecture questions
|
||||||
|
- Product + operations + engineering agreement on control-plane UX behavior
|
||||||
|
|
||||||
|
## Phase 1: Schema Foundation (Additive)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Add `stagePurposeKey`
|
||||||
|
- Add jury models (`Jury`, `JuryMembership`, `JuryStageBinding`)
|
||||||
|
- Add assignment policy models (`AssignmentPolicy`, `JudgePolicyOverride`, `AssignmentIntent`, `AssignmentException`)
|
||||||
|
- Add submission round models (`SubmissionRound`, bundle state fields)
|
||||||
|
- Add mentoring workspace + promotion models
|
||||||
|
- Add final confirmation models
|
||||||
|
|
||||||
|
### Data Migration Tasks
|
||||||
|
1. Backfill stage purpose keys from known stage names/slugs/types.
|
||||||
|
2. Create purpose-bound main juries for active Monaco programs with editable custom labels.
|
||||||
|
3. Create baseline assignment policies from existing stage config values.
|
||||||
|
4. Materialize current intake requirements into typed submission round artifacts.
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- All migrations run cleanly in staging
|
||||||
|
- Backfill report has zero unknown stage purpose for Monaco pipelines
|
||||||
|
- Referential integrity validated
|
||||||
|
|
||||||
|
## Phase 2: Policy Engine And Context Resolver
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Build centralized `competitionContextResolver`
|
||||||
|
- Build centralized `policyResolver`
|
||||||
|
- Refactor assignment/filtering/live access paths to consume resolver
|
||||||
|
|
||||||
|
### Key Deliverables
|
||||||
|
- `resolveContext(program/stage/project/user)` service
|
||||||
|
- `resolveAssignmentPolicy(stageId, juryId, userId)`
|
||||||
|
- policy-aware access checks for docs/evaluations/live controls
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- No core router uses legacy ad hoc logic for stage/jury policy checks
|
||||||
|
- Policy resolution explainability endpoint available (admin debug)
|
||||||
|
|
||||||
|
## Phase 3: Invite/Member/Onboarding Integration (Critical)
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Refactor admin member invite flow to create jury memberships plus dual pre-assignment modes (intent mode and direct policy-validated assignment mode)
|
||||||
|
- Align team invite flow with unified invitation context metadata
|
||||||
|
- Update accept-invite routing to membership-aware onboarding
|
||||||
|
|
||||||
|
### Key Deliverables
|
||||||
|
- `/admin/members/invite` UX and backend contract update
|
||||||
|
- `user.bulkCreate` migration to support both assignment intent creation and direct policy-validated assignments
|
||||||
|
- onboarding path resolver by role + memberships, including clear bias-disclosure text for jury onboarding
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Invite-created users can be traced from invite -> membership -> assignment materialization
|
||||||
|
- No orphan invitation states
|
||||||
|
- Regression tests pass for admin invite + team invite + accept invite
|
||||||
|
|
||||||
|
## Phase 4: Submission Bundle Lifecycle + Document Locking
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Introduce submission rounds and bundle states in runtime
|
||||||
|
- Enforce edit/read-only behavior by round and stage purpose
|
||||||
|
- Support provenance-aware promotion from mentor workspace
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Applicants can only edit current round slots
|
||||||
|
- Judges can see current + prior bundles by policy
|
||||||
|
- Promotion events produce immutable trace
|
||||||
|
|
||||||
|
## Phase 5: Assignment Engine Hardening For Monaco Semantics
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Implement strict hard/soft cap behavior with buffer
|
||||||
|
- Implement startup/concept weighting as soft-bias preference per judge (non-deterministic)
|
||||||
|
- Implement deterministic unassigned queue and manual fallback
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Policy compliance report proves no hard-cap violations
|
||||||
|
- Soft-cap overflow behavior matches configured buffer rules
|
||||||
|
- Ratio/bias behavior is explainable and visibly disclosed as suggestive in onboarding and admin UI
|
||||||
|
- Manual queue receives all residual items with reasons
|
||||||
|
|
||||||
|
## Phase 6: Awards + Mentoring + Finals + Confirmation
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Add award participation mode semantics, admin-confirmed pull-out flow, and single-judge award decision mode
|
||||||
|
- Complete mentor workspace file/comment/promotion behavior
|
||||||
|
- Link live finals to final confirmation workflow and result lock/unlock governance
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Mode A/B award behavior validated in staging scenarios, including admin-confirmed pull-out
|
||||||
|
- Final confirmation requires configured jury agreement with quorum fallback + admin approval (or configured single-judge award path), with admin override support and audit trail
|
||||||
|
- Result lock snapshot generated and immutable until explicit super-admin unlock workflow is used
|
||||||
|
|
||||||
|
## Phase 7: UX Consolidation And Operator Experience
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Simplify admin pipeline editors with purpose-aware controls
|
||||||
|
- Remove/disable ambiguous config controls
|
||||||
|
- Add context-rich dashboards and compliance panels
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Operators can configure full Monaco flow without advanced/manual JSON edits
|
||||||
|
- Support docs and admin runbooks updated
|
||||||
|
|
||||||
|
## Phase 8: Cutover And Decommission Legacy Paths
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
- Enable new contracts by default
|
||||||
|
- Deprecate legacy code paths and config fallback branches
|
||||||
|
- cleanup migration of deprecated fields where safe
|
||||||
|
|
||||||
|
### Exit Criteria
|
||||||
|
- Production traffic fully on new architecture
|
||||||
|
- Legacy path usage dashboards show zero critical usage
|
||||||
|
- post-cutover audit and incident-free burn-in period complete
|
||||||
|
|
||||||
|
## 3) Workstream Breakdown
|
||||||
|
|
||||||
|
## 3.1 Backend Workstream
|
||||||
|
- Schema/migrations
|
||||||
|
- Policy resolver
|
||||||
|
- Router refactors
|
||||||
|
- Event/audit normalization
|
||||||
|
|
||||||
|
## 3.2 Frontend Workstream
|
||||||
|
- Admin pipeline UX
|
||||||
|
- Member invite and onboarding flows
|
||||||
|
- Applicant/jury/mentor stage-aware views
|
||||||
|
- finals control + confirmation UI
|
||||||
|
|
||||||
|
## 3.3 Data/Operations Workstream
|
||||||
|
- Backfills and integrity checks
|
||||||
|
- Job monitoring and reconciliation scripts
|
||||||
|
- release runbooks and rollback procedures
|
||||||
|
|
||||||
|
## 3.4 QA Workstream
|
||||||
|
- End-to-end test scenarios for full Monaco flow
|
||||||
|
- policy compliance assertions
|
||||||
|
- performance and reliability testing
|
||||||
|
|
||||||
|
## 4) Migration Sequencing Rules
|
||||||
|
1. No destructive migration before dual-read period completes.
|
||||||
|
2. Every new write path must emit old + new audit evidence during transition.
|
||||||
|
3. Use idempotent backfills with resumable checkpoints.
|
||||||
|
4. Keep feature toggles per major subsystem (invite policy, assignment policy, docs, confirmation).
|
||||||
|
|
||||||
|
## 5) Rollback Strategy
|
||||||
|
|
||||||
|
### 5.1 Technical Rollback
|
||||||
|
- Feature flag rollback first
|
||||||
|
- Keep old write handlers until two stable releases after cutover
|
||||||
|
- Avoid schema drops until rollback window closes
|
||||||
|
|
||||||
|
### 5.2 Operational Rollback
|
||||||
|
- Predefined rollback runbook for each phase
|
||||||
|
- operator checklist for reverting admin workflows
|
||||||
|
- communication templates for judges/admins if rollback impacts active stages
|
||||||
|
|
||||||
|
## 6) Delivery Governance
|
||||||
|
- Weekly architecture sync (backend/frontend/product/ops)
|
||||||
|
- Phase gate reviews with evidence packages
|
||||||
|
- Strict “no silent contract drift” policy for late-stage changes
|
||||||
|
|
||||||
|
## 7) Suggested Initial Milestones (Order)
|
||||||
|
1. Approve purpose keys + jury model + assignment semantics.
|
||||||
|
2. Implement schema foundation and context resolver.
|
||||||
|
3. Integrate invite/onboarding/team flows.
|
||||||
|
4. Enforce round bundle lifecycle.
|
||||||
|
5. Harden assignment policy behavior.
|
||||||
|
6. Complete awards/mentoring/finals/confirmation chain.
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# 07. Testing, Observability, And Release Gates
|
||||||
|
|
||||||
|
## 1) QA Strategy
|
||||||
|
The redesign is policy-heavy. Testing must validate:
|
||||||
|
1. Functional correctness
|
||||||
|
2. Policy compliance
|
||||||
|
3. Audit completeness
|
||||||
|
4. Role-based access integrity
|
||||||
|
5. Operational resilience under load and deadline pressure
|
||||||
|
|
||||||
|
## 2) Test Pyramid
|
||||||
|
|
||||||
|
### 2.1 Unit Tests
|
||||||
|
Target modules:
|
||||||
|
- context resolver
|
||||||
|
- policy resolver
|
||||||
|
- assignment policy evaluator (hard/soft/buffer/category-bias weighting)
|
||||||
|
- round bundle lock evaluator
|
||||||
|
- final confirmation rule evaluator
|
||||||
|
|
||||||
|
### 2.2 Integration Tests
|
||||||
|
Target flows:
|
||||||
|
- stage transitions with purpose validation
|
||||||
|
- filtering outcomes + override
|
||||||
|
- assignment run + unassigned queue + manual assignment
|
||||||
|
- document upload/edit lock transitions
|
||||||
|
- mentor promotion pipeline
|
||||||
|
- live voting -> confirmation -> result lock
|
||||||
|
|
||||||
|
### 2.3 End-to-End Tests
|
||||||
|
Target user journeys:
|
||||||
|
- applicant R1 -> eligibility -> Jury1 -> R2 -> Jury2 -> finals prep -> finals -> result
|
||||||
|
- admin stage manager live finals operations
|
||||||
|
- award mode A and mode B paths
|
||||||
|
- admin invite -> onboarding -> jury assignment materialization
|
||||||
|
- team invite -> accept invite -> team collaboration
|
||||||
|
|
||||||
|
## 3) Monaco Flow Test Matrix
|
||||||
|
|
||||||
|
| Flow Step | Required Test Cases |
|
||||||
|
|---|---|
|
||||||
|
| Submission R1 | open/close behavior, late policy modes, required docs enforcement |
|
||||||
|
| Eligibility filter | deterministic pass/fail, AI banding, manual queue, admin override trail |
|
||||||
|
| Jury1 | cap modes, category-bias preference behavior (suggestive only), deadline reminder triggers, score submission |
|
||||||
|
| Submission R2 | R1 read-only for applicants, R2 editable, admin full override |
|
||||||
|
| Jury2 | visibility of R1+R2 docs, finalist recommendation and override |
|
||||||
|
| Awards | separate pool, dual track, admin-confirmed pull-out behavior, jury overlap, single-judge decision mode |
|
||||||
|
| Mentoring | private file sharing, comments, promotion to official slot with provenance |
|
||||||
|
| Jury3 live | stage manager controls, voting windows, notes, audience vote mode |
|
||||||
|
| Final confirmation | unanimous jury agreement on active quorum (or configured rule), juror replacement/absence handling, single-judge award exception flow, admin finalize, admin override path, result lock |
|
||||||
|
|
||||||
|
## 4) Invite/Member/Onboarding Test Matrix (Critical)
|
||||||
|
|
||||||
|
| Workflow | Must Validate |
|
||||||
|
|---|---|
|
||||||
|
| Admin member invite (`/admin/members/invite`) | creates user + jury membership + selected pre-assignment mode (intent and/or direct policy-validated assignment); respects role authorization |
|
||||||
|
| Bulk invite CSV | invalid rows, duplicate handling, unauthorized role attempts, staged pre-assignment mode selection and materialization |
|
||||||
|
| Invite acceptance (`/accept-invite`) | token validity, role-context preview, routing to correct onboarding path |
|
||||||
|
| Jury onboarding | cap/bias override onboarding when enabled, including explicit disclosure that category bias is a suggestion not a rule |
|
||||||
|
| Team invite (`/applicant/team`) | lead-only invite/remove, account setup vs existing account behavior |
|
||||||
|
| Cross-context conflict | same user invited as jury and team member still resolves correct permissions by context |
|
||||||
|
|
||||||
|
## 5) Regression Coverage Extension Against Existing Test Suite
|
||||||
|
Current suite already has useful coverage (`tests/unit/*`, `tests/integration/*`). Extend with:
|
||||||
|
|
||||||
|
1. `tests/unit/assignment-policy-hard-soft.test.ts`
|
||||||
|
2. `tests/unit/final-confirmation-rules.test.ts`
|
||||||
|
3. `tests/integration/invite-jury-membership-intent.test.ts`
|
||||||
|
4. `tests/integration/submission-bundle-locking.test.ts`
|
||||||
|
5. `tests/integration/mentor-promotion-provenance.test.ts`
|
||||||
|
6. `tests/integration/award-mode-routing.test.ts`
|
||||||
|
7. `tests/integration/live-to-final-lock.test.ts`
|
||||||
|
|
||||||
|
## 6) Audit Completeness Checks
|
||||||
|
Add automated assertions that every critical operation emits both domain event and audit record:
|
||||||
|
- eligibility override
|
||||||
|
- assignment exception
|
||||||
|
- stage advancement override
|
||||||
|
- award winner override
|
||||||
|
- promotion event
|
||||||
|
- final confirmation finalize/lock
|
||||||
|
|
||||||
|
Failure to emit required audit evidence should fail tests.
|
||||||
|
|
||||||
|
## 7) Observability Blueprint
|
||||||
|
|
||||||
|
## 7.1 Metrics
|
||||||
|
|
||||||
|
### Competition Lifecycle Metrics
|
||||||
|
- `projects_submitted_total` by category and stage
|
||||||
|
- `eligibility_pass_rate`
|
||||||
|
- `manual_review_rate`
|
||||||
|
- `assignment_coverage_percent`
|
||||||
|
- `unassigned_projects_count`
|
||||||
|
- `over_cap_exception_count`
|
||||||
|
- `submission_late_rate`
|
||||||
|
- `mentor_promotion_count`
|
||||||
|
- `live_vote_completion_rate`
|
||||||
|
- `final_confirmation_duration_seconds`
|
||||||
|
|
||||||
|
### Reliability Metrics
|
||||||
|
- API latency by router
|
||||||
|
- background job success/failure rates (filtering/assignment/award jobs)
|
||||||
|
- reminder send success/failure
|
||||||
|
- invitation send success/failure
|
||||||
|
|
||||||
|
## 7.2 Logs And Tracing
|
||||||
|
- Correlation id per operation across router -> service -> DB writes
|
||||||
|
- Structured logs include `programId`, `stageId`, `stagePurposeKey`, `juryId`, `projectId`, `userId`
|
||||||
|
|
||||||
|
## 7.3 Alerts
|
||||||
|
- assignment run leaves residual without queue record
|
||||||
|
- final confirmation attempted without satisfied quorum requirement
|
||||||
|
- result lock write failure
|
||||||
|
- result unlock attempted by non-super-admin
|
||||||
|
- invite acceptance failures spike
|
||||||
|
- live voting session state mismatch (open with no active project)
|
||||||
|
|
||||||
|
## 8) Release Gates (Must Pass)
|
||||||
|
|
||||||
|
## Gate A: Schema + Backfill Readiness
|
||||||
|
- all migrations idempotent in staging
|
||||||
|
- no unresolved backfill anomalies
|
||||||
|
|
||||||
|
## Gate B: Policy Engine Correctness
|
||||||
|
- hard/soft cap test pass
|
||||||
|
- ratio policy test pass
|
||||||
|
- access resolver test pass
|
||||||
|
|
||||||
|
## Gate C: End-To-End Flow Simulation
|
||||||
|
- full Monaco dry run with seeded data and mixed categories
|
||||||
|
- award mode A/B dry runs
|
||||||
|
|
||||||
|
## Gate D: Invite/Onboarding Stability
|
||||||
|
- admin invite + team invite + acceptance flows pass under load
|
||||||
|
|
||||||
|
## Gate E: Finals And Confirmation Integrity
|
||||||
|
- live voting and final confirmation produce correct lock artifacts
|
||||||
|
- super-admin unlock/relock path preserves complete audit integrity
|
||||||
|
|
||||||
|
## Gate F: Operational Readiness
|
||||||
|
- runbooks approved
|
||||||
|
- dashboards and alerts active
|
||||||
|
- support playbooks distributed
|
||||||
|
|
||||||
|
## 9) Performance And Capacity Scenarios
|
||||||
|
- Large intake window close with bulk late submissions
|
||||||
|
- Filtering and assignment jobs across high project volume
|
||||||
|
- Live finals peak voting concurrency (jury + audience)
|
||||||
|
- Bulk invitation campaigns with email retries
|
||||||
|
|
||||||
|
## 10) Sign-Off Criteria
|
||||||
|
Release is approved only when:
|
||||||
|
1. All phase gates pass.
|
||||||
|
2. No Sev-1/Sev-2 defects remain open in critical competition flows.
|
||||||
|
3. Audit completeness checks pass for all critical operations.
|
||||||
|
4. Operators can execute runbook drills successfully.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 08. Open Questions For Flow Owners
|
||||||
|
|
||||||
|
## 1) Resolved Decisions (Captured)
|
||||||
|
As of Sunday, February 15, 2026, the following decisions are confirmed:
|
||||||
|
|
||||||
|
1. Final confirmation scope:
|
||||||
|
- Unanimity applies by default to category winners and jury-based award winners.
|
||||||
|
- Admin override must be possible in both cases with full audit reasoning.
|
||||||
|
- Some special awards can use a single-judge decision mode after eligibility filtering.
|
||||||
|
|
||||||
|
2. Soft-cap buffer:
|
||||||
|
- Default soft-cap buffer is `+10`.
|
||||||
|
|
||||||
|
3. Startup/concept ratio behavior:
|
||||||
|
- Ratio is a slight bias/suggestion only.
|
||||||
|
- It must not be fully deterministic.
|
||||||
|
- Onboarding must explicitly disclose this to judges.
|
||||||
|
|
||||||
|
4. Mode A pull-out behavior:
|
||||||
|
- Pull-out from main pool is admin-confirmed (not automatic).
|
||||||
|
|
||||||
|
5. Mentoring promotion authority:
|
||||||
|
- Promotion to official submission is allowed for team lead + admin.
|
||||||
|
|
||||||
|
6. Admin member invite pre-assignment:
|
||||||
|
- Both modes should be supported when possible:
|
||||||
|
- assignment intent mode
|
||||||
|
- direct policy-validated assignment mode
|
||||||
|
|
||||||
|
7. Jury absence during final confirmation:
|
||||||
|
- Quorum fallback is allowed.
|
||||||
|
- If a judge is absent, admins can either:
|
||||||
|
- replace the judge, or
|
||||||
|
- mark the judge as absent so they do not count toward required confirmation votes.
|
||||||
|
|
||||||
|
8. Result unlock policy:
|
||||||
|
- Unlock after finalization is allowed only for super admins.
|
||||||
|
- Unlock must require explicit reason/audit metadata.
|
||||||
|
|
||||||
|
9. Jury identity naming:
|
||||||
|
- Main and award juries are custom-labeled.
|
||||||
|
- Runtime behavior binds to jury purpose/stage contracts, not fixed `Jury1/Jury2/Jury3` labels.
|
||||||
|
|
||||||
|
## 2) Remaining P0 Questions (Need Before Schema Freeze)
|
||||||
|
None as of Sunday, February 15, 2026.
|
||||||
|
|
||||||
|
## 3) Remaining P1 Questions (Needed Before UI/Router Refactor)
|
||||||
|
|
||||||
|
1. Judge self-service onboarding controls:
|
||||||
|
- Are judges allowed to edit their own cap/bias preferences during onboarding for both main semi-final and finalist juries, or admin-only?
|
||||||
|
|
||||||
|
2. Mentoring stage applicability:
|
||||||
|
- Is mentoring strictly finalists-only by default, or configurable to include semi-finalists?
|
||||||
|
|
||||||
|
3. Document mutability after lock:
|
||||||
|
- Can admins replace locked submissions post-deadline, and should applicants see replacement history?
|
||||||
|
|
||||||
|
4. Audience reveal timing:
|
||||||
|
- Should audience totals be visible to Jury 3 in real time or only at deliberation start?
|
||||||
|
|
||||||
|
## 4) Remaining P2 Questions
|
||||||
|
|
||||||
|
1. Reporting visibility:
|
||||||
|
- Should per-jury cap/bias compliance be visible to all admins or only super admins?
|
||||||
|
|
||||||
|
2. Override transparency:
|
||||||
|
- Which override details are visible to jury users versus admin-only?
|
||||||
|
|
||||||
|
3. Applicant communication policy:
|
||||||
|
- Which exact transitions trigger applicant notifications?
|
||||||
|
|
||||||
|
4. Tie-break policy ordering:
|
||||||
|
- Confirm strict precedence when ties remain after configured scoring.
|
||||||
|
|
||||||
|
## 5) Invite/Onboarding Follow-Up Questions
|
||||||
|
|
||||||
|
1. Invitation expiry policy:
|
||||||
|
- Keep role-specific invite expiry differences (7-day admin invite vs 30-day team invite) or unify?
|
||||||
|
|
||||||
|
2. Multi-role default landing:
|
||||||
|
- If a user is both jury and applicant team member, what is default post-login landing and should role-switch be mandatory?
|
||||||
|
|
||||||
|
3. CSV template scope:
|
||||||
|
- Should CSV bulk invite support explicit jury binding + stage + pre-assignment mode columns?
|
||||||
|
|
||||||
|
## 6) Suggested Defaults For Remaining Items
|
||||||
|
1. Judges can edit cap/bias in onboarding only when admin enables this per jury.
|
||||||
|
2. Mentoring defaults to finalists-only, with optional semi-finalist extension.
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# 09. Appendix: System Inventory (As Reviewed)
|
||||||
|
|
||||||
|
## 1) Data Model Inventory (Selected Competition-Critical Models)
|
||||||
|
|
||||||
|
### Pipeline/Stage Engine
|
||||||
|
- `Pipeline`
|
||||||
|
- `Track`
|
||||||
|
- `Stage`
|
||||||
|
- `StageTransition`
|
||||||
|
- `ProjectStageState`
|
||||||
|
- `Cohort`
|
||||||
|
- `CohortProject`
|
||||||
|
- `LiveProgressCursor`
|
||||||
|
- `OverrideAction`
|
||||||
|
- `DecisionAuditLog`
|
||||||
|
|
||||||
|
### Assignment/Evaluation
|
||||||
|
- `Assignment`
|
||||||
|
- `AssignmentJob`
|
||||||
|
- `Evaluation`
|
||||||
|
- `EvaluationForm`
|
||||||
|
- `EvaluationSummary`
|
||||||
|
- `EvaluationDiscussion`
|
||||||
|
- `DiscussionComment`
|
||||||
|
- `ConflictOfInterest`
|
||||||
|
- `GracePeriod`
|
||||||
|
- `ReminderLog`
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
- `FilteringRule`
|
||||||
|
- `FilteringResult`
|
||||||
|
- `FilteringJob`
|
||||||
|
|
||||||
|
### Awards
|
||||||
|
- `SpecialAward`
|
||||||
|
- `AwardEligibility`
|
||||||
|
- `AwardJuror`
|
||||||
|
- `AwardVote`
|
||||||
|
|
||||||
|
### Mentorship
|
||||||
|
- `MentorAssignment`
|
||||||
|
- `MentorMessage`
|
||||||
|
- `MentorNote`
|
||||||
|
- `MentorMilestone`
|
||||||
|
- `MentorMilestoneCompletion`
|
||||||
|
|
||||||
|
### Documents/Submission
|
||||||
|
- `FileRequirement`
|
||||||
|
- `ProjectFile`
|
||||||
|
|
||||||
|
### Live Voting
|
||||||
|
- `LiveVotingSession`
|
||||||
|
- `LiveVote`
|
||||||
|
- `AudienceVoter`
|
||||||
|
|
||||||
|
### Identity/Team
|
||||||
|
- `User`
|
||||||
|
- `TeamMember`
|
||||||
|
|
||||||
|
## 2) Service Inventory (Competition-Critical)
|
||||||
|
- `src/server/services/stage-engine.ts`
|
||||||
|
- `src/server/services/stage-filtering.ts`
|
||||||
|
- `src/server/services/stage-assignment.ts`
|
||||||
|
- `src/server/services/live-control.ts`
|
||||||
|
- `src/server/services/stage-notifications.ts`
|
||||||
|
- `src/server/services/evaluation-reminders.ts`
|
||||||
|
- `src/server/services/smart-assignment.ts`
|
||||||
|
- `src/server/services/ai-assignment.ts`
|
||||||
|
- `src/server/services/ai-filtering.ts`
|
||||||
|
- `src/server/services/award-eligibility-job.ts`
|
||||||
|
- `src/server/services/mentor-matching.ts`
|
||||||
|
|
||||||
|
## 3) tRPC Router Inventory
|
||||||
|
- `program`
|
||||||
|
- `project`
|
||||||
|
- `user`
|
||||||
|
- `assignment`
|
||||||
|
- `evaluation`
|
||||||
|
- `file`
|
||||||
|
- `export`
|
||||||
|
- `audit`
|
||||||
|
- `settings`
|
||||||
|
- `gracePeriod`
|
||||||
|
- `learningResource`
|
||||||
|
- `partner`
|
||||||
|
- `notionImport`
|
||||||
|
- `typeformImport`
|
||||||
|
- `tag`
|
||||||
|
- `applicant`
|
||||||
|
- `liveVoting`
|
||||||
|
- `analytics`
|
||||||
|
- `avatar`
|
||||||
|
- `logo`
|
||||||
|
- `application`
|
||||||
|
- `mentor`
|
||||||
|
- `filtering`
|
||||||
|
- `specialAward`
|
||||||
|
- `notification`
|
||||||
|
- `message`
|
||||||
|
- `webhook`
|
||||||
|
- `projectPool`
|
||||||
|
- `wizardTemplate`
|
||||||
|
- `dashboard`
|
||||||
|
- `pipeline`
|
||||||
|
- `stage`
|
||||||
|
- `stageFiltering`
|
||||||
|
- `stageAssignment`
|
||||||
|
- `cohort`
|
||||||
|
- `live`
|
||||||
|
- `decision`
|
||||||
|
- `award`
|
||||||
|
|
||||||
|
## 4) Page Inventory (High-Level by Role Group)
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- members, invite members, programs, apply settings, pipeline wizard/advanced/editor, projects, mentors, awards, reports, settings, audit, messaging, partner, learning
|
||||||
|
|
||||||
|
### Applicant
|
||||||
|
- dashboard, team, pipeline views, stage docs/status views, mentoring, document center
|
||||||
|
|
||||||
|
### Jury
|
||||||
|
- stage list, assignment list, project evaluate pages, compare page, live page, awards, learning
|
||||||
|
|
||||||
|
### Mentor
|
||||||
|
- dashboard, projects, project detail, resources
|
||||||
|
|
||||||
|
### Auth/Public
|
||||||
|
- apply, edition apply, accept invite, login, onboarding, set password, public vote/live score, submission pages
|
||||||
|
|
||||||
|
## 5) Existing Test Inventory
|
||||||
|
|
||||||
|
### Unit
|
||||||
|
- `tests/unit/stage-engine.test.ts`
|
||||||
|
- `tests/unit/stage-assignment.test.ts`
|
||||||
|
- `tests/unit/stage-filtering.test.ts`
|
||||||
|
- `tests/unit/live-control.test.ts`
|
||||||
|
- `tests/unit/override-validation.test.ts`
|
||||||
|
- `tests/unit/award-governance.test.ts`
|
||||||
|
|
||||||
|
### Integration
|
||||||
|
- `tests/integration/pipeline-crud.test.ts`
|
||||||
|
- `tests/integration/stage-config.test.ts`
|
||||||
|
- `tests/integration/assignment-preview.test.ts`
|
||||||
|
- `tests/integration/cohort-voting.test.ts`
|
||||||
|
- `tests/integration/live-runtime.test.ts`
|
||||||
|
- `tests/integration/decision-audit.test.ts`
|
||||||
|
|
||||||
|
## 6) Legacy/Transition Observations
|
||||||
|
1. `roundId` fields still exist as legacy optional references in multiple models.
|
||||||
|
2. Stage/pipeline is active architecture but legacy wording remains in some routers/comments/UI labels.
|
||||||
|
3. Some critical business rules still rely on JSON keys and implicit route behavior.
|
||||||
|
|
||||||
|
This inventory is the baseline used for the redesign and migration plans in this folder.
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# 10. Monaco Reference Configuration (Example)
|
||||||
|
|
||||||
|
This is an example of how a Monaco 2026 competition would be configured in the redesigned system.
|
||||||
|
|
||||||
|
## 1) Program Profile
|
||||||
|
- Program: Monaco Ocean Protection Challenge 2026
|
||||||
|
- Categories: `STARTUP`, `BUSINESS_CONCEPT`
|
||||||
|
- Main pipeline: `mopc-2026-main`
|
||||||
|
- Award tracks: configurable per award
|
||||||
|
|
||||||
|
## 2) Stage Purpose Layout (Main Track)
|
||||||
|
|
||||||
|
1. `submission_r1_intake`
|
||||||
|
- type: `INTAKE`
|
||||||
|
- open: 2026-02-01
|
||||||
|
- close: 2026-05-31
|
||||||
|
- late policy: `flag`
|
||||||
|
|
||||||
|
2. `eligibility_filter`
|
||||||
|
- type: `FILTER`
|
||||||
|
- trigger: admin or auto-on-close
|
||||||
|
|
||||||
|
3. `jury1_evaluation`
|
||||||
|
- type: `EVALUATION`
|
||||||
|
- open: 2026-06-05
|
||||||
|
- close: 2026-06-25
|
||||||
|
- jury binding: `MAIN_SEMIFINAL`
|
||||||
|
|
||||||
|
4. `submission_r2_intake`
|
||||||
|
- type: `INTAKE`
|
||||||
|
- open: 2026-06-28
|
||||||
|
- close: 2026-07-20
|
||||||
|
|
||||||
|
5. `jury2_evaluation`
|
||||||
|
- type: `EVALUATION`
|
||||||
|
- open: 2026-07-24
|
||||||
|
- close: 2026-08-10
|
||||||
|
- jury binding: `MAIN_FINALIST`
|
||||||
|
|
||||||
|
6. `jury3_live_finals`
|
||||||
|
- type: `LIVE_FINAL`
|
||||||
|
- event date window: 2026-09-01
|
||||||
|
- jury binding: `MAIN_LIVE_FINALS`
|
||||||
|
|
||||||
|
7. `final_confirmation`
|
||||||
|
- type: `SELECTION`
|
||||||
|
- rule: unanimous active quorum + one admin approval
|
||||||
|
- absent juror handling: replace juror or mark as excused (admin-audited)
|
||||||
|
- admin override: enabled for both category and award scopes (mandatory reason/audit)
|
||||||
|
- unlock policy: super-admin only
|
||||||
|
|
||||||
|
8. `results_publication`
|
||||||
|
- type: `RESULTS`
|
||||||
|
- publication mode: manual
|
||||||
|
|
||||||
|
## 3) Jury Definitions
|
||||||
|
|
||||||
|
- `MAIN_SEMIFINAL` (display label example: `Jury 1 - Technical Panel`)
|
||||||
|
- Purpose: semi-final selection
|
||||||
|
- members: list of judges
|
||||||
|
- `MAIN_FINALIST` (display label example: `Jury 2 - Selection Panel`)
|
||||||
|
- Purpose: finalist selection
|
||||||
|
- members: list of judges (overlap allowed)
|
||||||
|
- `MAIN_LIVE_FINALS` (display label example: `Jury 3 - Live Finals Panel`)
|
||||||
|
- Purpose: live final voting + deliberation
|
||||||
|
- members: list of judges (overlap allowed)
|
||||||
|
|
||||||
|
## 4) Assignment Policy Example
|
||||||
|
|
||||||
|
### Jury1 policy
|
||||||
|
- required reviews: 3
|
||||||
|
- default cap mode: `SOFT`
|
||||||
|
- default cap: 25
|
||||||
|
- buffer: 10
|
||||||
|
- startup/concept bias target per judge: 50/50 (suggestive, not deterministic)
|
||||||
|
|
||||||
|
### Per-judge override example
|
||||||
|
- judge A: hard cap 18
|
||||||
|
- judge B: soft cap 22 buffer 8
|
||||||
|
- judge C: startup-biased preference (100/0 suggestion, not hard rule)
|
||||||
|
|
||||||
|
## 5) Submission Round Requirements
|
||||||
|
|
||||||
|
### Round 1 (`submission_r1_intake`)
|
||||||
|
Slots:
|
||||||
|
- `r1_exec_summary` (required)
|
||||||
|
- `r1_pitch_deck` (required)
|
||||||
|
- `r1_video_pitch` (optional)
|
||||||
|
|
||||||
|
### Round 2 (`submission_r2_intake`)
|
||||||
|
Slots:
|
||||||
|
- `r2_business_plan` (required)
|
||||||
|
- `r2_financials` (required)
|
||||||
|
- `r2_impact_metrics` (optional)
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- applicants edit current round only
|
||||||
|
- previous round read-only
|
||||||
|
- admin can replace with provenance
|
||||||
|
|
||||||
|
## 6) Award Mode Examples
|
||||||
|
|
||||||
|
### Example A: Separate Pool, Pull-Out
|
||||||
|
- award: “Blue Innovation Prize”
|
||||||
|
- participationMode: `SEPARATE_POOL`
|
||||||
|
- routingBehavior: `PULL_FROM_MAIN`
|
||||||
|
- pull-out confirmation: admin-confirmed before removal from main pool
|
||||||
|
|
||||||
|
### Example B: Dual Track
|
||||||
|
- award: “Community Impact Award”
|
||||||
|
- participationMode: `DUAL_TRACK`
|
||||||
|
- routingBehavior: `KEEP_IN_MAIN`
|
||||||
|
|
||||||
|
### Example C: Single-Judge Award
|
||||||
|
- award: “Chair’s Special Recognition”
|
||||||
|
- winnerDecisionMode: `SINGLE_JUDGE_DECIDES`
|
||||||
|
- singleJudgeUserId: configured award judge
|
||||||
|
- candidate set: only projects remaining eligible after award filtering
|
||||||
|
|
||||||
|
## 7) Mentoring Layer Example
|
||||||
|
|
||||||
|
Eligibility:
|
||||||
|
- finalist projects with `wantsMentorship=true`
|
||||||
|
|
||||||
|
Permissions:
|
||||||
|
- mentor and team can upload/comment private files
|
||||||
|
- team lead + admin can promote file to official submission
|
||||||
|
|
||||||
|
Promotion artifact:
|
||||||
|
- records source file, target slot, actor, timestamp, resulting official file id
|
||||||
|
|
||||||
|
## 8) Live Finals Example
|
||||||
|
|
||||||
|
- voting mode: criteria
|
||||||
|
- audience voting: enabled for end-of-category only
|
||||||
|
- audience totals visible to Jury3 during deliberation: true
|
||||||
|
- tie-break order:
|
||||||
|
1. highest Jury3 weighted score
|
||||||
|
2. admin-declared tie-break (if still tied)
|
||||||
|
|
||||||
|
## 9) Final Confirmation Example
|
||||||
|
|
||||||
|
- decision rule: unanimous (categories + jury-based awards)
|
||||||
|
- active quorum must submit confirmation vote (absent juror can be replaced or marked excused)
|
||||||
|
- admin approval required after quorum unanimity
|
||||||
|
- admin override allowed for category or award outcomes with mandatory reason code and audit snapshot
|
||||||
|
- result lock snapshot generated immediately on finalize
|
||||||
|
- unlock available only to super admins with mandatory reason + unlock event audit
|
||||||
|
|
||||||
|
## 10) Invite Flow Example
|
||||||
|
|
||||||
|
### Admin invite (jury)
|
||||||
|
- create user with role `JURY_MEMBER`
|
||||||
|
- attach `JuryMembership: MAIN_FINALIST` (custom display label in UI)
|
||||||
|
- create optional `AssignmentIntent` for stage `jury2_evaluation` and/or direct policy-validated assignments
|
||||||
|
- send invitation
|
||||||
|
- on accept: jury onboarding -> cap/bias preference override if allowed (disclosure shown that bias is suggestive only)
|
||||||
|
|
||||||
|
### Team invite
|
||||||
|
- create/attach `TeamMember`
|
||||||
|
- send invitation
|
||||||
|
- on accept: applicant context landing
|
||||||
65
docs/codex-architecture-redesign-docs/README.md
Normal file
65
docs/codex-architecture-redesign-docs/README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Monaco OPC Architecture Redesign (Codex Plan)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This documentation set is a comprehensive redesign plan to align the current MOPC platform with the exact Monaco Ocean Protection Challenge flow you defined:
|
||||||
|
|
||||||
|
- Startups + Concepts in one competition lifecycle
|
||||||
|
- AI eligibility filtering
|
||||||
|
- Purpose-bound main juries as explicit entities (custom-labeled per program; often Jury 1 / Jury 2 / Jury 3)
|
||||||
|
- Round-specific submission bundles
|
||||||
|
- Special award routing modes
|
||||||
|
- Mentoring collaboration layer with promotion to official submissions
|
||||||
|
- Live finals control + final confirmation/lock workflow
|
||||||
|
- Full admin override and auditability
|
||||||
|
- End-to-end integration across all platform functions (including member invitation/onboarding/team flows)
|
||||||
|
|
||||||
|
The plan is intentionally designed to reduce current ambiguity and complexity while maximizing customizability through typed policies and explicit workflow entities.
|
||||||
|
|
||||||
|
## Scope Of This Plan
|
||||||
|
This plan covers:
|
||||||
|
|
||||||
|
1. Current-state architecture audit (schema, services, routers, UI surfaces)
|
||||||
|
2. Target-state architecture mapped 1:1 to Monaco flow
|
||||||
|
3. Gap analysis and simplification decisions
|
||||||
|
4. Typed domain model and policy contracts
|
||||||
|
5. Platform-wide integration matrix (all major functions/pages)
|
||||||
|
6. Implementation roadmap and migration/cutover strategy
|
||||||
|
7. QA, observability, release gates
|
||||||
|
8. Open decisions/questions for flow owners
|
||||||
|
|
||||||
|
This plan does **not** implement code changes. It is an implementation-ready architecture and delivery blueprint.
|
||||||
|
|
||||||
|
## Guiding Principles
|
||||||
|
1. Keep the stage engine; remove ambiguity around stage purpose.
|
||||||
|
2. Move critical competition behavior from loosely-typed `configJson` into typed models/policies.
|
||||||
|
3. Preserve admin override authority everywhere, but formalize override intent and audit structure.
|
||||||
|
4. Make jury/award/mentoring/finals flows first-class entities instead of inferred behavior.
|
||||||
|
5. Keep UX simple by exposing only context-relevant controls at each stage.
|
||||||
|
6. Ensure every user-facing workflow is backed by the redesigned architecture contract.
|
||||||
|
|
||||||
|
## Document Index
|
||||||
|
- `01-current-platform-architecture-audit.md`
|
||||||
|
- `02-monaco-flow-target-architecture.md`
|
||||||
|
- `03-gap-analysis-and-simplification-decisions.md`
|
||||||
|
- `04-unified-domain-model-and-config-contracts.md`
|
||||||
|
- `05-platform-wide-integration-matrix.md`
|
||||||
|
- `06-implementation-roadmap-and-migration-plan.md`
|
||||||
|
- `07-testing-observability-and-release-gates.md`
|
||||||
|
- `08-open-questions-for-flow-owners.md`
|
||||||
|
|
||||||
|
## Recommended Reading Order
|
||||||
|
1. Read `02-monaco-flow-target-architecture.md` for the desired end state.
|
||||||
|
2. Read `03-gap-analysis-and-simplification-decisions.md` to understand what must change and why.
|
||||||
|
3. Read `04-unified-domain-model-and-config-contracts.md` for schema/API/behavior contracts.
|
||||||
|
4. Read `05-platform-wide-integration-matrix.md` to see exact surface-level impact.
|
||||||
|
5. Read `06-implementation-roadmap-and-migration-plan.md` + `07-testing-observability-and-release-gates.md` for execution.
|
||||||
|
6. Resolve items in `08-open-questions-for-flow-owners.md` before Phase 1 starts.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
The redesign is successful when:
|
||||||
|
|
||||||
|
- The platform can run your full Monaco flow without workarounds.
|
||||||
|
- Admins can configure and override all required decisions transparently.
|
||||||
|
- Judges, mentors, applicants, and audience each see role-appropriate, stage-aware UX.
|
||||||
|
- Invite/onboarding/team management, assignment, judging, mentoring, and finals workflows all reference the same underlying competition model.
|
||||||
|
- Audit evidence can reconstruct every advance/reject/override/winner decision.
|
||||||
File diff suppressed because it is too large
Load Diff
61
docs/unified-architecture-redesign/00-README.md
Normal file
61
docs/unified-architecture-redesign/00-README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Unified Architecture Redesign — Monaco Ocean Protection Challenge
|
||||||
|
|
||||||
|
> **Status**: Design specification (pre-implementation)
|
||||||
|
> **Date**: 2026-02-15
|
||||||
|
> **Scope**: Complete redesign of the round/pipeline system for multi-round competition management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document set specifies the architecture redesign for the MOPC platform's competition system. The core change eliminates the Track abstraction layer and renames Pipeline → **Competition**, Stage → **Round**, producing a flat, linear round sequence with typed configurations per round type.
|
||||||
|
|
||||||
|
The design supports the full Monaco 2026 competition flow: Intake → AI Filtering → Jury 1 Evaluation → Semifinal Submission → Jury 2 Evaluation + Special Awards → Mentoring → Live Finals (Jury 3) → Deliberation → Result Lock.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Document Index
|
||||||
|
|
||||||
|
| # | Document | Description |
|
||||||
|
|---|----------|-------------|
|
||||||
|
| 01 | [Architecture & Decisions](./01-architecture-and-decisions.md) | Core decisions, ADRs, guiding principles, before/after architecture |
|
||||||
|
| 02 | [Data Model](./02-data-model.md) | Complete Prisma schema, typed config Zod schemas, enum definitions |
|
||||||
|
| 03 | [Competition Flow](./03-competition-flow.md) | Round-by-round specification (R1–R8) with cross-cutting behaviors |
|
||||||
|
| 04 | [Jury Groups & Assignment Policy](./04-jury-groups-and-assignment-policy.md) | JuryGroup model, cap modes, assignment algorithm, policy precedence |
|
||||||
|
| 05 | [Special Awards](./05-special-awards.md) | Award modes (stay-in-main vs separate pool), per-award pipelines |
|
||||||
|
| 06 | [Mentoring & Document Lifecycle](./06-mentoring-and-document-lifecycle.md) | Multi-round submissions, mentor workspace, file promotion |
|
||||||
|
| 07 | [Live Finals & Deliberation](./07-live-finals-and-deliberation.md) | Stage manager, Jury 3 live experience, deliberation voting, ResultLock |
|
||||||
|
| 08 | [Platform Integration Matrix](./08-platform-integration-matrix.md) | Page-by-page impact map for admin, jury, applicant, mentor UIs |
|
||||||
|
| 09 | [Implementation Roadmap](./09-implementation-roadmap.md) | 9-phase plan with dependencies, durations, rollback points |
|
||||||
|
| 10 | [Migration Strategy](./10-migration-strategy.md) | 4-phase schema migration, feature flags, data mapping, rollback |
|
||||||
|
| 11 | [Testing & QA](./11-testing-and-qa.md) | Test pyramid, Monaco flow test matrix, deliberation test matrix |
|
||||||
|
| 12 | [Observability & Release Gates](./12-observability-and-release-gates.md) | Metrics, structured logging, alerts, release gates A–F |
|
||||||
|
| 13 | [Open Questions & Governance](./13-open-questions-and-governance.md) | Resolved decisions, remaining P1/P2 questions, delivery governance |
|
||||||
|
|
||||||
|
## Recommended Reading Order
|
||||||
|
|
||||||
|
1. **01 Architecture & Decisions** — understand the "why" and core decisions
|
||||||
|
2. **02 Data Model** — the schema that everything builds on
|
||||||
|
3. **03 Competition Flow** — how the 8 rounds work end-to-end
|
||||||
|
4. **04–07** — deep dives into specific subsystems (read as needed)
|
||||||
|
5. **08 Integration Matrix** — understand the blast radius across the platform
|
||||||
|
6. **09–10** — implementation and migration planning
|
||||||
|
7. **11–12** — quality and operational readiness
|
||||||
|
8. **13 Open Questions** — remaining decisions and governance
|
||||||
|
|
||||||
|
## Key Terminology
|
||||||
|
|
||||||
|
| Old Term | New Term | Notes |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| Pipeline | **Competition** | Top-level competition round container |
|
||||||
|
| Stage | **Round** | Individual phase within a competition |
|
||||||
|
| Track | *(eliminated)* | Replaced by SpecialAward with routing modes |
|
||||||
|
| StageType | **RoundType** | INTAKE, FILTERING, EVALUATION, SUBMISSION, MENTORING, LIVE_FINAL, DELIBERATION |
|
||||||
|
| configJson | **Typed Config** | Per-round-type Zod-validated configuration |
|
||||||
|
| WinnerProposal | **DeliberationSession** | New deliberation voting model |
|
||||||
|
| WinnerApproval | **DeliberationVote** | Per-juror vote in deliberation |
|
||||||
|
| Confirmation | **Deliberation** | Deliberation IS the confirmation step |
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
All documents cross-reference each other by filename (e.g., "see [02-data-model.md](./02-data-model.md)"). Model names, config types, and enum values are consistent across all documents.
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# Architecture & Decisions
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document captures the core architectural decisions for the MOPC platform redesign. The redesign replaces the current Pipeline → Track → Stage model with a flatter Competition → Round model, introduces typed configurations per round type, and promotes juries, submission windows, and deliberation to first-class entities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Problems
|
||||||
|
|
||||||
|
| Problem | Impact |
|
||||||
|
|---------|--------|
|
||||||
|
| **3-level nesting** (Pipeline → Track → Stage) | Cognitive overhead for admins; unnecessary abstraction for a linear flow |
|
||||||
|
| **Generic `configJson` blobs** per stage type | No type safety; hard to know what's configurable without reading code |
|
||||||
|
| **No explicit jury entities** | Juries are implicit (per-stage assignments); can't manage "Jury 1" as an entity |
|
||||||
|
| **Single submission round** | No way to open a second submission window for semi-finalists |
|
||||||
|
| **Track layer for main flow** | MAIN track adds indirection with no value for a linear competition |
|
||||||
|
| **No mentoring workspace** | Mentor file exchange exists but no comments, messaging, or promotion to submission |
|
||||||
|
| **No winner confirmation/deliberation** | No multi-party agreement step to cement winners |
|
||||||
|
| **Missing round types** | Can't model "Semi-finalist Submission", "Mentoring", or "Deliberation" steps |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Before & After Architecture
|
||||||
|
|
||||||
|
### BEFORE (Current System)
|
||||||
|
|
||||||
|
```
|
||||||
|
Program
|
||||||
|
└── Pipeline (generic container)
|
||||||
|
├── Track: "Main Competition" (MAIN)
|
||||||
|
│ ├── Stage: "Intake" (INTAKE, configJson: {...})
|
||||||
|
│ ├── Stage: "Filtering" (FILTER, configJson: {...})
|
||||||
|
│ ├── Stage: "Evaluation" (EVALUATION, configJson: {...})
|
||||||
|
│ ├── Stage: "Selection" (SELECTION, configJson: {...})
|
||||||
|
│ ├── Stage: "Live Finals" (LIVE_FINAL, configJson: {...})
|
||||||
|
│ └── Stage: "Results" (RESULTS, configJson: {...})
|
||||||
|
├── Track: "Award 1" (AWARD)
|
||||||
|
└── Track: "Award 2" (AWARD)
|
||||||
|
|
||||||
|
Juries: implicit (per-stage assignments, no named entity)
|
||||||
|
Submissions: single round (one INTAKE stage)
|
||||||
|
Mentoring: basic (messages + notes, no workspace)
|
||||||
|
Winner confirmation: none
|
||||||
|
```
|
||||||
|
|
||||||
|
### AFTER (Redesigned System)
|
||||||
|
|
||||||
|
```
|
||||||
|
Program
|
||||||
|
└── Competition (replaces Pipeline, purpose-built)
|
||||||
|
├── Rounds (linear sequence, replaces Track + Stage):
|
||||||
|
│ ├── R1: "Application Window" ─────── (INTAKE)
|
||||||
|
│ ├── R2: "AI Screening" ──────────── (FILTERING)
|
||||||
|
│ ├── R3: "Jury 1 - Semi-finalist" ── (EVALUATION) ── juryGroupId: jury-1
|
||||||
|
│ ├── R4: "Semi-finalist Docs" ─────── (SUBMISSION)
|
||||||
|
│ ├── R5: "Jury 2 - Finalist" ──────── (EVALUATION) ── juryGroupId: jury-2
|
||||||
|
│ ├── R6: "Finalist Mentoring" ─────── (MENTORING)
|
||||||
|
│ ├── R7: "Live Finals" ────────────── (LIVE_FINAL) ── juryGroupId: jury-3
|
||||||
|
│ └── R8: "Deliberation" ───────────── (DELIBERATION)
|
||||||
|
│
|
||||||
|
├── Jury Groups (explicit, named entities):
|
||||||
|
│ ├── "Jury 1" ── members, caps, ratios ── linked to R3
|
||||||
|
│ ├── "Jury 2" ── members, caps, ratios ── linked to R5
|
||||||
|
│ └── "Jury 3" ── members ── linked to R7 + R8
|
||||||
|
│
|
||||||
|
├── Submission Windows (multi-round):
|
||||||
|
│ ├── Window 1: "Round 1 Docs" ── requirements: [Exec Summary, Business Plan]
|
||||||
|
│ └── Window 2: "Round 2 Docs" ── requirements: [Updated Plan, Video Pitch]
|
||||||
|
│
|
||||||
|
└── Special Awards (standalone entities):
|
||||||
|
├── "Innovation Award" ── mode: STAY_IN_MAIN
|
||||||
|
└── "Impact Award" ── mode: SEPARATE_POOL
|
||||||
|
|
||||||
|
Juries: first-class JuryGroup entities with members, caps, ratios
|
||||||
|
Submissions: multi-round with per-window file requirements
|
||||||
|
Mentoring: full workspace with messaging, file exchange, comments, promotion
|
||||||
|
Deliberation: structured voting with multiple modes, tie-breaking, result lock
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Guiding Principles
|
||||||
|
|
||||||
|
| # | Principle | Description |
|
||||||
|
|---|-----------|-------------|
|
||||||
|
| 1 | **Domain over abstraction** | Models map directly to competition concepts (Jury 1, Round 2, Submission Window). No unnecessary intermediate layers. |
|
||||||
|
| 2 | **Linear by default** | The main competition flow is sequential. Branching exists only for special awards (standalone entities). |
|
||||||
|
| 3 | **Typed configs over JSON blobs** | Each round type has an explicit Zod-validated configuration schema. No more guessing what fields are available. |
|
||||||
|
| 4 | **Explicit entities** | Juries, submission windows, deliberation sessions, and mentor workspaces are first-class database models. |
|
||||||
|
| 5 | **Admin override everywhere** | Any automated decision can be manually overridden with full audit trail. |
|
||||||
|
| 6 | **Deep integration** | Jury groups link to rounds, rounds link to submissions, submissions link to evaluations. No orphaned features. |
|
||||||
|
| 7 | **No silent contract drift** | All schema changes, config shape changes, and behavior changes go through review. Late-stage changes require explicit architecture sign-off. |
|
||||||
|
| 8 | **Score independence** | Juries evaluate independently. Cross-jury visibility is admin-configurable, not automatic. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
### ADR-01: Eliminate the Track Layer
|
||||||
|
|
||||||
|
**Decision:** Remove the `Track` model entirely. The main competition is a flat sequence of Rounds. Special awards become standalone entities with routing modes.
|
||||||
|
|
||||||
|
**Rationale:** The MOPC competition has one main flow (R1→R8). The Track concept (MAIN/AWARD/SHOWCASE with RoutingMode and DecisionMode) was designed for branching flows that don't exist. Awards don't need their own track — they're parallel processes that reference the same projects.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `Track` model deleted
|
||||||
|
- `TrackKind`, `RoutingMode` enums deleted
|
||||||
|
- `ProjectStageState.trackId` removed
|
||||||
|
- Special awards modeled as standalone `SpecialAward` entities with `STAY_IN_MAIN` / `SEPARATE_POOL` modes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-02: Rename Pipeline → Competition, Stage → Round
|
||||||
|
|
||||||
|
**Decision:** Rename `Pipeline` to `Competition` and `Stage` to `Round` throughout the codebase.
|
||||||
|
|
||||||
|
**Rationale:** "Competition" and "Round" map directly to how admins and participants think about the system. A competition has rounds. This reduces cognitive overhead in every conversation, document, and UI label.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- All database models, tRPC routers, services, and UI pages renamed
|
||||||
|
- Migration adds new tables, backfills, then drops old tables
|
||||||
|
- See [10-migration-strategy.md](./10-migration-strategy.md) for full mapping
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-03: Typed Configuration per Round Type
|
||||||
|
|
||||||
|
**Decision:** Replace the generic `configJson: Json` blob with 7 typed Zod schemas — one per RoundType.
|
||||||
|
|
||||||
|
**Rationale:** `configJson` provided flexibility at the cost of discoverability and safety. Developers and admins couldn't know what fields were available without reading service code. Typed configs give compile-time and runtime validation.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- 7 Zod schemas: `IntakeConfig`, `FilteringConfig`, `EvaluationConfig`, `SubmissionConfig`, `MentoringConfig`, `LiveFinalConfig`, `DeliberationConfig`
|
||||||
|
- `configJson` field preserved for storage but validated against the appropriate schema on read/write
|
||||||
|
- Admin UI round configuration forms generated from schema definitions
|
||||||
|
- See [02-data-model.md](./02-data-model.md) for full schema definitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-04: JuryGroup as First-Class Entity
|
||||||
|
|
||||||
|
**Decision:** Create `JuryGroup` and `JuryGroupMember` models as named, manageable entities with caps, ratios, and policy configuration.
|
||||||
|
|
||||||
|
**Rationale:** Currently, juries are implicit — a set of assignments for a stage. This makes it impossible to manage "Jury 1" as a thing, configure per-juror caps, or support jury member overlap across rounds. Making JuryGroups explicit enables a dedicated "Juries" admin section.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- New `JuryGroup` model with label, competition binding, and default policies
|
||||||
|
- New `JuryGroupMember` model with per-member cap overrides and ratio preferences
|
||||||
|
- Members can belong to multiple JuryGroups (Jury 1 + Jury 2 + award juries)
|
||||||
|
- See [04-jury-groups-and-assignment-policy.md](./04-jury-groups-and-assignment-policy.md) for details
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-05: Deliberation as Confirmation
|
||||||
|
|
||||||
|
**Decision:** Replace the WinnerProposal → jury sign-off → admin approval confirmation flow with a structured deliberation voting system. Deliberation IS the confirmation — no separate step needed.
|
||||||
|
|
||||||
|
**Rationale:** The original confirmation flow required unanimous jury agreement plus admin approval, which is rigid. The deliberation model supports two configurable voting modes (SINGLE_WINNER_VOTE and FULL_RANKING), multiple tie-breaking methods, admin override, and per-category independence — all while serving as the final agreement mechanism.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `WinnerProposal`, `WinnerApproval` models removed
|
||||||
|
- New models: `DeliberationSession`, `DeliberationVote`, `DeliberationResult`, `DeliberationParticipant`
|
||||||
|
- New enums: `DeliberationMode`, `DeliberationStatus`, `TieBreakMethod`, `DeliberationParticipantStatus`
|
||||||
|
- `ResultLock` model cements the outcome
|
||||||
|
- See [07-live-finals-and-deliberation.md](./07-live-finals-and-deliberation.md) for the full deliberation specification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-06: Score Independence with Configurable Visibility
|
||||||
|
|
||||||
|
**Decision:** Juries are fully independent during active evaluation. During Live Finals and Deliberation, prior jury data is visible to Jury 3 only if admin enables `showPriorJuryData`. All cross-jury data is available in reports.
|
||||||
|
|
||||||
|
**Rationale:** Independent evaluation prevents bias during scoring. However, during the live finals phase, Jury 3 may benefit from seeing the evaluation history. This is a judgment call that should be made by the program admin per competition.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- No cross-jury queries in jury evaluation pages
|
||||||
|
- `showPriorJuryData` toggle on `LiveFinalConfig` and `DeliberationSession`
|
||||||
|
- Reports section has full cross-jury analytics for internal use
|
||||||
|
- See [03-competition-flow.md](./03-competition-flow.md) cross-cutting behaviors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-07: Top-N Configurable Winner Model
|
||||||
|
|
||||||
|
**Decision:** Winners are Top N (configurable, default 3) per category, all projects ranked within their category, with podium UI and cross-category comparison view.
|
||||||
|
|
||||||
|
**Rationale:** Different competitions may want different numbers of winners. All finalist projects should be ranked regardless — this provides maximum flexibility for award ceremonies and reporting.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `topN` field in `DeliberationConfig`
|
||||||
|
- `finalRank` field in `DeliberationResult`
|
||||||
|
- UI: podium display for top 3, full ranking table, cross-category comparison view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-08: 5-Layer Policy Precedence
|
||||||
|
|
||||||
|
**Decision:** Assignment and configuration policies resolve through a 5-layer precedence chain.
|
||||||
|
|
||||||
|
**Rationale:** Different levels of the system need to set defaults while allowing overrides. A judge might have a program-wide cap default but need a competition-specific override.
|
||||||
|
|
||||||
|
| Layer | Scope | Example |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| 1. System default | Platform-wide | softCapBuffer = 10 |
|
||||||
|
| 2. Program default | Per-program settings | defaultCapMode = SOFT |
|
||||||
|
| 3. Jury group default | Per-JuryGroup | maxProjectsPerJuror = 15 |
|
||||||
|
| 4. Per-member override | Individual judge | judge-A.maxProjects = 20 |
|
||||||
|
| 5. Admin override | Always wins | force-assign project X to judge-A |
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- Policy resolution function consulted during assignment
|
||||||
|
- Admin overrides logged to `DecisionAuditLog`
|
||||||
|
- See [04-jury-groups-and-assignment-policy.md](./04-jury-groups-and-assignment-policy.md) for implementation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-09: AI Ranked Shortlist at Every Evaluation Round
|
||||||
|
|
||||||
|
**Decision:** AI generates a recommended ranked shortlist per category at the end of every evaluation round (Jury 1, Jury 2, and any award evaluation). Admin can always override.
|
||||||
|
|
||||||
|
**Rationale:** Consistent AI assistance across all evaluation rounds reduces admin workload and provides data-driven recommendations.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `generateAiShortlist` flag in `EvaluationConfig`
|
||||||
|
- AI shortlist service invoked at round completion
|
||||||
|
- Admin UI shows AI recommendations alongside manual selection controls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-10: Assignment Intent Lifecycle Management
|
||||||
|
|
||||||
|
**Decision:** Track assignment intents through a full lifecycle (PENDING → HONORED → OVERRIDDEN → EXPIRED → CANCELLED) rather than a simple "create and forget" approach.
|
||||||
|
|
||||||
|
**Rationale:** Intents created at invite time may not be fulfilled due to COI conflicts, cap limits, or admin changes. Without lifecycle tracking, stale intents accumulate with no visibility into why they weren't honored. The lifecycle state machine provides clear audit trails and enables admin dashboards showing intent fulfillment rates.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `AssignmentIntentStatus` enum with 5 states (all terminal except PENDING)
|
||||||
|
- Algorithm must check and honor PENDING intents before general assignment
|
||||||
|
- Round close triggers batch expiry of unmatched intents
|
||||||
|
- See [04-jury-groups-and-assignment-policy.md](./04-jury-groups-and-assignment-policy.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-11: Rejected — Submission Bundle State Tracking
|
||||||
|
|
||||||
|
**Decision:** Do NOT implement a formal `SubmissionBundle` entity with state machine (INCOMPLETE → COMPLETE → LOCKED). Instead, derive completeness from slot requirements vs. uploaded files.
|
||||||
|
|
||||||
|
**Rationale:** The Codex plan proposed a `SubmissionBundle` model that tracked aggregate state across all file slots. This adds a model, a state machine, and synchronization logic (must update bundle state whenever any file changes). The simpler approach — checking `required slots - uploaded files` at query time — achieves the same result without the maintenance burden. The `SubmissionWindow.lockOnClose` flag handles the lock lifecycle.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- No `SubmissionBundle` model needed
|
||||||
|
- Completeness is a computed property, not a stored state
|
||||||
|
- `SubmissionWindow` + `FileRequirement` + `ProjectFile` are sufficient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ADR-12: Optional Purpose Keys for Analytics Grouping
|
||||||
|
|
||||||
|
**Decision:** Add an optional `Round.purposeKey: String?` field for analytics grouping rather than a new `PurposeKey` enum.
|
||||||
|
|
||||||
|
**Rationale:** Different programs may want to compare rounds across competitions (e.g., "all jury-1 selections across 2025 and 2026"). A `purposeKey` like "jury1_selection" enables this without making it a structural dependency. A free-text string is preferred over an enum because new analytics categories shouldn't require schema migrations.
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `Round.purposeKey` is optional and has no runtime behavior
|
||||||
|
- Used only for cross-competition analytics queries and reporting
|
||||||
|
- Convention: use snake_case descriptive keys (e.g., "jury1_selection", "semifinal_docs", "live_finals")
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Governance Policy
|
||||||
|
|
||||||
|
### No Silent Contract Drift
|
||||||
|
|
||||||
|
Any change to the following after Phase 0 (Contract Freeze) requires explicit architecture review:
|
||||||
|
|
||||||
|
- Prisma model additions or field changes
|
||||||
|
- Zod config schema modifications
|
||||||
|
- RoundType enum changes
|
||||||
|
- tRPC procedure signature changes
|
||||||
|
- Assignment policy behavior changes
|
||||||
|
|
||||||
|
Changes are not blocked — they require a documented decision with rationale, impact analysis, and sign-off from the architecture owner. See [13-open-questions-and-governance.md](./13-open-questions-and-governance.md) for governance process.
|
||||||
1801
docs/unified-architecture-redesign/02-data-model.md
Normal file
1801
docs/unified-architecture-redesign/02-data-model.md
Normal file
File diff suppressed because it is too large
Load Diff
2949
docs/unified-architecture-redesign/03-competition-flow.md
Normal file
2949
docs/unified-architecture-redesign/03-competition-flow.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,349 @@
|
|||||||
|
# Jury Groups & Assignment Policy
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Jury groups are first-class entities in the redesigned system. Each jury (Jury 1, Jury 2, Jury 3, award juries) is an explicit `JuryGroup` with named members, configurable assignment caps, category ratio preferences, and policy overrides. This document covers the data model, assignment algorithm, policy precedence, and admin controls.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### JuryGroup
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model JuryGroup {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
competitionId String
|
||||||
|
label String // "Jury 1", "Jury 2", "Live Finals Jury", custom per program
|
||||||
|
description String?
|
||||||
|
roundId String? // which round this jury evaluates (nullable for award juries)
|
||||||
|
|
||||||
|
// Default policies for this group
|
||||||
|
defaultCapMode CapMode @default(SOFT)
|
||||||
|
defaultMaxProjects Int @default(15)
|
||||||
|
softCapBuffer Int @default(10)
|
||||||
|
defaultCategoryBias Json? // { STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 } or null for no preference
|
||||||
|
allowOnboardingSelfService Boolean @default(true) // judges can adjust cap/ratio during onboarding
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
competition Competition @relation(fields: [competitionId], references: [id])
|
||||||
|
round Round? @relation(fields: [roundId], references: [id])
|
||||||
|
members JuryGroupMember[]
|
||||||
|
|
||||||
|
@@index([competitionId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### JuryGroupMember
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model JuryGroupMember {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
juryGroupId String
|
||||||
|
userId String
|
||||||
|
role JuryGroupMemberRole @default(MEMBER) // CHAIR, MEMBER, OBSERVER
|
||||||
|
|
||||||
|
// Per-member overrides (null = use group default)
|
||||||
|
capMode CapMode? // override group default
|
||||||
|
maxProjects Int? // override group default
|
||||||
|
categoryBias Json? // override group default ratio
|
||||||
|
|
||||||
|
// Onboarding self-service values (set by judge during onboarding if allowed)
|
||||||
|
selfServiceCap Int?
|
||||||
|
selfServiceBias Json?
|
||||||
|
|
||||||
|
availabilityNotes String? // Free-text availability info
|
||||||
|
|
||||||
|
joinedAt DateTime @default(now())
|
||||||
|
|
||||||
|
juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id])
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@unique([juryGroupId, userId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum JuryGroupMemberRole {
|
||||||
|
CHAIR // Jury lead — can manage session, see aggregate data, moderate deliberation
|
||||||
|
MEMBER // Regular juror — votes, scores, provides feedback
|
||||||
|
OBSERVER // View-only — can see evaluations/deliberations but cannot vote or score
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Role Permissions:**
|
||||||
|
|
||||||
|
| Permission | CHAIR | MEMBER | OBSERVER |
|
||||||
|
|-----------|-------|--------|----------|
|
||||||
|
| Submit evaluations | Yes | Yes | No |
|
||||||
|
| Vote in deliberation | Yes | Yes | No |
|
||||||
|
| See aggregate scores (during deliberation) | Yes | No* | No |
|
||||||
|
| Moderate deliberation discussion | Yes | No | No |
|
||||||
|
| View all assigned projects | Yes | Yes | Yes (read-only) |
|
||||||
|
| Be counted toward quorum | Yes | Yes | No |
|
||||||
|
|
||||||
|
*Members see aggregates only if `showCollectiveRankings` is enabled in DeliberationConfig.
|
||||||
|
|
||||||
|
### Supporting Enums
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum CapMode {
|
||||||
|
HARD // AI cannot assign more than maxProjects
|
||||||
|
SOFT // AI tries to stay under maxProjects, can go up to maxProjects + softCapBuffer
|
||||||
|
NONE // No cap (unlimited assignments)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cap Mode Behavior
|
||||||
|
|
||||||
|
| Cap Mode | AI Assignment Behavior | Manual Assignment |
|
||||||
|
|----------|----------------------|-------------------|
|
||||||
|
| **HARD** | AI will NOT assign more than `maxProjects`. Period. | Admin can still force-assign beyond cap (creates `AssignmentException`). |
|
||||||
|
| **SOFT** | AI tries to stay at `maxProjects`. If excess projects remain after distributing to all judges at their cap, excess is distributed evenly among SOFT judges up to `maxProjects + softCapBuffer`. | Same as HARD — admin can override. |
|
||||||
|
| **NONE** | No limit. AI distributes projects as needed. | No limit. |
|
||||||
|
|
||||||
|
### Effective Cap Calculation
|
||||||
|
|
||||||
|
The effective cap for a judge considers the 5-layer policy precedence:
|
||||||
|
|
||||||
|
```
|
||||||
|
effectiveCap(member) =
|
||||||
|
member.selfServiceCap // Layer 4: onboarding self-service (if allowed and set)
|
||||||
|
?? member.maxProjects // Layer 4: admin per-member override
|
||||||
|
?? member.juryGroup.defaultMaxProjects // Layer 3: jury group default
|
||||||
|
?? program.defaultMaxProjects // Layer 2: program default
|
||||||
|
?? 15 // Layer 1: system default
|
||||||
|
```
|
||||||
|
|
||||||
|
Admin override (Layer 5) bypasses this entirely via `AssignmentException`.
|
||||||
|
|
||||||
|
### Soft Cap Overflow Algorithm
|
||||||
|
|
||||||
|
1. Assign projects to all judges up to their effective cap, respecting category bias
|
||||||
|
2. Count remaining unassigned projects
|
||||||
|
3. If remaining > 0 and SOFT-cap judges exist:
|
||||||
|
- Distribute remaining projects evenly among SOFT judges
|
||||||
|
- Stop when each SOFT judge reaches `effectiveCap + softCapBuffer`
|
||||||
|
4. Any projects still unassigned go to the **Unassigned Queue**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Category Quotas & Ratio Bias
|
||||||
|
|
||||||
|
### CategoryQuotas (per JuryGroup)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CategoryQuotas = {
|
||||||
|
STARTUP: { min: number; max: number }
|
||||||
|
BUSINESS_CONCEPT: { min: number; max: number }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Category Bias (per member)
|
||||||
|
|
||||||
|
Category bias is a **soft preference**, not a hard constraint. It influences the assignment algorithm's project selection but does not guarantee exact ratios.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CategoryBias = {
|
||||||
|
STARTUP: number // e.g., 0.6 = prefer 60% startups
|
||||||
|
BUSINESS_CONCEPT: number // e.g., 0.4 = prefer 40% concepts
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Special cases:
|
||||||
|
- `{ STARTUP: 1.0, BUSINESS_CONCEPT: 0.0 }` — judge reviews only startups
|
||||||
|
- `{ STARTUP: 0.0, BUSINESS_CONCEPT: 1.0 }` — judge reviews only concepts
|
||||||
|
- `null` — no preference, algorithm decides
|
||||||
|
|
||||||
|
**Disclosure:** When bias is set, it is disclosed to judges on their dashboard (e.g., "You have been assigned primarily Startup projects based on your preference").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5-Layer Policy Precedence
|
||||||
|
|
||||||
|
| Layer | Scope | Who Sets It | Override Behavior |
|
||||||
|
|-------|-------|-------------|-------------------|
|
||||||
|
| 1 | System default | Platform config | Baseline for all programs |
|
||||||
|
| 2 | Program default | Program admin | Overrides system default for this program |
|
||||||
|
| 3 | Jury group default | Program admin | Overrides program default for this jury |
|
||||||
|
| 4a | Per-member override | Program admin | Overrides jury group default for this judge |
|
||||||
|
| 4b | Self-service override | Judge (during onboarding) | Only if `allowOnboardingSelfService` is true. Cannot exceed admin-set maximum. |
|
||||||
|
| 5 | Admin override | Program admin / Super admin | Always wins. Creates `AssignmentException` with audit trail. |
|
||||||
|
|
||||||
|
Policy resolution proceeds from Layer 5 → Layer 1. The first non-null value wins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Judge Onboarding Self-Service
|
||||||
|
|
||||||
|
When `allowOnboardingSelfService` is enabled on the JuryGroup:
|
||||||
|
|
||||||
|
1. Judge receives invitation email with link to accept
|
||||||
|
2. During onboarding, judge sees their assigned cap and category preference
|
||||||
|
3. Judge can adjust:
|
||||||
|
- **Max projects**: up or down within admin-defined bounds
|
||||||
|
- **Category preference**: ratio of startups vs concepts
|
||||||
|
4. Adjusted values stored in `JuryGroupMember.selfServiceCap` and `JuryGroupMember.selfServiceBias`
|
||||||
|
5. Admin can review and override self-service values at any time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assignment Algorithm
|
||||||
|
|
||||||
|
### Input
|
||||||
|
- List of eligible projects (with category)
|
||||||
|
- List of JuryGroupMembers (with effective caps, biases, COI declarations)
|
||||||
|
- CategoryQuotas for the JuryGroup
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **COI Check**: For each judge-project pair, check COI declarations. Skip if COI exists.
|
||||||
|
2. **Category Allocation**: Divide projects into STARTUP and BUSINESS_CONCEPT pools.
|
||||||
|
3. **Primary Assignment** (up to effective cap):
|
||||||
|
- For each judge, select projects matching their category bias
|
||||||
|
- Apply geo-diversity penalty and familiarity bonus (existing algorithm)
|
||||||
|
- Stop when judge reaches effective cap
|
||||||
|
4. **Soft Cap Overflow** (if unassigned projects remain):
|
||||||
|
- Identify SOFT-cap judges
|
||||||
|
- Distribute remaining projects evenly up to cap + buffer
|
||||||
|
5. **Unassigned Queue**: Any remaining projects enter the queue with reason codes
|
||||||
|
|
||||||
|
### Reason Codes for Unassigned Queue
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| `ALL_HARD_CAPPED` | All judges at hard cap |
|
||||||
|
| `SOFT_BUFFER_EXHAUSTED` | All soft-cap judges at cap + buffer |
|
||||||
|
| `COI_CONFLICT` | Project has COI with all available judges |
|
||||||
|
| `CATEGORY_IMBALANCE` | No judges available for this category |
|
||||||
|
| `MANUAL_ONLY` | Project flagged for manual assignment |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AssignmentIntent (Pre-Assignment at Invite Time)
|
||||||
|
|
||||||
|
From the invite/onboarding integration: when a jury member is invited, the admin can optionally pre-assign specific projects.
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model AssignmentIntent {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
juryGroupMemberId String
|
||||||
|
roundId String
|
||||||
|
projectId String
|
||||||
|
source AssignmentIntentSource // INVITE | ADMIN | SYSTEM
|
||||||
|
status AssignmentIntentStatus @default(PENDING)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
juryGroupMember JuryGroupMember @relation(...)
|
||||||
|
round Round @relation(...)
|
||||||
|
project Project @relation(...)
|
||||||
|
|
||||||
|
@@unique([juryGroupMemberId, roundId, projectId])
|
||||||
|
@@index([status])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AssignmentIntentSource {
|
||||||
|
INVITE // set during invitation
|
||||||
|
ADMIN // set by admin after invite
|
||||||
|
SYSTEM // set by AI suggestion
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AssignmentIntentStatus {
|
||||||
|
PENDING // Created, awaiting assignment algorithm execution
|
||||||
|
HONORED // Algorithm materialized this intent into an Assignment record
|
||||||
|
OVERRIDDEN // Admin changed the assignment, superseding this intent
|
||||||
|
EXPIRED // Round completed without this intent being honored
|
||||||
|
CANCELLED // Explicitly cancelled by admin or system
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intent Lifecycle State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
PENDING ──── [algorithm runs, intent matched] ──── → HONORED
|
||||||
|
│
|
||||||
|
├──── [admin changes assignment] ──────────── → OVERRIDDEN
|
||||||
|
├──── [round completes unmatched] ─────────── → EXPIRED
|
||||||
|
└──── [admin/system cancels] ──────────────── → CANCELLED
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lifecycle rules:**
|
||||||
|
- **PENDING → HONORED**: When the assignment algorithm runs and creates an `Assignment` record matching this intent's judge+project pair. The `AssignmentIntent.id` is stored on the `Assignment` as provenance.
|
||||||
|
- **PENDING → OVERRIDDEN**: When an admin manually assigns the project to a different judge, or removes the intended judge from the JuryGroup. The override is logged to `DecisionAuditLog`.
|
||||||
|
- **PENDING → EXPIRED**: When the round transitions to CLOSED and this intent was never materialized. Automatic batch update on round close.
|
||||||
|
- **PENDING → CANCELLED**: When an admin explicitly removes the intent, or when the judge is removed from the JuryGroup. Source logged.
|
||||||
|
- **Terminal states**: HONORED, OVERRIDDEN, EXPIRED, CANCELLED are all terminal. No transitions out.
|
||||||
|
|
||||||
|
**Algorithm integration:**
|
||||||
|
1. Before algorithmic assignment, load all PENDING intents for the round
|
||||||
|
2. For each intent, attempt to create the assignment (respecting COI, cap checks)
|
||||||
|
3. If assignment succeeds → mark intent HONORED
|
||||||
|
4. If assignment fails (COI, cap exceeded) → keep PENDING, add to unassigned queue with reason code `INTENT_BLOCKED`
|
||||||
|
5. Admin can manually resolve blocked intents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AssignmentException (Audited Over-Cap Assignment)
|
||||||
|
|
||||||
|
When an admin manually assigns a project to a judge who is at or beyond their cap:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model AssignmentException {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
assignmentId String // the actual assignment record
|
||||||
|
reason String // admin must provide justification
|
||||||
|
overCapBy Int // how many over the effective cap
|
||||||
|
approvedById String // admin who approved
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
approvedBy User @relation(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All exceptions are visible in the assignment audit view and included in compliance reports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Jury Membership
|
||||||
|
|
||||||
|
Judges can belong to multiple JuryGroups simultaneously:
|
||||||
|
|
||||||
|
- Judge A is on Jury 1 AND Jury 2
|
||||||
|
- Judge B is on Jury 2 AND the Innovation Award jury
|
||||||
|
- Judge C is on Jury 1, Jury 2, AND Jury 3
|
||||||
|
|
||||||
|
Each membership has independent cap/bias configuration. A judge's Jury 1 cap of 20 doesn't affect their Jury 2 cap of 10.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin "Juries" UI Section
|
||||||
|
|
||||||
|
A dedicated admin section for managing JuryGroups:
|
||||||
|
|
||||||
|
- **Create Jury Group**: name, description, linked round, default policies
|
||||||
|
- **Manage Members**: add/remove members, set per-member overrides, view self-service adjustments
|
||||||
|
- **View Assignments**: see all assignments for the group, identify unassigned projects
|
||||||
|
- **Manual Assignment**: drag-and-drop or bulk-assign unassigned projects
|
||||||
|
- **Assignment Audit**: view all exceptions, overrides, and intent fulfillment
|
||||||
|
|
||||||
|
This is separate from the round configuration — JuryGroups are entities that exist independently and are linked to rounds.
|
||||||
|
|
||||||
|
See [08-platform-integration-matrix.md](./08-platform-integration-matrix.md) for full page mapping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Override
|
||||||
|
|
||||||
|
Admin can override ANY assignment decision at ANY time:
|
||||||
|
|
||||||
|
- Reassign a project from one judge to another
|
||||||
|
- Assign a project to a judge beyond their cap (creates `AssignmentException`)
|
||||||
|
- Remove an assignment
|
||||||
|
- Change a judge's cap or bias mid-round
|
||||||
|
- Override the algorithm's category allocation
|
||||||
|
|
||||||
|
All overrides are logged to `DecisionAuditLog` with the admin's identity, timestamp, and reason.
|
||||||
248
docs/unified-architecture-redesign/05-special-awards.md
Normal file
248
docs/unified-architecture-redesign/05-special-awards.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# Special Awards
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Special awards run alongside the main competition flow, typically activating during or after the Jury 2 evaluation round (R5). Each award has its own jury, document requirements, review window, and selection process. Awards do NOT use the deliberation model — they are decided by their own jury/judge mechanism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Award Routing Modes
|
||||||
|
|
||||||
|
Every special award operates in one of two configurable modes:
|
||||||
|
|
||||||
|
### Mode A: SEPARATE_POOL (Pull-Out)
|
||||||
|
|
||||||
|
Projects are filtered into a dedicated award pool. The award may:
|
||||||
|
- **Pull projects out** of the main competition pool, OR
|
||||||
|
- **Keep projects in both** the main pool and the award pool
|
||||||
|
|
||||||
|
Pull-out requires **admin confirmation** (`routingConfirmationMode: ADMIN_CONFIRMED`). The admin reviews which projects are pulled out and approves before the pull-out takes effect.
|
||||||
|
|
||||||
|
```
|
||||||
|
Main Pool ──┬──→ continues in main competition
|
||||||
|
└──→ [admin confirms] ──→ Award Pool ──→ Award Review ──→ Award Winner
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode B: STAY_IN_MAIN (Dual Track)
|
||||||
|
|
||||||
|
Projects remain in the main competition but are flagged as "eligible for award." The award evaluation runs in parallel with the main flow.
|
||||||
|
|
||||||
|
```
|
||||||
|
Main Pool ──→ continues in main competition
|
||||||
|
│
|
||||||
|
└──→ flagged "eligible" ──→ Award Review ──→ Award Winner
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Award Mini-Pipeline
|
||||||
|
|
||||||
|
Each special award has its own mini-pipeline:
|
||||||
|
|
||||||
|
1. **Filtering** → AI screens eligible projects based on award criteria
|
||||||
|
2. **Review** → Award jury evaluates eligible projects
|
||||||
|
3. **Selection** → Winner(s) selected by jury vote or single-judge decision
|
||||||
|
|
||||||
|
This mini-pipeline is independent of the main competition rounds. It has its own:
|
||||||
|
- **Jury** (or reuses judges from main juries, with overlap allowed)
|
||||||
|
- **Document requirements** (if the award needs specific docs beyond main submissions)
|
||||||
|
- **Review window** (own start/end dates)
|
||||||
|
- **Selection process** (jury vote or single-judge)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
### SpecialAward
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model SpecialAward {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
competitionId String
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
|
||||||
|
// Routing
|
||||||
|
routingMode AwardRoutingMode // STAY_IN_MAIN | SEPARATE_POOL
|
||||||
|
pullOutBehavior PullOutBehavior? // REMOVE_FROM_MAIN | KEEP_IN_BOTH (only for SEPARATE_POOL)
|
||||||
|
routingConfirmationMode RoutingConfirmation @default(ADMIN_CONFIRMED)
|
||||||
|
|
||||||
|
// Eligibility
|
||||||
|
eligibilityMode AwardEligibilityMode // AI_SUGGESTED | MANUAL | ALL_ELIGIBLE | ROUND_BASED
|
||||||
|
eligibilityCriteria Json? // AI screening criteria (if AI_SUGGESTED)
|
||||||
|
eligibilityRoundId String? // filter from this round's output (if ROUND_BASED)
|
||||||
|
|
||||||
|
// Jury
|
||||||
|
juryGroupId String? // dedicated jury group (null = single judge)
|
||||||
|
|
||||||
|
// Single judge mode
|
||||||
|
winnerDecisionMode WinnerDecisionMode @default(JURY_VOTE)
|
||||||
|
singleJudgeUserId String? // only if winnerDecisionMode = SINGLE_JUDGE
|
||||||
|
|
||||||
|
// Doc requirements
|
||||||
|
requiresAdditionalDocs Boolean @default(false)
|
||||||
|
docRequirements Json? // additional file slot definitions
|
||||||
|
|
||||||
|
// Review window
|
||||||
|
reviewWindowStart DateTime?
|
||||||
|
reviewWindowEnd DateTime?
|
||||||
|
|
||||||
|
// Audience voting
|
||||||
|
audienceVotingEnabled Boolean @default(false)
|
||||||
|
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
competition Competition @relation(...)
|
||||||
|
juryGroup JuryGroup? @relation(...)
|
||||||
|
winners AwardWinner[]
|
||||||
|
|
||||||
|
@@index([competitionId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### AwardWinner
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model AwardWinner {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
awardId String
|
||||||
|
projectId String
|
||||||
|
rank Int @default(1) // 1 = winner, 2+ = runner-up
|
||||||
|
decidedById String // judge or admin who confirmed
|
||||||
|
decisionMode WinnerDecisionMode
|
||||||
|
reason String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
award SpecialAward @relation(...)
|
||||||
|
project Project @relation(...)
|
||||||
|
|
||||||
|
@@unique([awardId, projectId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supporting Enums
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum AwardRoutingMode {
|
||||||
|
STAY_IN_MAIN // Mode B: projects stay in main pool
|
||||||
|
SEPARATE_POOL // Mode A: projects enter dedicated pool
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PullOutBehavior {
|
||||||
|
REMOVE_FROM_MAIN // pulled out of main competition
|
||||||
|
KEEP_IN_BOTH // in both main and award pools
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RoutingConfirmation {
|
||||||
|
ADMIN_CONFIRMED // admin must approve pull-out
|
||||||
|
AUTOMATIC // pull-out happens automatically on eligibility
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AwardEligibilityMode {
|
||||||
|
AI_SUGGESTED // AI screens projects against criteria
|
||||||
|
MANUAL // admin manually selects eligible projects
|
||||||
|
ALL_ELIGIBLE // all projects in the competition are eligible
|
||||||
|
ROUND_BASED // projects that passed a specific round are eligible
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WinnerDecisionMode {
|
||||||
|
JURY_VOTE // jury evaluates and votes
|
||||||
|
SINGLE_JUDGE // one designated judge decides
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Eligibility & Filtering
|
||||||
|
|
||||||
|
Award eligibility is determined by the `eligibilityMode`:
|
||||||
|
|
||||||
|
| Mode | How Projects Become Eligible |
|
||||||
|
|------|------------------------------|
|
||||||
|
| `AI_SUGGESTED` | AI screens all projects against the award's `eligibilityCriteria`. Uses the existing AI screening system. |
|
||||||
|
| `MANUAL` | Admin manually flags projects as eligible for this award. |
|
||||||
|
| `ALL_ELIGIBLE` | Every project in the competition is automatically eligible. |
|
||||||
|
| `ROUND_BASED` | Projects that passed a specific round (e.g., filtering, Jury 1) are eligible. |
|
||||||
|
|
||||||
|
For `AI_SUGGESTED`, the filtering uses the same AI screening infrastructure as the main R2 filtering round, just with award-specific criteria.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-Award Jury
|
||||||
|
|
||||||
|
Each award can have its own `JuryGroup`:
|
||||||
|
- Members can overlap with main juries (same judge on Jury 2 and Innovation Award jury)
|
||||||
|
- Independent cap and assignment configuration
|
||||||
|
- Own scoring rubric if needed
|
||||||
|
|
||||||
|
For simpler awards, `SINGLE_JUDGE` mode allows one designated judge to make the decision directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-Award Document Requirements
|
||||||
|
|
||||||
|
If `requiresAdditionalDocs` is true, the award defines additional file slots that eligible projects must submit:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AwardDocRequirement = {
|
||||||
|
slotKey: string // e.g., "innovation_statement"
|
||||||
|
label: string // "Innovation Impact Statement"
|
||||||
|
required: boolean
|
||||||
|
maxFileSize: number // bytes
|
||||||
|
acceptedTypes: string[] // ["application/pdf", "video/mp4"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These are separate from the main submission windows. Award docs are uploaded through the applicant's award-specific section.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Award Review Window
|
||||||
|
|
||||||
|
Each award has its own review window (start/end dates) that is independent of the main competition schedule. The review window:
|
||||||
|
- Can overlap with Jury 2 evaluation or run after it
|
||||||
|
- Shows countdown on the award jury's dashboard
|
||||||
|
- Triggers email reminders as the deadline approaches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## No Deliberation for Awards
|
||||||
|
|
||||||
|
Special awards are decided by their own jury/judge mechanism:
|
||||||
|
- **JURY_VOTE**: Award jury members evaluate and vote. Simple majority or highest score wins.
|
||||||
|
- **SINGLE_JUDGE**: Designated judge reviews and selects the winner(s).
|
||||||
|
|
||||||
|
There is no `DeliberationSession` for awards. The award winner is confirmed by the deciding jury/judge and recorded as an `AwardWinner`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audience Voting for Awards
|
||||||
|
|
||||||
|
If `audienceVotingEnabled` is true on an award:
|
||||||
|
- Audience can vote for their preferred project within the award pool
|
||||||
|
- Audience vote can influence the award decision (configurable weight)
|
||||||
|
- Audience vote results are visible to the award jury during their review
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Controls
|
||||||
|
|
||||||
|
- **Create/edit awards**: name, mode, eligibility, jury, doc requirements, review window
|
||||||
|
- **Confirm pull-out**: for SEPARATE_POOL mode, admin reviews and approves which projects are pulled
|
||||||
|
- **Override eligibility**: admin can add/remove projects from award eligibility at any time
|
||||||
|
- **Override winner**: admin can override the jury/judge decision with audit trail
|
||||||
|
- **View award status**: see all awards, their eligible projects, jury progress, and winners
|
||||||
|
|
||||||
|
All admin actions on awards are logged to `DecisionAuditLog`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
- **R5 (Jury 2)**: Awards typically activate during or after Jury 2. Award filtering runs alongside Jury 2 evaluation.
|
||||||
|
- **Live Finals (R7)**: Award winners may be announced during the live finals ceremony.
|
||||||
|
- **Reports**: Award selections, jury decisions, and audit trails are included in competition reports.
|
||||||
|
|
||||||
|
See [03-competition-flow.md](./03-competition-flow.md) for how awards fit into the overall competition flow.
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# Mentoring & Document Lifecycle
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document covers two interconnected systems: (1) the multi-round document lifecycle that governs how submissions flow through the competition, and (2) the mentoring workspace that provides a collaboration layer for finalist teams with assigned mentors.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: Document Lifecycle
|
||||||
|
|
||||||
|
### SubmissionWindow
|
||||||
|
|
||||||
|
Each round that requires document collection has an associated `SubmissionWindow`. A competition can have multiple windows (e.g., Round 1 application docs, Round 2 semifinal docs).
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model SubmissionWindow {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
competitionId String
|
||||||
|
roundId String
|
||||||
|
label String // "Round 1 Application Documents", "Round 2 Semifinal Documents"
|
||||||
|
|
||||||
|
opensAt DateTime
|
||||||
|
closesAt DateTime
|
||||||
|
deadlinePolicy DeadlinePolicy @default(HARD)
|
||||||
|
gracePeriodMinutes Int? // only used with GRACE policy
|
||||||
|
lockOnClose Boolean @default(true) // automatically lock submissions when window closes
|
||||||
|
isLocked Boolean @default(false) // manual lock toggle (admin can lock before close)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
competition Competition @relation(...)
|
||||||
|
round Round @relation(...)
|
||||||
|
requirements SubmissionFileRequirement[]
|
||||||
|
|
||||||
|
@@index([competitionId])
|
||||||
|
@@index([roundId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DeadlinePolicy
|
||||||
|
|
||||||
|
| Policy | Behavior |
|
||||||
|
|--------|----------|
|
||||||
|
| `HARD` | Submissions are cut off at `closesAt`. No uploads after the deadline. |
|
||||||
|
| `FLAG` | Submissions after `closesAt` are accepted but marked as **Late**. Admin can see late status. |
|
||||||
|
| `GRACE` | A grace period (`gracePeriodMinutes`) after `closesAt` during which submissions are still accepted without penalty. After grace, behaves as HARD. |
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
enum DeadlinePolicy {
|
||||||
|
HARD
|
||||||
|
FLAG
|
||||||
|
GRACE
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SubmissionFileRequirement
|
||||||
|
|
||||||
|
Each window defines required file slots:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model SubmissionFileRequirement {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
submissionWindowId String
|
||||||
|
slotKey String // "executive_summary", "business_plan", "pitch_video"
|
||||||
|
label String // "Executive Summary"
|
||||||
|
description String? // guidance text for applicants
|
||||||
|
required Boolean @default(true)
|
||||||
|
maxFileSize Int @default(10485760) // 10MB default
|
||||||
|
acceptedTypes String[] // ["application/pdf", "video/mp4"]
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
|
||||||
|
submissionWindow SubmissionWindow @relation(...)
|
||||||
|
|
||||||
|
@@unique([submissionWindowId, slotKey])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Round Document Visibility
|
||||||
|
|
||||||
|
| Actor | Current Round Docs | Previous Round Docs |
|
||||||
|
|-------|-------------------|-------------------|
|
||||||
|
| **Applicant** | Can upload/edit until window closes | **Read-only** (view/download only) |
|
||||||
|
| **Judge** | Sees current round docs in review | Sees previous round docs (clearly separated in UI) |
|
||||||
|
| **Admin** | Full control (upload/remove/replace) | Full control (upload/remove/replace) |
|
||||||
|
| **Mentor** | Sees within mentor workspace | Sees within mentor workspace |
|
||||||
|
|
||||||
|
When a submission window closes or a round advances:
|
||||||
|
- Applicant's editing permissions for that window's docs are revoked
|
||||||
|
- Docs remain visible for download
|
||||||
|
- Judges in subsequent rounds can view all prior docs
|
||||||
|
|
||||||
|
### RoundSubmissionVisibility
|
||||||
|
|
||||||
|
Controls which prior round docs are visible to judges:
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model RoundSubmissionVisibility {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
roundId String // the round where the judge is reviewing
|
||||||
|
visibleRoundId String // the round whose docs are visible
|
||||||
|
separateInUi Boolean @default(true) // show in separate section
|
||||||
|
|
||||||
|
round Round @relation("reviewingRound", ...)
|
||||||
|
visibleRound Round @relation("visibleRound", ...)
|
||||||
|
|
||||||
|
@@unique([roundId, visibleRoundId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, Jury 2 (R5) would have visibility entries for R1 (Intake docs) and R4 (Semifinal docs), both with `separateInUi: true`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Mentoring Workspace
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Mentoring is NOT a judging stage. It's a collaboration layer for finalist teams who have requested mentoring. Mentors help teams polish their submissions before live finals.
|
||||||
|
|
||||||
|
### Who Gets Mentoring
|
||||||
|
|
||||||
|
- Only finalist teams (or whatever stage admin configures) that have "requested mentor" enabled
|
||||||
|
- Mentor assignment works similarly to judge assignment (from a mentor pool)
|
||||||
|
- Each mentored team gets one assigned mentor
|
||||||
|
|
||||||
|
### Workspace Features
|
||||||
|
|
||||||
|
The mentor-team workspace provides three capabilities:
|
||||||
|
|
||||||
|
#### 1. Messaging / Chat
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model MentorMessage {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
projectId String // the mentored project
|
||||||
|
mentorId String // the assigned mentor
|
||||||
|
senderId String // who sent the message
|
||||||
|
senderRole MentorMessageRole
|
||||||
|
content String @db.Text
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
project Project @relation(...)
|
||||||
|
mentor User @relation("mentorMessages", ...)
|
||||||
|
sender User @relation("sentMentorMessages", ...)
|
||||||
|
|
||||||
|
@@index([projectId, mentorId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MentorMessageRole {
|
||||||
|
MENTOR
|
||||||
|
APPLICANT
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Messaging allows real-time communication between mentor and team. Admins can also send messages into the workspace.
|
||||||
|
|
||||||
|
#### 2. File Upload
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model MentorFile {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
projectId String
|
||||||
|
mentorId String
|
||||||
|
uploadedById String
|
||||||
|
uploaderRole MentorMessageRole
|
||||||
|
fileName String
|
||||||
|
fileKey String // MinIO storage key
|
||||||
|
fileSize Int
|
||||||
|
mimeType String
|
||||||
|
description String?
|
||||||
|
|
||||||
|
// Promotion tracking
|
||||||
|
promotedToSlot String? // slotKey if promoted to official submission
|
||||||
|
promotedAt DateTime?
|
||||||
|
promotedById String?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
project Project @relation(...)
|
||||||
|
comments MentorFileComment[]
|
||||||
|
|
||||||
|
@@index([projectId, mentorId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both mentor and team can upload files. Files are stored in MinIO and accessed via pre-signed URLs.
|
||||||
|
|
||||||
|
#### 3. Threaded File Comments
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model MentorFileComment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
fileId String
|
||||||
|
authorId String
|
||||||
|
authorRole MentorMessageRole
|
||||||
|
content String @db.Text
|
||||||
|
parentId String? // for threading
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
file MentorFile @relation(...)
|
||||||
|
author User @relation(...)
|
||||||
|
parent MentorFileComment? @relation("commentThread", ...)
|
||||||
|
replies MentorFileComment[] @relation("commentThread")
|
||||||
|
|
||||||
|
@@index([fileId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Comments are threaded (parentId for replies). Each comment is tied to a specific file.
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
|
||||||
|
All mentoring workspace content is **private by default**:
|
||||||
|
- Visible to: the assigned mentor, the team members, and admins
|
||||||
|
- NOT visible to: other teams, other mentors, judges, audience
|
||||||
|
|
||||||
|
### File Promotion to Official Submission
|
||||||
|
|
||||||
|
A mentoring file can be "promoted" to become an official submission for a required document slot in the active submission round.
|
||||||
|
|
||||||
|
**Example flow:**
|
||||||
|
1. Team uploads "Business Plan Draft v3.pdf" to mentor workspace
|
||||||
|
2. Mentor reviews and approves
|
||||||
|
3. Team (or admin) marks it as the official "Business Plan" submission for Round 2
|
||||||
|
4. System creates a `SubmissionPromotionEvent` with immutable provenance
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model SubmissionPromotionEvent {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
projectId String
|
||||||
|
roundId String
|
||||||
|
slotKey String // the doc slot being filled
|
||||||
|
sourceType PromotionSourceType
|
||||||
|
sourceFileId String // MentorFile.id or admin-uploaded file ID
|
||||||
|
promotedById String // who triggered the promotion
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
project Project @relation(...)
|
||||||
|
round Round @relation(...)
|
||||||
|
promotedBy User @relation(...)
|
||||||
|
|
||||||
|
@@index([projectId, roundId])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PromotionSourceType {
|
||||||
|
MENTOR_FILE // promoted from mentor workspace
|
||||||
|
ADMIN_REPLACEMENT // admin replaced the file directly
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Promotion authority:**
|
||||||
|
- **Team lead**: can promote their own mentor workspace files
|
||||||
|
- **Admin**: can promote any file or directly replace with admin-uploaded file
|
||||||
|
- **Mentor**: can promote IF admin enables this per competition (configurable)
|
||||||
|
|
||||||
|
**Provenance:** Every promotion records who promoted, when, from which source file, and the source type. This audit trail is immutable.
|
||||||
|
|
||||||
|
### MentoringConfig (Round Type Configuration)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const MentoringConfig = z.object({
|
||||||
|
eligibility: z.enum(['FINALISTS_ONLY', 'SEMIFINALISTS_AND_ABOVE', 'CONFIGURABLE']),
|
||||||
|
requireMentorRequest: z.boolean().default(true), // team must opt-in
|
||||||
|
assignmentMethod: z.enum(['MANUAL', 'AI_SUGGESTED', 'AI_AUTO', 'ALGORITHM']),
|
||||||
|
workspaceFeatures: z.object({
|
||||||
|
messagingEnabled: z.boolean().default(true),
|
||||||
|
fileUploadEnabled: z.boolean().default(true),
|
||||||
|
fileCommentsEnabled: z.boolean().default(true),
|
||||||
|
}),
|
||||||
|
promotionTarget: z.string().optional(), // roundId where promoted files go
|
||||||
|
allowMentorPromotion: z.boolean().default(false), // can mentors promote files?
|
||||||
|
deadlinePolicy: z.nativeEnum(DeadlinePolicy).default('FLAG'),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mentor Dashboard
|
||||||
|
|
||||||
|
Mentors have a dedicated dashboard showing:
|
||||||
|
- **Assigned teams**: list of all teams they're mentoring
|
||||||
|
- **Per-team workspace**: click through to messaging, file exchange, comments
|
||||||
|
- **Deadline tracking**: upcoming deadlines for the mentored teams
|
||||||
|
- **Progress indicators**: which teams have submitted their required docs, which are pending
|
||||||
|
- **File review queue**: files uploaded by teams that need mentor review
|
||||||
|
|
||||||
|
See [08-platform-integration-matrix.md](./08-platform-integration-matrix.md) for the full mentor page mapping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Admin Document Controls
|
||||||
|
|
||||||
|
Admins have full control over documents across all rounds:
|
||||||
|
|
||||||
|
| Action | Scope | Audit |
|
||||||
|
|--------|-------|-------|
|
||||||
|
| View any submission | All rounds, all projects | Read-only, no audit needed |
|
||||||
|
| Upload file for a project | Any round, any slot | Logged with `sourceType: ADMIN_REPLACEMENT` |
|
||||||
|
| Remove/replace a file | Any round, any slot | Logged with previous file reference |
|
||||||
|
| Lock a submission window early | Specific window | Logged |
|
||||||
|
| Extend a deadline | Specific window | Logged |
|
||||||
|
| Promote a mentor file | Any project's workspace | Creates `SubmissionPromotionEvent` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration Points
|
||||||
|
|
||||||
|
- **R1 (Intake)**: First `SubmissionWindow` with R1 document requirements
|
||||||
|
- **R4 (Semifinal Submission)**: Second `SubmissionWindow` with R2 requirements; R1 docs locked for applicants
|
||||||
|
- **R5 (Jury 2)**: Judges see R1 + R2 docs clearly separated via `RoundSubmissionVisibility`
|
||||||
|
- **R6 (Mentoring)**: Mentor workspace active, file promotion targets the active submission window
|
||||||
|
- **R7 (Live Finals)**: Jury 3 sees all submitted docs from all rounds
|
||||||
|
|
||||||
|
See [03-competition-flow.md](./03-competition-flow.md) for the complete round-by-round specification.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,503 @@
|
|||||||
|
# 08. Platform Integration Matrix
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
This document maps the **blast radius** of the Competition/Round redesign across every page, router, and service in the MOPC platform. It serves as a reference for developers to understand:
|
||||||
|
|
||||||
|
- **Which pages** need UI updates to reflect new terminology and data models
|
||||||
|
- **Which routers** require refactoring vs. minor updates vs. no changes
|
||||||
|
- **Which services** must be renamed, rewritten, or extended
|
||||||
|
- **Integration acceptance criteria** to verify completeness
|
||||||
|
|
||||||
|
Use this document alongside the data model ([02-data-model.md]) and competition flow ([03-competition-flow.md]) to plan implementation work.
|
||||||
|
|
||||||
|
### Terminology Translation
|
||||||
|
|
||||||
|
The redesign replaces Pipeline/Track/Stage terminology with Competition/Round:
|
||||||
|
|
||||||
|
| Old Term | New Term | Notes |
|
||||||
|
|----------|----------|-------|
|
||||||
|
| Pipeline | Competition | Top-level container for a complete program round |
|
||||||
|
| Stage | Round | Individual phases within a competition (Intake, Jury 1, Jury 2, etc.) |
|
||||||
|
| Track | *Eliminated* | Replaced by SpecialAward routing modes and JuryGroup memberships |
|
||||||
|
| StageType | RoundType | 7 typed round kinds (Intake, Eligibility Filter, Jury Evaluation 1/2/3, Live Finals, Results) |
|
||||||
|
| configJson | Typed configs | Each RoundType has its own Zod-validated config schema |
|
||||||
|
|
||||||
|
### Reading This Document
|
||||||
|
|
||||||
|
- **Priority levels**: Critical (blocks launch), High (required for full feature parity), Medium (enhances UX), Low (nice-to-have)
|
||||||
|
- **Classifications**: Directly Governed (major refactor), Context-Dependent (moderate updates), Utility (minimal/no changes)
|
||||||
|
- **File paths**: All paths relative to `G:\Repos\MOPC\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Admin Pages Impact Matrix
|
||||||
|
|
||||||
|
Admin pages require the most extensive changes due to terminology shifts and new JuryGroup management.
|
||||||
|
|
||||||
|
| Page/Route | Current State | Required Changes | Priority | Related Docs |
|
||||||
|
|------------|---------------|------------------|----------|--------------|
|
||||||
|
| **Dashboard & Overview** |
|
||||||
|
| `src/app/(admin)/admin/page.tsx` | Program overview, pipeline status | Update "Pipelines" → "Competitions", show Competition status tiles | High | [03-competition-flow.md] |
|
||||||
|
| **Competition Management** (was Pipeline) |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipelines/page.tsx` | Pipeline list view | Rename to "Competitions", update filters/columns | Critical | [02-data-model.md] |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/page.tsx` | Pipeline detail view | Rename to Competition detail, show Round timeline | Critical | [03-competition-flow.md] |
|
||||||
|
| `src/app/(admin)/admin/rounds/new-pipeline/page.tsx` | Pipeline creation wizard | Rename to "New Competition", use wizard-template.ts | Critical | [05-competition-wizard.md] |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/wizard/page.tsx` | Stage setup wizard | Rename to "Round Setup", enforce RoundType requirements, validate skeleton completeness | Critical | [05-competition-wizard.md] |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/edit/page.tsx` | Basic pipeline editing | Rename to Competition editing, prevent invalid Round sequences | High | [05-competition-wizard.md] |
|
||||||
|
| `src/app/(admin)/admin/rounds/pipeline/[id]/advanced/page.tsx` | Advanced config editing | Add policy editors for JuryGroup binding, assignment policies, submission windows, deliberation config | High | [04-jury-groups.md] |
|
||||||
|
| **Round Configuration** (was Stage) |
|
||||||
|
| `src/components/admin/pipeline/stage-config-editor.tsx` | Generic stage config | Render RoundType-aware controls only, use typed configs (IntakeConfig, FilterConfig, etc.) | Critical | [06-round-configs.md] |
|
||||||
|
| `src/components/admin/pipeline/sections/assignment-section.tsx` | min/max load + overflow | Replace with hard/soft cap + buffer + category bias controls (labeled as suggestive, not deterministic) | High | [04-jury-groups.md] |
|
||||||
|
| `src/components/admin/pipeline/sections/filtering-section.tsx` | Rules + AI criteria | Bind to Eligibility Filter RoundType, standardized outcomes | High | [07-deliberation.md] |
|
||||||
|
| `src/components/admin/pipeline/sections/awards-section.tsx` | Award routing/scoring | Add participation mode (separate_pool/dual_track), pull-out behavior, single-judge config | High | [04-jury-groups.md] |
|
||||||
|
| `src/components/admin/pipeline/sections/live-finals-section.tsx` | Live/audience settings | Add Jury 3 deliberation window, final deliberation linkage, cursor control config | High | [07-deliberation.md] |
|
||||||
|
| **Juries Management (NEW)** |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/juries/page.tsx` | None (create new) | List all JuryGroups (Jury 1, 2, 3, award juries), show member counts, caps, overlaps | Critical | [04-jury-groups.md] |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/juries/[id]/page.tsx` | None (create new) | JuryGroup detail: members, cap config, category ratios, bias settings | Critical | [04-jury-groups.md] |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/juries/[id]/members/page.tsx` | None (create new) | Add/remove jury members, configure individual caps/biases, track overlaps | Critical | [04-jury-groups.md] |
|
||||||
|
| **Assignment Management** |
|
||||||
|
| `src/app/(admin)/admin/projects/pool/page.tsx` | Project pool operations | Use AssignmentIntent for manual queuing, show reason tracking, policy compliance status | High | [04-jury-groups.md] |
|
||||||
|
| `src/components/admin/assignment/*` | Stage-based assignment | Update to Round-based, show JuryGroup context, cap violations, buffer status | High | [04-jury-groups.md] |
|
||||||
|
| **Filtering Configuration** |
|
||||||
|
| `src/app/(admin)/admin/projects/page.tsx` | Project list by filters | Add Round-aware views, show submission bundle health, Round 1/2 document status | Medium | [06-round-configs.md] |
|
||||||
|
| **Submission Window Management** |
|
||||||
|
| *(Enhanced)* Round config pages | Embedded in stage config | Extract submission window config to dedicated UI: slots, grace periods, late policy, resubmit rules | High | [06-round-configs.md] |
|
||||||
|
| **Special Awards Configuration** |
|
||||||
|
| `src/app/(admin)/admin/awards/page.tsx` | Award list | Expose Mode A/B semantics, admin-confirmed pull-out workflow | High | [04-jury-groups.md] |
|
||||||
|
| `src/app/(admin)/admin/awards/[id]/page.tsx` | Award detail | Show participation mode, JuryGroup bindings, single-judge config | High | [04-jury-groups.md] |
|
||||||
|
| `src/app/(admin)/admin/awards/[id]/edit/page.tsx` | Award editing | Add strict validation for mode, Round bindings, jury requirements | High | [04-jury-groups.md] |
|
||||||
|
| `src/app/(admin)/admin/awards/new/page.tsx` | Award creation | Wizard for Mode A/B selection, JuryGroup assignment | High | [04-jury-groups.md] |
|
||||||
|
| **Live Finals Management** |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/rounds/[id]/live/page.tsx` | None (create new) | Stage manager controls: cursor position, project advance, deliberation timer, audience vote toggle | Critical | [07-deliberation.md] |
|
||||||
|
| **Deliberation Management (NEW)** |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/rounds/[id]/deliberation/page.tsx` | None (create new) | Session management, lock controls, quorum status, admin override panel | Critical | [07-deliberation.md] |
|
||||||
|
| **Results / Result Lock Management** |
|
||||||
|
| `src/app/(admin)/admin/reports/stages/page.tsx` | Stage reporting | Include deliberation session status, result lock state, compliance metrics (cap violations, override counts) | High | [07-deliberation.md] |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/rounds/[id]/results/lock/page.tsx` | None (create new) | Final result lock interface with snapshot preview, mandatory reason, audit trail | Critical | [07-deliberation.md] |
|
||||||
|
| *(NEW)* `src/app/(admin)/admin/rounds/[id]/results/unlock/page.tsx` | None (super admin only) | Result unlock workflow with mandatory justification, relock capability | Critical | [07-deliberation.md] |
|
||||||
|
| **User/Invite Management** |
|
||||||
|
| `src/app/(admin)/admin/members/page.tsx` | User list | Add JuryGroup membership column, show pending AssignmentIntents | Medium | [04-jury-groups.md] |
|
||||||
|
| `src/app/(admin)/admin/members/invite/page.tsx` | User invitation | Bind to JuryGroup selection, support AssignmentIntent mode vs. direct assignment | Critical | [04-jury-groups.md] |
|
||||||
|
| `src/app/(admin)/admin/members/[id]/page.tsx` | User detail | Show all JuryGroup memberships, assignment history, cap status | Medium | [04-jury-groups.md] |
|
||||||
|
| **Reports / Analytics** |
|
||||||
|
| `src/app/(admin)/admin/reports/page.tsx` | Generic reports | Add Competition-aware KPIs: assignment saturation, override rate, policy compliance | Medium | [03-competition-flow.md] |
|
||||||
|
| `src/app/(admin)/admin/audit/page.tsx` | Audit log search | Add filter facets: RoundType, JuryGroup ID, deliberation session | Medium | [07-deliberation.md] |
|
||||||
|
| **Settings** |
|
||||||
|
| `src/app/(admin)/admin/settings/page.tsx` | Platform settings | No terminology changes needed | Low | - |
|
||||||
|
| `src/app/(admin)/admin/settings/tags/page.tsx` | Tag management | No changes | Low | - |
|
||||||
|
| `src/app/(admin)/admin/settings/webhooks/page.tsx` | Webhook config | Update event names: pipeline.* → competition.*, stage.* → round.* | Medium | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Jury Pages Impact Matrix
|
||||||
|
|
||||||
|
Jury pages must reflect Round-based navigation and JuryGroup membership.
|
||||||
|
|
||||||
|
| Page/Route | Current State | Required Changes | Priority | Related Docs |
|
||||||
|
|------------|---------------|------------------|----------|--------------|
|
||||||
|
| **Jury Dashboard** |
|
||||||
|
| `src/app/(jury)/jury/page.tsx` | Stage countdown, assigned projects | Show Round countdown, multi-JuryGroup memberships, completion obligations per group | High | [04-jury-groups.md] |
|
||||||
|
| **Round Navigation** (was Stage) |
|
||||||
|
| `src/app/(jury)/jury/stages/page.tsx` | Active stages list | Rename to "Rounds", show only Rounds bound to user's JuryGroups | Critical | [03-competition-flow.md] |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/assignments/page.tsx` | Assignment list | Update to Round context, show cap status, category ratio compliance, buffer usage | High | [04-jury-groups.md] |
|
||||||
|
| **Project Review** |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/projects/[projectId]/page.tsx` | Project details | Separate Round 1 vs. Round 2 documents clearly, show submission window provenance | High | [06-round-configs.md] |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/projects/[projectId]/evaluate/page.tsx` | Evaluation form | Enforce Round/JuryGroup policy, show graceful lock behavior, CoI declaration | Critical | [04-jury-groups.md] |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/projects/[projectId]/evaluation/page.tsx` | View evaluation | Show evaluation history, AI summary integration | Medium | [06-round-configs.md] |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/compare/page.tsx` | Compare projects | Update to Round context, show Round 1/2 docs side-by-side | Medium | - |
|
||||||
|
| **Live Finals Voting** |
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/live/page.tsx` | Live finals interaction | Require Jury 3 membership, show current cursor position, enable voting only for current project | Critical | [07-deliberation.md] |
|
||||||
|
| **Deliberation Voting (NEW)** |
|
||||||
|
| *(NEW)* `src/app/(jury)/jury/stages/[stageId]/deliberation/page.tsx` | None (create new) | Deliberation interface: category scope, project shortlist, vote casting, quorum status | Critical | [07-deliberation.md] |
|
||||||
|
| **Awards** |
|
||||||
|
| `src/app/(jury)/jury/awards/page.tsx` | Award assignments | Bind to award JuryGroup memberships, hide single-judge awards | High | [04-jury-groups.md] |
|
||||||
|
| `src/app/(jury)/jury/awards/[id]/page.tsx` | Award voting | Show participation mode, suppress jury voting for single-judge awards | High | [04-jury-groups.md] |
|
||||||
|
| **Learning Resources** |
|
||||||
|
| `src/app/(jury)/jury/learning/page.tsx` | Learning materials | No terminology changes | Low | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Applicant Pages Impact Matrix
|
||||||
|
|
||||||
|
Applicant pages must reflect Round-based journey and submission windows.
|
||||||
|
|
||||||
|
| Page/Route | Current State | Required Changes | Priority | Related Docs |
|
||||||
|
|------------|---------------|------------------|----------|--------------|
|
||||||
|
| **Applicant Dashboard** |
|
||||||
|
| `src/app/(applicant)/applicant/page.tsx` | Generic dashboard | Show Round-based journey (R1 Intake → Eligibility → Jury 1 Result → R2 Submission → Jury 2 → Live Finals → Results) | High | [03-competition-flow.md] |
|
||||||
|
| **Pipeline/Journey View** |
|
||||||
|
| `src/app/(applicant)/applicant/pipeline/page.tsx` | Pipeline visibility | Rename to "Competition Journey", show RoundType-based progress, submission window status | Critical | [03-competition-flow.md] |
|
||||||
|
| `src/app/(applicant)/applicant/pipeline/[stageId]/status/page.tsx` | Stage status | Update to Round status, show jury assignment count, evaluation progress | Medium | [06-round-configs.md] |
|
||||||
|
| **Document Upload** |
|
||||||
|
| `src/app/(applicant)/applicant/pipeline/[stageId]/documents/page.tsx` | Stage docs | Enforce Round-based submission windows, show slot state (write/read-only), late submission warnings | Critical | [06-round-configs.md] |
|
||||||
|
| `src/app/(applicant)/applicant/documents/page.tsx` | Docs overview | Show Round 1 vs. Round 2 bundles, official slot timeline, promotion provenance | High | [06-round-configs.md] |
|
||||||
|
| **Team Management** |
|
||||||
|
| `src/app/(applicant)/applicant/team/page.tsx` | Team invites/members | Gate permissions via context resolver, ensure invite acceptance routes correctly | Medium | [02-data-model.md] |
|
||||||
|
| **Mentoring Workspace** |
|
||||||
|
| `src/app/(applicant)/applicant/mentor/page.tsx` | Mentor interaction | Show mentor messages, workspace files, promotion candidates, official slot mapping | High | [06-round-configs.md] |
|
||||||
|
| **Public Submission Forms** |
|
||||||
|
| `src/app/(public)/apply/[slug]/page.tsx` | Stage/public form | Enforce SubmissionWindow contract (slots, grace period, late policy) | Critical | [06-round-configs.md] |
|
||||||
|
| `src/app/(public)/apply/edition/[programSlug]/page.tsx` | Edition mode intake | Map to Intake RoundType, enforce Round 1 submission policies | Critical | [06-round-configs.md] |
|
||||||
|
| `src/app/(public)/my-submission/[id]/page.tsx` | Submission detail | Show Round context, submission window status, document bundle health | Medium | [06-round-configs.md] |
|
||||||
|
| `src/app/(public)/my-submission/[id]/team/page.tsx` | Team management | Mirror applicant team flow, no divergent logic | Medium | [02-data-model.md] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Mentor Pages Impact Matrix
|
||||||
|
|
||||||
|
Mentor pages need workspace enhancements and file promotion logic.
|
||||||
|
|
||||||
|
| Page/Route | Current State | Required Changes | Priority | Related Docs |
|
||||||
|
|------------|---------------|------------------|----------|--------------|
|
||||||
|
| **Mentor Dashboard** |
|
||||||
|
| `src/app/(mentor)/mentor/page.tsx` | Assigned teams overview | Show Round progression, submission window deadlines, file promotion status | High | [06-round-configs.md] |
|
||||||
|
| `src/app/(mentor)/mentor/projects/page.tsx` | Team list | Add Round context, show document bundle completeness | Medium | [06-round-configs.md] |
|
||||||
|
| **Mentor Workspace** |
|
||||||
|
| `src/app/(mentor)/mentor/projects/[id]/page.tsx` | Project detail | Add workspace file uploads, threaded comments, file promotion actions (team-lead/admin only) | Critical | [06-round-configs.md] |
|
||||||
|
| *(NEW)* `src/app/(mentor)/mentor/projects/[id]/workspace/page.tsx` | None (create new) | Dedicated workspace tab: private files, comments, promotion queue, official slot preview | Critical | [06-round-configs.md] |
|
||||||
|
| *(NEW)* `src/app/(mentor)/mentor/projects/[id]/milestones/page.tsx` | None (create new or enhance) | Milestone tracking, deadline alerts, progress reporting | Medium | - |
|
||||||
|
| **Learning Resources** |
|
||||||
|
| `src/app/(mentor)/mentor/resources/page.tsx` | Mentor materials | No terminology changes | Low | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Observer & Audience Pages Impact Matrix
|
||||||
|
|
||||||
|
Observer and audience pages have minimal changes but need consistent terminology.
|
||||||
|
|
||||||
|
| Page/Route | Current State | Required Changes | Priority | Related Docs |
|
||||||
|
|------------|---------------|------------------|----------|--------------|
|
||||||
|
| **Observer Dashboard** |
|
||||||
|
| `src/app/(observer)/observer/page.tsx` | Read-only overview | Update "Pipelines" → "Competitions" | Low | - |
|
||||||
|
| `src/app/(observer)/observer/reports/page.tsx` | Observer reports | Update terminology, no functional changes | Low | - |
|
||||||
|
| **Audience Voting** |
|
||||||
|
| `src/app/(public)/vote/[sessionId]/page.tsx` | Audience vote form | Enforce category/session mode, anti-duplicate policy | Medium | [07-deliberation.md] |
|
||||||
|
| `src/app/(public)/vote/stage/[sessionId]/page.tsx` | Stage-specific voting | Update to Round context | Medium | [07-deliberation.md] |
|
||||||
|
| **Live Scores** |
|
||||||
|
| `src/app/(public)/live-scores/[sessionId]/page.tsx` | Live score display | Update terminology, no functional changes | Low | - |
|
||||||
|
| `src/app/(public)/live-scores/stage/[sessionId]/page.tsx` | Stage-specific scores | Update to Round context | Low | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. tRPC Router Classification
|
||||||
|
|
||||||
|
Every router is classified by its relationship to Competition/Round context.
|
||||||
|
|
||||||
|
| Router | Classification | Required Changes | Priority | Related Docs |
|
||||||
|
|--------|----------------|------------------|----------|--------------|
|
||||||
|
| **Directly Governed** (core competition logic, major refactoring required) |
|
||||||
|
| `pipeline.ts` | Directly Governed | **Rename to `competition.ts`**. Replace Pipeline model with Competition, update all CRUD operations, wizard templates, context resolution | Critical | [02-data-model.md], [05-competition-wizard.md] |
|
||||||
|
| `stage.ts` | Directly Governed | **Rename to `round.ts`**. Replace Stage model with Round, implement RoundType-based config validation, update transition logic | Critical | [02-data-model.md], [06-round-configs.md] |
|
||||||
|
| `stageFiltering.ts` | Directly Governed | **Rename to `roundFiltering.ts`**. Update to use Round context, standardized eligibility outcomes | High | [06-round-configs.md] |
|
||||||
|
| `stageAssignment.ts` | Directly Governed | **Rename to `roundAssignment.ts`**. Update to Round + JuryGroup context, implement new assignment preview/run logic | Critical | [04-jury-groups.md] |
|
||||||
|
| *(NEW)* `juryGroup.ts` | Directly Governed | **Create new router**. CRUD for JuryGroups, member management, cap config, overlap validation | Critical | [04-jury-groups.md] |
|
||||||
|
| *(NEW)* `deliberation.ts` | Directly Governed | **Create new router**. Session management, voting, quorum calculation, result locking, admin overrides | Critical | [07-deliberation.md] |
|
||||||
|
| `live.ts` | Directly Governed | Update to require Jury 3 context, bind cursor to Live Finals RoundType, add deliberation handoff | High | [07-deliberation.md] |
|
||||||
|
| `live-voting.ts` | Directly Governed | Tie session lifecycle to RoundType, add deliberation session support | High | [07-deliberation.md] |
|
||||||
|
| `cohort.ts` | Directly Governed | Align with Live Finals and Selection policies, category boundaries | Medium | [03-competition-flow.md] |
|
||||||
|
| `decision.ts` | Directly Governed | Extend to deliberation decisions, result lock events, admin override audit | High | [07-deliberation.md] |
|
||||||
|
| `specialAward.ts` | Directly Governed | Add strict validation for participation mode, Round bindings, admin-confirmed pull-out, single-judge config | High | [04-jury-groups.md] |
|
||||||
|
| `award.ts` | Directly Governed | Expose Mode A/B semantics, JuryGroup bindings | High | [04-jury-groups.md] |
|
||||||
|
| **Context-Dependent** (consume competition context but have own logic, moderate updates) |
|
||||||
|
| `assignment.ts` | Context-Dependent | Update to Round + JuryGroup context, centralize policy engine usage for all assignment paths | Critical | [04-jury-groups.md] |
|
||||||
|
| `evaluation.ts` | Context-Dependent | Incorporate JuryGroup binding checks, deliberation contribution rights, Round-based evaluation windows | High | [04-jury-groups.md] |
|
||||||
|
| `applicant.ts` | Context-Dependent | Add SubmissionBundleState, enforce slot state/round lock/promotion provenance/late policy | Critical | [06-round-configs.md] |
|
||||||
|
| `application.ts` | Context-Dependent | Create SubmissionBundleState on submission, normalize category key mapping, enforce Round policies | Critical | [06-round-configs.md] |
|
||||||
|
| `file.ts` | Context-Dependent | Use submission round + RoundType-aware visibility, enforce slot/promotion rules | High | [06-round-configs.md] |
|
||||||
|
| `project.ts` | Context-Dependent | Add Round-aware views, submission bundle health, Round 1/2 document separation | High | [06-round-configs.md] |
|
||||||
|
| `project-pool.ts` | Context-Dependent | Use AssignmentIntent for manual queue, reason tracking | High | [04-jury-groups.md] |
|
||||||
|
| `mentor.ts` | Context-Dependent | Add workspace file/comment/promotion endpoints, enforce team-lead/admin promotion permissions | Critical | [06-round-configs.md] |
|
||||||
|
| `user.ts` | Context-Dependent | Support dual invite behavior (AssignmentIntent vs. direct assignment), track JuryGroup memberships | High | [04-jury-groups.md] |
|
||||||
|
| `notification.ts` | Context-Dependent | Expand event taxonomy: jury membership, promotion, deliberation lock, Round transitions | High | [03-competition-flow.md] |
|
||||||
|
| `message.ts` | Context-Dependent | Ensure context tags reference Round/JuryGroup/project where applicable | Medium | - |
|
||||||
|
| `dashboard.ts` | Context-Dependent | Update to show Competition status, Round progress, JuryGroup obligations | Medium | [03-competition-flow.md] |
|
||||||
|
| `analytics.ts` | Context-Dependent | Add Competition KPIs: assignment saturation, override rate, policy compliance | Medium | [03-competition-flow.md] |
|
||||||
|
| `export.ts` | Context-Dependent | Include result lock version, policy compliance evidence, Round context | Medium | [07-deliberation.md] |
|
||||||
|
| `audit.ts` | Context-Dependent | Add filter facets: RoundType, JuryGroup ID, deliberation session | Medium | [07-deliberation.md] |
|
||||||
|
| `filtering.ts` | Context-Dependent | Update to Round context, standardized eligibility outcomes | High | [06-round-configs.md] |
|
||||||
|
| `gracePeriod.ts` | Context-Dependent | Integrate with SubmissionWindow grace period logic | Medium | [06-round-configs.md] |
|
||||||
|
| **Utility** (minimal or no changes, context-independent) |
|
||||||
|
| `avatar.ts` | Utility | No changes | None | - |
|
||||||
|
| `logo.ts` | Utility | No changes | None | - |
|
||||||
|
| `tag.ts` | Utility | No changes | None | - |
|
||||||
|
| `partner.ts` | Utility | No changes | None | - |
|
||||||
|
| `learningResource.ts` | Utility | No changes | None | - |
|
||||||
|
| `settings.ts` | Utility | No changes | None | - |
|
||||||
|
| `webhook.ts` | Utility | Update event names: pipeline.* → competition.*, stage.* → round.* | Low | - |
|
||||||
|
| `typeform-import.ts` | Utility | No changes (legacy import) | None | - |
|
||||||
|
| `notion-import.ts` | Utility | No changes (legacy import) | None | - |
|
||||||
|
| `wizard-template.ts` | Utility | Update template structure: Pipeline → Competition, Stage → Round | High | [05-competition-wizard.md] |
|
||||||
|
| `program.ts` | Utility | No changes (Program model unchanged) | None | - |
|
||||||
|
| `_app.ts` | Utility | Update router exports (pipeline → competition, stage → round, add juryGroup, deliberation) | Critical | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Service Layer Classification
|
||||||
|
|
||||||
|
Services are the backend orchestration layer. Many require renaming and logic updates.
|
||||||
|
|
||||||
|
| Service | Required Changes | Depends On | Priority | Related Docs |
|
||||||
|
|---------|------------------|------------|----------|--------------|
|
||||||
|
| **Core Orchestration Services** (rename + refactor) |
|
||||||
|
| `stage-engine.ts` | **Rename to `round-engine.ts`**. Implement RoundType-aware transition guards, validate Round sequence requirements | Round model, typed configs | Critical | [03-competition-flow.md], [06-round-configs.md] |
|
||||||
|
| `stage-filtering.ts` | **Rename to `round-filtering.ts`**. Standardized eligibility outcomes with reason schema, AI filtering integration | Round model, AI services | High | [06-round-configs.md] |
|
||||||
|
| `stage-assignment.ts` | **Rename to `round-assignment.ts`**. Full hard/soft cap policy engine, category bias scoring, JuryGroup binding | Round model, JuryGroup model, assignment policies | Critical | [04-jury-groups.md] |
|
||||||
|
| `stage-notifications.ts` | **Rename to `round-notifications.ts`**. Expand event taxonomy: jury membership, Round transitions, deliberation, promotion, result lock | Round model, notification router | High | [03-competition-flow.md] |
|
||||||
|
| **Live & Deliberation Services** |
|
||||||
|
| `live-control.ts` | Update to bind cursor to Live Finals RoundType, require Jury 3 membership, add deliberation handoff logic | Round model, JuryGroup model | High | [07-deliberation.md] |
|
||||||
|
| *(NEW)* `deliberation.ts` | **Create new service**. Session orchestration, quorum calculation, unanimous-with-fallback voting, result locking, admin override handling | Round model, JuryGroup model, decision router | Critical | [07-deliberation.md] |
|
||||||
|
| *(NEW)* `result-lock.ts` | **Create new service**. Final result snapshot creation, lock/unlock workflow, audit trail, super-admin-only unlock | Round model, deliberation service | Critical | [07-deliberation.md] |
|
||||||
|
| **AI Services** |
|
||||||
|
| `ai-filtering.ts` | Update to use Round context, standardized eligibility schema | Round model, anonymization | High | [06-round-configs.md] |
|
||||||
|
| `ai-assignment.ts` | Update to use JuryGroup context, Round-based project pool | Round model, JuryGroup model | High | [04-jury-groups.md] |
|
||||||
|
| `ai-evaluation-summary.ts` | **Add AI shortlist generation**. Generate deliberation shortlists from evaluation scores, handle ties | Round model, evaluation router | Critical | [07-deliberation.md] |
|
||||||
|
| `ai-tagging.ts` | No changes (project-level tagging, context-independent) | None | None | - |
|
||||||
|
| `ai-award-eligibility.ts` | Update to use Round context, participation mode awareness | Round model, specialAward router | Medium | [04-jury-groups.md] |
|
||||||
|
| `ai-errors.ts` | No changes (utility error handling) | None | None | - |
|
||||||
|
| **Assignment & Matching Services** |
|
||||||
|
| `smart-assignment.ts` | **Partial refactor**. Convert soft heuristics into policy-compliant scoring, treat category bias as transparent scoring (not strict quota), integrate with round-assignment.ts | Round model, JuryGroup model | High | [04-jury-groups.md] |
|
||||||
|
| `mentor-matching.ts` | Update to gate matching by finalist status, mentorship request, Round policy | Round model, mentor router | Medium | [06-round-configs.md] |
|
||||||
|
| **Background Jobs** |
|
||||||
|
| `award-eligibility-job.ts` | Update to use Round context, persist decision reasoning under unified audit taxonomy | Round model, audit router | Medium | [04-jury-groups.md] |
|
||||||
|
| `evaluation-reminders.ts` | Update to RoundType-aware reminder templates, JuryGroup-specific obligations, deliberation reminders | Round model, JuryGroup model | High | [04-jury-groups.md] |
|
||||||
|
| `email-digest.ts` | Update terminology in email templates (Pipeline → Competition, Stage → Round) | Round model | Medium | - |
|
||||||
|
| **Utility Services** |
|
||||||
|
| `anonymization.ts` | No changes (AI privacy utility) | None | None | - |
|
||||||
|
| `notification.ts` | Update event templates to use Competition/Round terminology | Round model | Medium | - |
|
||||||
|
| `in-app-notification.ts` | Update notification rendering to use new terminology | Round model | Medium | - |
|
||||||
|
| `webhook-dispatcher.ts` | Update event names: pipeline.* → competition.*, stage.* → round.* | Round model | Low | - |
|
||||||
|
| **Workspace Services** (new or enhanced) |
|
||||||
|
| *(NEW)* `mentor-workspace.ts` | **Create new or enhance mentor.ts service**. File upload, threaded comments, promotion queue, official slot mapping, team-lead/admin-only promotion | Round model, file router, mentor router | Critical | [06-round-configs.md] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Integration Acceptance Criteria
|
||||||
|
|
||||||
|
The integration is considered complete when ALL of the following are verified:
|
||||||
|
|
||||||
|
### 9.1 Terminology & Data Model
|
||||||
|
- [ ] All `Pipeline` references eliminated from codebase (code, comments, docs)
|
||||||
|
- [ ] All `Stage` references eliminated from codebase (code, comments, docs)
|
||||||
|
- [ ] All `Track` references eliminated from codebase (code, comments, docs)
|
||||||
|
- [ ] `Competition` model fully implemented with typed configs
|
||||||
|
- [ ] `Round` model fully implemented with 7 RoundType configs
|
||||||
|
- [ ] `JuryGroup` model fully implemented with member management
|
||||||
|
- [ ] All database migrations applied successfully
|
||||||
|
- [ ] Prisma schema validated and generated
|
||||||
|
|
||||||
|
### 9.2 Core Functionality
|
||||||
|
- [ ] Competition CRUD fully functional (create, read, update, delete)
|
||||||
|
- [ ] Round CRUD fully functional (create, read, update, delete, reorder)
|
||||||
|
- [ ] JuryGroup CRUD fully functional (create, assign members, configure caps/biases)
|
||||||
|
- [ ] All 7 typed RoundType configs validated by Zod on save
|
||||||
|
- [ ] Round transition engine working (intake → eligibility → jury 1 → jury 2 → jury 3 → live finals → results)
|
||||||
|
- [ ] Assignment engine respects hard/soft caps, buffer, category bias
|
||||||
|
- [ ] Filtering engine produces standardized eligibility outcomes
|
||||||
|
- [ ] AI services integration working (filtering, assignment, evaluation summary, shortlist generation)
|
||||||
|
|
||||||
|
### 9.3 Deliberation & Results
|
||||||
|
- [ ] Deliberation session management working (create, vote, quorum, lock)
|
||||||
|
- [ ] Unanimous-with-quorum-fallback voting logic functional
|
||||||
|
- [ ] AI shortlist generation from evaluation scores working
|
||||||
|
- [ ] Admin override panel functional with audit trail
|
||||||
|
- [ ] Result lock/unlock workflow functional (super-admin only)
|
||||||
|
- [ ] Result lock snapshots persisted with version history
|
||||||
|
- [ ] DecisionAuditLog captures all deliberation events
|
||||||
|
|
||||||
|
### 9.4 Admin UX
|
||||||
|
- [ ] Admin dashboard shows Competition status (not Pipeline)
|
||||||
|
- [ ] Competition wizard uses new Competition/Round terminology
|
||||||
|
- [ ] Round configuration editor uses RoundType-specific controls
|
||||||
|
- [ ] JuryGroup management pages functional (list, detail, members)
|
||||||
|
- [ ] Assignment management shows JuryGroup context, cap violations
|
||||||
|
- [ ] Live finals stage manager working (cursor, deliberation timer)
|
||||||
|
- [ ] Deliberation admin panel working (session controls, overrides)
|
||||||
|
- [ ] Result lock UI functional with preview and audit trail
|
||||||
|
|
||||||
|
### 9.5 Jury UX
|
||||||
|
- [ ] Jury dashboard shows Round countdown (not Stage)
|
||||||
|
- [ ] Jury sees only Rounds bound to their JuryGroups
|
||||||
|
- [ ] Project review shows Round 1 vs. Round 2 documents separately
|
||||||
|
- [ ] Evaluation form enforces Round/JuryGroup policy
|
||||||
|
- [ ] Live finals voting requires Jury 3 membership
|
||||||
|
- [ ] Deliberation voting interface functional (shortlist, vote, quorum)
|
||||||
|
- [ ] Award voting respects JuryGroup bindings, hides single-judge awards
|
||||||
|
|
||||||
|
### 9.6 Applicant UX
|
||||||
|
- [ ] Applicant pipeline renamed to "Competition Journey"
|
||||||
|
- [ ] Journey shows RoundType-based progress (Intake → Eligibility → Jury 1 → ...)
|
||||||
|
- [ ] Document upload enforces submission window slots
|
||||||
|
- [ ] Round 1 vs. Round 2 bundles clearly separated
|
||||||
|
- [ ] Late submission warnings displayed per policy
|
||||||
|
- [ ] Mentor workspace shows file promotion candidates
|
||||||
|
- [ ] Team management gates permissions correctly
|
||||||
|
|
||||||
|
### 9.7 Mentor UX
|
||||||
|
- [ ] Mentor dashboard shows Round progression
|
||||||
|
- [ ] Mentor workspace file upload working
|
||||||
|
- [ ] Threaded comments functional
|
||||||
|
- [ ] File promotion actions restricted to team-lead/admin
|
||||||
|
- [ ] Official slot preview shows promoted files correctly
|
||||||
|
|
||||||
|
### 9.8 Audience & Observer UX
|
||||||
|
- [ ] Audience voting enforces category/session mode
|
||||||
|
- [ ] Anti-duplicate voting policy working
|
||||||
|
- [ ] Observer pages use Competition/Round terminology
|
||||||
|
- [ ] Live scores display updated terminology
|
||||||
|
|
||||||
|
### 9.9 Technical Requirements
|
||||||
|
- [ ] All tRPC routers updated (pipeline → competition, stage → round, etc.)
|
||||||
|
- [ ] All services renamed (stage-engine → round-engine, etc.)
|
||||||
|
- [ ] All page routes updated (pipeline → competition terminology in UI)
|
||||||
|
- [ ] TypeScript strict mode passing with no errors
|
||||||
|
- [ ] All unit tests passing (update test factories for Competition/Round)
|
||||||
|
- [ ] Integration tests covering Competition/Round/JuryGroup workflows
|
||||||
|
- [ ] Database seed script updated to use new models
|
||||||
|
- [ ] Docker entrypoint working with new migrations
|
||||||
|
|
||||||
|
### 9.10 Audit & Compliance
|
||||||
|
- [ ] All admin actions audited via DecisionAuditLog
|
||||||
|
- [ ] Deliberation decisions recorded with session context
|
||||||
|
- [ ] Result lock/unlock events captured
|
||||||
|
- [ ] Assignment policy compliance tracked
|
||||||
|
- [ ] Override counts reportable per Competition
|
||||||
|
- [ ] Cap violation metrics available in reports
|
||||||
|
|
||||||
|
### 9.11 Documentation & Training
|
||||||
|
- [ ] All architecture docs updated (this document, [02], [03], [04], [05], [06], [07])
|
||||||
|
- [ ] API documentation regenerated (tRPC router docs)
|
||||||
|
- [ ] User guide updated (admin, jury, applicant, mentor)
|
||||||
|
- [ ] Migration guide written (old → new terminology mapping)
|
||||||
|
- [ ] Training materials prepared for admins
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Procedure-Level Migration Tables
|
||||||
|
|
||||||
|
These tables detail the exact tRPC procedure renames and signature changes required for each portal.
|
||||||
|
|
||||||
|
### 10.1 Jury Portal Procedures
|
||||||
|
|
||||||
|
| Current Procedure | New Procedure | Signature Change | Notes |
|
||||||
|
|-------------------|---------------|-----------------|-------|
|
||||||
|
| `stageAssignment.myAssignments` | `roundAssignment.myAssignments` | `{ stageId }` → `{ roundId, juryGroupId? }` | Filter by JuryGroup membership |
|
||||||
|
| `stageAssignment.getAssignmentDetail` | `roundAssignment.getAssignmentDetail` | `{ assignmentId }` → `{ assignmentId }` | Add JuryGroup context to response |
|
||||||
|
| `evaluation.getStageForm` | `evaluation.getRoundForm` | `{ stageId }` → `{ roundId }` | Return round-type-aware form |
|
||||||
|
| `evaluation.submitScore` | `evaluation.submitScore` | `{ stageId, projectId, ... }` → `{ roundId, projectId, ... }` | Add JuryGroup validation |
|
||||||
|
| `evaluation.checkStageWindow` | `evaluation.checkRoundWindow` | `{ stageId }` → `{ roundId }` | Return SubmissionWindow status |
|
||||||
|
| `evaluation.listStageEvaluations` | `evaluation.listRoundEvaluations` | `{ stageId }` → `{ roundId, juryGroupId? }` | Multi-jury filtering |
|
||||||
|
| `evaluation.declareCOI` | `evaluation.declareCOI` | `{ assignmentId }` → `{ assignmentId }` | No change (COI is on Assignment) |
|
||||||
|
| `file.listByProjectForStage` | `file.listByProjectForRound` | `{ projectId, stageId }` → `{ projectId, roundId }` | Return multi-window grouped files with labels |
|
||||||
|
| `live.getCurrentProject` | `live.getCurrentProject` | `{ stageId }` → `{ roundId }` | Require Jury 3 JuryGroup membership |
|
||||||
|
| `live.submitVote` | `live.submitVote` | `{ stageId, ... }` → `{ roundId, ... }` | Require Jury 3 membership |
|
||||||
|
| `live.getVotingStatus` | `live.getVotingStatus` | `{ stageId }` → `{ roundId }` | Add deliberation session linkage |
|
||||||
|
|
||||||
|
### 10.2 Applicant Portal Procedures
|
||||||
|
|
||||||
|
| Current Procedure | New Procedure | Signature Change | Notes |
|
||||||
|
|-------------------|---------------|-----------------|-------|
|
||||||
|
| `pipeline.getApplicantView` | `competition.getApplicantView` | `{ pipelineId }` → `{ competitionId }` | Return rounds with status, progress % |
|
||||||
|
| `stage.getApplicantTimeline` | `round.getApplicantTimeline` | `{ stageId }` → `{ roundId }` | Include SubmissionWindow deadlines |
|
||||||
|
| `stage.getRequirements` | `submissionWindow.getRequirements` | `{ stageId }` → `{ submissionWindowId }` | Window-based, not round-based |
|
||||||
|
| `applicant.getMyDashboard` | `applicant.getMyDashboard` | returns `openStages[]` → returns `openRounds[]` | Include SubmissionWindow status per round |
|
||||||
|
| `application.submit` | `application.submit` | `{ stageId, ... }` → `{ roundId, submissionWindowId, ... }` | Window-scoped submission |
|
||||||
|
| `file.uploadForStage` | `file.uploadForWindow` | `{ stageId, slotKey }` → `{ submissionWindowId, requirementId }` | Requirement-based validation |
|
||||||
|
| `file.getMyFiles` | `file.getMyFiles` | `{ stageId? }` → `{ submissionWindowId? }` | Multi-window grouping |
|
||||||
|
|
||||||
|
### 10.3 Component Renames
|
||||||
|
|
||||||
|
| Current Component | New Component | Used By |
|
||||||
|
|-------------------|---------------|---------|
|
||||||
|
| `StageTimeline` | `RoundTimeline` | Applicant journey, jury overview |
|
||||||
|
| `StageWindowBadge` | `RoundStatusBadge` | All portals — status indicators |
|
||||||
|
| `RequirementUploadSlot` | `SubmissionSlot` | Applicant document upload |
|
||||||
|
| `PipelineBreadcrumb` | `CompetitionBreadcrumb` | All portals — navigation |
|
||||||
|
| `StageConfigEditor` | `RoundConfigEditor` | Admin round configuration |
|
||||||
|
| `PipelineWizard` | `CompetitionWizard` | Admin competition creation |
|
||||||
|
|
||||||
|
### 10.4 Route Migrations
|
||||||
|
|
||||||
|
| Current Route | New Route | Portal |
|
||||||
|
|---------------|-----------|--------|
|
||||||
|
| `/admin/rounds/pipelines/*` | `/admin/competitions/*` | Admin |
|
||||||
|
| `/admin/rounds/pipeline/[id]/*` | `/admin/competitions/[id]/*` | Admin |
|
||||||
|
| `/jury/stages/[stageId]/*` | `/jury/rounds/[roundId]/*` | Jury |
|
||||||
|
| `/jury/stages/[stageId]/live/*` | `/jury/rounds/[roundId]/live/*` | Jury |
|
||||||
|
| `/jury/stages/[stageId]/deliberation/*` | `/jury/rounds/[roundId]/deliberation/*` | Jury (NEW) |
|
||||||
|
| `/applicant/pipeline/*` | `/applicant/competition/*` | Applicant |
|
||||||
|
| `/applicant/pipeline/[stageId]/documents/*` | `/applicant/competition/[roundId]/documents/*` | Applicant |
|
||||||
|
| `/vote/stage/[sessionId]/*` | `/vote/round/[sessionId]/*` | Public |
|
||||||
|
| `/live-scores/stage/[sessionId]/*` | `/live-scores/round/[sessionId]/*` | Public |
|
||||||
|
|
||||||
|
### 10.5 Background Job & Event Migrations
|
||||||
|
|
||||||
|
| Current Job/Event | New Job/Event | Changes |
|
||||||
|
|-------------------|---------------|---------|
|
||||||
|
| `evaluationReminder` cron | `evaluationReminder` cron | `stageId` → `roundId` in query and templates |
|
||||||
|
| `stage.transitioned` event | `round.transitioned` event | Event name + payload rename |
|
||||||
|
| `pipeline.created` event | `competition.created` event | Event name + payload rename |
|
||||||
|
| `pipeline.statusChanged` event | `competition.statusChanged` event | Event name + payload rename |
|
||||||
|
| `stage.assignment.completed` event | `round.assignment.completed` event | Event name + JuryGroup context in payload |
|
||||||
|
| `stage.filtering.completed` event | `round.filtering.completed` event | Event name rename |
|
||||||
|
| `stage-notifications` digest events | `round-notifications` digest events | All event keys rename |
|
||||||
|
| `award-eligibility-job` background | `award-eligibility-job` background | `stageId` → `roundId` in context |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Phased Rollout Recommendation
|
||||||
|
|
||||||
|
Given the scope, recommend phased implementation:
|
||||||
|
|
||||||
|
### Phase 1: Data Model & Core Routers (Weeks 1-2)
|
||||||
|
- Create Competition, Round, JuryGroup models
|
||||||
|
- Rename pipeline.ts → competition.ts, stage.ts → round.ts
|
||||||
|
- Create juryGroup.ts router
|
||||||
|
- Update database schema and migrations
|
||||||
|
- Update test factories
|
||||||
|
|
||||||
|
### Phase 2: Admin UX & Wizards (Weeks 3-4)
|
||||||
|
- Update Competition wizard
|
||||||
|
- Implement RoundType config editors
|
||||||
|
- Create JuryGroup management pages
|
||||||
|
- Update assignment management UI
|
||||||
|
|
||||||
|
### Phase 3: Services & Assignment Engine (Weeks 5-6)
|
||||||
|
- Rename stage-engine.ts → round-engine.ts
|
||||||
|
- Rename stage-assignment.ts → round-assignment.ts
|
||||||
|
- Implement new assignment policy engine
|
||||||
|
- Update AI services
|
||||||
|
|
||||||
|
### Phase 4: Deliberation & Results (Weeks 7-8)
|
||||||
|
- Create deliberation.ts service and router
|
||||||
|
- Implement result-lock.ts service
|
||||||
|
- Build deliberation admin panel
|
||||||
|
- Build deliberation jury interface
|
||||||
|
|
||||||
|
### Phase 5: Jury & Applicant UX (Weeks 9-10)
|
||||||
|
- Update jury pages (Round navigation, deliberation voting)
|
||||||
|
- Update applicant pages (Competition Journey, submission windows)
|
||||||
|
- Update mentor workspace
|
||||||
|
|
||||||
|
### Phase 6: Testing & Validation (Weeks 11-12)
|
||||||
|
- Integration testing
|
||||||
|
- UAT with admin/jury/applicant test users
|
||||||
|
- Performance testing
|
||||||
|
- Documentation finalization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Cross-Reference Map
|
||||||
|
|
||||||
|
This document integrates concepts from:
|
||||||
|
|
||||||
|
- **[02-data-model.md]** — Competition, Round, JuryGroup schemas
|
||||||
|
- **[03-competition-flow.md]** — 7-round flow, state transitions
|
||||||
|
- **[04-jury-groups-and-assignment-policy.md]** — JuryGroup management, caps, biases, assignment rules
|
||||||
|
- **[05-competition-wizard.md]** — Wizard UI for Competition setup
|
||||||
|
- **[06-round-configs.md]** — 7 typed RoundType configs, submission windows
|
||||||
|
- **[07-deliberation-and-result-lock.md]** — Deliberation sessions, quorum voting, result locking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Platform Integration Matrix**
|
||||||
247
docs/unified-architecture-redesign/09-implementation-roadmap.md
Normal file
247
docs/unified-architecture-redesign/09-implementation-roadmap.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# Implementation Roadmap
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The redesign is implemented in 9 phases, progressing from contract definition through schema migration, feature implementation, and legacy decommission. Each phase has defined scope, dependencies, and rollback points.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase Summary
|
||||||
|
|
||||||
|
| Phase | Scope | Duration | Dependencies |
|
||||||
|
|-------|-------|----------|--------------|
|
||||||
|
| 0 | **Contract Freeze** — type definitions, Zod schemas, feature flags, test factories | 1 week | None |
|
||||||
|
| 1 | **Schema & Runtime Foundation** — Prisma migration, Competition/Round/JuryGroup CRUD | 2 weeks | Phase 0 |
|
||||||
|
| 2 | **Policy Engine** — centralized context resolver, policy resolution, assignment policy evaluator | 1 week | Phase 1 |
|
||||||
|
| 3 | **Invite/Onboarding Integration** — jury memberships on invite, assignment intent, onboarding routing | 1–2 weeks | Phase 1 |
|
||||||
|
| 4 | **Backend Orchestration** — enhanced assignment, submission round manager, mentor workspace, deliberation service | 2 weeks | Phase 2, 3 |
|
||||||
|
| 5 | **Admin Control Plane + Participant UX** — competition wizard, round management, jury/applicant/mentor dashboards | 2 weeks | Phase 4 |
|
||||||
|
| 6 | **Special Awards + Live Finals + Deliberation** — award modes, stage manager, deliberation voting UI | 2 weeks | Phase 4, 5 |
|
||||||
|
| 7 | **Platform-Wide Refit** — remove Pipeline/Track/Stage references, drop old tables | 2 weeks | Phase 5, 6 |
|
||||||
|
| 8 | **Cutover & Legacy Decommission** — enable new contracts, deprecate legacy, burn-in period | 1 week | Phase 7 |
|
||||||
|
|
||||||
|
**Total estimated duration**: 14–15 weeks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 0 ──→ Phase 1 ──→ Phase 2 ──→ Phase 4 ──→ Phase 5 ──→ Phase 7 ──→ Phase 8
|
||||||
|
└──→ Phase 3 ──┘ └──→ Phase 6 ──┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- Phases 2 and 3 can run in parallel after Phase 1
|
||||||
|
- Phase 4 requires both Phase 2 and 3
|
||||||
|
- Phases 5 and 6 can partially overlap
|
||||||
|
- Phase 7 requires both Phase 5 and 6
|
||||||
|
|
||||||
|
**Critical path**: 0 → 1 → 2 → 4 → 5 → 7 → 8 (12 weeks minimum)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase Details
|
||||||
|
|
||||||
|
### Phase 0: Contract Freeze (1 week)
|
||||||
|
|
||||||
|
**Goal**: Lock down all type definitions, schemas, and interfaces before any implementation begins.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- All Prisma model definitions finalized (see [02-data-model.md](./02-data-model.md))
|
||||||
|
- All 7 Zod config schemas finalized (IntakeConfig through DeliberationConfig)
|
||||||
|
- All new enum definitions finalized
|
||||||
|
- Feature flag infrastructure in place
|
||||||
|
- Test factory functions for all new models
|
||||||
|
- TypeScript type exports for all new models
|
||||||
|
|
||||||
|
**Rollback**: No production changes. Delete type files if needed.
|
||||||
|
|
||||||
|
**Gate**: All type definitions reviewed and approved. No changes to schemas after this phase without architecture sign-off (see "No Silent Contract Drift" in [13-open-questions-and-governance.md](./13-open-questions-and-governance.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: Schema & Runtime Foundation (2 weeks)
|
||||||
|
|
||||||
|
**Goal**: New database tables exist alongside old ones. Basic CRUD operations work.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Prisma migration: add Competition, Round, JuryGroup, JuryGroupMember, SubmissionWindow, SubmissionFileRequirement, DeliberationSession, DeliberationVote, DeliberationResult, DeliberationParticipant, ResultLock, ResultUnlockEvent, MentorMessage, AssignmentIntent, AssignmentException, SubmissionPromotionEvent tables
|
||||||
|
- New enums: RoundType, RoundStatus, CompetitionStatus, CapMode, DeadlinePolicy, DeliberationMode, DeliberationStatus, TieBreakMethod, etc.
|
||||||
|
- Basic tRPC routers: `competition.create`, `competition.getById`, `round.create`, `juryGroup.create`, `juryGroup.addMember`
|
||||||
|
- Admin can create a Competition with Rounds through the API (no UI yet)
|
||||||
|
|
||||||
|
**Rollback**: Drop new tables. No old tables modified.
|
||||||
|
|
||||||
|
**Gate**: All new tables exist with correct schemas. CRUD operations pass integration tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Policy Engine (1 week)
|
||||||
|
|
||||||
|
**Goal**: Centralized resolution of competition context and assignment policies.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- `CompetitionContextResolver` service: given a roundId, returns the full competition context (competition, round type, config, jury group, submission windows, etc.)
|
||||||
|
- Policy resolution function: 5-layer precedence chain for assignment caps, ratios, and modes
|
||||||
|
- Assignment policy evaluator: `canAssignMore(member)`, `getEffectiveCap(member)`, `getRemainingCapacity(member)`
|
||||||
|
- Unit tests for all policy combinations
|
||||||
|
|
||||||
|
**Rollback**: Remove new services. No data changes.
|
||||||
|
|
||||||
|
**Gate**: Policy resolution returns correct values for all 5 layers. Edge cases (null overrides, conflicting settings) handled correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Invite/Onboarding Integration (1–2 weeks)
|
||||||
|
|
||||||
|
**Goal**: Jury memberships are created at invite time. Judges can self-service during onboarding.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Invite flow creates `JuryGroupMember` record when judge is invited to a jury
|
||||||
|
- `AssignmentIntent` records can be created during invite (pre-assignment)
|
||||||
|
- Onboarding page shows cap/ratio preferences when `allowOnboardingSelfService` is true
|
||||||
|
- Judge can adjust cap and category bias during onboarding
|
||||||
|
- Admin can review and override self-service values
|
||||||
|
|
||||||
|
**Rollback**: Revert invite flow changes. JuryGroupMember records can be deleted.
|
||||||
|
|
||||||
|
**Gate**: End-to-end flow: invite judge → judge accepts → onboarding shows jury details → judge adjusts preferences → admin sees updated values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Backend Orchestration (2 weeks)
|
||||||
|
|
||||||
|
**Goal**: All backend services for the 8-round flow are operational.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- **RoundEngine** service: state machine for round transitions (replaces StageEngine)
|
||||||
|
- **Enhanced Assignment** service: hard/soft cap algorithm, category bias, unassigned queue, COI check
|
||||||
|
- **SubmissionRoundManager**: manages submission windows, file requirements, read-only enforcement
|
||||||
|
- **MentorWorkspaceService**: messaging, file upload, file comments, file promotion
|
||||||
|
- **DeliberationService**: session creation, vote submission, aggregation (Borda count / vote tally), tie-breaking, result lock
|
||||||
|
- **AI Shortlist** service: generates ranked recommendations at end of evaluation rounds
|
||||||
|
- **ResultLockService**: lock/unlock with audit trail
|
||||||
|
|
||||||
|
**Rollback**: Remove new services. Feature flags prevent activation.
|
||||||
|
|
||||||
|
**Gate**: All services pass unit tests. Integration test covers full R1→R8 flow with test data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Admin Control Plane + Participant UX (2 weeks)
|
||||||
|
|
||||||
|
**Goal**: Admin can configure and run a competition through the UI. Participants see the correct experience.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- **Competition Wizard**: create competition with rounds, configure each round type
|
||||||
|
- **Round Management**: view/edit round configs, manage submission windows, set deadlines
|
||||||
|
- **Juries Management (NEW section)**: create JuryGroups, add/remove members, configure caps/ratios, view assignments
|
||||||
|
- **Assignment Dashboard**: view assignments, manage unassigned queue, manual override
|
||||||
|
- **Jury Dashboard**: countdown, assigned projects, multi-round doc viewing, scoring/feedback
|
||||||
|
- **Applicant Portal**: multi-round doc upload, read-only enforcement, submission status
|
||||||
|
- **Mentor Dashboard**: assigned teams, messaging, file management, promotion
|
||||||
|
|
||||||
|
**Rollback**: Revert UI pages. Backend services still work via API.
|
||||||
|
|
||||||
|
**Gate**: Admin can create, configure, and manage a full competition through the UI. Jury, applicant, and mentor dashboards functional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Special Awards + Live Finals + Deliberation (2 weeks)
|
||||||
|
|
||||||
|
**Goal**: The most complex runtime features are operational.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- **Special Awards**: create awards with routing modes, eligibility filtering, per-award jury, doc requirements, review windows, single-judge mode
|
||||||
|
- **Live Finals Stage Manager**: admin controls for ceremony flow, project cursor, voting windows
|
||||||
|
- **Jury 3 Live Interface**: real-time notes, all docs from prior rounds, prior jury data (if enabled), live scoring
|
||||||
|
- **Audience Voting**: optional per-category audience vote, reveal timing configuration
|
||||||
|
- **Deliberation UI**: session management, juror voting interface (both modes), result display, tie-breaking, admin lock
|
||||||
|
- **Result Lock**: admin locks results, unlock requires super-admin with reason
|
||||||
|
|
||||||
|
**Rollback**: Feature flags disable awards, live finals, and deliberation. Fall back to basic flow.
|
||||||
|
|
||||||
|
**Gate**: Full live finals ceremony simulation. Deliberation voting in both modes. Result lock and unlock. Award creation and selection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 7: Platform-Wide Refit (2 weeks)
|
||||||
|
|
||||||
|
**Goal**: Remove all references to Pipeline, Track, and Stage from the codebase. Estimated 120+ files, ~350 hours of work.
|
||||||
|
|
||||||
|
**Sub-phases** (ordered by dependency):
|
||||||
|
|
||||||
|
| Sub-Phase | Scope | Files | Est. Hours |
|
||||||
|
|-----------|-------|-------|-----------|
|
||||||
|
| **7a** | Database: rename FKs (stageId→roundId) in AudienceVote, COIDeclaration, ProjectStatusHistory, DigestLog, PartnerAccess | 1 (schema) + migration | 8 |
|
||||||
|
| **7b** | Types & libraries: rewrite pipeline-wizard.ts, wizard-config.ts, pipeline-defaults.ts, pipeline-validation.ts, stage-config-schema.ts | 6 | 32 |
|
||||||
|
| **7c** | Core routers: rename pipeline.ts→competition.ts, stage.ts→round.ts, track.ts→(delete), stageAssignment.ts→roundAssignment.ts | 4 | 24 |
|
||||||
|
| **7d** | Dependent routers: update all 20 routers with stageId/pipelineId references (evaluation, file, live, invite, notification, etc.) | 20 | 80 |
|
||||||
|
| **7e** | Services: rename stage-engine→round-engine, stage-filtering→round-filtering, stage-assignment→round-assignment, stage-notifications→round-notifications, live-control (update refs) | 11 | 50 |
|
||||||
|
| **7f** | Admin pages: rename 35+ pages referencing pipeline/stage, update breadcrumbs, wizard, config editors | 35+ | 52 |
|
||||||
|
| **7g** | Jury pages: update 16 jury pages + components (stage routes→round routes, assignment queries, evaluation forms) | 16 | 18 |
|
||||||
|
| **7h** | Applicant pages: update 8 pages (pipeline visualization→competition progress, stage documents→round documents) | 8 | 12 |
|
||||||
|
| **7i** | Infrastructure: update Docker entrypoint, cron jobs, webhook event names, notification templates, seed data | 4 | 11 |
|
||||||
|
| **7j** | Tests: update 13+ test files + helpers.ts, rename factories, update all assertions | 13+ | 45 |
|
||||||
|
|
||||||
|
**Week 1**: Sub-phases 7a–7e (database, types, routers, services — backend foundation)
|
||||||
|
**Week 2**: Sub-phases 7f–7j (UI pages, infrastructure, tests — frontend + verification)
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- All `pipeline` references renamed to `competition` in routers, services, types, UI
|
||||||
|
- All `stage` references renamed to `round`
|
||||||
|
- All `track` references removed
|
||||||
|
- All imports, type aliases, and variable names updated
|
||||||
|
- Old database tables dropped (Pipeline, Track, Stage, ProjectStageState)
|
||||||
|
- Seed data updated to use new models
|
||||||
|
- All background jobs and webhook events using new names
|
||||||
|
|
||||||
|
**Rollback**: This is the point of no return for the old schema. Rollback requires restoring from backup.
|
||||||
|
|
||||||
|
**Gate**: `grep -r "pipeline\|Pipeline\|stage\|Stage\|track\|Track" src/` returns zero results (excluding legitimate uses like "stage manager" in live finals). All tests pass. Build succeeds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 8: Cutover & Legacy Decommission (1 week)
|
||||||
|
|
||||||
|
**Goal**: Production system running entirely on the new architecture.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- Feature flags set to new system for all subsystems
|
||||||
|
- Legacy API endpoints deprecated (return 410 Gone with migration guide)
|
||||||
|
- Burn-in period: monitor for errors, performance regressions
|
||||||
|
- Documentation updated (CLAUDE.md, README, API docs)
|
||||||
|
- Seed data updated
|
||||||
|
- Docker entrypoint updated for new migration flow
|
||||||
|
|
||||||
|
**Rollback**: Feature flags can revert to legacy for individual subsystems (only if old tables still exist, i.e., Phase 7 wasn't completed).
|
||||||
|
|
||||||
|
**Gate**: 72-hour burn-in with zero critical errors. All release gates A–F passed (see [12-observability-and-release-gates.md](./12-observability-and-release-gates.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Strategy
|
||||||
|
|
||||||
|
| Phase | Rollback Method | Data Loss Risk |
|
||||||
|
|-------|----------------|----------------|
|
||||||
|
| 0 | Delete type files | None |
|
||||||
|
| 1 | Drop new tables | None (old tables untouched) |
|
||||||
|
| 2 | Remove services | None |
|
||||||
|
| 3 | Revert invite flow | JuryGroupMember records deleted |
|
||||||
|
| 4 | Feature flag off | None |
|
||||||
|
| 5 | Revert UI pages | None |
|
||||||
|
| 6 | Feature flag off | None |
|
||||||
|
| 7 | **Point of no return** — restore from backup | Requires backup |
|
||||||
|
| 8 | Feature flags to legacy | Only if Phase 7 not completed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|-----------|
|
||||||
|
| Schema migration breaks existing data | Phase 1 adds tables alongside old ones. No modifications to existing tables until Phase 7. |
|
||||||
|
| Assignment algorithm incorrect | Extensive unit tests in Phase 2. Manual override always available. |
|
||||||
|
| Deliberation edge cases | Phase 6 includes deliberation test matrix (see [11-testing-and-qa.md](./11-testing-and-qa.md)). |
|
||||||
|
| Live finals concurrency | Load testing in Phase 6. See [12-observability-and-release-gates.md](./12-observability-and-release-gates.md). |
|
||||||
|
| Late requirement changes | "No Silent Contract Drift" policy enforced after Phase 0. |
|
||||||
565
docs/unified-architecture-redesign/10-migration-strategy.md
Normal file
565
docs/unified-architecture-redesign/10-migration-strategy.md
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
# Migration Strategy
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The migration from Pipeline/Track/Stage to Competition/Round follows a 4-phase approach designed to minimize risk. New tables are added alongside old ones, data is backfilled, code is migrated, and finally old tables are dropped. Feature flags allow granular control of the transition.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Phases
|
||||||
|
|
||||||
|
### Phase 1: Schema Additions (Non-Breaking)
|
||||||
|
|
||||||
|
Add all new tables and enums alongside existing tables. No existing tables are modified or dropped.
|
||||||
|
|
||||||
|
**New tables added:**
|
||||||
|
- `Competition` (parallel to `Pipeline`)
|
||||||
|
- `Round` (parallel to `Stage`)
|
||||||
|
- `JuryGroup`, `JuryGroupMember`
|
||||||
|
- `SubmissionWindow`, `SubmissionFileRequirement`
|
||||||
|
- `RoundSubmissionVisibility`
|
||||||
|
- `ProjectRoundState` (parallel to `ProjectStageState`)
|
||||||
|
- `DeliberationSession`, `DeliberationVote`, `DeliberationResult`, `DeliberationParticipant`
|
||||||
|
- `ResultLock`, `ResultUnlockEvent`
|
||||||
|
- `MentorMessage`
|
||||||
|
- `AssignmentIntent`, `AssignmentException`
|
||||||
|
- `SubmissionPromotionEvent`
|
||||||
|
- `AwardWinner`
|
||||||
|
|
||||||
|
**New enums added:**
|
||||||
|
- `CompetitionStatus`: DRAFT, ACTIVE, COMPLETED, ARCHIVED
|
||||||
|
- `RoundType`: INTAKE, FILTERING, EVALUATION, SUBMISSION, MENTORING, LIVE_FINAL, DELIBERATION
|
||||||
|
- `RoundStatus`: DRAFT, READY, ACTIVE, COMPLETED, SKIPPED
|
||||||
|
- `CapMode`: HARD, SOFT, NONE
|
||||||
|
- `DeadlinePolicy`: HARD, FLAG, GRACE
|
||||||
|
- `DeliberationMode`: SINGLE_WINNER_VOTE, FULL_RANKING
|
||||||
|
- `DeliberationStatus`: OPEN, VOTING, TALLYING, RUNOFF, LOCKED
|
||||||
|
- `TieBreakMethod`: RUNOFF, ADMIN_DECIDES, SCORE_FALLBACK
|
||||||
|
- `DeliberationParticipantStatus`: REQUIRED, ABSENT_EXCUSED, REPLACED, REPLACEMENT_ACTIVE
|
||||||
|
- `AwardRoutingMode`: STAY_IN_MAIN, SEPARATE_POOL
|
||||||
|
- `AwardEligibilityMode`: AI_SUGGESTED, MANUAL, ALL_ELIGIBLE, ROUND_BASED
|
||||||
|
- `WinnerDecisionMode`: JURY_VOTE, SINGLE_JUDGE
|
||||||
|
- `MentorMessageRole`: MENTOR, APPLICANT, ADMIN
|
||||||
|
- `PromotionSourceType`: MENTOR_FILE, ADMIN_REPLACEMENT
|
||||||
|
- `AssignmentIntentSource`: INVITE, ADMIN, SYSTEM
|
||||||
|
|
||||||
|
**Rollback**: Drop new tables and enums. Zero impact on existing system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Data Migration (Backfill)
|
||||||
|
|
||||||
|
Backfill new tables from existing data. Old tables remain populated and active.
|
||||||
|
|
||||||
|
**Mapping table:**
|
||||||
|
|
||||||
|
| Old Model | New Model | Mapping Logic |
|
||||||
|
|-----------|-----------|---------------|
|
||||||
|
| `Pipeline` | `Competition` | 1:1. Copy id, programId, name, status, settings → typed config split |
|
||||||
|
| `Track` (MAIN) | *(eliminated)* | Main track's stages become Competition's rounds directly |
|
||||||
|
| `Track` (AWARD) | `SpecialAward` | One SpecialAward per AWARD track |
|
||||||
|
| `Stage` | `Round` | 1:1. Copy type mapping (see below), order, status |
|
||||||
|
| `Stage.configJson` | `Round.configJson` | Parse and validate against typed schema for the round type |
|
||||||
|
| `ProjectStageState` | `ProjectRoundState` | Map stageId → roundId, trackId dropped |
|
||||||
|
| Judge assignments | `JuryGroupMember` + assignments | Create JuryGroups from distinct jury configurations |
|
||||||
|
| `SpecialAward` | `SpecialAward` (enhanced) | Add routing mode, eligibility mode fields |
|
||||||
|
|
||||||
|
**Stage type to Round type mapping:**
|
||||||
|
|
||||||
|
| Old StageType | New RoundType | Notes |
|
||||||
|
|---------------|---------------|-------|
|
||||||
|
| `INTAKE` | `INTAKE` | Direct mapping |
|
||||||
|
| `FILTER` | `FILTERING` | Renamed for clarity |
|
||||||
|
| `EVALUATION` | `EVALUATION` | Direct mapping (used for both Jury 1 and Jury 2) |
|
||||||
|
| `SELECTION` | *(absorbed)* | Selection logic moves into evaluation round config |
|
||||||
|
| `LIVE_FINAL` | `LIVE_FINAL` | Direct mapping |
|
||||||
|
| `RESULTS` | *(absorbed)* | Results are part of the deliberation round |
|
||||||
|
| *(new)* | `SUBMISSION` | New round type for multi-round document collection |
|
||||||
|
| *(new)* | `MENTORING` | New round type for mentor collaboration |
|
||||||
|
| *(new)* | `DELIBERATION` | New round type replacing confirmation |
|
||||||
|
|
||||||
|
**Backfill script**: A TypeScript migration script reads all existing Pipelines/Tracks/Stages and creates corresponding Competition/Round records. The script is idempotent (can be run multiple times safely).
|
||||||
|
|
||||||
|
**Rollback**: Delete backfilled data from new tables. Old tables unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Code Migration
|
||||||
|
|
||||||
|
Update all services, routers, and UI to use new models. Feature flags control which code path is active.
|
||||||
|
|
||||||
|
**Feature flag strategy:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const FEATURE_FLAGS = {
|
||||||
|
USE_COMPETITION_MODEL: false, // Phase 3: switch to Competition/Round queries
|
||||||
|
USE_JURY_GROUPS: false, // Phase 3: switch to JuryGroup-based assignment
|
||||||
|
USE_DELIBERATION: false, // Phase 6: switch to new deliberation model
|
||||||
|
USE_MENTOR_WORKSPACE: false, // Phase 4: enable enhanced mentor features
|
||||||
|
USE_SUBMISSION_WINDOWS: false, // Phase 4: enable multi-round submissions
|
||||||
|
HIDE_LEGACY_PIPELINE_UI: false, // Phase 7: hide old Pipeline/Stage UI
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dual-read pattern**: During transition, services can read from both old and new tables:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function getCompetitionContext(id: string) {
|
||||||
|
if (FEATURE_FLAGS.USE_COMPETITION_MODEL) {
|
||||||
|
return getFromCompetitionModel(id)
|
||||||
|
}
|
||||||
|
return getFromPipelineModel(id) // legacy path
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Router migration order:**
|
||||||
|
1. `pipeline.ts` → `competition.ts` (create new router, keep old active behind flag)
|
||||||
|
2. `stage.ts` → `round.ts` (same approach)
|
||||||
|
3. Remove `track.ts` (logic absorbed into competition + specialAward routers)
|
||||||
|
4. Update `assignment.ts` to use JuryGroup model
|
||||||
|
5. Update `evaluation.ts` to use Round model
|
||||||
|
6. Add `deliberation.ts` (new router)
|
||||||
|
7. Update `mentor.ts` with workspace features
|
||||||
|
8. Update all UI pages to use new routers
|
||||||
|
|
||||||
|
**Rollback**: Flip feature flags back to legacy. Both code paths exist during this phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Cleanup (Point of No Return)
|
||||||
|
|
||||||
|
Drop old tables and remove legacy code paths. This is irreversible.
|
||||||
|
|
||||||
|
**Tables dropped:**
|
||||||
|
- `Pipeline`
|
||||||
|
- `Track`
|
||||||
|
- `Stage`
|
||||||
|
- `ProjectStageState`
|
||||||
|
- Legacy configJson schemas
|
||||||
|
|
||||||
|
**Enums dropped:**
|
||||||
|
- `StageType` (replaced by `RoundType`)
|
||||||
|
- `TrackKind` (MAIN/AWARD — eliminated)
|
||||||
|
- `RoutingMode` (SHARED/EXCLUSIVE — replaced by AwardRoutingMode)
|
||||||
|
- `WinnerProposalStatus` (replaced by `DeliberationStatus`)
|
||||||
|
- `WinnerApprovalRole` (replaced by `DeliberationParticipantStatus`)
|
||||||
|
|
||||||
|
**Models dropped:**
|
||||||
|
- `WinnerProposal` (replaced by `DeliberationSession`)
|
||||||
|
- `WinnerApproval` (replaced by `DeliberationVote`)
|
||||||
|
|
||||||
|
**Code removed:**
|
||||||
|
- All feature flag conditionals (only new path remains)
|
||||||
|
- All dual-read logic
|
||||||
|
- All legacy router files
|
||||||
|
- All legacy service files
|
||||||
|
|
||||||
|
**Rollback**: Restore from database backup. This phase should only be executed after thorough testing in Phases 1–3 and the burn-in period in Phase 8 of the implementation roadmap.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Mapping Reference
|
||||||
|
|
||||||
|
### Complete Field Mapping
|
||||||
|
|
||||||
|
| Old Field | New Field | Notes |
|
||||||
|
|-----------|-----------|-------|
|
||||||
|
| `Pipeline.id` | `Competition.id` | Preserve IDs for foreign key consistency |
|
||||||
|
| `Pipeline.programId` | `Competition.programId` | Direct |
|
||||||
|
| `Pipeline.name` | `Competition.name` | Direct |
|
||||||
|
| `Pipeline.description` | `Competition.description` | Direct |
|
||||||
|
| `Pipeline.status` | `Competition.status` | Map to CompetitionStatus enum |
|
||||||
|
| `Pipeline.settingsJson` | `Competition.settingsJson` | Carry forward, validate |
|
||||||
|
| `Stage.id` | `Round.id` | Preserve IDs |
|
||||||
|
| `Stage.trackId` | *(dropped)* | No more track reference |
|
||||||
|
| `Stage.pipelineId` | `Round.competitionId` | Rename |
|
||||||
|
| `Stage.type` | `Round.type` | Map StageType → RoundType |
|
||||||
|
| `Stage.order` | `Round.order` | Direct |
|
||||||
|
| `Stage.status` | `Round.status` | Map to RoundStatus |
|
||||||
|
| `Stage.configJson` | `Round.configJson` | Validate against typed schema |
|
||||||
|
| `Stage.startsAt` | `Round.startsAt` | Direct |
|
||||||
|
| `Stage.endsAt` | `Round.endsAt` | Direct |
|
||||||
|
| `ProjectStageState.stageId` | `ProjectRoundState.roundId` | Rename |
|
||||||
|
| `ProjectStageState.trackId` | *(dropped)* | No more track |
|
||||||
|
| `ProjectStageState.projectId` | `ProjectRoundState.projectId` | Direct |
|
||||||
|
| `ProjectStageState.state` | `ProjectRoundState.state` | Map values |
|
||||||
|
|
||||||
|
### Additional FK Migrations (stageId → roundId)
|
||||||
|
|
||||||
|
These existing models have `stageId` foreign keys that must be renamed to `roundId`. They are NOT being replaced by new models — just renamed:
|
||||||
|
|
||||||
|
| Model | Old FK | New FK | Notes |
|
||||||
|
|-------|--------|--------|-------|
|
||||||
|
| `AudienceVote` | `stageId` | `roundId` | Live voting records |
|
||||||
|
| `COIDeclaration` | `stageId` | `roundId` | Conflict of interest declarations |
|
||||||
|
| `ProjectStatusHistory` | `stageId` | `roundId` | Project state change log |
|
||||||
|
| `DigestLog` | `stageId` | `roundId` | Notification digest tracking |
|
||||||
|
| `MentorNote` | `stageId` | `roundId` | Mentor notes (legacy model, still used) |
|
||||||
|
| `MentorMilestone` | `stageId` | `roundId` | Mentor milestone definitions |
|
||||||
|
| `PartnerStageAccess` | `stageId` | `roundId` | Rename model to `PartnerRoundAccess` |
|
||||||
|
| `LearningResource` | `stageId` | `roundId` | Learning resources linked to rounds |
|
||||||
|
| `TaggingJob` | `stageId` | `roundId` | AI tagging job references |
|
||||||
|
| `FilteringResult` | `stageId` | `roundId` | Filtering results per project |
|
||||||
|
| `FilteringJob` | `stageId` | `roundId` | Filtering batch job records |
|
||||||
|
| `EvaluationReminder` | `stageId` | `roundId` | Cron-triggered reminder records |
|
||||||
|
| `GracePeriod` | `stageId` | `roundId` | Grace period extensions |
|
||||||
|
| `LiveProgressCursor` | `stageId` | `roundId` | Live ceremony cursor |
|
||||||
|
| `LiveVotingSession` | `stageId` | `roundId` | Live voting session |
|
||||||
|
|
||||||
|
**Migration approach**: These are simple column renames with FK constraint updates. Run as a single migration after the main table additions in Phase 1.
|
||||||
|
|
||||||
|
### New Field Additions (Non-Breaking)
|
||||||
|
|
||||||
|
These fields are added to existing new models during Phase 1. All have defaults, so they are non-breaking:
|
||||||
|
|
||||||
|
| Model | Field | Type | Default | Purpose |
|
||||||
|
|-------|-------|------|---------|---------|
|
||||||
|
| `Round` | `purposeKey` | `String?` | null | Optional analytics tag |
|
||||||
|
| `JuryGroupMember` | `role` | `JuryGroupMemberRole` | `MEMBER` | Replaces `isLead: Boolean` |
|
||||||
|
| `SubmissionWindow` | `isLocked` | `Boolean` | `false` | Manual lock independent of window close |
|
||||||
|
| `AssignmentIntent` | `status` | `AssignmentIntentStatus` | `PENDING` | Proper lifecycle enum |
|
||||||
|
|
||||||
|
**Data migration for `isLead → role`**: For existing data where `isLead = true`, set `role = CHAIR`. For `isLead = false`, set `role = MEMBER`. Drop `isLead` column after backfill.
|
||||||
|
|
||||||
|
### New Enums (Phase 1)
|
||||||
|
|
||||||
|
| Enum | Values | Used By |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `JuryGroupMemberRole` | CHAIR, MEMBER, OBSERVER | `JuryGroupMember.role` |
|
||||||
|
| `AssignmentIntentStatus` | PENDING, HONORED, OVERRIDDEN, EXPIRED, CANCELLED | `AssignmentIntent.status` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-Migration Checklist
|
||||||
|
|
||||||
|
Before starting Phase 1:
|
||||||
|
- [ ] All type definitions finalized (Phase 0 of roadmap)
|
||||||
|
- [ ] Database backup taken
|
||||||
|
- [ ] Migration script written and tested on staging
|
||||||
|
- [ ] Feature flags infrastructure in place
|
||||||
|
- [ ] Rollback procedures documented and tested
|
||||||
|
- [ ] All team members briefed on migration plan
|
||||||
|
|
||||||
|
Before starting Phase 4 (cleanup):
|
||||||
|
- [ ] All feature flags pointing to new code paths
|
||||||
|
- [ ] 72-hour burn-in period completed with zero critical errors
|
||||||
|
- [ ] All release gates A–F passed (see [12-observability-and-release-gates.md](./12-observability-and-release-gates.md))
|
||||||
|
- [ ] Database backup taken (point-of-no-return backup)
|
||||||
|
- [ ] Rollback from backup tested on staging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timeline Alignment
|
||||||
|
|
||||||
|
| Migration Phase | Roadmap Phase | When |
|
||||||
|
|----------------|---------------|------|
|
||||||
|
| Phase 1: Schema additions | Implementation Phase 1 | Weeks 2–3 |
|
||||||
|
| Phase 2: Data backfill | Implementation Phase 1 | Weeks 3–4 |
|
||||||
|
| Phase 3: Code migration | Implementation Phases 3–6 | Weeks 4–12 |
|
||||||
|
| Phase 4: Cleanup | Implementation Phase 7 | Week 12–13 |
|
||||||
|
|
||||||
|
See [09-implementation-roadmap.md](./09-implementation-roadmap.md) for the full implementation timeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Inventory Appendix
|
||||||
|
|
||||||
|
Complete file-by-file migration effort estimate. Risk: L = Low (rename only), M = Medium (logic changes), H = High (rewrite).
|
||||||
|
|
||||||
|
### Database & Schema (3 files, ~28 hours)
|
||||||
|
|
||||||
|
| File | Changes | Risk |
|
||||||
|
|------|---------|------|
|
||||||
|
| `prisma/schema.prisma` | 40+ model/enum updates, 17+ FK renames | H |
|
||||||
|
| `prisma/seed.ts` | 300+ lines referencing Pipeline/Stage | M |
|
||||||
|
| `prisma/migrations/*` | New migration files for all schema changes | M |
|
||||||
|
|
||||||
|
### tRPC Routers (24 files, ~104 hours)
|
||||||
|
|
||||||
|
| File | Changes | Risk |
|
||||||
|
|------|---------|------|
|
||||||
|
| `src/server/routers/pipeline.ts` | Rename to `competition.ts`, update all queries | H |
|
||||||
|
| `src/server/routers/stage.ts` | Rename to `round.ts`, update all queries | H |
|
||||||
|
| `src/server/routers/track.ts` | Remove entirely, absorb into competition + specialAward | H |
|
||||||
|
| `src/server/routers/evaluation.ts` | stageId→roundId, add JuryGroup awareness | M |
|
||||||
|
| `src/server/routers/assignment.ts` | JuryGroup model, intent lifecycle | H |
|
||||||
|
| `src/server/routers/file.ts` | submissionWindowId, multi-round grouping | M |
|
||||||
|
| `src/server/routers/mentor.ts` | Workspace features, file promotion | M |
|
||||||
|
| `src/server/routers/award.ts` | competitionId, evaluationRoundId | M |
|
||||||
|
| `src/server/routers/audience-vote.ts` | stageId→roundId | L |
|
||||||
|
| `src/server/routers/coi.ts` | roundId awareness | L |
|
||||||
|
| `src/server/routers/notification.ts` | Stage event→round event mapping | M |
|
||||||
|
| `src/server/routers/cron.ts` | stageId→roundId in all scheduled jobs | M |
|
||||||
|
| `src/server/routers/deliberation.ts` | **NEW** — full deliberation router | H |
|
||||||
|
| `src/server/routers/result-lock.ts` | **NEW** — result lock/unlock procedures | M |
|
||||||
|
| `src/server/routers/jury-group.ts` | **NEW** — jury management procedures | H |
|
||||||
|
| `src/server/routers/submission-window.ts` | **NEW** — submission window CRUD | M |
|
||||||
|
| Other 8 routers | Indirect references (imports, type refs) | L |
|
||||||
|
|
||||||
|
### Services (11 files, ~50 hours)
|
||||||
|
|
||||||
|
| File | Changes | Risk |
|
||||||
|
|------|---------|------|
|
||||||
|
| `src/server/services/stage-engine.ts` | Rename to `round-engine.ts`, full rewrite | H |
|
||||||
|
| `src/server/services/stage-filtering.ts` | Rename to `round-filtering.ts`, roundId refs | M |
|
||||||
|
| `src/server/services/stage-assignment.ts` | Rename to `round-assignment.ts`, JuryGroup model | H |
|
||||||
|
| `src/server/services/stage-notifications.ts` | Rename to `round-notifications.ts`, event types | M |
|
||||||
|
| `src/server/services/live-control.ts` | stageId→roundId | M |
|
||||||
|
| `src/server/services/ai-filtering.ts` | stageId→roundId in AI context | L |
|
||||||
|
| `src/server/services/ai-assignment.ts` | JuryGroup awareness | M |
|
||||||
|
| `src/server/services/ai-evaluation-summary.ts` | roundId refs | L |
|
||||||
|
| `src/server/services/ai-tagging.ts` | stageId→roundId | L |
|
||||||
|
| `src/server/services/ai-award-eligibility.ts` | competitionId awareness | L |
|
||||||
|
| `src/server/services/anonymization.ts` | No changes expected | L |
|
||||||
|
|
||||||
|
### Types & Libraries (6 files, ~32 hours)
|
||||||
|
|
||||||
|
| File | Changes | Risk |
|
||||||
|
|------|---------|------|
|
||||||
|
| `src/types/pipeline-wizard.ts` | Complete rewrite → `competition-wizard.ts` | H |
|
||||||
|
| `src/types/wizard-config.ts` | Complete rewrite for round-based wizard | H |
|
||||||
|
| `src/lib/pipeline-defaults.ts` | Rename to `competition-defaults.ts` | M |
|
||||||
|
| `src/lib/pipeline-validation.ts` | Rename to `competition-validation.ts` | M |
|
||||||
|
| `src/lib/pipeline-conversions.ts` | Rename to `competition-conversions.ts` | M |
|
||||||
|
| `src/lib/stage-config-schema.ts` | Rename to `round-config-schema.ts`, Zod schemas | M |
|
||||||
|
|
||||||
|
### Admin Pages (35+ files, ~52 hours)
|
||||||
|
|
||||||
|
| File Pattern | Count | Changes | Risk |
|
||||||
|
|-------------|-------|---------|------|
|
||||||
|
| `src/app/(admin)/admin/pipelines/*` | 8 pages | Rename to `competitions/*` | H |
|
||||||
|
| `src/app/(admin)/admin/stages/*` | 5 pages | Rename to `rounds/*` | H |
|
||||||
|
| Pipeline wizard components | 6 | Full rewrite for competition model | H |
|
||||||
|
| Stage config components | 8 | Update to round-type configs | M |
|
||||||
|
| Other admin components | 10 | Indirect references | L |
|
||||||
|
|
||||||
|
### Jury Pages (16 files, ~18 hours)
|
||||||
|
|
||||||
|
| File Pattern | Count | Changes | Risk |
|
||||||
|
|-------------|-------|---------|------|
|
||||||
|
| `src/app/(jury)/jury/stages/[stageId]/*` | 6 pages | Rename to `rounds/[roundId]/*` | M |
|
||||||
|
| Jury evaluation components | 4 | roundId, JuryGroup filtering | M |
|
||||||
|
| Jury shared components | 6 | Breadcrumbs, timeline, badges | L |
|
||||||
|
|
||||||
|
### Applicant Pages (8 files, ~12 hours)
|
||||||
|
|
||||||
|
| File Pattern | Count | Changes | Risk |
|
||||||
|
|-------------|-------|---------|------|
|
||||||
|
| `src/app/(applicant)/applicant/pipeline/*` | 4 pages | Rename to `competition/*` | H |
|
||||||
|
| Applicant components | 4 | StageTimeline→RoundTimeline | M |
|
||||||
|
|
||||||
|
### Tests (13+ files, ~45 hours)
|
||||||
|
|
||||||
|
| File | Changes | Risk |
|
||||||
|
|------|---------|------|
|
||||||
|
| `tests/helpers.ts` | Update all factories | M |
|
||||||
|
| `tests/unit/stage-engine.test.ts` | Rename + update references | M |
|
||||||
|
| `tests/unit/stage-filtering.test.ts` | Rename + update | M |
|
||||||
|
| `tests/unit/stage-assignment.test.ts` | Rename + add JuryGroup tests | M |
|
||||||
|
| `tests/integration/pipeline-crud.test.ts` | Rename to competition-crud | M |
|
||||||
|
| `tests/integration/evaluation-flow.test.ts` | Round model, multi-round docs | M |
|
||||||
|
| New test files (8 files) | See [11-testing-and-qa.md](./11-testing-and-qa.md) | H |
|
||||||
|
|
||||||
|
### Infrastructure (4 files, ~11 hours)
|
||||||
|
|
||||||
|
| File | Changes | Risk |
|
||||||
|
|------|---------|------|
|
||||||
|
| `docker/docker-entrypoint.sh` | Migration flow update | L |
|
||||||
|
| Cron job configs | stageId→roundId in scheduled tasks | M |
|
||||||
|
| Webhook event definitions | Stage→Round event names | M |
|
||||||
|
| Email templates | Stage references in copy | L |
|
||||||
|
|
||||||
|
### Totals
|
||||||
|
|
||||||
|
| Category | Files | Est. Hours |
|
||||||
|
|----------|-------|------------|
|
||||||
|
| Database & Schema | 3 | 28 |
|
||||||
|
| tRPC Routers | 24 | 104 |
|
||||||
|
| Services | 11 | 50 |
|
||||||
|
| Types & Libraries | 6 | 32 |
|
||||||
|
| Admin UI | 35+ | 52 |
|
||||||
|
| Jury UI | 16 | 18 |
|
||||||
|
| Applicant UI | 8 | 12 |
|
||||||
|
| Tests | 13+ | 45 |
|
||||||
|
| Infrastructure | 4 | 11 |
|
||||||
|
| **Total** | **120+** | **~352** |
|
||||||
|
|
||||||
|
> **Note**: Conservative estimates for one developer. Parallel frontend/backend work reduces calendar time. Critical path: Schema → Services → Routers → UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: System Inventory — File-by-File Migration Map
|
||||||
|
|
||||||
|
Complete inventory of all files requiring changes, grouped by category with effort estimates.
|
||||||
|
|
||||||
|
### Prisma & Database (1 file, HIGH effort)
|
||||||
|
|
||||||
|
| File | Changes Required | Effort |
|
||||||
|
|------|-----------------|--------|
|
||||||
|
| `prisma/schema.prisma` | 40+ model/enum updates, 17+ FK renames (stageId→roundId), add new enums, add new fields | HIGH |
|
||||||
|
|
||||||
|
### tRPC Routers (24 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/server/routers/pipeline.ts` | **Rename** → `competition.ts` + all procedures | HIGH |
|
||||||
|
| `src/server/routers/stage.ts` | **Rename** → `round.ts` + all procedures | HIGH |
|
||||||
|
| `src/server/routers/track.ts` | **Remove** — logic absorbed into competition + specialAward | HIGH |
|
||||||
|
| `src/server/routers/evaluation.ts` | Update to use Round model, roundId params, JuryGroup queries | HIGH |
|
||||||
|
| `src/server/routers/assignment.ts` | Update to use JuryGroup model, roundId params | HIGH |
|
||||||
|
| `src/server/routers/file.ts` | Update `listByProjectForStage` → `listByProjectForRound`, multi-round grouping | MEDIUM |
|
||||||
|
| `src/server/routers/mentor.ts` | Add workspace features, file promotion procedures | MEDIUM |
|
||||||
|
| `src/server/routers/applicant.ts` | Update `getMyDashboard` (openStages→openRounds), pipeline view | MEDIUM |
|
||||||
|
| `src/server/routers/application.ts` | Update stage mode config → round-type-aware config | MEDIUM |
|
||||||
|
| `src/server/routers/live-control.ts` | Update stageId → roundId in cursor management | MEDIUM |
|
||||||
|
| `src/server/routers/award.ts` | Update to use competitionId, enhanced routing modes | MEDIUM |
|
||||||
|
| `src/server/routers/notification.ts` | Update event keys from stageId → roundId | LOW |
|
||||||
|
| `src/server/routers/analytics.ts` | Update stage references → round references | LOW |
|
||||||
|
| `src/server/routers/digest.ts` | Update stageId references | LOW |
|
||||||
|
| `src/server/routers/coi.ts` | Update stageId → roundId in COI declarations | LOW |
|
||||||
|
| `src/server/routers/audit.ts` | Update event type names (stage.* → round.*) | LOW |
|
||||||
|
| **New:** `src/server/routers/deliberation.ts` | New router for deliberation lifecycle | HIGH |
|
||||||
|
| **New:** `src/server/routers/result-lock.ts` | New router for result locking/unlocking | MEDIUM |
|
||||||
|
| **New:** `src/server/routers/jury-group.ts` | New router for jury group management | MEDIUM |
|
||||||
|
| **New:** `src/server/routers/submission-window.ts` | New router for submission window management | MEDIUM |
|
||||||
|
|
||||||
|
### Services (11 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/server/services/stage-engine.ts` | **Rename** → `round-engine.ts`, update all type references | HIGH |
|
||||||
|
| `src/server/services/stage-filtering.ts` | **Rename** → `round-filtering.ts`, update to RoundType.FILTERING | HIGH |
|
||||||
|
| `src/server/services/stage-assignment.ts` | **Rename** → `round-assignment.ts`, add JuryGroup-based assignment | HIGH |
|
||||||
|
| `src/server/services/stage-notifications.ts` | **Rename** → `round-notifications.ts`, update event keys | MEDIUM |
|
||||||
|
| `src/server/services/live-control.ts` | Update stageId → roundId in cursor management | MEDIUM |
|
||||||
|
| `src/server/services/ai-filtering.ts` | Update stage references → round references | LOW |
|
||||||
|
| `src/server/services/ai-assignment.ts` | Update to use JuryGroup model | MEDIUM |
|
||||||
|
| `src/server/services/ai-evaluation-summary.ts` | Update stage references | LOW |
|
||||||
|
| **New:** `src/server/services/deliberation-engine.ts` | Deliberation lifecycle, vote aggregation, Borda count | HIGH |
|
||||||
|
| **New:** `src/server/services/result-lock.ts` | Lock/unlock with audit trail | MEDIUM |
|
||||||
|
| **New:** `src/server/services/mentor-workspace.ts` | Messaging, file management, promotion | MEDIUM |
|
||||||
|
|
||||||
|
### Types & Libraries (6 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/types/pipeline-wizard.ts` | **Rewrite** → `competition-wizard.ts` with Competition/Round types | HIGH |
|
||||||
|
| `src/types/wizard-config.ts` | **Rewrite** → update all Stage→Round type references | HIGH |
|
||||||
|
| `src/lib/pipeline-defaults.ts` | **Rename** → `competition-defaults.ts`, update all defaults | MEDIUM |
|
||||||
|
| `src/lib/pipeline-validation.ts` | **Rename** → `competition-validation.ts` | MEDIUM |
|
||||||
|
| `src/lib/pipeline-conversions.ts` | **Rename** → `competition-conversions.ts` | MEDIUM |
|
||||||
|
| `src/lib/stage-config-schema.ts` | **Rename** → `round-config-schema.ts`, update to 7 typed Zod schemas | HIGH |
|
||||||
|
|
||||||
|
### Admin Pages (13+ files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/app/(admin)/pipelines/*` | **Rename** → `competitions/*`, update all router calls | HIGH |
|
||||||
|
| `src/app/(admin)/pipelines/[id]/stages/*` | **Rename** → `competitions/[id]/rounds/*` | HIGH |
|
||||||
|
| `src/app/(admin)/pipelines/[id]/tracks/*` | **Remove** — track UI eliminated | MEDIUM |
|
||||||
|
| **New:** `src/app/(admin)/competitions/[id]/juries/*` | New jury management section (3 pages) | HIGH |
|
||||||
|
| **New:** `src/app/(admin)/competitions/[id]/deliberation/*` | New deliberation management | HIGH |
|
||||||
|
| **New:** `src/app/(admin)/competitions/[id]/results/*` | New results & locks page | MEDIUM |
|
||||||
|
| **New:** `src/app/(admin)/competitions/[id]/submission-windows/*` | New submission window management | MEDIUM |
|
||||||
|
|
||||||
|
### Jury Pages (6 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/app/(jury)/stages/[stageId]/*` | **Rename** → `rounds/[roundId]/*` (6 routes) | HIGH |
|
||||||
|
| `src/app/(jury)/stages/[stageId]/evaluate/*` | Update to multi-round doc viewing, JuryGroup context | HIGH |
|
||||||
|
| `src/app/(jury)/stages/[stageId]/compare/*` | Update cross-round project comparison | MEDIUM |
|
||||||
|
| **New:** `src/app/(jury)/rounds/[roundId]/deliberation/*` | Juror deliberation voting interface | HIGH |
|
||||||
|
|
||||||
|
### Applicant Pages (8 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/app/(applicant)/pipeline/*` | **Rename** → `competition/*`, redesign progress visualization | HIGH |
|
||||||
|
| `src/app/(applicant)/pipeline/[stageId]/documents/*` | **Rename** → multi-round aware doc upload with read-only enforcement | HIGH |
|
||||||
|
| `src/app/(applicant)/pipeline/[stageId]/status/*` | **Rename** → round-based status with cross-round visibility | MEDIUM |
|
||||||
|
|
||||||
|
### Shared Components (24+ files)
|
||||||
|
|
||||||
|
| Component | Change | Effort |
|
||||||
|
|-----------|--------|--------|
|
||||||
|
| `StageTimeline` | **Rename** → `RoundTimeline`, update props from stageId to roundId | MEDIUM |
|
||||||
|
| `StageWindowBadge` | **Rename** → `RoundStatusBadge` | LOW |
|
||||||
|
| `RequirementUploadSlot` | Update `stageId` prop → `roundId` + `submissionWindowId` | LOW |
|
||||||
|
| `PipelineWizard` | **Rename** → `CompetitionWizard`, rewrite step flow | HIGH |
|
||||||
|
| `StageSidebar` | **Rename** → `RoundSidebar` | LOW |
|
||||||
|
| `StageConfigEditor` | **Rename** → `RoundConfigEditor`, update to typed Zod schemas | HIGH |
|
||||||
|
| `TrackSelector` | **Remove** | LOW |
|
||||||
|
| Breadcrumb components | Update Pipeline→Track→Stage to Competition→Round | LOW |
|
||||||
|
|
||||||
|
### Infrastructure & Config (5 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `prisma/seed.ts` | Update all Pipeline/Stage creation to Competition/Round (300+ lines) | HIGH |
|
||||||
|
| `docker/docker-entrypoint.sh` | Update migration flow for new tables | LOW |
|
||||||
|
| `tests/helpers.ts` | Add new factory functions for Competition/Round/JuryGroup/etc. | MEDIUM |
|
||||||
|
| `.env.example` | Add any new feature flag env vars | LOW |
|
||||||
|
| `CLAUDE.md` | Update architecture documentation | LOW |
|
||||||
|
|
||||||
|
### Background Jobs / Cron (3 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `src/server/cron/evaluation-reminders.ts` | Update stageId → roundId in deadline checks | LOW |
|
||||||
|
| `src/server/cron/digest-sender.ts` | Update stageId references in digest generation | LOW |
|
||||||
|
| `src/server/cron/ai-tagging.ts` | Update stageId → roundId in tagging jobs | LOW |
|
||||||
|
|
||||||
|
### Test Files (12 files)
|
||||||
|
|
||||||
|
| File | Change Type | Effort |
|
||||||
|
|------|------------|--------|
|
||||||
|
| `tests/unit/stage-engine.test.ts` | **Rename** → `round-engine.test.ts`, update all references | MEDIUM |
|
||||||
|
| `tests/unit/stage-filtering.test.ts` | **Rename** → `round-filtering.test.ts` | MEDIUM |
|
||||||
|
| `tests/unit/stage-assignment.test.ts` | **Rename**, add JuryGroup-based tests, cap mode tests | MEDIUM |
|
||||||
|
| `tests/integration/pipeline-crud.test.ts` | **Rename** → `competition-crud.test.ts` | MEDIUM |
|
||||||
|
| `tests/integration/evaluation-flow.test.ts` | Update to Round model, multi-round doc visibility | MEDIUM |
|
||||||
|
| **New:** `tests/unit/policy-resolution.test.ts` | 5-layer policy precedence tests | HIGH |
|
||||||
|
| **New:** `tests/unit/borda-count.test.ts` | Borda count aggregation tests | LOW |
|
||||||
|
| **New:** `tests/unit/deliberation-tally.test.ts` | Vote tallying for both modes | MEDIUM |
|
||||||
|
| **New:** `tests/unit/config-validation.test.ts` | All 7 Zod schema validation tests | MEDIUM |
|
||||||
|
| **New:** `tests/integration/deliberation-flow.test.ts` | Full deliberation lifecycle | HIGH |
|
||||||
|
| **New:** `tests/integration/mentor-workspace.test.ts` | Messaging, files, comments, promotion | MEDIUM |
|
||||||
|
| **New:** `tests/e2e/monaco-full-flow.test.ts` | Complete 8-round simulation | HIGH |
|
||||||
|
|
||||||
|
### Webhook & Event Mapping
|
||||||
|
|
||||||
|
| Old Event Type | New Event Type |
|
||||||
|
|----------------|----------------|
|
||||||
|
| `stage.transitioned` | `round.transitioned` |
|
||||||
|
| `stage.opened` | `round.opened` |
|
||||||
|
| `stage.closed` | `round.closed` |
|
||||||
|
| `pipeline.created` | `competition.created` |
|
||||||
|
| `pipeline.statusChanged` | `competition.statusChanged` |
|
||||||
|
| `stage.assignmentCompleted` | `round.assignmentCompleted` |
|
||||||
|
| `stage.evaluationCompleted` | `round.evaluationCompleted` |
|
||||||
|
| `stage.filteringCompleted` | `round.filteringCompleted` |
|
||||||
|
| *(new)* | `deliberation.sessionCreated` |
|
||||||
|
| *(new)* | `deliberation.voteSubmitted` |
|
||||||
|
| *(new)* | `deliberation.resultLocked` |
|
||||||
|
| *(new)* | `deliberation.resultUnlocked` |
|
||||||
|
| *(new)* | `mentoring.filePromoted` |
|
||||||
|
| *(new)* | `submission.windowOpened` |
|
||||||
|
| *(new)* | `submission.windowClosed` |
|
||||||
|
|
||||||
|
### Effort Summary
|
||||||
|
|
||||||
|
| Category | Files | New | Modified | Removed | Estimated Hours |
|
||||||
|
|----------|-------|-----|----------|---------|----------------|
|
||||||
|
| Prisma & DB | 1 | 0 | 1 | 0 | 16–24 |
|
||||||
|
| tRPC Routers | 24 | 4 | 16 | 1 | 40–60 |
|
||||||
|
| Services | 11 | 3 | 8 | 0 | 30–40 |
|
||||||
|
| Types & Libs | 6 | 0 | 6 | 0 | 16–24 |
|
||||||
|
| Admin Pages | 13+ | 4 | 9 | 1 | 30–40 |
|
||||||
|
| Jury Pages | 6+ | 1 | 5 | 0 | 20–30 |
|
||||||
|
| Applicant Pages | 8 | 0 | 8 | 0 | 16–24 |
|
||||||
|
| Components | 24+ | 0 | 20+ | 2+ | 20–30 |
|
||||||
|
| Infrastructure | 5 | 0 | 5 | 0 | 8–12 |
|
||||||
|
| Background Jobs | 3 | 0 | 3 | 0 | 4–6 |
|
||||||
|
| Tests | 12 | 7 | 5 | 0 | 30–40 |
|
||||||
|
| **Total** | **113+** | **19** | **86+** | **4+** | **230–330** |
|
||||||
303
docs/unified-architecture-redesign/11-testing-and-qa.md
Normal file
303
docs/unified-architecture-redesign/11-testing-and-qa.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# Testing & QA
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the test strategy for the redesigned competition system. It covers the test pyramid, specific test matrices for the Monaco competition flow and deliberation system, regression coverage, audit verification, and performance scenarios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Pyramid
|
||||||
|
|
||||||
|
```
|
||||||
|
╱╲
|
||||||
|
╱ E2E ╲ ~10 tests — Full Monaco flow simulation
|
||||||
|
╱────────╲
|
||||||
|
╱Integration╲ ~50 tests — Service + database interaction
|
||||||
|
╱──────────────╲
|
||||||
|
╱ Unit Tests ╲ ~200+ tests — Pure logic, no I/O
|
||||||
|
╱────────────────────╲
|
||||||
|
```
|
||||||
|
|
||||||
|
| Level | What | Tools | Speed |
|
||||||
|
|-------|------|-------|-------|
|
||||||
|
| **Unit** | Pure functions: policy resolution, cap calculation, Borda count, tie-breaking logic, config validation | Vitest | < 1s each |
|
||||||
|
| **Integration** | Service + Prisma: assignment algorithm with real DB, round transitions, deliberation vote aggregation | Vitest + test DB | < 5s each |
|
||||||
|
| **E2E** | Full flow: create competition → run all 8 rounds → lock results | Vitest + test DB + tRPC callers | < 30s each |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monaco Flow Test Matrix
|
||||||
|
|
||||||
|
End-to-end tests that simulate the complete Monaco 2026 competition:
|
||||||
|
|
||||||
|
### R1: Intake
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Submit project before deadline | Submission accepted, project created with category |
|
||||||
|
| Submit project after deadline (HARD policy) | Submission rejected |
|
||||||
|
| Submit project after deadline (FLAG policy) | Submission accepted, marked as Late |
|
||||||
|
| Submit with missing required doc | Validation error returned |
|
||||||
|
| Admin uploads doc for applicant | File associated, provenance recorded |
|
||||||
|
| Submission window not yet open | Submission rejected |
|
||||||
|
|
||||||
|
### R2: AI Filtering
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| AI marks project eligible | ProjectRoundState = ELIGIBLE |
|
||||||
|
| AI marks project ineligible | ProjectRoundState = INELIGIBLE, reason stored |
|
||||||
|
| AI marks project for manual review | ProjectRoundState = MANUAL_REVIEW |
|
||||||
|
| Admin overrides ineligible → eligible | Override recorded in audit log |
|
||||||
|
| Admin overrides eligible → ineligible | Override recorded in audit log |
|
||||||
|
| Filtering with no AI (deterministic rules only) | Rules-based filtering works without AI |
|
||||||
|
|
||||||
|
### R3: Jury 1 Evaluation
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Assignment respects hard cap | Judge gets ≤ maxProjects |
|
||||||
|
| Assignment distributes overflow to soft cap judges | Overflow distributed evenly, up to cap + buffer |
|
||||||
|
| Unassigned projects enter queue | Remaining projects have reason codes |
|
||||||
|
| COI declared → project skipped | COI judge doesn't get COI project |
|
||||||
|
| Judge submits score within window | Score recorded |
|
||||||
|
| Judge submits score outside window | Rejected |
|
||||||
|
| AI shortlist generated at round end | Ranked recommendations exist per category |
|
||||||
|
| Admin overrides shortlist selection | Override recorded, selected projects advance |
|
||||||
|
|
||||||
|
### R4: Semifinal Submission
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Applicant uploads R2 docs | Files accepted for correct slots |
|
||||||
|
| Applicant tries to edit R1 docs | Rejected (read-only) |
|
||||||
|
| Admin replaces R1 doc | Replacement accepted with provenance |
|
||||||
|
| Applicant downloads R1 doc | Download succeeds |
|
||||||
|
|
||||||
|
### R5: Jury 2 Evaluation + Special Awards
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Judge sees R1 + R2 docs clearly separated | Both sets of docs returned with round labels |
|
||||||
|
| Award Mode A: projects filtered into award pool | Eligible projects appear in award pool |
|
||||||
|
| Award Mode A: admin confirms pull-out | Pull-out confirmed, project moved |
|
||||||
|
| Award Mode B: projects flagged but stay in main | Project in both main and award evaluation |
|
||||||
|
| Award single-judge decision | Single judge can select winner |
|
||||||
|
| Top N finalists selected (N configurable) | Exactly N projects advance per category |
|
||||||
|
|
||||||
|
### R6: Mentoring
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Mentor sends message to team | Message delivered and visible |
|
||||||
|
| Team uploads file to workspace | File visible to mentor + team + admin |
|
||||||
|
| Mentor comments on file | Comment created with thread support |
|
||||||
|
| File promoted to official submission | SubmissionPromotionEvent created with provenance |
|
||||||
|
| Non-mentored finalist has no workspace | No workspace features visible |
|
||||||
|
|
||||||
|
### R7: Live Finals
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Admin advances project cursor | Current project updates for all Jury 3 members |
|
||||||
|
| Jury 3 member submits live score | Score recorded within voting window |
|
||||||
|
| Audience vote submitted | Vote counted |
|
||||||
|
| Audience vote outside window | Rejected |
|
||||||
|
| Jury 3 sees prior jury data (when enabled) | Prior jury scores/feedback visible |
|
||||||
|
| Jury 3 doesn't see prior jury data (when disabled) | Prior jury data hidden |
|
||||||
|
|
||||||
|
### R8: Deliberation
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| SINGLE_WINNER_VOTE: juror picks winner | Vote recorded |
|
||||||
|
| SINGLE_WINNER_VOTE: tally produces ranking | Most votes = rank 1, others by vote count |
|
||||||
|
| FULL_RANKING: juror submits ordinal ranks | All ranks recorded |
|
||||||
|
| FULL_RANKING: Borda count aggregation | Correct Borda scores computed |
|
||||||
|
| Tie detected → runoff initiated | Session status = RUNOFF, new vote round |
|
||||||
|
| Tie detected → admin breaks tie | Admin decision recorded |
|
||||||
|
| Admin overrides entire result | Override result stored, flag set |
|
||||||
|
| Admin locks result | ResultLock created with snapshot |
|
||||||
|
| Super-admin unlocks result | ResultUnlockEvent created with reason |
|
||||||
|
| Non-super-admin tries to unlock | Rejected |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliberation-Specific Test Matrix
|
||||||
|
|
||||||
|
Detailed tests for the deliberation subsystem:
|
||||||
|
|
||||||
|
### SINGLE_WINNER_VOTE Mode
|
||||||
|
|
||||||
|
| Scenario | Expected Outcome |
|
||||||
|
|----------|-----------------|
|
||||||
|
| 3 jurors, unanimous pick | Winner = picked project, rank 1 |
|
||||||
|
| 3 jurors, split vote (2-1) | Winner = project with 2 votes |
|
||||||
|
| 3 jurors, three-way tie (1-1-1) | Tie-break method triggered |
|
||||||
|
| Juror submits vote, then changes | Latest vote overwrites (within window) |
|
||||||
|
| Voting window closes | No more votes accepted |
|
||||||
|
|
||||||
|
### FULL_RANKING Mode
|
||||||
|
|
||||||
|
| Scenario | Expected Outcome |
|
||||||
|
|----------|-----------------|
|
||||||
|
| 3 jurors rank 5 projects | Borda count: 1st=5pts, 2nd=4pts, ... 5th=1pt |
|
||||||
|
| Clear winner (highest Borda) | Rank 1 = highest Borda score |
|
||||||
|
| Two projects tied on Borda | Tie-break method triggered |
|
||||||
|
| Juror ranks only top 3 (partial) | Unranked projects get 0 points from this juror |
|
||||||
|
|
||||||
|
### Tie-Breaking
|
||||||
|
|
||||||
|
| Method | Test Case | Expected |
|
||||||
|
|--------|-----------|----------|
|
||||||
|
| RUNOFF | Two projects tied → runoff vote | New voting round with only tied projects |
|
||||||
|
| RUNOFF | Runoff still tied → second runoff | Session supports multiple runoff rounds |
|
||||||
|
| ADMIN_DECIDES | Tie detected → admin picks winner | Admin decision recorded, tie resolved |
|
||||||
|
| SCORE_FALLBACK | Tie → fall back to Jury 3 scores | Higher Jury 3 score wins |
|
||||||
|
|
||||||
|
### Participant Management
|
||||||
|
|
||||||
|
| Scenario | Expected |
|
||||||
|
|----------|----------|
|
||||||
|
| Juror marked ABSENT_EXCUSED | Doesn't count toward quorum |
|
||||||
|
| Juror REPLACED | Replacement can vote, original cannot |
|
||||||
|
| Quorum not met | Session cannot proceed to VOTING |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invite/Onboarding Test Matrix
|
||||||
|
|
||||||
|
| Test Case | Validates |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Admin invites judge to Jury 1 | JuryGroupMember created |
|
||||||
|
| Judge accepts invite and onboards | Member status active, self-service values set |
|
||||||
|
| Judge adjusts cap during onboarding | selfServiceCap updated |
|
||||||
|
| Judge adjusts ratio during onboarding | selfServiceBias updated |
|
||||||
|
| Self-service disabled → fields hidden | No self-service options shown |
|
||||||
|
| Admin overrides self-service values | Override takes precedence |
|
||||||
|
| Invite with pre-assignment intent | AssignmentIntent record created |
|
||||||
|
| Invite with pre-assignment + COI conflict | Intent stays PENDING, reason `INTENT_BLOCKED` |
|
||||||
|
| Judge removed from group with pending intent | Intent status → CANCELLED |
|
||||||
|
| Round completes with unmatched intent | Intent status → EXPIRED |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Config Schema Validation Test Matrix
|
||||||
|
|
||||||
|
All 7 round-type Zod schemas must validate both correct and malformed input:
|
||||||
|
|
||||||
|
| Schema | Valid Input Test | Invalid Input Test |
|
||||||
|
|--------|-----------------|-------------------|
|
||||||
|
| `IntakeConfigSchema` | Valid with `deadlinePolicy: "FLAG"`, `maxFileSizeMB: 50` | Rejects negative `maxFileSizeMB`, unknown `deadlinePolicy` |
|
||||||
|
| `FilteringConfigSchema` | Valid with `aiEnabled: true`, `autoAdvanceEligible: false` | Rejects `autoAdvanceEligible` without `aiEnabled` set |
|
||||||
|
| `EvaluationConfigSchema` | Valid with rubric, `requireFeedback: true`, `feedbackMinLength: 50` | Rejects `feedbackMinLength < 0`, missing rubric |
|
||||||
|
| `SubmissionConfigSchema` | Valid with doc slot requirements, `deadlinePolicy: "HARD"` | Rejects empty `requiredDocSlots`, invalid MIME types |
|
||||||
|
| `MentoringConfigSchema` | Valid with `eligibility: "all_advancing"`, `filePromotionEnabled: true` | Rejects unknown `eligibility` value |
|
||||||
|
| `LiveFinalConfigSchema` | Valid with `audienceVoteWeight: 0.3`, `presentationDurationMinutes: 15` | Rejects `audienceVoteWeight > 1`, negative duration |
|
||||||
|
| `DeliberationConfigSchema` | Valid with `mode: "FULL_RANKING"`, `topN: 3`, `tieBreakMethod: "RUNOFF"` | Rejects `topN < 1`, unknown `mode` |
|
||||||
|
|
||||||
|
**Edge cases (all schemas):**
|
||||||
|
|
||||||
|
| Test Case | Expected |
|
||||||
|
|-----------|----------|
|
||||||
|
| Empty object | Defaults applied, valid |
|
||||||
|
| Extra unknown fields | Stripped (Zod `.strip()`) |
|
||||||
|
| `null` config | Rejected — config is required |
|
||||||
|
| Partial config (some fields) | Missing fields get defaults |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assignment Intent Lifecycle Tests
|
||||||
|
|
||||||
|
| Test Case | Expected |
|
||||||
|
|-----------|----------|
|
||||||
|
| Create intent at invite time | `AssignmentIntent` created with status `PENDING`, source `INVITE` |
|
||||||
|
| Algorithm runs, intent matchable | Intent transitions to `HONORED`, `Assignment` record created |
|
||||||
|
| Algorithm runs, intent blocked by COI | Intent stays `PENDING`, unassigned queue entry with `INTENT_BLOCKED` |
|
||||||
|
| Algorithm runs, intent blocked by cap | Intent stays `PENDING`, unassigned queue entry with `INTENT_BLOCKED` |
|
||||||
|
| Admin reassigns project to different judge | Intent transitions to `OVERRIDDEN`, audit logged |
|
||||||
|
| Judge removed from JuryGroup | Intent transitions to `CANCELLED` |
|
||||||
|
| Round closes with pending intent | Intent transitions to `EXPIRED` (batch update) |
|
||||||
|
| Admin explicitly cancels intent | Intent transitions to `CANCELLED` |
|
||||||
|
| Intent already `HONORED` → no further transitions | Terminal state enforced |
|
||||||
|
| Intent already `EXPIRED` → no further transitions | Terminal state enforced |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Regression Coverage
|
||||||
|
|
||||||
|
Existing test files that need updates for the redesign:
|
||||||
|
|
||||||
|
| Test File | Required Updates |
|
||||||
|
|-----------|-----------------|
|
||||||
|
| `tests/unit/stage-engine.test.ts` | Rename to round-engine, update all type references |
|
||||||
|
| `tests/unit/stage-filtering.test.ts` | Update to use RoundType.FILTERING, Competition model |
|
||||||
|
| `tests/unit/stage-assignment.test.ts` | Add JuryGroup-based assignment tests, cap mode tests |
|
||||||
|
| `tests/integration/pipeline-crud.test.ts` | Rename to competition-crud, test Competition/Round CRUD |
|
||||||
|
| `tests/integration/evaluation-flow.test.ts` | Update to use Round model, multi-round doc visibility |
|
||||||
|
|
||||||
|
New test files to create:
|
||||||
|
|
||||||
|
| Test File | Coverage |
|
||||||
|
|-----------|----------|
|
||||||
|
| `tests/unit/policy-resolution.test.ts` | 5-layer policy precedence |
|
||||||
|
| `tests/unit/borda-count.test.ts` | Borda count aggregation |
|
||||||
|
| `tests/unit/deliberation-tally.test.ts` | Vote tallying for both modes |
|
||||||
|
| `tests/unit/config-validation.test.ts` | All 7 Zod schema validations |
|
||||||
|
| `tests/integration/deliberation-flow.test.ts` | Full deliberation lifecycle |
|
||||||
|
| `tests/integration/mentor-workspace.test.ts` | Messaging, files, comments, promotion |
|
||||||
|
| `tests/integration/submission-windows.test.ts` | Multi-round doc lifecycle |
|
||||||
|
| `tests/e2e/monaco-full-flow.test.ts` | Complete 8-round simulation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Completeness Checks
|
||||||
|
|
||||||
|
Every critical operation must emit an audit record to `DecisionAuditLog`:
|
||||||
|
|
||||||
|
| Operation | Must Audit |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Admin overrides eligibility | Yes — who, when, project, old→new status |
|
||||||
|
| Admin overrides assignment | Yes — who, when, judge, project, reason |
|
||||||
|
| Admin overrides shortlist selection | Yes — who, when, selected projects |
|
||||||
|
| Admin overrides deliberation result | Yes — who, when, reason, original→override |
|
||||||
|
| Result lock | Yes — who, when, snapshot |
|
||||||
|
| Result unlock | Yes — who, when, reason (super-admin only) |
|
||||||
|
| File promotion | Yes — who, when, source file, target slot |
|
||||||
|
| Award pull-out confirmation | Yes — who, when, projects pulled |
|
||||||
|
| Judge cap/ratio override | Yes — who, when, old→new values |
|
||||||
|
|
||||||
|
Test: For each operation above, verify an audit record exists with all required fields.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance & Capacity Scenarios
|
||||||
|
|
||||||
|
| Scenario | Target | Test Method |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| Bulk intake: 500 projects submitted in 1 hour | All submissions processed, no timeouts | Load test with concurrent submissions |
|
||||||
|
| Assignment: 500 projects across 30 judges | Assignment completes < 30s | Integration test with timing |
|
||||||
|
| Live voting: 50 audience members voting simultaneously | All votes recorded, no conflicts | Concurrent request test |
|
||||||
|
| Deliberation: 15 jurors submitting rankings | Aggregation completes < 5s | Integration test with timing |
|
||||||
|
| Multi-round doc query: judge views 3 rounds of docs | Response < 2s | Query performance test |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Data Factories
|
||||||
|
|
||||||
|
All tests use factory functions (see `tests/helpers.ts`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// New factories needed
|
||||||
|
createTestCompetition(overrides?)
|
||||||
|
createTestRound(competitionId, type, overrides?)
|
||||||
|
createTestJuryGroup(competitionId, overrides?)
|
||||||
|
createTestJuryGroupMember(juryGroupId, userId, overrides?)
|
||||||
|
createTestSubmissionWindow(competitionId, roundId, overrides?)
|
||||||
|
createTestDeliberationSession(competitionId, roundId, overrides?)
|
||||||
|
createTestDeliberationVote(sessionId, juryMemberId, overrides?)
|
||||||
|
createTestSpecialAward(competitionId, overrides?)
|
||||||
|
createTestMentorFile(projectId, mentorId, overrides?)
|
||||||
|
```
|
||||||
|
|
||||||
|
See [09-implementation-roadmap.md](./09-implementation-roadmap.md) Phase 0 for factory creation timeline.
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
# Observability & Release Gates
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the metrics, logging, alerting, and release gate criteria for the redesigned competition system. Every phase of the implementation must pass its corresponding release gate before proceeding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Competition Lifecycle Metrics
|
||||||
|
|
||||||
|
### Submission Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `projects_submitted_total` | Counter | Total projects submitted, by category and round |
|
||||||
|
| `projects_submitted_late` | Counter | Projects submitted after deadline (FLAG policy) |
|
||||||
|
| `submission_window_utilization` | Gauge | % of deadline elapsed vs submissions received |
|
||||||
|
| `file_upload_duration_seconds` | Histogram | Time to upload a submission file |
|
||||||
|
| `file_upload_size_bytes` | Histogram | Size distribution of uploaded files |
|
||||||
|
|
||||||
|
### Filtering Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `eligibility_pass_rate` | Gauge | % of projects passing AI filtering |
|
||||||
|
| `eligibility_manual_review_count` | Counter | Projects flagged for manual review |
|
||||||
|
| `eligibility_override_count` | Counter | Admin overrides of AI decisions |
|
||||||
|
| `ai_filtering_duration_seconds` | Histogram | Time for AI to process one project |
|
||||||
|
|
||||||
|
### Assignment Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `assignment_coverage_percent` | Gauge | % of projects assigned to at least one judge |
|
||||||
|
| `assignment_unassigned_queue_size` | Gauge | Projects in unassigned queue |
|
||||||
|
| `assignment_cap_utilization` | Gauge | Average % of cap used per judge |
|
||||||
|
| `assignment_exception_count` | Counter | Over-cap manual assignments |
|
||||||
|
| `assignment_coi_skip_count` | Counter | Assignments skipped due to COI |
|
||||||
|
| `assignment_duration_seconds` | Histogram | Time to run assignment algorithm |
|
||||||
|
|
||||||
|
### Evaluation Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `evaluations_completed_total` | Counter | Scores submitted, by jury group |
|
||||||
|
| `evaluation_completion_rate` | Gauge | % of assigned evaluations completed |
|
||||||
|
| `ai_shortlist_generated` | Counter | AI shortlists generated per round |
|
||||||
|
| `admin_shortlist_override_count` | Counter | Admin overrides of AI shortlist |
|
||||||
|
|
||||||
|
### Deliberation Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `deliberation_session_count` | Counter | Sessions created, by mode |
|
||||||
|
| `deliberation_duration_seconds` | Histogram | Time from VOTING to LOCKED |
|
||||||
|
| `deliberation_runoff_count` | Counter | Runoff rounds triggered |
|
||||||
|
| `deliberation_admin_override_count` | Counter | Admin overrides of deliberation result |
|
||||||
|
| `result_lock_count` | Counter | Results locked |
|
||||||
|
| `result_unlock_count` | Counter | Results unlocked (should be rare) |
|
||||||
|
|
||||||
|
### Live Finals Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `live_voting_concurrent_users` | Gauge | Concurrent jury + audience users during voting |
|
||||||
|
| `live_vote_submission_duration_ms` | Histogram | Time to submit a live vote |
|
||||||
|
| `audience_vote_total` | Counter | Total audience votes submitted |
|
||||||
|
| `stage_manager_cursor_advances` | Counter | Project cursor advances by admin |
|
||||||
|
|
||||||
|
### Mentoring Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `mentor_messages_sent` | Counter | Messages sent in mentor workspaces |
|
||||||
|
| `mentor_files_uploaded` | Counter | Files uploaded to mentor workspaces |
|
||||||
|
| `mentor_file_promotions` | Counter | Files promoted to official submissions |
|
||||||
|
| `mentor_assignment_coverage` | Gauge | % of mentoring-requesting teams with assigned mentors |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reliability Metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `api_request_duration_seconds` | Histogram | tRPC procedure latency, by router and procedure |
|
||||||
|
| `api_error_rate` | Gauge | % of requests returning errors, by router |
|
||||||
|
| `job_success_total` | Counter | Background job completions |
|
||||||
|
| `job_failure_total` | Counter | Background job failures |
|
||||||
|
| `email_send_total` | Counter | Emails sent (reminders, invitations, notifications) |
|
||||||
|
| `email_send_failure_total` | Counter | Failed email sends |
|
||||||
|
| `db_query_duration_seconds` | Histogram | Prisma query latency |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Event Metrics
|
||||||
|
|
||||||
|
All admin override actions emit `DecisionAuditLog` records. These metrics track audit completeness and quality:
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `audit_events_total` | Counter | Total audit log entries, by `actionType` |
|
||||||
|
| `audit_events_with_diff` | Counter | Audit entries that include both `beforeState` and `afterState` in `detailsJson` |
|
||||||
|
| `audit_coverage_percent` | Gauge | % of override actions (eligibility, assignment, shortlist, deliberation, lock/unlock, promotion, cap change) that have a corresponding audit record |
|
||||||
|
| `audit_missing_diff_count` | Counter | Override actions where `detailsJson` is missing before/after state (should be 0) |
|
||||||
|
| `audit_correlation_coverage` | Gauge | % of audit entries that include full correlation context (`competitionId`, `roundId`, `projectId`) |
|
||||||
|
|
||||||
|
### Audit Completeness Validation
|
||||||
|
|
||||||
|
During each phase gate review, run an automated check that verifies:
|
||||||
|
|
||||||
|
1. **Every admin override has an audit record** — query `DecisionAuditLog` for each override type and verify count matches the operation count
|
||||||
|
2. **Before/after state present** — for all OVERRIDE-type actions, `detailsJson` must contain `{ before: {...}, after: {...} }` structure
|
||||||
|
3. **Correlation completeness** — every audit entry references at minimum `userId`, `competitionId`, and `timestamp`
|
||||||
|
4. **No orphaned operations** — cross-reference assignment exceptions, result unlocks, eligibility overrides, and file promotions against audit log
|
||||||
|
|
||||||
|
### Audit Event Correlation
|
||||||
|
|
||||||
|
For tracing admin actions across the system, audit log entries should include these correlation fields when available:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type AuditCorrelation = {
|
||||||
|
competitionId: string
|
||||||
|
roundId?: string
|
||||||
|
roundType?: RoundType
|
||||||
|
juryGroupId?: string
|
||||||
|
projectId?: string
|
||||||
|
sessionId?: string // deliberation session
|
||||||
|
assignmentIntentId?: string // when overriding an intent
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables queries like: "Show all admin overrides for Competition X, Round 3, sorted by time" or "Show all actions taken on Project Y across all rounds."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structured Logging
|
||||||
|
|
||||||
|
All log entries must include correlation IDs for tracing:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type LogContext = {
|
||||||
|
// Always present
|
||||||
|
requestId: string
|
||||||
|
userId?: string
|
||||||
|
userRole?: string
|
||||||
|
|
||||||
|
// Competition context (when available)
|
||||||
|
programId?: string
|
||||||
|
competitionId?: string
|
||||||
|
roundId?: string
|
||||||
|
roundType?: RoundType
|
||||||
|
|
||||||
|
// Entity context (when relevant)
|
||||||
|
juryGroupId?: string
|
||||||
|
projectId?: string
|
||||||
|
awardId?: string
|
||||||
|
sessionId?: string // deliberation session
|
||||||
|
|
||||||
|
// Operation context
|
||||||
|
operation: string // e.g., "assignment.run", "deliberation.submitVote"
|
||||||
|
result: 'success' | 'failure' | 'skipped'
|
||||||
|
details?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Log Events
|
||||||
|
|
||||||
|
| Event | Level | When |
|
||||||
|
|-------|-------|------|
|
||||||
|
| `competition.created` | INFO | New competition created |
|
||||||
|
| `round.transitioned` | INFO | Round status changed |
|
||||||
|
| `assignment.completed` | INFO | Assignment algorithm finished |
|
||||||
|
| `assignment.unassigned` | WARN | Projects remain unassigned |
|
||||||
|
| `assignment.exception` | WARN | Over-cap assignment made |
|
||||||
|
| `evaluation.submitted` | INFO | Judge submitted a score |
|
||||||
|
| `deliberation.voteSubmitted` | INFO | Juror submitted deliberation vote |
|
||||||
|
| `deliberation.tieDetected` | WARN | Tie detected in deliberation |
|
||||||
|
| `deliberation.adminOverride` | WARN | Admin overrode deliberation result |
|
||||||
|
| `resultLock.locked` | INFO | Result locked |
|
||||||
|
| `resultLock.unlocked` | WARN | Result unlocked (should be rare) |
|
||||||
|
| `eligibility.override` | WARN | Admin overrode AI eligibility decision |
|
||||||
|
| `file.promoted` | INFO | Mentor file promoted to submission |
|
||||||
|
| `liveVoting.windowClosed` | INFO | Live voting window closed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alert Definitions
|
||||||
|
|
||||||
|
| Alert | Condition | Severity | Action |
|
||||||
|
|-------|-----------|----------|--------|
|
||||||
|
| **Unresolved manual queue** | `assignment_unassigned_queue_size > 0` for > 24h | WARNING | Notify admin to manually assign |
|
||||||
|
| **Quorum mismatch** | Deliberation participants < required quorum | CRITICAL | Block voting, notify admin |
|
||||||
|
| **Result lock failure** | ResultLock creation fails | CRITICAL | Retry, notify super-admin |
|
||||||
|
| **Unlock by non-super-admin** | ResultUnlock attempted by non-super-admin | CRITICAL | Block, log security event |
|
||||||
|
| **AI filtering timeout** | `ai_filtering_duration_seconds > 60` | WARNING | Check OpenAI API, retry |
|
||||||
|
| **Evaluation completion low** | `evaluation_completion_rate < 50%` with < 24h remaining | WARNING | Send reminder emails |
|
||||||
|
| **Live voting overload** | `live_voting_concurrent_users > 200` | WARNING | Monitor for performance degradation |
|
||||||
|
| **Email delivery failure spike** | `email_send_failure_total` increases > 10 in 1h | WARNING | Check SMTP connection |
|
||||||
|
| **API error rate spike** | `api_error_rate > 5%` for any router | CRITICAL | Investigate, potential rollback |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release Gates
|
||||||
|
|
||||||
|
Each release gate must be passed before the corresponding implementation phase can proceed to production.
|
||||||
|
|
||||||
|
### Gate A: Schema + Backfill Readiness
|
||||||
|
|
||||||
|
**When**: Before Phase 1 code reaches production
|
||||||
|
|
||||||
|
**Criteria**:
|
||||||
|
- [ ] All Prisma migrations apply cleanly on staging database
|
||||||
|
- [ ] Backfill script runs successfully on staging data
|
||||||
|
- [ ] No existing table modified (new tables only)
|
||||||
|
- [ ] Rollback tested: drop new tables, verify existing system works
|
||||||
|
- [ ] Migration takes < 5 minutes on production-sized dataset
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Gate B: Policy Engine Correctness
|
||||||
|
|
||||||
|
**When**: Before Phase 2 code reaches production
|
||||||
|
|
||||||
|
**Criteria**:
|
||||||
|
- [ ] 5-layer policy precedence returns correct values for all combinations
|
||||||
|
- [ ] Cap mode behavior correct: HARD stops at cap, SOFT allows buffer, NONE unlimited
|
||||||
|
- [ ] Category bias applied correctly in assignment
|
||||||
|
- [ ] COI check prevents assignment to declared conflicts
|
||||||
|
- [ ] Edge cases: null overrides, conflicting settings, zero cap
|
||||||
|
- [ ] All policy unit tests pass (100% coverage on resolution logic)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Gate C: End-to-End Monaco Flow Simulation
|
||||||
|
|
||||||
|
**When**: Before Phase 5 code reaches production
|
||||||
|
|
||||||
|
**Criteria**:
|
||||||
|
- [ ] Full 8-round flow completes on staging with realistic data
|
||||||
|
- [ ] All round transitions work correctly
|
||||||
|
- [ ] Multi-round document visibility correct for all roles
|
||||||
|
- [ ] AI shortlist generated at end of evaluation rounds
|
||||||
|
- [ ] Assignment algorithm handles 500+ projects across 30 judges
|
||||||
|
- [ ] Result: finalists selected, no data inconsistencies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Gate D: Invite/Onboarding Stability Under Load
|
||||||
|
|
||||||
|
**When**: Before Phase 3 code reaches production
|
||||||
|
|
||||||
|
**Criteria**:
|
||||||
|
- [ ] 50 concurrent invite acceptances process correctly
|
||||||
|
- [ ] JuryGroupMember records created for all accepted invites
|
||||||
|
- [ ] Onboarding self-service values saved correctly
|
||||||
|
- [ ] AssignmentIntent records created for pre-assigned judges
|
||||||
|
- [ ] No race conditions in concurrent onboarding
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Gate E: Live Finals + Deliberation Integrity
|
||||||
|
|
||||||
|
**When**: Before Phase 6 code reaches production
|
||||||
|
|
||||||
|
**Criteria**:
|
||||||
|
- [ ] Stage manager cursor advances correctly for all Jury 3 members
|
||||||
|
- [ ] Live voting window enforcement (no votes outside window)
|
||||||
|
- [ ] 50 concurrent audience votes processed without conflicts
|
||||||
|
- [ ] Deliberation SINGLE_WINNER_VOTE tallying correct
|
||||||
|
- [ ] Deliberation FULL_RANKING Borda count correct
|
||||||
|
- [ ] Tie-breaking: RUNOFF creates new vote round
|
||||||
|
- [ ] Tie-breaking: ADMIN_DECIDES records decision
|
||||||
|
- [ ] ResultLock creates immutable snapshot
|
||||||
|
- [ ] ResultUnlock blocked for non-super-admin
|
||||||
|
- [ ] Audience vote totals correctly shown to Jury 3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Gate F: Operational Readiness
|
||||||
|
|
||||||
|
**When**: Before Phase 8 (cutover) begins
|
||||||
|
|
||||||
|
**Criteria**:
|
||||||
|
- [ ] All metrics listed above are being collected and visible in dashboard
|
||||||
|
- [ ] All alerts listed above are configured and tested
|
||||||
|
- [ ] Structured logging includes all correlation IDs
|
||||||
|
- [ ] Runbook exists for common operational scenarios
|
||||||
|
- [ ] On-call team briefed on new system components
|
||||||
|
- [ ] Backup and restore tested for new tables
|
||||||
|
- [ ] Performance baseline established (API latency, DB query times)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit Event Metrics
|
||||||
|
|
||||||
|
Track audit completeness and integrity across the redesigned system:
|
||||||
|
|
||||||
|
| Metric | Target | Alert Threshold |
|
||||||
|
|--------|--------|-----------------|
|
||||||
|
| Audit coverage (admin overrides with audit record) | 100% | Any override without audit record |
|
||||||
|
| Before/after state completeness | 100% of override actions | Missing `before` or `after` in details JSON |
|
||||||
|
| Assignment intent fulfillment rate | > 80% | < 50% intents HONORED per round |
|
||||||
|
| Result lock integrity | 0 unauthorized unlocks | Any unlock without super-admin role |
|
||||||
|
| File replacement provenance chain | 100% linked | Any replacement without `replacedById` |
|
||||||
|
|
||||||
|
**Audit Correlation Tracking:**
|
||||||
|
- Every admin override must include `correlationId` linking the audit entry to the triggering action (e.g., which assignment algorithm run triggered an intent override)
|
||||||
|
- Round transition events must correlate with the `DecisionAuditLog` entries for that round
|
||||||
|
- Result lock/unlock events must reference the `DeliberationSession.id` they apply to
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sign-Off Criteria
|
||||||
|
|
||||||
|
Final sign-off requires:
|
||||||
|
|
||||||
|
1. **All 6 release gates passed** (A through F)
|
||||||
|
2. **72-hour burn-in period** with zero critical errors
|
||||||
|
3. **All test matrices passed** (see [11-testing-and-qa.md](./11-testing-and-qa.md))
|
||||||
|
4. **Documentation updated** (CLAUDE.md, API docs, admin guide)
|
||||||
|
5. **Architecture owner sign-off** on schema finality
|
||||||
|
6. **Product owner sign-off** on feature completeness
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
# Open Questions & Governance
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document tracks all design decisions — resolved and remaining — and defines the governance process for the redesign. Resolved decisions are numbered for reference. Remaining questions are prioritized (P1 = must resolve before implementation, P2 = can resolve during implementation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolved Decisions
|
||||||
|
|
||||||
|
| # | Decision | Resolution | Resolved By |
|
||||||
|
|---|----------|------------|-------------|
|
||||||
|
| 1 | **Winner count per category** | Top N (configurable, default 3). All projects ranked within category. Podium UI for top 3. Cross-category comparison view. | User Q&A |
|
||||||
|
| 2 | **Score model for final winners** | Jury 3 live scores only for final winner determination. All-jury composite rankings available in reports/analytics. Configurable between "Jury 3 only" and "Jury 3 + audience blend" per competition. | User Q&A |
|
||||||
|
| 3 | **Cross-jury visibility** | Fully independent during active evaluation — no cross-jury score visibility. During Live Finals (R7) and Deliberation (R8), prior jury scores/feedback/notes visible to Jury 3 **if admin enables** (`showPriorJuryData` toggle). All cross-jury data available in reports section for internal analytics. | User Q&A |
|
||||||
|
| 4 | **Confirmation model** | Deliberation IS the confirmation — no separate WinnerProposal → jury sign-off → admin approval. Deliberation voting serves as jury agreement. Replaces "all jury agree + admin approval" from original flow spec. | User Q&A |
|
||||||
|
| 5 | **Deliberation modes** | Two admin-configurable modes: SINGLE_WINNER_VOTE (each juror picks one winner) and FULL_RANKING (ordinal 1st, 2nd, 3rd... aggregated via Borda count). | User Q&A |
|
||||||
|
| 6 | **Deliberation scope** | Separate per category (Startup and Concept deliberations are independent sessions). No deliberation for special awards — awards decided by their own jury/judge mechanism. | User Q&A |
|
||||||
|
| 7 | **Tie-breaking** | Multiple methods supported: runoff vote (new vote with tied projects only), admin tie-break (admin decides between tied), admin override (override entire result). All configurable per deliberation session. | User Q&A |
|
||||||
|
| 8 | **Post-deliberation flow** | Admin reviews final deliberation result → locks → ResultLock snapshot created. Unlock requires super-admin with mandatory reason. | User Q&A |
|
||||||
|
| 9 | **Soft-cap buffer** | Default +10 over the soft cap. Configurable per JuryGroup via `softCapBuffer` field. | User Q&A |
|
||||||
|
| 10 | **Startup/concept ratio** | Suggestive bias only (not deterministic). Disclosed to judges. Judges see a note like "You have been assigned primarily Startup projects based on your preference." | User Q&A |
|
||||||
|
| 11 | **Mode A pull-out** | Admin-confirmed (`routingConfirmationMode: ADMIN_CONFIRMED`). Admin must review and approve which projects are pulled from the main pool into the award pool. | User Q&A |
|
||||||
|
| 12 | **File promotion authority** | Team lead and admin can promote mentor workspace files to official submissions. Mentor can promote IF admin enables it per competition. | User Q&A |
|
||||||
|
| 13 | **Invite pre-assignment** | Both modes supported: AssignmentIntent (intent at invite time, honored by algorithm) and direct admin assignment. | User Q&A |
|
||||||
|
| 14 | **Jury absence handling** | Quorum fallback with participant status types: REQUIRED, ABSENT_EXCUSED, REPLACED, REPLACEMENT_ACTIVE. Absent-excused don't count toward quorum. Admin can replace juror or mark as excused. | User Q&A |
|
||||||
|
| 15 | **Result unlock** | Super-admin only, mandatory reason. Creates ResultUnlockEvent with audit trail. | User Q&A |
|
||||||
|
| 16 | **Jury naming** | Custom labels per program. JuryGroup has a `label` field (e.g., "Jury 1", "Selection Panel", "Live Finals Jury"). | User Q&A |
|
||||||
|
| 17 | **Judge onboarding self-service** | Judges CAN adjust their cap and category ratio preference during onboarding. Admin-configurable toggle (`allowOnboardingSelfService`) to enable/disable per JuryGroup. | Flow spec + Q&A |
|
||||||
|
| 18 | **AI ranked shortlist** | AI generates recommended ranked shortlist per category at the end of EVERY evaluation round (Jury 1, Jury 2, and any award evaluation). Admin can always override. | Flow spec + Q&A |
|
||||||
|
| 19 | **Audience vote totals** | Shown to Jury 3 during deliberation phase. Configurable reveal timing (real-time, after jury vote, at deliberation). | Flow spec + Q&A |
|
||||||
|
| 20 | **Assignment intent lifecycle** | Full lifecycle tracking: PENDING → HONORED (algorithm matched) / OVERRIDDEN (admin changed) / EXPIRED (round closed) / CANCELLED (removed). All terminal states immutable. See [04-jury-groups-and-assignment-policy.md](./04-jury-groups-and-assignment-policy.md). | Gap analysis |
|
||||||
|
| 21 | **Submission bundle state** | **REJECTED** — Per-file tracking with SubmissionWindow enforcement is simpler than a formal SubmissionBundle entity. Completeness is derived from slot requirements vs. uploaded files. See Rejected Alternatives below. | Gap analysis |
|
||||||
|
| 22 | **Purpose keys for analytics** | Optional `Round.purposeKey: String?` for analytics grouping (e.g., "jury1_selection", "semifinal_docs"). NOT a new enum — purely semantic. RoundType + label is sufficient for routing; purposeKey is for cross-competition reporting only. | Gap analysis |
|
||||||
|
| 23 | **Enhanced audit with before/after state** | `DecisionAuditLog.details` JSON field already supports before/after state. Convention: include `{ before: {...}, after: {...} }` structure for all override actions. No new DB fields needed — documented as convention. | Gap analysis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rejected Alternatives
|
||||||
|
|
||||||
|
Design concepts from the Codex architecture plan that were evaluated and rejected in favor of simpler approaches:
|
||||||
|
|
||||||
|
| # | Concept | Source | Rejection Rationale |
|
||||||
|
|---|---------|--------|-------------------|
|
||||||
|
| R1 | **SubmissionBundle entity** | Codex docs | Added complexity without benefit. Per-file tracking with `SubmissionWindow` + slot requirements achieves the same completeness tracking. Deriving bundle state from `required slots - uploaded files` is simpler than maintaining a separate state machine. |
|
||||||
|
| R2 | **FinalConfirmation as separate step** | Codex docs | Merged into Deliberation per Decision #4. Deliberation voting IS the confirmation — a separate WinnerProposal → jury sign-off → admin approval flow added unnecessary ceremony. Admin reviews and locks the deliberation result directly. |
|
||||||
|
| R3 | **Purpose Keys as enum** | Codex docs | Made optional `String?` instead of enum (Decision #22). A fixed enum would require schema migration for each new analytics category. Free-text purposeKey with conventional values is more flexible. |
|
||||||
|
| R4 | **Jury-Stage binding** | Codex docs | JuryGroups are independent entities linked to rounds via `roundId`, not bound to stages. This allows a jury to be reused across competitions and rounds without schema changes. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ambiguity Log
|
||||||
|
|
||||||
|
Design decisions where both approaches were viable. Documenting the trade-off for future reference:
|
||||||
|
|
||||||
|
| # | Ambiguity | Decision | Trade-Off |
|
||||||
|
|---|-----------|----------|-----------|
|
||||||
|
| A1 | **Cap enforcement: strict vs. flexible** | 3-mode system (HARD/SOFT/NONE) | More complex than binary on/off, but covers all real-world scenarios. SOFT mode with buffer is the common case. |
|
||||||
|
| A2 | **Cross-jury data visibility: always vs. configurable** | Configurable per round via `showPriorJuryData` | Default OFF prevents bias, but some programs want continuity. Toggle gives admin control. |
|
||||||
|
| A3 | **Deliberation mode: single vs. dual** | Both modes supported (SINGLE_WINNER_VOTE / FULL_RANKING) | Two code paths to maintain, but programs have genuinely different needs. Borda count for detailed ranking, simple vote for quick decisions. |
|
||||||
|
| A4 | **Document mutability: immutable vs. admin-replaceable** | Admin can replace with full provenance tracking | Trades simplicity for flexibility. Provenance chain (`replacedById`, audit log) ensures accountability. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining P1 Questions (Before UI/Router Refactor)
|
||||||
|
|
||||||
|
These must be resolved before starting Phase 5 (Admin Control Plane + Participant UX):
|
||||||
|
|
||||||
|
### P1-1: Mentoring Scope
|
||||||
|
|
||||||
|
**Question**: Is mentoring available only to finalists, or configurable to include semi-finalists?
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- A) Finalists only (simplest, matches current flow)
|
||||||
|
- B) Configurable via `MentoringConfig.eligibility` enum: FINALISTS_ONLY | SEMIFINALISTS_AND_ABOVE | CONFIGURABLE
|
||||||
|
|
||||||
|
**Impact**: Affects who sees the "Request Mentor" toggle and when mentor assignment runs.
|
||||||
|
|
||||||
|
**Current leaning**: The `MentoringConfig` already has an `eligibility` field with options. Implement B for flexibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1-2: Document Mutability After Lock
|
||||||
|
|
||||||
|
**Question**: Can admins replace locked (read-only) submissions from prior rounds?
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- A) No — once a round's docs are locked, they're immutable for everyone
|
||||||
|
- B) Yes — admin can replace with full provenance (`sourceType: ADMIN_REPLACEMENT`)
|
||||||
|
|
||||||
|
**Impact**: Affects document lifecycle rules and audit requirements.
|
||||||
|
|
||||||
|
**Current leaning**: B — admin override everywhere is a guiding principle. Provenance tracking ensures accountability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P1-3: Audience Reveal Timing
|
||||||
|
|
||||||
|
**Question**: Are audience vote totals visible to Jury 3 in real time during the live event, or only when deliberation begins?
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- A) Real-time (jury sees audience votes as they come in)
|
||||||
|
- B) After jury vote (jury submits their scores first, then audience results revealed)
|
||||||
|
- C) At deliberation start (audience results shown when deliberation session opens)
|
||||||
|
- D) Configurable via `LiveFinalConfig.audienceRevealTiming`
|
||||||
|
|
||||||
|
**Impact**: Affects whether audience votes can bias Jury 3 scoring.
|
||||||
|
|
||||||
|
**Current leaning**: D — make it configurable. Default to C (at deliberation start) to prevent audience bias on jury scores.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining P2 Questions (Can Resolve During Implementation)
|
||||||
|
|
||||||
|
### P2-1: Reporting Visibility
|
||||||
|
|
||||||
|
**Question**: Are per-jury compliance reports (assignment coverage, evaluation completion, etc.) visible to all program admins or super-admin only?
|
||||||
|
|
||||||
|
**Impact**: Affects report page access control.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2-2: Override Transparency
|
||||||
|
|
||||||
|
**Question**: Which override details are visible to jury users? For example, if admin overrides a judge's assignment, does the judge see "Admin assigned this project to you" or just the project appearing?
|
||||||
|
|
||||||
|
**Impact**: Affects jury dashboard messaging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2-3: Notification Triggers
|
||||||
|
|
||||||
|
**Question**: What exact round transitions trigger applicant email notifications? (e.g., "You've advanced to the semi-finals", "Your submission window is open", "You've been selected as a finalist")
|
||||||
|
|
||||||
|
**Impact**: Affects notification service configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### P2-4: CSV Bulk Invite
|
||||||
|
|
||||||
|
**Question**: What columns should the CSV bulk invite format include? Current thinking: name, email, role, juryGroupLabel, roundLabel, preAssignmentMode (INTENT | DIRECT), maxProjects, categoryBias.
|
||||||
|
|
||||||
|
**Impact**: Affects invite import service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Delivery Governance
|
||||||
|
|
||||||
|
### Weekly Architecture Sync
|
||||||
|
|
||||||
|
- **Who**: Backend lead, frontend lead, product owner, ops
|
||||||
|
- **When**: Weekly, 30 minutes
|
||||||
|
- **Agenda**: Phase progress, blockers, upcoming decisions, contract drift review
|
||||||
|
|
||||||
|
### Phase Gate Reviews
|
||||||
|
|
||||||
|
Each phase completion triggers a review:
|
||||||
|
1. Deliverables checklist reviewed
|
||||||
|
2. Release gate criteria verified (see [12-observability-and-release-gates.md](./12-observability-and-release-gates.md))
|
||||||
|
3. Test results reviewed
|
||||||
|
4. Any outstanding P1 questions flagged
|
||||||
|
5. Sign-off from architecture owner and product owner
|
||||||
|
|
||||||
|
### No Silent Contract Drift
|
||||||
|
|
||||||
|
After Phase 0 (Contract Freeze), any change to the following requires explicit review:
|
||||||
|
|
||||||
|
| Change Type | Approval Required |
|
||||||
|
|-------------|-------------------|
|
||||||
|
| Prisma model addition or field change | Architecture owner |
|
||||||
|
| Zod config schema modification | Architecture owner |
|
||||||
|
| RoundType enum change | Architecture owner + product owner |
|
||||||
|
| tRPC procedure signature change | Architecture owner |
|
||||||
|
| Assignment policy behavior change | Architecture owner + product owner |
|
||||||
|
| New feature flag addition | Architecture owner |
|
||||||
|
|
||||||
|
Changes are NOT blocked — they require documentation (what changed, why, impact) and sign-off.
|
||||||
|
|
||||||
|
### Evidence Package Template
|
||||||
|
|
||||||
|
Each phase gate review includes:
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase [X] Evidence Package
|
||||||
|
- Date: YYYY-MM-DD
|
||||||
|
- Phase: [name]
|
||||||
|
- Status: PASS / CONDITIONAL PASS / FAIL
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
- [ ] [deliverable 1] — status, link to PR/commit
|
||||||
|
- [ ] [deliverable 2] — status
|
||||||
|
|
||||||
|
Test Results:
|
||||||
|
- Unit: X/X passing
|
||||||
|
- Integration: X/X passing
|
||||||
|
- E2E: X/X passing
|
||||||
|
|
||||||
|
Release Gate: [A/B/C/D/E/F]
|
||||||
|
- [ ] Criterion 1 — status
|
||||||
|
- [ ] Criterion 2 — status
|
||||||
|
|
||||||
|
Open Items:
|
||||||
|
- [any blockers or deferred items]
|
||||||
|
|
||||||
|
Sign-off:
|
||||||
|
- Architecture: [name] [date]
|
||||||
|
- Product: [name] [date]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference Monaco 2026 Configuration
|
||||||
|
|
||||||
|
A concrete example showing how the Monaco 2026 competition would be configured in the redesigned system:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
competition:
|
||||||
|
name: "Monaco Ocean Protection Challenge 2026"
|
||||||
|
programId: "monaco-opc-2026"
|
||||||
|
status: DRAFT
|
||||||
|
|
||||||
|
rounds:
|
||||||
|
- order: 1
|
||||||
|
label: "Application Window"
|
||||||
|
type: INTAKE
|
||||||
|
config:
|
||||||
|
deadlinePolicy: FLAG # accept late with flag
|
||||||
|
requiredDocSlots: ["executive_summary", "business_plan", "team_profile"]
|
||||||
|
|
||||||
|
- order: 2
|
||||||
|
label: "AI Eligibility Screening"
|
||||||
|
type: FILTERING
|
||||||
|
config:
|
||||||
|
aiEnabled: true
|
||||||
|
autoAdvanceEligible: false # admin reviews before advancing
|
||||||
|
|
||||||
|
- order: 3
|
||||||
|
label: "Jury 1 — Semi-Finalist Selection"
|
||||||
|
type: EVALUATION
|
||||||
|
juryGroupId: jury-1
|
||||||
|
config:
|
||||||
|
scoringRubric: { criteria: [...], maxScore: 100 }
|
||||||
|
requireFeedback: true
|
||||||
|
generateAiShortlist: true
|
||||||
|
|
||||||
|
- order: 4
|
||||||
|
label: "Semi-Finalist Documents"
|
||||||
|
type: SUBMISSION
|
||||||
|
config:
|
||||||
|
requiredDocSlots: ["updated_business_plan", "financial_projections", "impact_report"]
|
||||||
|
lockPriorRoundDocs: true
|
||||||
|
deadlinePolicy: HARD
|
||||||
|
|
||||||
|
- order: 5
|
||||||
|
label: "Jury 2 — Finalist Selection"
|
||||||
|
type: EVALUATION
|
||||||
|
juryGroupId: jury-2
|
||||||
|
config:
|
||||||
|
scoringRubric: { criteria: [...], maxScore: 100 }
|
||||||
|
requireFeedback: true
|
||||||
|
generateAiShortlist: true
|
||||||
|
showPriorJuryData: false # independent evaluation
|
||||||
|
|
||||||
|
- order: 6
|
||||||
|
label: "Finalist Mentoring"
|
||||||
|
type: MENTORING
|
||||||
|
config:
|
||||||
|
eligibility: FINALISTS_ONLY
|
||||||
|
requireMentorRequest: true
|
||||||
|
assignmentMethod: MANUAL
|
||||||
|
allowMentorPromotion: false
|
||||||
|
|
||||||
|
- order: 7
|
||||||
|
label: "Live Finals — Jury 3"
|
||||||
|
type: LIVE_FINAL
|
||||||
|
juryGroupId: jury-3
|
||||||
|
config:
|
||||||
|
audienceVotingEnabled: true
|
||||||
|
audienceRevealTiming: AT_DELIBERATION
|
||||||
|
audienceBlendWeight: 0 # jury only for scoring
|
||||||
|
showPriorJuryData: true # Jury 3 sees prior jury history
|
||||||
|
scoringMode: CRITERIA_BASED
|
||||||
|
presentationOrder: MANUAL
|
||||||
|
|
||||||
|
- order: 8
|
||||||
|
label: "Final Deliberation"
|
||||||
|
type: DELIBERATION
|
||||||
|
juryGroupId: jury-3
|
||||||
|
config:
|
||||||
|
mode: FULL_RANKING
|
||||||
|
showCollectiveRankings: true
|
||||||
|
tieBreakMethod: ADMIN_DECIDES
|
||||||
|
topN: 3
|
||||||
|
allowAdminOverride: true
|
||||||
|
|
||||||
|
juryGroups:
|
||||||
|
- label: "Jury 1"
|
||||||
|
defaultCapMode: SOFT
|
||||||
|
defaultMaxProjects: 15
|
||||||
|
softCapBuffer: 10
|
||||||
|
allowOnboardingSelfService: true
|
||||||
|
defaultCategoryBias: { STARTUP: 0.5, BUSINESS_CONCEPT: 0.5 }
|
||||||
|
|
||||||
|
- label: "Jury 2"
|
||||||
|
defaultCapMode: SOFT
|
||||||
|
defaultMaxProjects: 10
|
||||||
|
softCapBuffer: 10
|
||||||
|
allowOnboardingSelfService: true
|
||||||
|
|
||||||
|
- label: "Jury 3 — Live Finals"
|
||||||
|
defaultCapMode: NONE # all finalists reviewed
|
||||||
|
allowOnboardingSelfService: false
|
||||||
|
|
||||||
|
specialAwards:
|
||||||
|
- name: "Innovation Award"
|
||||||
|
routingMode: STAY_IN_MAIN
|
||||||
|
eligibilityMode: AI_SUGGESTED
|
||||||
|
winnerDecisionMode: JURY_VOTE
|
||||||
|
juryGroupLabel: "Innovation Award Panel"
|
||||||
|
|
||||||
|
- name: "Ocean Impact Award"
|
||||||
|
routingMode: SEPARATE_POOL
|
||||||
|
pullOutBehavior: KEEP_IN_BOTH
|
||||||
|
routingConfirmationMode: ADMIN_CONFIRMED
|
||||||
|
eligibilityMode: AI_SUGGESTED
|
||||||
|
winnerDecisionMode: SINGLE_JUDGE
|
||||||
|
singleJudgeLabel: "Impact Award Chair"
|
||||||
|
|
||||||
|
submissionWindows:
|
||||||
|
- label: "Round 1 Application Documents"
|
||||||
|
roundOrder: 1
|
||||||
|
requirements:
|
||||||
|
- slotKey: "executive_summary"
|
||||||
|
label: "Executive Summary"
|
||||||
|
required: true
|
||||||
|
acceptedTypes: ["application/pdf"]
|
||||||
|
- slotKey: "business_plan"
|
||||||
|
label: "Business Plan"
|
||||||
|
required: true
|
||||||
|
acceptedTypes: ["application/pdf"]
|
||||||
|
- slotKey: "team_profile"
|
||||||
|
label: "Team Profile"
|
||||||
|
required: true
|
||||||
|
acceptedTypes: ["application/pdf"]
|
||||||
|
|
||||||
|
- label: "Round 2 Semi-Finalist Documents"
|
||||||
|
roundOrder: 4
|
||||||
|
requirements:
|
||||||
|
- slotKey: "updated_business_plan"
|
||||||
|
label: "Updated Business Plan"
|
||||||
|
required: true
|
||||||
|
- slotKey: "financial_projections"
|
||||||
|
label: "Financial Projections"
|
||||||
|
required: true
|
||||||
|
- slotKey: "impact_report"
|
||||||
|
label: "Environmental Impact Report"
|
||||||
|
required: true
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration, when loaded into the system, would create the full Monaco 2026 competition with all 8 rounds, 3 main juries, 2 special awards, and 2 submission windows — ready for admin to set dates and invite jury members.
|
||||||
321
messages/en.json
321
messages/en.json
@@ -1,321 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"loading": "Loading...",
|
|
||||||
"save": "Save",
|
|
||||||
"cancel": "Cancel",
|
|
||||||
"delete": "Delete",
|
|
||||||
"edit": "Edit",
|
|
||||||
"create": "Create",
|
|
||||||
"close": "Close",
|
|
||||||
"confirm": "Confirm",
|
|
||||||
"back": "Back",
|
|
||||||
"next": "Next",
|
|
||||||
"submit": "Submit",
|
|
||||||
"search": "Search",
|
|
||||||
"filter": "Filter",
|
|
||||||
"export": "Export",
|
|
||||||
"import": "Import",
|
|
||||||
"refresh": "Refresh",
|
|
||||||
"actions": "Actions",
|
|
||||||
"status": "Status",
|
|
||||||
"name": "Name",
|
|
||||||
"email": "Email",
|
|
||||||
"role": "Role",
|
|
||||||
"date": "Date",
|
|
||||||
"description": "Description",
|
|
||||||
"settings": "Settings",
|
|
||||||
"yes": "Yes",
|
|
||||||
"no": "No",
|
|
||||||
"all": "All",
|
|
||||||
"none": "None",
|
|
||||||
"noResults": "No results found",
|
|
||||||
"error": "Error",
|
|
||||||
"success": "Success",
|
|
||||||
"warning": "Warning",
|
|
||||||
"info": "Info",
|
|
||||||
"required": "Required",
|
|
||||||
"optional": "Optional",
|
|
||||||
"total": "Total",
|
|
||||||
"page": "Page",
|
|
||||||
"of": "of",
|
|
||||||
"showing": "Showing",
|
|
||||||
"entries": "entries",
|
|
||||||
"perPage": "per page",
|
|
||||||
"language": "Language",
|
|
||||||
"english": "English",
|
|
||||||
"french": "French"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"signIn": "Sign In",
|
|
||||||
"signOut": "Sign Out",
|
|
||||||
"signUp": "Sign Up",
|
|
||||||
"email": "Email Address",
|
|
||||||
"password": "Password",
|
|
||||||
"forgotPassword": "Forgot Password?",
|
|
||||||
"resetPassword": "Reset Password",
|
|
||||||
"newPassword": "New Password",
|
|
||||||
"confirmPassword": "Confirm Password",
|
|
||||||
"currentPassword": "Current Password",
|
|
||||||
"magicLink": "Sign in with Magic Link",
|
|
||||||
"magicLinkSent": "Check your email for a sign-in link",
|
|
||||||
"invalidCredentials": "Invalid email or password",
|
|
||||||
"accountLocked": "Account locked. Try again later.",
|
|
||||||
"setPassword": "Set Password",
|
|
||||||
"passwordRequirements": "Password must be at least 8 characters",
|
|
||||||
"passwordsDoNotMatch": "Passwords do not match",
|
|
||||||
"welcomeBack": "Welcome back",
|
|
||||||
"signInDescription": "Enter your email to sign in to your account",
|
|
||||||
"orContinueWith": "Or continue with"
|
|
||||||
},
|
|
||||||
"nav": {
|
|
||||||
"dashboard": "Dashboard",
|
|
||||||
"programs": "Programs",
|
|
||||||
"rounds": "Rounds",
|
|
||||||
"projects": "Projects",
|
|
||||||
"users": "Users",
|
|
||||||
"evaluations": "Evaluations",
|
|
||||||
"assignments": "Assignments",
|
|
||||||
"analytics": "Analytics",
|
|
||||||
"settings": "Settings",
|
|
||||||
"audit": "Audit Log",
|
|
||||||
"myProjects": "My Projects",
|
|
||||||
"myEvaluations": "My Evaluations",
|
|
||||||
"profile": "Profile",
|
|
||||||
"help": "Help",
|
|
||||||
"notifications": "Notifications",
|
|
||||||
"mentoring": "Mentoring",
|
|
||||||
"liveVoting": "Live Voting",
|
|
||||||
"applications": "Applications",
|
|
||||||
"messages": "Messages"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Dashboard",
|
|
||||||
"welcome": "Welcome, {name}",
|
|
||||||
"overview": "Overview",
|
|
||||||
"recentActivity": "Recent Activity",
|
|
||||||
"pendingEvaluations": "Pending Evaluations",
|
|
||||||
"completedEvaluations": "Completed Evaluations",
|
|
||||||
"totalProjects": "Total Projects",
|
|
||||||
"activeRounds": "Active Rounds",
|
|
||||||
"assignedProjects": "Assigned Projects",
|
|
||||||
"upcomingDeadlines": "Upcoming Deadlines",
|
|
||||||
"quickActions": "Quick Actions",
|
|
||||||
"noActivity": "No recent activity"
|
|
||||||
},
|
|
||||||
"programs": {
|
|
||||||
"title": "Programs",
|
|
||||||
"createProgram": "Create Program",
|
|
||||||
"editProgram": "Edit Program",
|
|
||||||
"programName": "Program Name",
|
|
||||||
"year": "Year",
|
|
||||||
"status": "Status",
|
|
||||||
"rounds": "Rounds",
|
|
||||||
"projects": "Projects",
|
|
||||||
"noPrograms": "No programs found",
|
|
||||||
"deleteConfirm": "Are you sure you want to delete this program?"
|
|
||||||
},
|
|
||||||
"rounds": {
|
|
||||||
"title": "Rounds",
|
|
||||||
"createRound": "Create Round",
|
|
||||||
"editRound": "Edit Round",
|
|
||||||
"roundName": "Round Name",
|
|
||||||
"roundType": "Round Type",
|
|
||||||
"startDate": "Start Date",
|
|
||||||
"endDate": "End Date",
|
|
||||||
"votingWindow": "Voting Window",
|
|
||||||
"criteria": "Evaluation Criteria",
|
|
||||||
"status": "Status",
|
|
||||||
"active": "Active",
|
|
||||||
"closed": "Closed",
|
|
||||||
"upcoming": "Upcoming",
|
|
||||||
"noRounds": "No rounds found"
|
|
||||||
},
|
|
||||||
"projects": {
|
|
||||||
"title": "Projects",
|
|
||||||
"createProject": "Create Project",
|
|
||||||
"editProject": "Edit Project",
|
|
||||||
"projectName": "Project Title",
|
|
||||||
"teamName": "Team Name",
|
|
||||||
"country": "Country",
|
|
||||||
"category": "Category",
|
|
||||||
"status": "Status",
|
|
||||||
"description": "Description",
|
|
||||||
"files": "Files",
|
|
||||||
"evaluations": "Evaluations",
|
|
||||||
"noProjects": "No projects found",
|
|
||||||
"importCsv": "Import CSV",
|
|
||||||
"bulkStatusUpdate": "Bulk Status Update",
|
|
||||||
"viewDetails": "View Details",
|
|
||||||
"assignMentor": "Assign Mentor",
|
|
||||||
"oceanIssue": "Ocean Issue"
|
|
||||||
},
|
|
||||||
"evaluations": {
|
|
||||||
"title": "Evaluations",
|
|
||||||
"submitEvaluation": "Submit Evaluation",
|
|
||||||
"draft": "Draft",
|
|
||||||
"submitted": "Submitted",
|
|
||||||
"score": "Score",
|
|
||||||
"feedback": "Feedback",
|
|
||||||
"criteria": "Criteria",
|
|
||||||
"globalScore": "Global Score",
|
|
||||||
"decision": "Decision",
|
|
||||||
"recommend": "Recommend",
|
|
||||||
"doNotRecommend": "Do Not Recommend",
|
|
||||||
"saveAsDraft": "Save as Draft",
|
|
||||||
"finalSubmit": "Final Submit",
|
|
||||||
"confirmSubmit": "Are you sure? This action cannot be undone.",
|
|
||||||
"progress": "Progress",
|
|
||||||
"completionRate": "Completion Rate",
|
|
||||||
"noEvaluations": "No evaluations yet",
|
|
||||||
"evaluationSummary": "Evaluation Summary",
|
|
||||||
"strengths": "Strengths",
|
|
||||||
"weaknesses": "Weaknesses",
|
|
||||||
"overallAssessment": "Overall Assessment"
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"title": "Users",
|
|
||||||
"createUser": "Create User",
|
|
||||||
"editUser": "Edit User",
|
|
||||||
"inviteUser": "Invite User",
|
|
||||||
"bulkImport": "Bulk Import",
|
|
||||||
"sendInvitation": "Send Invitation",
|
|
||||||
"resendInvitation": "Resend Invitation",
|
|
||||||
"role": "Role",
|
|
||||||
"status": "Status",
|
|
||||||
"active": "Active",
|
|
||||||
"invited": "Invited",
|
|
||||||
"suspended": "Suspended",
|
|
||||||
"noUsers": "No users found",
|
|
||||||
"expertiseTags": "Expertise Tags",
|
|
||||||
"maxAssignments": "Max Assignments",
|
|
||||||
"lastLogin": "Last Login",
|
|
||||||
"deleteConfirm": "Are you sure you want to delete this user?"
|
|
||||||
},
|
|
||||||
"assignments": {
|
|
||||||
"title": "Assignments",
|
|
||||||
"assign": "Assign",
|
|
||||||
"unassign": "Unassign",
|
|
||||||
"bulkAssign": "Bulk Assign",
|
|
||||||
"smartAssign": "Smart Assignment",
|
|
||||||
"manual": "Manual",
|
|
||||||
"algorithm": "Algorithm",
|
|
||||||
"aiAuto": "AI Auto",
|
|
||||||
"noAssignments": "No assignments",
|
|
||||||
"assignedTo": "Assigned to",
|
|
||||||
"assignedBy": "Assigned by"
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"title": "Files",
|
|
||||||
"upload": "Upload File",
|
|
||||||
"download": "Download",
|
|
||||||
"delete": "Delete File",
|
|
||||||
"fileName": "File Name",
|
|
||||||
"fileType": "File Type",
|
|
||||||
"fileSize": "File Size",
|
|
||||||
"uploadDate": "Upload Date",
|
|
||||||
"noFiles": "No files uploaded",
|
|
||||||
"dragAndDrop": "Drag and drop files here",
|
|
||||||
"maxSize": "Maximum file size: {size}",
|
|
||||||
"version": "Version",
|
|
||||||
"versionHistory": "Version History",
|
|
||||||
"replaceFile": "Replace File",
|
|
||||||
"bulkDownload": "Bulk Download"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": "Settings",
|
|
||||||
"general": "General",
|
|
||||||
"branding": "Branding",
|
|
||||||
"email": "Email",
|
|
||||||
"security": "Security",
|
|
||||||
"ai": "AI Configuration",
|
|
||||||
"storage": "Storage",
|
|
||||||
"language": "Language",
|
|
||||||
"defaultLanguage": "Default Language",
|
|
||||||
"availableLanguages": "Available Languages",
|
|
||||||
"languageDescription": "Configure the platform's language settings",
|
|
||||||
"saved": "Settings saved successfully",
|
|
||||||
"saveFailed": "Failed to save settings"
|
|
||||||
},
|
|
||||||
"liveVoting": {
|
|
||||||
"title": "Live Voting",
|
|
||||||
"session": "Session",
|
|
||||||
"startVoting": "Start Voting",
|
|
||||||
"stopVoting": "Stop Voting",
|
|
||||||
"endSession": "End Session",
|
|
||||||
"timeRemaining": "Time Remaining",
|
|
||||||
"castVote": "Cast Your Vote",
|
|
||||||
"voteSubmitted": "Vote submitted",
|
|
||||||
"results": "Results",
|
|
||||||
"juryScore": "Jury Score",
|
|
||||||
"audienceScore": "Audience Score",
|
|
||||||
"weightedTotal": "Weighted Total",
|
|
||||||
"noVotes": "No votes yet",
|
|
||||||
"votingClosed": "Voting has closed",
|
|
||||||
"presentationSettings": "Presentation Settings",
|
|
||||||
"audienceVoting": "Audience Voting"
|
|
||||||
},
|
|
||||||
"mentor": {
|
|
||||||
"title": "Mentoring",
|
|
||||||
"myMentees": "My Mentees",
|
|
||||||
"projectDetails": "Project Details",
|
|
||||||
"sendMessage": "Send Message",
|
|
||||||
"notes": "Notes",
|
|
||||||
"addNote": "Add Note",
|
|
||||||
"milestones": "Milestones",
|
|
||||||
"completeMilestone": "Mark Complete",
|
|
||||||
"activity": "Activity",
|
|
||||||
"lastViewed": "Last Viewed",
|
|
||||||
"noMentees": "No mentees assigned"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"title": "Profile",
|
|
||||||
"editProfile": "Edit Profile",
|
|
||||||
"name": "Full Name",
|
|
||||||
"email": "Email",
|
|
||||||
"phone": "Phone Number",
|
|
||||||
"country": "Country",
|
|
||||||
"bio": "Bio",
|
|
||||||
"expertise": "Areas of Expertise",
|
|
||||||
"notifications": "Notification Preferences",
|
|
||||||
"digestFrequency": "Digest Frequency",
|
|
||||||
"availability": "Availability",
|
|
||||||
"workload": "Preferred Workload",
|
|
||||||
"changePassword": "Change Password",
|
|
||||||
"deleteAccount": "Delete Account",
|
|
||||||
"deleteAccountConfirm": "This action cannot be undone. All your data will be permanently deleted."
|
|
||||||
},
|
|
||||||
"onboarding": {
|
|
||||||
"welcome": "Welcome to MOPC",
|
|
||||||
"setupProfile": "Let's set up your profile",
|
|
||||||
"step1": "Personal Information",
|
|
||||||
"step2": "Expertise & Preferences",
|
|
||||||
"step3": "Review & Complete",
|
|
||||||
"complete": "Complete Setup",
|
|
||||||
"skip": "Skip for now"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"generic": "Something went wrong. Please try again.",
|
|
||||||
"notFound": "Page not found",
|
|
||||||
"unauthorized": "You are not authorized to access this page",
|
|
||||||
"forbidden": "Access denied",
|
|
||||||
"serverError": "Internal server error",
|
|
||||||
"networkError": "Network error. Please check your connection.",
|
|
||||||
"sessionExpired": "Your session has expired. Please sign in again.",
|
|
||||||
"validationError": "Please check the form for errors"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"title": "Notifications",
|
|
||||||
"markAllRead": "Mark all as read",
|
|
||||||
"noNotifications": "No notifications",
|
|
||||||
"viewAll": "View all notifications"
|
|
||||||
},
|
|
||||||
"coi": {
|
|
||||||
"title": "Conflict of Interest",
|
|
||||||
"declaration": "COI Declaration",
|
|
||||||
"declareConflict": "Declare Conflict of Interest",
|
|
||||||
"noConflict": "No conflict of interest",
|
|
||||||
"hasConflict": "Conflict declared",
|
|
||||||
"reason": "Reason for conflict",
|
|
||||||
"confirmDeclaration": "I confirm this declaration is accurate"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
321
messages/fr.json
321
messages/fr.json
@@ -1,321 +0,0 @@
|
|||||||
{
|
|
||||||
"common": {
|
|
||||||
"loading": "Chargement...",
|
|
||||||
"save": "Enregistrer",
|
|
||||||
"cancel": "Annuler",
|
|
||||||
"delete": "Supprimer",
|
|
||||||
"edit": "Modifier",
|
|
||||||
"create": "Cr\u00e9er",
|
|
||||||
"close": "Fermer",
|
|
||||||
"confirm": "Confirmer",
|
|
||||||
"back": "Retour",
|
|
||||||
"next": "Suivant",
|
|
||||||
"submit": "Soumettre",
|
|
||||||
"search": "Rechercher",
|
|
||||||
"filter": "Filtrer",
|
|
||||||
"export": "Exporter",
|
|
||||||
"import": "Importer",
|
|
||||||
"refresh": "Actualiser",
|
|
||||||
"actions": "Actions",
|
|
||||||
"status": "Statut",
|
|
||||||
"name": "Nom",
|
|
||||||
"email": "E-mail",
|
|
||||||
"role": "R\u00f4le",
|
|
||||||
"date": "Date",
|
|
||||||
"description": "Description",
|
|
||||||
"settings": "Param\u00e8tres",
|
|
||||||
"yes": "Oui",
|
|
||||||
"no": "Non",
|
|
||||||
"all": "Tous",
|
|
||||||
"none": "Aucun",
|
|
||||||
"noResults": "Aucun r\u00e9sultat trouv\u00e9",
|
|
||||||
"error": "Erreur",
|
|
||||||
"success": "Succ\u00e8s",
|
|
||||||
"warning": "Avertissement",
|
|
||||||
"info": "Information",
|
|
||||||
"required": "Obligatoire",
|
|
||||||
"optional": "Facultatif",
|
|
||||||
"total": "Total",
|
|
||||||
"page": "Page",
|
|
||||||
"of": "de",
|
|
||||||
"showing": "Affichage de",
|
|
||||||
"entries": "entr\u00e9es",
|
|
||||||
"perPage": "par page",
|
|
||||||
"language": "Langue",
|
|
||||||
"english": "Anglais",
|
|
||||||
"french": "Fran\u00e7ais"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"signIn": "Se connecter",
|
|
||||||
"signOut": "Se d\u00e9connecter",
|
|
||||||
"signUp": "S'inscrire",
|
|
||||||
"email": "Adresse e-mail",
|
|
||||||
"password": "Mot de passe",
|
|
||||||
"forgotPassword": "Mot de passe oubli\u00e9 ?",
|
|
||||||
"resetPassword": "R\u00e9initialiser le mot de passe",
|
|
||||||
"newPassword": "Nouveau mot de passe",
|
|
||||||
"confirmPassword": "Confirmer le mot de passe",
|
|
||||||
"currentPassword": "Mot de passe actuel",
|
|
||||||
"magicLink": "Se connecter avec un lien magique",
|
|
||||||
"magicLinkSent": "V\u00e9rifiez votre e-mail pour un lien de connexion",
|
|
||||||
"invalidCredentials": "E-mail ou mot de passe invalide",
|
|
||||||
"accountLocked": "Compte verrouill\u00e9. R\u00e9essayez plus tard.",
|
|
||||||
"setPassword": "D\u00e9finir le mot de passe",
|
|
||||||
"passwordRequirements": "Le mot de passe doit contenir au moins 8 caract\u00e8res",
|
|
||||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
|
|
||||||
"welcomeBack": "Bon retour",
|
|
||||||
"signInDescription": "Entrez votre e-mail pour vous connecter \u00e0 votre compte",
|
|
||||||
"orContinueWith": "Ou continuer avec"
|
|
||||||
},
|
|
||||||
"nav": {
|
|
||||||
"dashboard": "Tableau de bord",
|
|
||||||
"programs": "Programmes",
|
|
||||||
"rounds": "Tours",
|
|
||||||
"projects": "Projets",
|
|
||||||
"users": "Utilisateurs",
|
|
||||||
"evaluations": "\u00c9valuations",
|
|
||||||
"assignments": "Affectations",
|
|
||||||
"analytics": "Analytique",
|
|
||||||
"settings": "Param\u00e8tres",
|
|
||||||
"audit": "Journal d'audit",
|
|
||||||
"myProjects": "Mes projets",
|
|
||||||
"myEvaluations": "Mes \u00e9valuations",
|
|
||||||
"profile": "Profil",
|
|
||||||
"help": "Aide",
|
|
||||||
"notifications": "Notifications",
|
|
||||||
"mentoring": "Mentorat",
|
|
||||||
"liveVoting": "Vote en direct",
|
|
||||||
"applications": "Candidatures",
|
|
||||||
"messages": "Messages"
|
|
||||||
},
|
|
||||||
"dashboard": {
|
|
||||||
"title": "Tableau de bord",
|
|
||||||
"welcome": "Bienvenue, {name}",
|
|
||||||
"overview": "Aper\u00e7u",
|
|
||||||
"recentActivity": "Activit\u00e9 r\u00e9cente",
|
|
||||||
"pendingEvaluations": "\u00c9valuations en attente",
|
|
||||||
"completedEvaluations": "\u00c9valuations termin\u00e9es",
|
|
||||||
"totalProjects": "Total des projets",
|
|
||||||
"activeRounds": "Tours actifs",
|
|
||||||
"assignedProjects": "Projets assign\u00e9s",
|
|
||||||
"upcomingDeadlines": "\u00c9ch\u00e9ances \u00e0 venir",
|
|
||||||
"quickActions": "Actions rapides",
|
|
||||||
"noActivity": "Aucune activit\u00e9 r\u00e9cente"
|
|
||||||
},
|
|
||||||
"programs": {
|
|
||||||
"title": "Programmes",
|
|
||||||
"createProgram": "Cr\u00e9er un programme",
|
|
||||||
"editProgram": "Modifier le programme",
|
|
||||||
"programName": "Nom du programme",
|
|
||||||
"year": "Ann\u00e9e",
|
|
||||||
"status": "Statut",
|
|
||||||
"rounds": "Tours",
|
|
||||||
"projects": "Projets",
|
|
||||||
"noPrograms": "Aucun programme trouv\u00e9",
|
|
||||||
"deleteConfirm": "\u00cates-vous s\u00fbr de vouloir supprimer ce programme ?"
|
|
||||||
},
|
|
||||||
"rounds": {
|
|
||||||
"title": "Tours",
|
|
||||||
"createRound": "Cr\u00e9er un tour",
|
|
||||||
"editRound": "Modifier le tour",
|
|
||||||
"roundName": "Nom du tour",
|
|
||||||
"roundType": "Type de tour",
|
|
||||||
"startDate": "Date de d\u00e9but",
|
|
||||||
"endDate": "Date de fin",
|
|
||||||
"votingWindow": "Fen\u00eatre de vote",
|
|
||||||
"criteria": "Crit\u00e8res d'\u00e9valuation",
|
|
||||||
"status": "Statut",
|
|
||||||
"active": "Actif",
|
|
||||||
"closed": "Cl\u00f4tur\u00e9",
|
|
||||||
"upcoming": "\u00c0 venir",
|
|
||||||
"noRounds": "Aucun tour trouv\u00e9"
|
|
||||||
},
|
|
||||||
"projects": {
|
|
||||||
"title": "Projets",
|
|
||||||
"createProject": "Cr\u00e9er un projet",
|
|
||||||
"editProject": "Modifier le projet",
|
|
||||||
"projectName": "Titre du projet",
|
|
||||||
"teamName": "Nom de l'\u00e9quipe",
|
|
||||||
"country": "Pays",
|
|
||||||
"category": "Cat\u00e9gorie",
|
|
||||||
"status": "Statut",
|
|
||||||
"description": "Description",
|
|
||||||
"files": "Fichiers",
|
|
||||||
"evaluations": "\u00c9valuations",
|
|
||||||
"noProjects": "Aucun projet trouv\u00e9",
|
|
||||||
"importCsv": "Importer CSV",
|
|
||||||
"bulkStatusUpdate": "Mise \u00e0 jour en masse du statut",
|
|
||||||
"viewDetails": "Voir les d\u00e9tails",
|
|
||||||
"assignMentor": "Assigner un mentor",
|
|
||||||
"oceanIssue": "Probl\u00e9matique oc\u00e9anique"
|
|
||||||
},
|
|
||||||
"evaluations": {
|
|
||||||
"title": "\u00c9valuations",
|
|
||||||
"submitEvaluation": "Soumettre l'\u00e9valuation",
|
|
||||||
"draft": "Brouillon",
|
|
||||||
"submitted": "Soumis",
|
|
||||||
"score": "Note",
|
|
||||||
"feedback": "Commentaires",
|
|
||||||
"criteria": "Crit\u00e8res",
|
|
||||||
"globalScore": "Note globale",
|
|
||||||
"decision": "D\u00e9cision",
|
|
||||||
"recommend": "Recommander",
|
|
||||||
"doNotRecommend": "Ne pas recommander",
|
|
||||||
"saveAsDraft": "Enregistrer comme brouillon",
|
|
||||||
"finalSubmit": "Soumission finale",
|
|
||||||
"confirmSubmit": "\u00cates-vous s\u00fbr ? Cette action est irr\u00e9versible.",
|
|
||||||
"progress": "Progression",
|
|
||||||
"completionRate": "Taux de compl\u00e9tion",
|
|
||||||
"noEvaluations": "Aucune \u00e9valuation pour le moment",
|
|
||||||
"evaluationSummary": "R\u00e9sum\u00e9 de l'\u00e9valuation",
|
|
||||||
"strengths": "Points forts",
|
|
||||||
"weaknesses": "Points faibles",
|
|
||||||
"overallAssessment": "\u00c9valuation globale"
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"title": "Utilisateurs",
|
|
||||||
"createUser": "Cr\u00e9er un utilisateur",
|
|
||||||
"editUser": "Modifier l'utilisateur",
|
|
||||||
"inviteUser": "Inviter un utilisateur",
|
|
||||||
"bulkImport": "Importation en masse",
|
|
||||||
"sendInvitation": "Envoyer l'invitation",
|
|
||||||
"resendInvitation": "Renvoyer l'invitation",
|
|
||||||
"role": "R\u00f4le",
|
|
||||||
"status": "Statut",
|
|
||||||
"active": "Actif",
|
|
||||||
"invited": "Invit\u00e9",
|
|
||||||
"suspended": "Suspendu",
|
|
||||||
"noUsers": "Aucun utilisateur trouv\u00e9",
|
|
||||||
"expertiseTags": "Tags d'expertise",
|
|
||||||
"maxAssignments": "Affectations max.",
|
|
||||||
"lastLogin": "Derni\u00e8re connexion",
|
|
||||||
"deleteConfirm": "\u00cates-vous s\u00fbr de vouloir supprimer cet utilisateur ?"
|
|
||||||
},
|
|
||||||
"assignments": {
|
|
||||||
"title": "Affectations",
|
|
||||||
"assign": "Affecter",
|
|
||||||
"unassign": "D\u00e9saffecter",
|
|
||||||
"bulkAssign": "Affectation en masse",
|
|
||||||
"smartAssign": "Affectation intelligente",
|
|
||||||
"manual": "Manuel",
|
|
||||||
"algorithm": "Algorithme",
|
|
||||||
"aiAuto": "IA automatique",
|
|
||||||
"noAssignments": "Aucune affectation",
|
|
||||||
"assignedTo": "Affect\u00e9 \u00e0",
|
|
||||||
"assignedBy": "Affect\u00e9 par"
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"title": "Fichiers",
|
|
||||||
"upload": "T\u00e9l\u00e9charger un fichier",
|
|
||||||
"download": "T\u00e9l\u00e9charger",
|
|
||||||
"delete": "Supprimer le fichier",
|
|
||||||
"fileName": "Nom du fichier",
|
|
||||||
"fileType": "Type de fichier",
|
|
||||||
"fileSize": "Taille du fichier",
|
|
||||||
"uploadDate": "Date de t\u00e9l\u00e9chargement",
|
|
||||||
"noFiles": "Aucun fichier t\u00e9l\u00e9charg\u00e9",
|
|
||||||
"dragAndDrop": "Glissez-d\u00e9posez les fichiers ici",
|
|
||||||
"maxSize": "Taille maximale du fichier : {size}",
|
|
||||||
"version": "Version",
|
|
||||||
"versionHistory": "Historique des versions",
|
|
||||||
"replaceFile": "Remplacer le fichier",
|
|
||||||
"bulkDownload": "T\u00e9l\u00e9chargement en masse"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"title": "Param\u00e8tres",
|
|
||||||
"general": "G\u00e9n\u00e9ral",
|
|
||||||
"branding": "Image de marque",
|
|
||||||
"email": "E-mail",
|
|
||||||
"security": "S\u00e9curit\u00e9",
|
|
||||||
"ai": "Configuration IA",
|
|
||||||
"storage": "Stockage",
|
|
||||||
"language": "Langue",
|
|
||||||
"defaultLanguage": "Langue par d\u00e9faut",
|
|
||||||
"availableLanguages": "Langues disponibles",
|
|
||||||
"languageDescription": "Configurer les param\u00e8tres de langue de la plateforme",
|
|
||||||
"saved": "Param\u00e8tres enregistr\u00e9s avec succ\u00e8s",
|
|
||||||
"saveFailed": "\u00c9chec de l'enregistrement des param\u00e8tres"
|
|
||||||
},
|
|
||||||
"liveVoting": {
|
|
||||||
"title": "Vote en direct",
|
|
||||||
"session": "Session",
|
|
||||||
"startVoting": "D\u00e9marrer le vote",
|
|
||||||
"stopVoting": "Arr\u00eater le vote",
|
|
||||||
"endSession": "Terminer la session",
|
|
||||||
"timeRemaining": "Temps restant",
|
|
||||||
"castVote": "Voter",
|
|
||||||
"voteSubmitted": "Vote soumis",
|
|
||||||
"results": "R\u00e9sultats",
|
|
||||||
"juryScore": "Note du jury",
|
|
||||||
"audienceScore": "Note du public",
|
|
||||||
"weightedTotal": "Total pond\u00e9r\u00e9",
|
|
||||||
"noVotes": "Aucun vote pour le moment",
|
|
||||||
"votingClosed": "Le vote est clos",
|
|
||||||
"presentationSettings": "Param\u00e8tres de pr\u00e9sentation",
|
|
||||||
"audienceVoting": "Vote du public"
|
|
||||||
},
|
|
||||||
"mentor": {
|
|
||||||
"title": "Mentorat",
|
|
||||||
"myMentees": "Mes mentees",
|
|
||||||
"projectDetails": "D\u00e9tails du projet",
|
|
||||||
"sendMessage": "Envoyer un message",
|
|
||||||
"notes": "Notes",
|
|
||||||
"addNote": "Ajouter une note",
|
|
||||||
"milestones": "Jalons",
|
|
||||||
"completeMilestone": "Marquer comme termin\u00e9",
|
|
||||||
"activity": "Activit\u00e9",
|
|
||||||
"lastViewed": "Derni\u00e8re consultation",
|
|
||||||
"noMentees": "Aucun mentee assign\u00e9"
|
|
||||||
},
|
|
||||||
"profile": {
|
|
||||||
"title": "Profil",
|
|
||||||
"editProfile": "Modifier le profil",
|
|
||||||
"name": "Nom complet",
|
|
||||||
"email": "E-mail",
|
|
||||||
"phone": "Num\u00e9ro de t\u00e9l\u00e9phone",
|
|
||||||
"country": "Pays",
|
|
||||||
"bio": "Biographie",
|
|
||||||
"expertise": "Domaines d'expertise",
|
|
||||||
"notifications": "Pr\u00e9f\u00e9rences de notification",
|
|
||||||
"digestFrequency": "Fr\u00e9quence du digest",
|
|
||||||
"availability": "Disponibilit\u00e9",
|
|
||||||
"workload": "Charge de travail pr\u00e9f\u00e9r\u00e9e",
|
|
||||||
"changePassword": "Changer le mot de passe",
|
|
||||||
"deleteAccount": "Supprimer le compte",
|
|
||||||
"deleteAccountConfirm": "Cette action est irr\u00e9versible. Toutes vos donn\u00e9es seront d\u00e9finitivement supprim\u00e9es."
|
|
||||||
},
|
|
||||||
"onboarding": {
|
|
||||||
"welcome": "Bienvenue sur MOPC",
|
|
||||||
"setupProfile": "Configurons votre profil",
|
|
||||||
"step1": "Informations personnelles",
|
|
||||||
"step2": "Expertise et pr\u00e9f\u00e9rences",
|
|
||||||
"step3": "V\u00e9rification et finalisation",
|
|
||||||
"complete": "Terminer la configuration",
|
|
||||||
"skip": "Passer pour le moment"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"generic": "Une erreur s'est produite. Veuillez r\u00e9essayer.",
|
|
||||||
"notFound": "Page non trouv\u00e9e",
|
|
||||||
"unauthorized": "Vous n'\u00eates pas autoris\u00e9 \u00e0 acc\u00e9der \u00e0 cette page",
|
|
||||||
"forbidden": "Acc\u00e8s refus\u00e9",
|
|
||||||
"serverError": "Erreur interne du serveur",
|
|
||||||
"networkError": "Erreur r\u00e9seau. V\u00e9rifiez votre connexion.",
|
|
||||||
"sessionExpired": "Votre session a expir\u00e9. Veuillez vous reconnecter.",
|
|
||||||
"validationError": "Veuillez v\u00e9rifier le formulaire pour les erreurs"
|
|
||||||
},
|
|
||||||
"notifications": {
|
|
||||||
"title": "Notifications",
|
|
||||||
"markAllRead": "Tout marquer comme lu",
|
|
||||||
"noNotifications": "Aucune notification",
|
|
||||||
"viewAll": "Voir toutes les notifications"
|
|
||||||
},
|
|
||||||
"coi": {
|
|
||||||
"title": "Conflit d'int\u00e9r\u00eats",
|
|
||||||
"declaration": "D\u00e9claration de COI",
|
|
||||||
"declareConflict": "D\u00e9clarer un conflit d'int\u00e9r\u00eats",
|
|
||||||
"noConflict": "Aucun conflit d'int\u00e9r\u00eats",
|
|
||||||
"hasConflict": "Conflit d\u00e9clar\u00e9",
|
|
||||||
"reason": "Raison du conflit",
|
|
||||||
"confirmDeclaration": "Je confirme que cette d\u00e9claration est exacte"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,55 @@
|
|||||||
import type { NextConfig } from 'next'
|
import type { NextConfig } from 'next'
|
||||||
import createNextIntlPlugin from 'next-intl/plugin'
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
const nextConfig: NextConfig = {
|
output: 'standalone',
|
||||||
output: 'standalone',
|
typedRoutes: true,
|
||||||
typedRoutes: true,
|
serverExternalPackages: ['@prisma/client', 'minio'],
|
||||||
serverExternalPackages: ['@prisma/client', 'minio'],
|
experimental: {
|
||||||
images: {
|
optimizePackageImports: ['lucide-react'],
|
||||||
remotePatterns: [
|
},
|
||||||
{
|
images: {
|
||||||
protocol: 'https',
|
remotePatterns: [
|
||||||
hostname: '*.minio.local',
|
{
|
||||||
},
|
protocol: 'https',
|
||||||
],
|
hostname: '*.minio.local',
|
||||||
},
|
},
|
||||||
}
|
],
|
||||||
|
},
|
||||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
|
async redirects() {
|
||||||
|
return [
|
||||||
export default withNextIntl(nextConfig)
|
// Legacy Pipeline/Stage routes → Competition/Round routes
|
||||||
|
{
|
||||||
|
source: '/admin/rounds/pipelines',
|
||||||
|
destination: '/admin/competitions',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/admin/rounds/pipeline/:path*',
|
||||||
|
destination: '/admin/competitions',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/jury/stages',
|
||||||
|
destination: '/jury/competitions',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/jury/stages/:path*',
|
||||||
|
destination: '/jury/competitions',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/applicant/pipeline',
|
||||||
|
destination: '/applicant/competitions',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/applicant/pipeline/:path*',
|
||||||
|
destination: '/applicant/competitions',
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nextConfig
|
||||||
|
|||||||
28631
package-lock.json
generated
28631
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
238
package.json
238
package.json
@@ -1,116 +1,122 @@
|
|||||||
{
|
{
|
||||||
"name": "mopc-platform",
|
"name": "mopc-platform",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"db:generate": "prisma generate",
|
"db:generate": "prisma generate",
|
||||||
"db:push": "prisma db push",
|
"db:push": "prisma db push",
|
||||||
"db:migrate": "prisma migrate dev",
|
"db:migrate": "prisma migrate dev",
|
||||||
"db:migrate:deploy": "prisma migrate deploy",
|
"db:migrate:deploy": "prisma migrate deploy",
|
||||||
"db:studio": "prisma studio",
|
"db:studio": "prisma studio",
|
||||||
"db:seed": "tsx prisma/seed.ts",
|
"db:seed": "tsx prisma/seed.ts",
|
||||||
"db:seed:candidatures": "tsx prisma/seed-candidatures.ts",
|
"db:seed:candidatures": "tsx prisma/seed-candidatures.ts",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.7.4",
|
"@auth/prisma-adapter": "^2.7.4",
|
||||||
"@blocknote/core": "^0.46.2",
|
"@blocknote/core": "^0.46.2",
|
||||||
"@blocknote/mantine": "^0.46.2",
|
"@blocknote/mantine": "^0.46.2",
|
||||||
"@blocknote/react": "^0.46.2",
|
"@blocknote/react": "^0.46.2",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@mantine/core": "^8.3.13",
|
"@mantine/core": "^8.3.13",
|
||||||
"@mantine/hooks": "^8.3.13",
|
"@mantine/hooks": "^8.3.13",
|
||||||
"@notionhq/client": "^2.3.0",
|
"@notionhq/client": "^2.3.0",
|
||||||
"@prisma/client": "^6.19.2",
|
"@prisma/client": "^6.19.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-avatar": "^1.1.2",
|
"@radix-ui/react-avatar": "^1.1.2",
|
||||||
"@radix-ui/react-checkbox": "^1.1.3",
|
"@radix-ui/react-checkbox": "^1.1.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.2",
|
"@radix-ui/react-collapsible": "^1.1.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.4",
|
"@radix-ui/react-popover": "^1.1.4",
|
||||||
"@radix-ui/react-progress": "^1.1.1",
|
"@radix-ui/react-progress": "^1.1.1",
|
||||||
"@radix-ui/react-radio-group": "^1.2.2",
|
"@radix-ui/react-radio-group": "^1.2.2",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.1.4",
|
"@radix-ui/react-select": "^2.1.4",
|
||||||
"@radix-ui/react-separator": "^1.1.1",
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
"@radix-ui/react-slider": "^1.2.2",
|
"@radix-ui/react-slider": "^1.2.2",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.2",
|
"@radix-ui/react-tabs": "^1.1.2",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
"@radix-ui/react-tooltip": "^1.1.6",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@tanstack/react-query": "^5.62.0",
|
"@tanstack/react-query": "^5.62.0",
|
||||||
"@trpc/client": "^11.0.0-rc.678",
|
"@trpc/client": "^11.0.0-rc.678",
|
||||||
"@trpc/react-query": "^11.0.0-rc.678",
|
"@trpc/react-query": "^11.0.0-rc.678",
|
||||||
"@trpc/server": "^11.0.0-rc.678",
|
"@trpc/server": "^11.0.0-rc.678",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.4",
|
"cmdk": "^1.0.4",
|
||||||
"date-fns": "^4.1.0",
|
"csv-parse": "^6.1.0",
|
||||||
"leaflet": "^1.9.4",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.563.0",
|
"franc": "^6.2.0",
|
||||||
"minio": "^8.0.2",
|
"html2canvas": "^1.4.1",
|
||||||
"motion": "^11.15.0",
|
"jspdf": "^4.1.0",
|
||||||
"next": "^15.1.0",
|
"jspdf-autotable": "^5.0.7",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"leaflet": "^1.9.4",
|
||||||
"next-intl": "^4.8.2",
|
"lucide-react": "^0.563.0",
|
||||||
"next-themes": "^0.4.6",
|
"minio": "^8.0.2",
|
||||||
"nodemailer": "^7.0.7",
|
"motion": "^11.15.0",
|
||||||
"openai": "^6.16.0",
|
"next": "^15.1.0",
|
||||||
"papaparse": "^5.4.1",
|
"next-auth": "^5.0.0-beta.25",
|
||||||
"react": "^19.0.0",
|
"next-themes": "^0.4.6",
|
||||||
"react-day-picker": "^9.13.0",
|
"nodemailer": "^7.0.7",
|
||||||
"react-dom": "^19.0.0",
|
"openai": "^6.16.0",
|
||||||
"react-easy-crop": "^5.5.6",
|
"papaparse": "^5.4.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"pdf-parse": "^2.4.5",
|
||||||
"react-leaflet": "^5.0.0",
|
"react": "^19.0.0",
|
||||||
"react-phone-number-input": "^3.4.14",
|
"react-day-picker": "^9.13.0",
|
||||||
"recharts": "^3.7.0",
|
"react-dom": "^19.0.0",
|
||||||
"sonner": "^2.0.7",
|
"react-easy-crop": "^5.5.6",
|
||||||
"superjson": "^2.2.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"tailwind-merge": "^3.4.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"use-debounce": "^10.0.4",
|
"react-phone-number-input": "^3.4.14",
|
||||||
"zod": "^3.24.1"
|
"recharts": "^3.7.0",
|
||||||
},
|
"sonner": "^2.0.7",
|
||||||
"devDependencies": {
|
"superjson": "^2.2.2",
|
||||||
"@playwright/test": "^1.49.1",
|
"tailwind-merge": "^3.4.0",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"use-debounce": "^10.0.4",
|
||||||
"@types/leaflet": "^1.9.21",
|
"zod": "^3.24.1"
|
||||||
"@types/node": "^25.0.10",
|
},
|
||||||
"@types/nodemailer": "^7.0.9",
|
"devDependencies": {
|
||||||
"@types/papaparse": "^5.3.15",
|
"@playwright/test": "^1.49.1",
|
||||||
"@types/react": "^19.0.2",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/leaflet": "^1.9.21",
|
||||||
"eslint": "^9.17.0",
|
"@types/node": "^25.0.10",
|
||||||
"eslint-config-next": "^15.1.0",
|
"@types/nodemailer": "^7.0.9",
|
||||||
"postcss": "^8.4.49",
|
"@types/papaparse": "^5.3.15",
|
||||||
"prettier": "^3.4.2",
|
"@types/pdf-parse": "^1.1.5",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"@types/react": "^19.0.2",
|
||||||
"prisma": "^6.19.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"tailwindcss": "^4.1.18",
|
"eslint": "^9.17.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"eslint-config-next": "^15.1.0",
|
||||||
"tsx": "^4.19.2",
|
"postcss": "^8.4.49",
|
||||||
"typescript": "^5.7.2",
|
"prettier": "^3.4.2",
|
||||||
"vitest": "^4.0.18"
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
},
|
"prisma": "^6.19.2",
|
||||||
"engines": {
|
"tailwindcss": "^4.1.18",
|
||||||
"node": ">=20.0.0"
|
"tailwindcss-animate": "^1.0.7",
|
||||||
},
|
"tsx": "^4.19.2",
|
||||||
"prisma": {
|
"typescript": "^5.7.2",
|
||||||
"seed": "tsx prisma/seed.ts"
|
"vitest": "^4.0.18"
|
||||||
}
|
},
|
||||||
}
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
|
|
||||||
async function check() {
|
|
||||||
const projectCount = await prisma.project.count()
|
|
||||||
console.log('Total projects:', projectCount)
|
|
||||||
|
|
||||||
const rounds = await prisma.round.findMany({
|
|
||||||
include: {
|
|
||||||
_count: { select: { projects: true } }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const r of rounds) {
|
|
||||||
console.log(`Round: ${r.name} (id: ${r.id})`)
|
|
||||||
console.log(` Projects: ${r._count.projects}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check sample projects with their round
|
|
||||||
const sampleProjects = await prisma.project.findMany({
|
|
||||||
select: { id: true, title: true, roundId: true },
|
|
||||||
take: 5
|
|
||||||
})
|
|
||||||
console.log('\nSample projects:')
|
|
||||||
for (const p of sampleProjects) {
|
|
||||||
console.log(` ${p.title}: roundId=${p.roundId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
check()
|
|
||||||
.then(() => prisma.$disconnect())
|
|
||||||
.catch(async (e) => {
|
|
||||||
console.error(e)
|
|
||||||
await prisma.$disconnect()
|
|
||||||
})
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
console.log('Checking all rounds...\n')
|
|
||||||
|
|
||||||
const rounds = await prisma.round.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
slug: true,
|
|
||||||
projects: { select: { id: true, title: true } },
|
|
||||||
_count: { select: { projects: true } }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`Found ${rounds.length} rounds:`)
|
|
||||||
for (const round of rounds) {
|
|
||||||
console.log(`- ${round.name} (slug: ${round.slug}): ${round._count.projects} projects`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find rounds with 9 or fewer projects (dummy data)
|
|
||||||
const dummyRounds = rounds.filter(r => r._count.projects <= 9)
|
|
||||||
|
|
||||||
if (dummyRounds.length > 0) {
|
|
||||||
console.log(`\nDeleting ${dummyRounds.length} dummy round(s)...`)
|
|
||||||
|
|
||||||
for (const round of dummyRounds) {
|
|
||||||
console.log(`\nProcessing: ${round.name}`)
|
|
||||||
|
|
||||||
const projectIds = round.projects.map(p => p.id)
|
|
||||||
|
|
||||||
if (projectIds.length > 0) {
|
|
||||||
// Delete team members
|
|
||||||
const teamDeleted = await prisma.teamMember.deleteMany({
|
|
||||||
where: { projectId: { in: projectIds } }
|
|
||||||
})
|
|
||||||
console.log(` Deleted ${teamDeleted.count} team members`)
|
|
||||||
|
|
||||||
// Delete projects
|
|
||||||
const projDeleted = await prisma.project.deleteMany({
|
|
||||||
where: { id: { in: projectIds } }
|
|
||||||
})
|
|
||||||
console.log(` Deleted ${projDeleted.count} projects`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the round
|
|
||||||
await prisma.round.delete({ where: { id: round.id } })
|
|
||||||
console.log(` Deleted round: ${round.name}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
const remaining = await prisma.round.count()
|
|
||||||
const projects = await prisma.project.count()
|
|
||||||
console.log(`\n✅ Cleanup complete!`)
|
|
||||||
console.log(` Remaining rounds: ${remaining}`)
|
|
||||||
console.log(` Total projects: ${projects}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => prisma.$disconnect())
|
|
||||||
.catch(async (e) => {
|
|
||||||
console.error(e)
|
|
||||||
await prisma.$disconnect()
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
|
|
||||||
async function cleanup() {
|
|
||||||
console.log('Cleaning up dummy data...\n')
|
|
||||||
|
|
||||||
// Find and delete the dummy round
|
|
||||||
const dummyRound = await prisma.round.findFirst({
|
|
||||||
where: { slug: 'round-1-2026' },
|
|
||||||
include: { projects: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
if (dummyRound) {
|
|
||||||
console.log(`Found dummy round: ${dummyRound.name}`)
|
|
||||||
console.log(`Projects in round: ${dummyRound.projects.length}`)
|
|
||||||
|
|
||||||
// Get project IDs to delete
|
|
||||||
const projectIds = dummyRound.projects.map(p => p.id)
|
|
||||||
|
|
||||||
// Delete team members for these projects
|
|
||||||
if (projectIds.length > 0) {
|
|
||||||
const teamDeleted = await prisma.teamMember.deleteMany({
|
|
||||||
where: { projectId: { in: projectIds } }
|
|
||||||
})
|
|
||||||
console.log(`Deleted ${teamDeleted.count} team members`)
|
|
||||||
|
|
||||||
// Delete the projects
|
|
||||||
const projDeleted = await prisma.project.deleteMany({
|
|
||||||
where: { id: { in: projectIds } }
|
|
||||||
})
|
|
||||||
console.log(`Deleted ${projDeleted.count} dummy projects`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the round
|
|
||||||
await prisma.round.delete({ where: { id: dummyRound.id } })
|
|
||||||
console.log('Deleted dummy round')
|
|
||||||
} else {
|
|
||||||
console.log('No dummy round found')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nCleanup complete!')
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup()
|
|
||||||
.then(() => prisma.$disconnect())
|
|
||||||
.catch(async (e) => {
|
|
||||||
console.error(e)
|
|
||||||
await prisma.$disconnect()
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
226
prisma/integrity-checks.ts
Normal file
226
prisma/integrity-checks.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
interface CheckResult {
|
||||||
|
name: string
|
||||||
|
passed: boolean
|
||||||
|
details: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runChecks(): Promise<CheckResult[]> {
|
||||||
|
const results: CheckResult[] = []
|
||||||
|
|
||||||
|
// 1. No orphan ProjectStageState (every PSS references valid project, track, stage)
|
||||||
|
const orphanStates = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "ProjectStageState" pss
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Project" p WHERE p.id = pss."projectId")
|
||||||
|
OR NOT EXISTS (SELECT 1 FROM "Track" t WHERE t.id = pss."trackId")
|
||||||
|
OR NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = pss."stageId")
|
||||||
|
`
|
||||||
|
const orphanCount = Number(orphanStates[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'No orphan ProjectStageState',
|
||||||
|
passed: orphanCount === 0,
|
||||||
|
details: orphanCount === 0 ? 'All PSS records reference valid entities' : `Found ${orphanCount} orphan records`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. Every project has at least one stage state
|
||||||
|
const projectsWithoutState = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "Project" p
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "ProjectStageState" pss WHERE pss."projectId" = p.id)
|
||||||
|
`
|
||||||
|
const noStateCount = Number(projectsWithoutState[0]?.count ?? 0)
|
||||||
|
const totalProjects = await prisma.project.count()
|
||||||
|
results.push({
|
||||||
|
name: 'Every project has at least one stage state',
|
||||||
|
passed: noStateCount === 0,
|
||||||
|
details: noStateCount === 0
|
||||||
|
? `All ${totalProjects} projects have stage states`
|
||||||
|
: `${noStateCount} projects missing stage states`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. No duplicate active states per (project, track, stage)
|
||||||
|
const duplicateStates = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM (
|
||||||
|
SELECT "projectId", "trackId", "stageId", COUNT(*) as cnt
|
||||||
|
FROM "ProjectStageState"
|
||||||
|
WHERE "exitedAt" IS NULL
|
||||||
|
GROUP BY "projectId", "trackId", "stageId"
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) dupes
|
||||||
|
`
|
||||||
|
const dupeCount = Number(duplicateStates[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'No duplicate active states per (project, track, stage)',
|
||||||
|
passed: dupeCount === 0,
|
||||||
|
details: dupeCount === 0 ? 'No duplicates found' : `Found ${dupeCount} duplicate active states`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. All transitions stay within same pipeline
|
||||||
|
const crossPipelineTransitions = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "StageTransition" st
|
||||||
|
JOIN "Stage" sf ON sf.id = st."fromStageId"
|
||||||
|
JOIN "Track" tf ON tf.id = sf."trackId"
|
||||||
|
JOIN "Stage" sto ON sto.id = st."toStageId"
|
||||||
|
JOIN "Track" tt ON tt.id = sto."trackId"
|
||||||
|
WHERE tf."pipelineId" != tt."pipelineId"
|
||||||
|
`
|
||||||
|
const crossCount = Number(crossPipelineTransitions[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'All transitions stay within same pipeline',
|
||||||
|
passed: crossCount === 0,
|
||||||
|
details: crossCount === 0 ? 'All transitions are within pipeline' : `Found ${crossCount} cross-pipeline transitions`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. Stage sortOrder unique per track
|
||||||
|
const duplicateSortOrders = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM (
|
||||||
|
SELECT "trackId", "sortOrder", COUNT(*) as cnt
|
||||||
|
FROM "Stage"
|
||||||
|
GROUP BY "trackId", "sortOrder"
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) dupes
|
||||||
|
`
|
||||||
|
const dupeSortCount = Number(duplicateSortOrders[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'Stage sortOrder unique per track',
|
||||||
|
passed: dupeSortCount === 0,
|
||||||
|
details: dupeSortCount === 0 ? 'All sort orders unique' : `Found ${dupeSortCount} duplicate sort orders`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 6. Track sortOrder unique per pipeline
|
||||||
|
const duplicateTrackOrders = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM (
|
||||||
|
SELECT "pipelineId", "sortOrder", COUNT(*) as cnt
|
||||||
|
FROM "Track"
|
||||||
|
GROUP BY "pipelineId", "sortOrder"
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) dupes
|
||||||
|
`
|
||||||
|
const dupeTrackCount = Number(duplicateTrackOrders[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'Track sortOrder unique per pipeline',
|
||||||
|
passed: dupeTrackCount === 0,
|
||||||
|
details: dupeTrackCount === 0 ? 'All track orders unique' : `Found ${dupeTrackCount} duplicate track orders`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 7. Every Pipeline has at least one Track; every Track has at least one Stage
|
||||||
|
const emptyPipelines = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "Pipeline" p
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Track" t WHERE t."pipelineId" = p.id)
|
||||||
|
`
|
||||||
|
const emptyPipelineCount = Number(emptyPipelines[0]?.count ?? 0)
|
||||||
|
const emptyTracks = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "Track" t
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s."trackId" = t.id)
|
||||||
|
`
|
||||||
|
const emptyTrackCount = Number(emptyTracks[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'Every Pipeline has Tracks; every Track has Stages',
|
||||||
|
passed: emptyPipelineCount === 0 && emptyTrackCount === 0,
|
||||||
|
details: emptyPipelineCount === 0 && emptyTrackCount === 0
|
||||||
|
? 'All pipelines have tracks and all tracks have stages'
|
||||||
|
: `${emptyPipelineCount} empty pipelines, ${emptyTrackCount} empty tracks`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 8. LiveProgressCursor references valid stage
|
||||||
|
const badCursors = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "LiveProgressCursor" lpc
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = lpc."stageId")
|
||||||
|
`
|
||||||
|
const badCursorCount = Number(badCursors[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'LiveProgressCursor references valid stage',
|
||||||
|
passed: badCursorCount === 0,
|
||||||
|
details: badCursorCount === 0
|
||||||
|
? 'All cursors reference valid stages'
|
||||||
|
: `Found ${badCursorCount} cursors with invalid stage references`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 10. Cohort references valid stage
|
||||||
|
const badCohorts = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "Cohort" c
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = c."stageId")
|
||||||
|
`
|
||||||
|
const badCohortCount = Number(badCohorts[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'Cohort references valid stage',
|
||||||
|
passed: badCohortCount === 0,
|
||||||
|
details: badCohortCount === 0
|
||||||
|
? 'All cohorts reference valid stages'
|
||||||
|
: `Found ${badCohortCount} cohorts with invalid stage references`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 11. Every EvaluationForm has a valid stageId
|
||||||
|
const badEvalForms = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "EvaluationForm" ef
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = ef."stageId")
|
||||||
|
`
|
||||||
|
const badFormCount = Number(badEvalForms[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'Every EvaluationForm references valid stage',
|
||||||
|
passed: badFormCount === 0,
|
||||||
|
details: badFormCount === 0
|
||||||
|
? 'All evaluation forms reference valid stages'
|
||||||
|
: `Found ${badFormCount} forms with invalid stage references`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 12. Every FileRequirement has a valid stageId
|
||||||
|
const badFileReqs = await prisma.$queryRaw<{ count: bigint }[]>`
|
||||||
|
SELECT COUNT(*) as count FROM "FileRequirement" fr
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM "Round" r WHERE r.id = fr."roundId")
|
||||||
|
`
|
||||||
|
const badFileReqCount = Number(badFileReqs[0]?.count ?? 0)
|
||||||
|
results.push({
|
||||||
|
name: 'Every FileRequirement references valid round',
|
||||||
|
passed: badFileReqCount === 0,
|
||||||
|
details: badFileReqCount === 0
|
||||||
|
? 'All file requirements reference valid rounds'
|
||||||
|
: `Found ${badFileReqCount} file requirements with invalid round references`,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 13. Count validation
|
||||||
|
const projectCountResult = await prisma.project.count()
|
||||||
|
const roundCount = await prisma.round.count()
|
||||||
|
const competitionCount = await prisma.competition.count()
|
||||||
|
const prsCount = await prisma.projectRoundState.count()
|
||||||
|
results.push({
|
||||||
|
name: 'Count validation',
|
||||||
|
passed: projectCountResult > 0 && roundCount > 0 && competitionCount > 0,
|
||||||
|
details: `Competitions: ${competitionCount}, Rounds: ${roundCount}, Projects: ${projectCountResult}, RoundStates: ${prsCount}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('🔍 Running integrity checks...\n')
|
||||||
|
|
||||||
|
const results = await runChecks()
|
||||||
|
|
||||||
|
let allPassed = true
|
||||||
|
for (const result of results) {
|
||||||
|
const icon = result.passed ? '✅' : '❌'
|
||||||
|
console.log(`${icon} ${result.name}`)
|
||||||
|
console.log(` ${result.details}\n`)
|
||||||
|
if (!result.passed) allPassed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('='.repeat(50))
|
||||||
|
if (allPassed) {
|
||||||
|
console.log('✅ All integrity checks passed!')
|
||||||
|
} else {
|
||||||
|
console.log('❌ Some integrity checks failed!')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('❌ Integrity check failed:', e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
})
|
||||||
@@ -5,7 +5,7 @@ CREATE SCHEMA IF NOT EXISTS "public";
|
|||||||
CREATE TYPE "UserRole" AS ENUM ('SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT');
|
CREATE TYPE "UserRole" AS ENUM ('SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT');
|
||||||
|
|
||||||
-- CreateEnum
|
-- CreateEnum
|
||||||
CREATE TYPE "UserStatus" AS ENUM ('INVITED', 'ACTIVE', 'SUSPENDED');
|
CREATE TYPE "UserStatus" AS ENUM ('NONE', 'INVITED', 'ACTIVE', 'SUSPENDED');
|
||||||
|
|
||||||
-- CreateEnum
|
-- CreateEnum
|
||||||
CREATE TYPE "ProgramStatus" AS ENUM ('DRAFT', 'ACTIVE', 'ARCHIVED');
|
CREATE TYPE "ProgramStatus" AS ENUM ('DRAFT', 'ACTIVE', 'ARCHIVED');
|
||||||
|
|||||||
@@ -16,105 +16,143 @@
|
|||||||
-- the enum.
|
-- the enum.
|
||||||
|
|
||||||
|
|
||||||
ALTER TYPE "SettingCategory" ADD VALUE 'DIGEST';
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'DIGEST'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
ALTER TYPE "SettingCategory" ADD VALUE 'ANALYTICS';
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'ANALYTICS'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
ALTER TYPE "SettingCategory" ADD VALUE 'AUDIT_CONFIG';
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'AUDIT_CONFIG'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
ALTER TYPE "SettingCategory" ADD VALUE 'INTEGRATIONS';
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'INTEGRATIONS'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
ALTER TYPE "SettingCategory" ADD VALUE 'LOCALIZATION';
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'LOCALIZATION'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
ALTER TYPE "SettingCategory" ADD VALUE 'COMMUNICATION';
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'COMMUNICATION'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "ApplicationForm" DROP CONSTRAINT "ApplicationForm_programId_fkey";
|
ALTER TABLE "ApplicationForm" DROP CONSTRAINT IF EXISTS "ApplicationForm_programId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "ApplicationForm" DROP CONSTRAINT "ApplicationForm_roundId_fkey";
|
ALTER TABLE "ApplicationForm" DROP CONSTRAINT IF EXISTS "ApplicationForm_roundId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "ApplicationFormField" DROP CONSTRAINT "ApplicationFormField_formId_fkey";
|
ALTER TABLE "ApplicationFormField" DROP CONSTRAINT IF EXISTS "ApplicationFormField_formId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "ApplicationFormField" DROP CONSTRAINT "ApplicationFormField_stepId_fkey";
|
ALTER TABLE "ApplicationFormField" DROP CONSTRAINT IF EXISTS "ApplicationFormField_stepId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "ApplicationFormSubmission" DROP CONSTRAINT "ApplicationFormSubmission_formId_fkey";
|
ALTER TABLE "ApplicationFormSubmission" DROP CONSTRAINT IF EXISTS "ApplicationFormSubmission_formId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "OnboardingStep" DROP CONSTRAINT "OnboardingStep_formId_fkey";
|
ALTER TABLE "OnboardingStep" DROP CONSTRAINT IF EXISTS "OnboardingStep_formId_fkey";
|
||||||
|
|
||||||
-- DropForeignKey
|
-- DropForeignKey
|
||||||
ALTER TABLE "SubmissionFile" DROP CONSTRAINT "SubmissionFile_submissionId_fkey";
|
ALTER TABLE "SubmissionFile" DROP CONSTRAINT IF EXISTS "SubmissionFile_submissionId_fkey";
|
||||||
|
|
||||||
-- DropIndex
|
-- DropIndex
|
||||||
DROP INDEX "User_email_idx";
|
DROP INDEX IF EXISTS "User_email_idx";
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "AssignmentJob" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
DO $$ BEGIN ALTER TABLE "AssignmentJob" ALTER COLUMN "updatedAt" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "AuditLog" ADD COLUMN "previousDataJson" JSONB,
|
DO $$ BEGIN
|
||||||
ADD COLUMN "sessionId" TEXT;
|
ALTER TABLE "AuditLog" ADD COLUMN "previousDataJson" JSONB;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AuditLog" ADD COLUMN "sessionId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "FilteringJob" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
DO $$ BEGIN ALTER TABLE "FilteringJob" ALTER COLUMN "updatedAt" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "LiveVote" ADD COLUMN "isAudienceVote" BOOLEAN NOT NULL DEFAULT false;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVote" ADD COLUMN "isAudienceVote" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "LiveVotingSession" ADD COLUMN "allowAudienceVotes" BOOLEAN NOT NULL DEFAULT false,
|
DO $$ BEGIN
|
||||||
ADD COLUMN "audienceVoteWeight" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
ALTER TABLE "LiveVotingSession" ADD COLUMN "allowAudienceVotes" BOOLEAN NOT NULL DEFAULT false;
|
||||||
ADD COLUMN "presentationSettingsJson" JSONB,
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
ADD COLUMN "tieBreakerMethod" TEXT NOT NULL DEFAULT 'admin_decides';
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN "audienceVoteWeight" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN "presentationSettingsJson" JSONB;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN "tieBreakerMethod" TEXT NOT NULL DEFAULT 'admin_decides';
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "MentorAssignment" ADD COLUMN "completionStatus" TEXT NOT NULL DEFAULT 'in_progress',
|
DO $$ BEGIN
|
||||||
ADD COLUMN "lastViewedAt" TIMESTAMP(3);
|
ALTER TABLE "MentorAssignment" ADD COLUMN "completionStatus" TEXT NOT NULL DEFAULT 'in_progress';
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorAssignment" ADD COLUMN "lastViewedAt" TIMESTAMP(3);
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "NotificationEmailSetting" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
DO $$ BEGIN ALTER TABLE "NotificationEmailSetting" ALTER COLUMN "updatedAt" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "Project" ADD COLUMN "draftDataJson" JSONB,
|
DO $$ BEGIN
|
||||||
ADD COLUMN "draftExpiresAt" TIMESTAMP(3),
|
ALTER TABLE "Project" ADD COLUMN "draftDataJson" JSONB;
|
||||||
ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Project" ADD COLUMN "draftExpiresAt" TIMESTAMP(3);
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Project" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "ProjectFile" ADD COLUMN "isLate" BOOLEAN NOT NULL DEFAULT false,
|
DO $$ BEGIN
|
||||||
ADD COLUMN "replacedById" TEXT,
|
ALTER TABLE "ProjectFile" ADD COLUMN "isLate" BOOLEAN NOT NULL DEFAULT false;
|
||||||
ADD COLUMN "roundId" TEXT,
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "replacedById" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "roundId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "TaggingJob" ALTER COLUMN "updatedAt" DROP DEFAULT;
|
DO $$ BEGIN ALTER TABLE "TaggingJob" ALTER COLUMN "updatedAt" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
ALTER TABLE "User" ADD COLUMN "availabilityJson" JSONB,
|
DO $$ BEGIN
|
||||||
ADD COLUMN "digestFrequency" TEXT NOT NULL DEFAULT 'none',
|
ALTER TABLE "User" ADD COLUMN "availabilityJson" JSONB;
|
||||||
ADD COLUMN "preferredWorkload" INTEGER;
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "User" ADD COLUMN "digestFrequency" TEXT NOT NULL DEFAULT 'none';
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "User" ADD COLUMN "preferredWorkload" INTEGER;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
-- DropTable
|
-- DropTable
|
||||||
DROP TABLE "ApplicationForm";
|
DROP TABLE IF EXISTS "ApplicationForm";
|
||||||
|
|
||||||
-- DropTable
|
-- DropTable
|
||||||
DROP TABLE "ApplicationFormField";
|
DROP TABLE IF EXISTS "ApplicationFormField";
|
||||||
|
|
||||||
-- DropTable
|
-- DropTable
|
||||||
DROP TABLE "ApplicationFormSubmission";
|
DROP TABLE IF EXISTS "ApplicationFormSubmission";
|
||||||
|
|
||||||
-- DropTable
|
-- DropTable
|
||||||
DROP TABLE "OnboardingStep";
|
DROP TABLE IF EXISTS "OnboardingStep";
|
||||||
|
|
||||||
-- DropTable
|
-- DropTable
|
||||||
DROP TABLE "SubmissionFile";
|
DROP TABLE IF EXISTS "SubmissionFile";
|
||||||
|
|
||||||
-- DropEnum
|
-- DropEnum
|
||||||
DROP TYPE "FormFieldType";
|
DROP TYPE IF EXISTS "FormFieldType";
|
||||||
|
|
||||||
-- DropEnum
|
-- DropEnum
|
||||||
DROP TYPE "SpecialFieldType";
|
DROP TYPE IF EXISTS "SpecialFieldType";
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "ReminderLog" (
|
CREATE TABLE IF NOT EXISTS "ReminderLog" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"roundId" TEXT NOT NULL,
|
"roundId" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" TEXT NOT NULL,
|
||||||
@@ -125,7 +163,7 @@ CREATE TABLE "ReminderLog" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "ConflictOfInterest" (
|
CREATE TABLE IF NOT EXISTS "ConflictOfInterest" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"assignmentId" TEXT NOT NULL,
|
"assignmentId" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" TEXT NOT NULL,
|
||||||
@@ -143,7 +181,7 @@ CREATE TABLE "ConflictOfInterest" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "EvaluationSummary" (
|
CREATE TABLE IF NOT EXISTS "EvaluationSummary" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"projectId" TEXT NOT NULL,
|
"projectId" TEXT NOT NULL,
|
||||||
"roundId" TEXT NOT NULL,
|
"roundId" TEXT NOT NULL,
|
||||||
@@ -157,7 +195,7 @@ CREATE TABLE "EvaluationSummary" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "ProjectStatusHistory" (
|
CREATE TABLE IF NOT EXISTS "ProjectStatusHistory" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"projectId" TEXT NOT NULL,
|
"projectId" TEXT NOT NULL,
|
||||||
"status" "ProjectStatus" NOT NULL,
|
"status" "ProjectStatus" NOT NULL,
|
||||||
@@ -168,7 +206,7 @@ CREATE TABLE "ProjectStatusHistory" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "MentorMessage" (
|
CREATE TABLE IF NOT EXISTS "MentorMessage" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"projectId" TEXT NOT NULL,
|
"projectId" TEXT NOT NULL,
|
||||||
"senderId" TEXT NOT NULL,
|
"senderId" TEXT NOT NULL,
|
||||||
@@ -180,7 +218,7 @@ CREATE TABLE "MentorMessage" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "DigestLog" (
|
CREATE TABLE IF NOT EXISTS "DigestLog" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" TEXT NOT NULL,
|
||||||
"digestType" TEXT NOT NULL,
|
"digestType" TEXT NOT NULL,
|
||||||
@@ -191,7 +229,7 @@ CREATE TABLE "DigestLog" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "RoundTemplate" (
|
CREATE TABLE IF NOT EXISTS "RoundTemplate" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"description" TEXT,
|
"description" TEXT,
|
||||||
@@ -208,7 +246,7 @@ CREATE TABLE "RoundTemplate" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "MentorNote" (
|
CREATE TABLE IF NOT EXISTS "MentorNote" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"mentorAssignmentId" TEXT NOT NULL,
|
"mentorAssignmentId" TEXT NOT NULL,
|
||||||
"authorId" TEXT NOT NULL,
|
"authorId" TEXT NOT NULL,
|
||||||
@@ -221,7 +259,7 @@ CREATE TABLE "MentorNote" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "MentorMilestone" (
|
CREATE TABLE IF NOT EXISTS "MentorMilestone" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"programId" TEXT NOT NULL,
|
"programId" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
@@ -236,7 +274,7 @@ CREATE TABLE "MentorMilestone" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "MentorMilestoneCompletion" (
|
CREATE TABLE IF NOT EXISTS "MentorMilestoneCompletion" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"milestoneId" TEXT NOT NULL,
|
"milestoneId" TEXT NOT NULL,
|
||||||
"mentorAssignmentId" TEXT NOT NULL,
|
"mentorAssignmentId" TEXT NOT NULL,
|
||||||
@@ -247,7 +285,7 @@ CREATE TABLE "MentorMilestoneCompletion" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Message" (
|
CREATE TABLE IF NOT EXISTS "Message" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"senderId" TEXT NOT NULL,
|
"senderId" TEXT NOT NULL,
|
||||||
"recipientType" TEXT NOT NULL,
|
"recipientType" TEXT NOT NULL,
|
||||||
@@ -266,7 +304,7 @@ CREATE TABLE "Message" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "MessageTemplate" (
|
CREATE TABLE IF NOT EXISTS "MessageTemplate" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"category" TEXT NOT NULL,
|
"category" TEXT NOT NULL,
|
||||||
@@ -282,7 +320,7 @@ CREATE TABLE "MessageTemplate" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "MessageRecipient" (
|
CREATE TABLE IF NOT EXISTS "MessageRecipient" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"messageId" TEXT NOT NULL,
|
"messageId" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" TEXT NOT NULL,
|
||||||
@@ -295,7 +333,7 @@ CREATE TABLE "MessageRecipient" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "Webhook" (
|
CREATE TABLE IF NOT EXISTS "Webhook" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"name" TEXT NOT NULL,
|
"name" TEXT NOT NULL,
|
||||||
"url" TEXT NOT NULL,
|
"url" TEXT NOT NULL,
|
||||||
@@ -312,7 +350,7 @@ CREATE TABLE "Webhook" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "WebhookDelivery" (
|
CREATE TABLE IF NOT EXISTS "WebhookDelivery" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"webhookId" TEXT NOT NULL,
|
"webhookId" TEXT NOT NULL,
|
||||||
"event" TEXT NOT NULL,
|
"event" TEXT NOT NULL,
|
||||||
@@ -328,7 +366,7 @@ CREATE TABLE "WebhookDelivery" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "EvaluationDiscussion" (
|
CREATE TABLE IF NOT EXISTS "EvaluationDiscussion" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"projectId" TEXT NOT NULL,
|
"projectId" TEXT NOT NULL,
|
||||||
"roundId" TEXT NOT NULL,
|
"roundId" TEXT NOT NULL,
|
||||||
@@ -341,7 +379,7 @@ CREATE TABLE "EvaluationDiscussion" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateTable
|
-- CreateTable
|
||||||
CREATE TABLE "DiscussionComment" (
|
CREATE TABLE IF NOT EXISTS "DiscussionComment" (
|
||||||
"id" TEXT NOT NULL,
|
"id" TEXT NOT NULL,
|
||||||
"discussionId" TEXT NOT NULL,
|
"discussionId" TEXT NOT NULL,
|
||||||
"userId" TEXT NOT NULL,
|
"userId" TEXT NOT NULL,
|
||||||
@@ -352,199 +390,257 @@ CREATE TABLE "DiscussionComment" (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "ReminderLog_roundId_idx" ON "ReminderLog"("roundId");
|
CREATE INDEX IF NOT EXISTS "ReminderLog_roundId_idx" ON "ReminderLog"("roundId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "ReminderLog_roundId_userId_type_key" ON "ReminderLog"("roundId", "userId", "type");
|
CREATE UNIQUE INDEX IF NOT EXISTS "ReminderLog_roundId_userId_type_key" ON "ReminderLog"("roundId", "userId", "type");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "ConflictOfInterest_assignmentId_key" ON "ConflictOfInterest"("assignmentId");
|
CREATE UNIQUE INDEX IF NOT EXISTS "ConflictOfInterest_assignmentId_key" ON "ConflictOfInterest"("assignmentId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "ConflictOfInterest_userId_idx" ON "ConflictOfInterest"("userId");
|
CREATE INDEX IF NOT EXISTS "ConflictOfInterest_userId_idx" ON "ConflictOfInterest"("userId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "ConflictOfInterest_roundId_hasConflict_idx" ON "ConflictOfInterest"("roundId", "hasConflict");
|
CREATE INDEX IF NOT EXISTS "ConflictOfInterest_roundId_hasConflict_idx" ON "ConflictOfInterest"("roundId", "hasConflict");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "EvaluationSummary_roundId_idx" ON "EvaluationSummary"("roundId");
|
CREATE INDEX IF NOT EXISTS "EvaluationSummary_roundId_idx" ON "EvaluationSummary"("roundId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "EvaluationSummary_projectId_roundId_key" ON "EvaluationSummary"("projectId", "roundId");
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationSummary_projectId_roundId_key" ON "EvaluationSummary"("projectId", "roundId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "ProjectStatusHistory_projectId_changedAt_idx" ON "ProjectStatusHistory"("projectId", "changedAt");
|
CREATE INDEX IF NOT EXISTS "ProjectStatusHistory_projectId_changedAt_idx" ON "ProjectStatusHistory"("projectId", "changedAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MentorMessage_projectId_createdAt_idx" ON "MentorMessage"("projectId", "createdAt");
|
CREATE INDEX IF NOT EXISTS "MentorMessage_projectId_createdAt_idx" ON "MentorMessage"("projectId", "createdAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "DigestLog_userId_idx" ON "DigestLog"("userId");
|
CREATE INDEX IF NOT EXISTS "DigestLog_userId_idx" ON "DigestLog"("userId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "DigestLog_sentAt_idx" ON "DigestLog"("sentAt");
|
CREATE INDEX IF NOT EXISTS "DigestLog_sentAt_idx" ON "DigestLog"("sentAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "RoundTemplate_programId_idx" ON "RoundTemplate"("programId");
|
CREATE INDEX IF NOT EXISTS "RoundTemplate_programId_idx" ON "RoundTemplate"("programId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MentorNote_mentorAssignmentId_idx" ON "MentorNote"("mentorAssignmentId");
|
CREATE INDEX IF NOT EXISTS "MentorNote_mentorAssignmentId_idx" ON "MentorNote"("mentorAssignmentId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MentorMilestone_programId_idx" ON "MentorMilestone"("programId");
|
CREATE INDEX IF NOT EXISTS "MentorMilestone_programId_idx" ON "MentorMilestone"("programId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MentorMilestone_sortOrder_idx" ON "MentorMilestone"("sortOrder");
|
CREATE INDEX IF NOT EXISTS "MentorMilestone_sortOrder_idx" ON "MentorMilestone"("sortOrder");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MentorMilestoneCompletion_mentorAssignmentId_idx" ON "MentorMilestoneCompletion"("mentorAssignmentId");
|
CREATE INDEX IF NOT EXISTS "MentorMilestoneCompletion_mentorAssignmentId_idx" ON "MentorMilestoneCompletion"("mentorAssignmentId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "MentorMilestoneCompletion_milestoneId_mentorAssignmentId_key" ON "MentorMilestoneCompletion"("milestoneId", "mentorAssignmentId");
|
CREATE UNIQUE INDEX IF NOT EXISTS "MentorMilestoneCompletion_milestoneId_mentorAssignmentId_key" ON "MentorMilestoneCompletion"("milestoneId", "mentorAssignmentId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "Message_senderId_idx" ON "Message"("senderId");
|
CREATE INDEX IF NOT EXISTS "Message_senderId_idx" ON "Message"("senderId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "Message_sentAt_idx" ON "Message"("sentAt");
|
CREATE INDEX IF NOT EXISTS "Message_sentAt_idx" ON "Message"("sentAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "Message_scheduledAt_idx" ON "Message"("scheduledAt");
|
CREATE INDEX IF NOT EXISTS "Message_scheduledAt_idx" ON "Message"("scheduledAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MessageTemplate_category_idx" ON "MessageTemplate"("category");
|
CREATE INDEX IF NOT EXISTS "MessageTemplate_category_idx" ON "MessageTemplate"("category");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MessageTemplate_isActive_idx" ON "MessageTemplate"("isActive");
|
CREATE INDEX IF NOT EXISTS "MessageTemplate_isActive_idx" ON "MessageTemplate"("isActive");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MessageRecipient_messageId_idx" ON "MessageRecipient"("messageId");
|
CREATE INDEX IF NOT EXISTS "MessageRecipient_messageId_idx" ON "MessageRecipient"("messageId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "MessageRecipient_userId_isRead_idx" ON "MessageRecipient"("userId", "isRead");
|
CREATE INDEX IF NOT EXISTS "MessageRecipient_userId_isRead_idx" ON "MessageRecipient"("userId", "isRead");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "Webhook_isActive_idx" ON "Webhook"("isActive");
|
CREATE INDEX IF NOT EXISTS "Webhook_isActive_idx" ON "Webhook"("isActive");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "WebhookDelivery_webhookId_idx" ON "WebhookDelivery"("webhookId");
|
CREATE INDEX IF NOT EXISTS "WebhookDelivery_webhookId_idx" ON "WebhookDelivery"("webhookId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "WebhookDelivery_status_idx" ON "WebhookDelivery"("status");
|
CREATE INDEX IF NOT EXISTS "WebhookDelivery_status_idx" ON "WebhookDelivery"("status");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "WebhookDelivery_createdAt_idx" ON "WebhookDelivery"("createdAt");
|
CREATE INDEX IF NOT EXISTS "WebhookDelivery_createdAt_idx" ON "WebhookDelivery"("createdAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "EvaluationDiscussion_roundId_idx" ON "EvaluationDiscussion"("roundId");
|
CREATE INDEX IF NOT EXISTS "EvaluationDiscussion_roundId_idx" ON "EvaluationDiscussion"("roundId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "EvaluationDiscussion_status_idx" ON "EvaluationDiscussion"("status");
|
CREATE INDEX IF NOT EXISTS "EvaluationDiscussion_status_idx" ON "EvaluationDiscussion"("status");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX "EvaluationDiscussion_projectId_roundId_key" ON "EvaluationDiscussion"("projectId", "roundId");
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationDiscussion_projectId_roundId_key" ON "EvaluationDiscussion"("projectId", "roundId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "DiscussionComment_discussionId_createdAt_idx" ON "DiscussionComment"("discussionId", "createdAt");
|
CREATE INDEX IF NOT EXISTS "DiscussionComment_discussionId_createdAt_idx" ON "DiscussionComment"("discussionId", "createdAt");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "AuditLog_entityType_entityId_timestamp_idx" ON "AuditLog"("entityType", "entityId", "timestamp");
|
CREATE INDEX IF NOT EXISTS "AuditLog_entityType_entityId_timestamp_idx" ON "AuditLog"("entityType", "entityId", "timestamp");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "Evaluation_status_formId_idx" ON "Evaluation"("status", "formId");
|
CREATE INDEX IF NOT EXISTS "Evaluation_status_formId_idx" ON "Evaluation"("status", "formId");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "GracePeriod_roundId_userId_extendedUntil_idx" ON "GracePeriod"("roundId", "userId", "extendedUntil");
|
CREATE INDEX IF NOT EXISTS "GracePeriod_roundId_userId_extendedUntil_idx" ON "GracePeriod"("roundId", "userId", "extendedUntil");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "LiveVote_isAudienceVote_idx" ON "LiveVote"("isAudienceVote");
|
CREATE INDEX IF NOT EXISTS "LiveVote_isAudienceVote_idx" ON "LiveVote"("isAudienceVote");
|
||||||
|
|
||||||
-- CreateIndex
|
-- CreateIndex
|
||||||
CREATE INDEX "ProjectFile_roundId_idx" ON "ProjectFile"("roundId");
|
CREATE INDEX IF NOT EXISTS "ProjectFile_roundId_idx" ON "ProjectFile"("roundId");
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_winnerOverriddenBy_fkey" FOREIGN KEY ("winnerOverriddenBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_winnerOverriddenBy_fkey" FOREIGN KEY ("winnerOverriddenBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_generatedById_fkey" FOREIGN KEY ("generatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_generatedById_fkey" FOREIGN KEY ("generatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "ProjectStatusHistory" ADD CONSTRAINT "ProjectStatusHistory_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectStatusHistory" ADD CONSTRAINT "ProjectStatusHistory_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorMessage" ADD CONSTRAINT "MentorMessage_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMessage" ADD CONSTRAINT "MentorMessage_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorMessage" ADD CONSTRAINT "MentorMessage_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMessage" ADD CONSTRAINT "MentorMessage_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "DigestLog" ADD CONSTRAINT "DigestLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DigestLog" ADD CONSTRAINT "DigestLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorNote" ADD CONSTRAINT "MentorNote_mentorAssignmentId_fkey" FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorNote" ADD CONSTRAINT "MentorNote_mentorAssignmentId_fkey" FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorNote" ADD CONSTRAINT "MentorNote_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorNote" ADD CONSTRAINT "MentorNote_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorMilestone" ADD CONSTRAINT "MentorMilestone_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMilestone" ADD CONSTRAINT "MentorMilestone_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorMilestoneCompletion" ADD CONSTRAINT "MentorMilestoneCompletion_milestoneId_fkey" FOREIGN KEY ("milestoneId") REFERENCES "MentorMilestone"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMilestoneCompletion" ADD CONSTRAINT "MentorMilestoneCompletion_milestoneId_fkey" FOREIGN KEY ("milestoneId") REFERENCES "MentorMilestone"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorMilestoneCompletion" ADD CONSTRAINT "MentorMilestoneCompletion_mentorAssignmentId_fkey" FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMilestoneCompletion" ADD CONSTRAINT "MentorMilestoneCompletion_mentorAssignmentId_fkey" FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MentorMilestoneCompletion" ADD CONSTRAINT "MentorMilestoneCompletion_completedById_fkey" FOREIGN KEY ("completedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMilestoneCompletion" ADD CONSTRAINT "MentorMilestoneCompletion_completedById_fkey" FOREIGN KEY ("completedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "MessageTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "MessageTemplate"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MessageRecipient" ADD CONSTRAINT "MessageRecipient_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MessageRecipient" ADD CONSTRAINT "MessageRecipient_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "MessageRecipient" ADD CONSTRAINT "MessageRecipient_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MessageRecipient" ADD CONSTRAINT "MessageRecipient_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "WebhookDelivery" ADD CONSTRAINT "WebhookDelivery_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "Webhook"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "WebhookDelivery" ADD CONSTRAINT "WebhookDelivery_webhookId_fkey" FOREIGN KEY ("webhookId") REFERENCES "Webhook"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_closedById_fkey" FOREIGN KEY ("closedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "DiscussionComment" ADD CONSTRAINT "DiscussionComment_discussionId_fkey" FOREIGN KEY ("discussionId") REFERENCES "EvaluationDiscussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DiscussionComment" ADD CONSTRAINT "DiscussionComment_discussionId_fkey" FOREIGN KEY ("discussionId") REFERENCES "EvaluationDiscussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AddForeignKey
|
-- AddForeignKey
|
||||||
ALTER TABLE "DiscussionComment" ADD CONSTRAINT "DiscussionComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DiscussionComment" ADD CONSTRAINT "DiscussionComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|||||||
@@ -6,36 +6,46 @@
|
|||||||
-- Missing Foreign Keys
|
-- Missing Foreign Keys
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
-- RoundTemplate → Program
|
-- RoundTemplate -> Program
|
||||||
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_programId_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_programId_fkey"
|
||||||
|
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- RoundTemplate → User (creator)
|
-- RoundTemplate -> User (creator)
|
||||||
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_createdBy_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_createdBy_fkey"
|
||||||
|
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- Message → Round
|
-- Message -> Round
|
||||||
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- EvaluationDiscussion → Round
|
-- EvaluationDiscussion -> Round
|
||||||
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- ProjectFile → ProjectFile (self-relation for file versioning)
|
-- ProjectFile -> ProjectFile (self-relation for file versioning)
|
||||||
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_replacedById_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("replacedById") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_replacedById_fkey"
|
||||||
|
FOREIGN KEY ("replacedById") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
-- Missing Indexes
|
-- Missing Indexes
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
CREATE INDEX "RoundTemplate_roundType_idx" ON "RoundTemplate"("roundType");
|
CREATE INDEX IF NOT EXISTS "RoundTemplate_roundType_idx" ON "RoundTemplate"("roundType");
|
||||||
CREATE INDEX "MentorNote_authorId_idx" ON "MentorNote"("authorId");
|
CREATE INDEX IF NOT EXISTS "MentorNote_authorId_idx" ON "MentorNote"("authorId");
|
||||||
CREATE INDEX "MentorMilestoneCompletion_completedById_idx" ON "MentorMilestoneCompletion"("completedById");
|
CREATE INDEX IF NOT EXISTS "MentorMilestoneCompletion_completedById_idx" ON "MentorMilestoneCompletion"("completedById");
|
||||||
CREATE INDEX "Webhook_createdById_idx" ON "Webhook"("createdById");
|
CREATE INDEX IF NOT EXISTS "Webhook_createdById_idx" ON "Webhook"("createdById");
|
||||||
CREATE INDEX "WebhookDelivery_event_idx" ON "WebhookDelivery"("event");
|
CREATE INDEX IF NOT EXISTS "WebhookDelivery_event_idx" ON "WebhookDelivery"("event");
|
||||||
CREATE INDEX "Message_roundId_idx" ON "Message"("roundId");
|
CREATE INDEX IF NOT EXISTS "Message_roundId_idx" ON "Message"("roundId");
|
||||||
CREATE INDEX "EvaluationDiscussion_closedById_idx" ON "EvaluationDiscussion"("closedById");
|
CREATE INDEX IF NOT EXISTS "EvaluationDiscussion_closedById_idx" ON "EvaluationDiscussion"("closedById");
|
||||||
CREATE INDEX "DiscussionComment_discussionId_idx" ON "DiscussionComment"("discussionId");
|
CREATE INDEX IF NOT EXISTS "DiscussionComment_discussionId_idx" ON "DiscussionComment"("discussionId");
|
||||||
CREATE INDEX "DiscussionComment_userId_idx" ON "DiscussionComment"("userId");
|
CREATE INDEX IF NOT EXISTS "DiscussionComment_userId_idx" ON "DiscussionComment"("userId");
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference
|
-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference
|
||||||
|
|
||||||
-- AlterTable: Evaluation.formId -> onDelete CASCADE
|
-- AlterTable: Evaluation.formId -> onDelete CASCADE
|
||||||
ALTER TABLE "Evaluation" DROP CONSTRAINT "Evaluation_formId_fkey";
|
ALTER TABLE "Evaluation" DROP CONSTRAINT IF EXISTS "Evaluation_formId_fkey";
|
||||||
ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey"
|
||||||
|
FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
-- AlterTable: ProjectFile.roundId -> onDelete SET NULL
|
-- AlterTable: ProjectFile.roundId -> onDelete SET NULL
|
||||||
ALTER TABLE "ProjectFile" DROP CONSTRAINT "ProjectFile_roundId_fkey";
|
ALTER TABLE "ProjectFile" DROP CONSTRAINT IF EXISTS "ProjectFile_roundId_fkey";
|
||||||
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey"
|
DO $$ BEGIN
|
||||||
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE IF NOT EXISTS "FileRequirement" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"acceptedMimeTypes" TEXT[],
|
||||||
|
"maxSizeMB" INTEGER,
|
||||||
|
"isRequired" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FileRequirement_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "FileRequirement_roundId_idx" ON "FileRequirement"("roundId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- AlterTable: add requirementId to ProjectFile
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "requirementId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectFile_requirementId_idx" ON "ProjectFile"("requirementId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_requirementId_fkey" FOREIGN KEY ("requirementId") REFERENCES "FileRequirement"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
-- Migration: Add all missing schema elements not covered by previous migrations
|
||||||
|
-- This brings the database fully in line with prisma/schema.prisma
|
||||||
|
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 1. MISSING TABLE: WizardTemplate
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "WizardTemplate" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"config" JSONB NOT NULL,
|
||||||
|
"isGlobal" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"programId" TEXT,
|
||||||
|
"createdBy" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WizardTemplate_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "WizardTemplate_programId_idx" ON "WizardTemplate"("programId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "WizardTemplate_isGlobal_idx" ON "WizardTemplate"("isGlobal");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_programId_fkey"
|
||||||
|
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_createdBy_fkey"
|
||||||
|
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 2. MISSING COLUMNS ON SpecialAward: eligibility job tracking fields
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStatus" TEXT;
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobTotal" INTEGER;
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobDone" INTEGER;
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobError" TEXT;
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStarted" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 3. Project.referralSource: Already in init migration. No action needed.
|
||||||
|
-- Round.slug: Already in init migration. No action needed.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 5. MISSING INDEXES
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- 5a. Assignment: @@index([projectId, userId])
|
||||||
|
CREATE INDEX IF NOT EXISTS "Assignment_projectId_userId_idx" ON "Assignment"("projectId", "userId");
|
||||||
|
|
||||||
|
-- 5b. AuditLog: @@index([sessionId])
|
||||||
|
CREATE INDEX IF NOT EXISTS "AuditLog_sessionId_idx" ON "AuditLog"("sessionId");
|
||||||
|
|
||||||
|
-- 5c. ProjectFile: @@index([projectId, roundId])
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectFile_projectId_roundId_idx" ON "ProjectFile"("projectId", "roundId");
|
||||||
|
|
||||||
|
-- 5d. MessageRecipient: @@index([userId])
|
||||||
|
CREATE INDEX IF NOT EXISTS "MessageRecipient_userId_idx" ON "MessageRecipient"("userId");
|
||||||
|
|
||||||
|
-- 5e. MessageRecipient: @@unique([messageId, userId, channel])
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MessageRecipient_messageId_userId_channel_key" ON "MessageRecipient"("messageId", "userId", "channel");
|
||||||
|
|
||||||
|
-- 5f. AwardEligibility: @@index([awardId, eligible]) - composite index
|
||||||
|
CREATE INDEX IF NOT EXISTS "AwardEligibility_awardId_eligible_idx" ON "AwardEligibility"("awardId", "eligible");
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 6. REMOVE STALE INDEX: Message_scheduledAt_idx
|
||||||
|
-- The schema does NOT have @@index([scheduledAt]) on Message.
|
||||||
|
-- The add_15_features migration created it, but the schema doesn't list it.
|
||||||
|
-- Leaving it as-is since it's harmless and could be useful.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 7. VERIFY: All models from add_15_features are present
|
||||||
|
-- DigestLog, RoundTemplate, MentorNote, MentorMilestone,
|
||||||
|
-- MentorMilestoneCompletion, Message, MessageTemplate, MessageRecipient,
|
||||||
|
-- Webhook, WebhookDelivery, EvaluationDiscussion, DiscussionComment
|
||||||
|
-- -> All confirmed created in 20260205223133_add_15_features migration.
|
||||||
|
-- -> All FKs confirmed in add_15_features + 20260208000000_add_missing_fks_indexes.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 8. VERIFY: Existing tables from init and subsequent migrations
|
||||||
|
-- All core tables (User, Account, Session, VerificationToken, Program, Round,
|
||||||
|
-- EvaluationForm, Project, ProjectFile, Assignment, Evaluation, GracePeriod,
|
||||||
|
-- SystemSettings, AuditLog, AIUsageLog, NotificationLog, InAppNotification,
|
||||||
|
-- NotificationEmailSetting, LearningResource, ResourceAccess, Partner,
|
||||||
|
-- ExpertiseTag, ProjectTag, LiveVotingSession, LiveVote, TeamMember,
|
||||||
|
-- MentorAssignment, FilteringRule, FilteringResult, FilteringJob,
|
||||||
|
-- AssignmentJob, TaggingJob, SpecialAward, AwardEligibility, AwardJuror,
|
||||||
|
-- AwardVote, ReminderLog, ConflictOfInterest, EvaluationSummary,
|
||||||
|
-- ProjectStatusHistory, MentorMessage, FileRequirement)
|
||||||
|
-- -> All confirmed present in migrations.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- SUMMARY OF CHANGES IN THIS MIGRATION:
|
||||||
|
--
|
||||||
|
-- NEW TABLE:
|
||||||
|
-- - WizardTemplate (with programId FK, createdBy FK, indexes)
|
||||||
|
--
|
||||||
|
-- NEW COLUMNS:
|
||||||
|
-- - SpecialAward.eligibilityJobStatus (TEXT, nullable)
|
||||||
|
-- - SpecialAward.eligibilityJobTotal (INTEGER, nullable)
|
||||||
|
-- - SpecialAward.eligibilityJobDone (INTEGER, nullable)
|
||||||
|
-- - SpecialAward.eligibilityJobError (TEXT, nullable)
|
||||||
|
-- - SpecialAward.eligibilityJobStarted (TIMESTAMP, nullable)
|
||||||
|
--
|
||||||
|
-- NEW INDEXES:
|
||||||
|
-- - Assignment_projectId_userId_idx
|
||||||
|
-- - AuditLog_sessionId_idx
|
||||||
|
-- - ProjectFile_projectId_roundId_idx
|
||||||
|
-- - MessageRecipient_userId_idx
|
||||||
|
-- - MessageRecipient_messageId_userId_channel_key (UNIQUE)
|
||||||
|
-- - AwardEligibility_awardId_eligible_idx
|
||||||
|
-- - WizardTemplate_programId_idx
|
||||||
|
-- - WizardTemplate_isGlobal_idx
|
||||||
|
--
|
||||||
|
-- NEW FOREIGN KEYS:
|
||||||
|
-- - WizardTemplate_programId_fkey -> Program(id) ON DELETE CASCADE
|
||||||
|
-- - WizardTemplate_createdBy_fkey -> User(id) ON DELETE RESTRICT
|
||||||
|
-- =============================================================================
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "AwardVote_awardId_userId_idx" ON "AwardVote"("awardId", "userId");
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
-- Migration: Add live voting enhancements (criteria voting, audience voting, AudienceVoter)
|
||||||
|
-- Brings LiveVotingSession, LiveVote, and new AudienceVoter model in sync with schema.prisma
|
||||||
|
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 1. LiveVotingSession: Add criteria-based & audience voting columns
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "votingMode" TEXT NOT NULL DEFAULT 'simple';
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "criteriaJson" JSONB;
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingMode" TEXT NOT NULL DEFAULT 'disabled';
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceMaxFavorites" INTEGER NOT NULL DEFAULT 3;
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceRequireId" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingDuration" INTEGER;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 2. LiveVote: Add criteria scores, audience voter link, make userId nullable
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "criterionScoresJson" JSONB;
|
||||||
|
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "audienceVoterId" TEXT;
|
||||||
|
|
||||||
|
-- Make userId nullable (was NOT NULL in init migration)
|
||||||
|
ALTER TABLE "LiveVote" ALTER COLUMN "userId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 3. AudienceVoter: New table for audience participation
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "AudienceVoter" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"identifier" TEXT,
|
||||||
|
"identifierType" TEXT,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AudienceVoter_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Unique constraint on token
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_token_key" UNIQUE ("token");
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS "AudienceVoter_sessionId_idx" ON "AudienceVoter"("sessionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "AudienceVoter_token_idx" ON "AudienceVoter"("token");
|
||||||
|
|
||||||
|
-- Foreign key: AudienceVoter.sessionId -> LiveVotingSession.id
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_sessionId_fkey"
|
||||||
|
FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 4. LiveVote: Foreign key and indexes for audienceVoterId
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "LiveVote_audienceVoterId_idx" ON "LiveVote"("audienceVoterId");
|
||||||
|
|
||||||
|
-- Foreign key: LiveVote.audienceVoterId -> AudienceVoter.id
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_audienceVoterId_fkey"
|
||||||
|
FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Unique constraint: sessionId + projectId + audienceVoterId
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_sessionId_projectId_audienceVoterId_key"
|
||||||
|
UNIQUE ("sessionId", "projectId", "audienceVoterId");
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- SUMMARY:
|
||||||
|
--
|
||||||
|
-- LiveVotingSession new columns:
|
||||||
|
-- - votingMode (TEXT, default 'simple')
|
||||||
|
-- - criteriaJson (JSONB, nullable)
|
||||||
|
-- - audienceVotingMode (TEXT, default 'disabled')
|
||||||
|
-- - audienceMaxFavorites (INTEGER, default 3)
|
||||||
|
-- - audienceRequireId (BOOLEAN, default false)
|
||||||
|
-- - audienceVotingDuration (INTEGER, nullable)
|
||||||
|
--
|
||||||
|
-- LiveVote changes:
|
||||||
|
-- - criterionScoresJson (JSONB, nullable) - new column
|
||||||
|
-- - audienceVoterId (TEXT, nullable) - new column
|
||||||
|
-- - userId changed from NOT NULL to nullable
|
||||||
|
-- - New unique: (sessionId, projectId, audienceVoterId)
|
||||||
|
-- - New index: audienceVoterId
|
||||||
|
-- - New FK: audienceVoterId -> AudienceVoter(id)
|
||||||
|
--
|
||||||
|
-- New table: AudienceVoter
|
||||||
|
-- - id, sessionId, token (unique), identifier, identifierType,
|
||||||
|
-- ipAddress, userAgent, createdAt
|
||||||
|
-- - FK: sessionId -> LiveVotingSession(id) CASCADE
|
||||||
|
-- =============================================================================
|
||||||
@@ -0,0 +1,429 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Migration: Pipeline & Stage System + Legacy roundId → stageId transition
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 1. NEW ENUM TYPES
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
DO $$ BEGIN CREATE TYPE "StageType" AS ENUM ('INTAKE', 'FILTER', 'EVALUATION', 'SELECTION', 'LIVE_FINAL', 'RESULTS'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "TrackKind" AS ENUM ('MAIN', 'AWARD', 'SHOWCASE'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "RoutingMode" AS ENUM ('PARALLEL', 'EXCLUSIVE', 'POST_MAIN'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "StageStatus" AS ENUM ('STAGE_DRAFT', 'STAGE_ACTIVE', 'STAGE_CLOSED', 'STAGE_ARCHIVED'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "ProjectStageStateValue" AS ENUM ('PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'ROUTED', 'COMPLETED', 'WITHDRAWN'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "DecisionMode" AS ENUM ('JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "OverrideReasonCode" AS ENUM ('DATA_CORRECTION', 'POLICY_EXCEPTION', 'JURY_CONFLICT', 'SPONSOR_DECISION', 'ADMIN_DISCRETION'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Add new values to existing UserRole enum
|
||||||
|
ALTER TYPE "UserRole" ADD VALUE IF NOT EXISTS 'AWARD_MASTER';
|
||||||
|
ALTER TYPE "UserRole" ADD VALUE IF NOT EXISTS 'AUDIENCE';
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 2. NEW TABLES: Pipeline infrastructure
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Pipeline
|
||||||
|
CREATE TABLE IF NOT EXISTS "Pipeline" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"programId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
||||||
|
"settingsJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Pipeline_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Pipeline_slug_key" ON "Pipeline"("slug");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Pipeline_programId_idx" ON "Pipeline"("programId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Pipeline_status_idx" ON "Pipeline"("status");
|
||||||
|
|
||||||
|
-- Track
|
||||||
|
CREATE TABLE IF NOT EXISTS "Track" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"pipelineId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"kind" "TrackKind" NOT NULL DEFAULT 'MAIN',
|
||||||
|
"routingMode" "RoutingMode",
|
||||||
|
"decisionMode" "DecisionMode",
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"settingsJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Track_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Track_pipelineId_slug_key" ON "Track"("pipelineId", "slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Track_pipelineId_sortOrder_key" ON "Track"("pipelineId", "sortOrder");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Track_pipelineId_idx" ON "Track"("pipelineId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Track_kind_idx" ON "Track"("kind");
|
||||||
|
|
||||||
|
-- Stage
|
||||||
|
CREATE TABLE IF NOT EXISTS "Stage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"trackId" TEXT NOT NULL,
|
||||||
|
"stageType" "StageType" NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"status" "StageStatus" NOT NULL DEFAULT 'STAGE_DRAFT',
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"configJson" JSONB,
|
||||||
|
"windowOpenAt" TIMESTAMP(3),
|
||||||
|
"windowCloseAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Stage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Stage_trackId_slug_key" ON "Stage"("trackId", "slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Stage_trackId_sortOrder_key" ON "Stage"("trackId", "sortOrder");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Stage_trackId_idx" ON "Stage"("trackId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Stage_stageType_idx" ON "Stage"("stageType");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Stage_status_idx" ON "Stage"("status");
|
||||||
|
|
||||||
|
-- StageTransition
|
||||||
|
CREATE TABLE IF NOT EXISTS "StageTransition" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"fromStageId" TEXT NOT NULL,
|
||||||
|
"toStageId" TEXT NOT NULL,
|
||||||
|
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"guardJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "StageTransition_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "StageTransition_fromStageId_toStageId_key" ON "StageTransition"("fromStageId", "toStageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "StageTransition_fromStageId_idx" ON "StageTransition"("fromStageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "StageTransition_toStageId_idx" ON "StageTransition"("toStageId");
|
||||||
|
|
||||||
|
-- ProjectStageState
|
||||||
|
CREATE TABLE IF NOT EXISTS "ProjectStageState" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"trackId" TEXT NOT NULL,
|
||||||
|
"stageId" TEXT NOT NULL,
|
||||||
|
"state" "ProjectStageStateValue" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"enteredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"exitedAt" TIMESTAMP(3),
|
||||||
|
"metadataJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ProjectStageState_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "ProjectStageState_projectId_trackId_stageId_key" ON "ProjectStageState"("projectId", "trackId", "stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectStageState_projectId_idx" ON "ProjectStageState"("projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectStageState_trackId_idx" ON "ProjectStageState"("trackId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectStageState_stageId_idx" ON "ProjectStageState"("stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectStageState_state_idx" ON "ProjectStageState"("state");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectStageState_projectId_trackId_idx" ON "ProjectStageState"("projectId", "trackId");
|
||||||
|
|
||||||
|
-- RoutingRule
|
||||||
|
CREATE TABLE IF NOT EXISTS "RoutingRule" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"pipelineId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"scope" TEXT NOT NULL DEFAULT 'global',
|
||||||
|
"sourceTrackId" TEXT,
|
||||||
|
"destinationTrackId" TEXT NOT NULL,
|
||||||
|
"destinationStageId" TEXT,
|
||||||
|
"predicateJson" JSONB NOT NULL,
|
||||||
|
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RoutingRule_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "RoutingRule_pipelineId_idx" ON "RoutingRule"("pipelineId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "RoutingRule_priority_idx" ON "RoutingRule"("priority");
|
||||||
|
CREATE INDEX IF NOT EXISTS "RoutingRule_isActive_idx" ON "RoutingRule"("isActive");
|
||||||
|
|
||||||
|
-- Cohort
|
||||||
|
CREATE TABLE IF NOT EXISTS "Cohort" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"stageId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"votingMode" TEXT NOT NULL DEFAULT 'simple',
|
||||||
|
"isOpen" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"windowOpenAt" TIMESTAMP(3),
|
||||||
|
"windowCloseAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Cohort_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Cohort_stageId_idx" ON "Cohort"("stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Cohort_isOpen_idx" ON "Cohort"("isOpen");
|
||||||
|
|
||||||
|
-- CohortProject
|
||||||
|
CREATE TABLE IF NOT EXISTS "CohortProject" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"cohortId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "CohortProject_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "CohortProject_cohortId_projectId_key" ON "CohortProject"("cohortId", "projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "CohortProject_cohortId_idx" ON "CohortProject"("cohortId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "CohortProject_projectId_idx" ON "CohortProject"("projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "CohortProject_sortOrder_idx" ON "CohortProject"("sortOrder");
|
||||||
|
|
||||||
|
-- LiveProgressCursor
|
||||||
|
CREATE TABLE IF NOT EXISTS "LiveProgressCursor" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"stageId" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"activeProjectId" TEXT,
|
||||||
|
"activeOrderIndex" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isPaused" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LiveProgressCursor_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "LiveProgressCursor_stageId_key" ON "LiveProgressCursor"("stageId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "LiveProgressCursor_sessionId_key" ON "LiveProgressCursor"("sessionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "LiveProgressCursor_sessionId_idx" ON "LiveProgressCursor"("sessionId");
|
||||||
|
|
||||||
|
-- OverrideAction
|
||||||
|
CREATE TABLE IF NOT EXISTS "OverrideAction" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT NOT NULL,
|
||||||
|
"entityId" TEXT NOT NULL,
|
||||||
|
"previousValue" JSONB,
|
||||||
|
"newValueJson" JSONB NOT NULL,
|
||||||
|
"reasonCode" "OverrideReasonCode" NOT NULL,
|
||||||
|
"reasonText" TEXT,
|
||||||
|
"actorId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "OverrideAction_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "OverrideAction_entityType_entityId_idx" ON "OverrideAction"("entityType", "entityId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "OverrideAction_actorId_idx" ON "OverrideAction"("actorId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "OverrideAction_reasonCode_idx" ON "OverrideAction"("reasonCode");
|
||||||
|
CREATE INDEX IF NOT EXISTS "OverrideAction_createdAt_idx" ON "OverrideAction"("createdAt");
|
||||||
|
|
||||||
|
-- DecisionAuditLog
|
||||||
|
CREATE TABLE IF NOT EXISTS "DecisionAuditLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"eventType" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT NOT NULL,
|
||||||
|
"entityId" TEXT NOT NULL,
|
||||||
|
"actorId" TEXT,
|
||||||
|
"detailsJson" JSONB,
|
||||||
|
"snapshotJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "DecisionAuditLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "DecisionAuditLog_eventType_idx" ON "DecisionAuditLog"("eventType");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DecisionAuditLog_entityType_entityId_idx" ON "DecisionAuditLog"("entityType", "entityId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DecisionAuditLog_actorId_idx" ON "DecisionAuditLog"("actorId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DecisionAuditLog_createdAt_idx" ON "DecisionAuditLog"("createdAt");
|
||||||
|
|
||||||
|
-- NotificationPolicy
|
||||||
|
CREATE TABLE IF NOT EXISTS "NotificationPolicy" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"eventType" TEXT NOT NULL,
|
||||||
|
"channel" TEXT NOT NULL DEFAULT 'EMAIL',
|
||||||
|
"templateId" TEXT,
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"configJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "NotificationPolicy_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "NotificationPolicy_eventType_key" ON "NotificationPolicy"("eventType");
|
||||||
|
CREATE INDEX IF NOT EXISTS "NotificationPolicy_eventType_idx" ON "NotificationPolicy"("eventType");
|
||||||
|
CREATE INDEX IF NOT EXISTS "NotificationPolicy_isActive_idx" ON "NotificationPolicy"("isActive");
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 3. DROP roundId FK constraints (roundId is now a raw legacy field)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
ALTER TABLE "EvaluationForm" DROP CONSTRAINT IF EXISTS "EvaluationForm_roundId_fkey";
|
||||||
|
ALTER TABLE "Assignment" DROP CONSTRAINT IF EXISTS "Assignment_roundId_fkey";
|
||||||
|
ALTER TABLE "GracePeriod" DROP CONSTRAINT IF EXISTS "GracePeriod_roundId_fkey";
|
||||||
|
ALTER TABLE "FileRequirement" DROP CONSTRAINT IF EXISTS "FileRequirement_roundId_fkey";
|
||||||
|
ALTER TABLE "FilteringRule" DROP CONSTRAINT IF EXISTS "FilteringRule_roundId_fkey";
|
||||||
|
ALTER TABLE "FilteringResult" DROP CONSTRAINT IF EXISTS "FilteringResult_roundId_fkey";
|
||||||
|
ALTER TABLE "FilteringJob" DROP CONSTRAINT IF EXISTS "FilteringJob_roundId_fkey";
|
||||||
|
ALTER TABLE "AssignmentJob" DROP CONSTRAINT IF EXISTS "AssignmentJob_roundId_fkey";
|
||||||
|
ALTER TABLE "ReminderLog" DROP CONSTRAINT IF EXISTS "ReminderLog_roundId_fkey";
|
||||||
|
ALTER TABLE "ConflictOfInterest" DROP CONSTRAINT IF EXISTS "ConflictOfInterest_roundId_fkey";
|
||||||
|
ALTER TABLE "EvaluationSummary" DROP CONSTRAINT IF EXISTS "EvaluationSummary_roundId_fkey";
|
||||||
|
ALTER TABLE "EvaluationDiscussion" DROP CONSTRAINT IF EXISTS "EvaluationDiscussion_roundId_fkey";
|
||||||
|
ALTER TABLE "LiveVotingSession" DROP CONSTRAINT IF EXISTS "LiveVotingSession_roundId_fkey";
|
||||||
|
ALTER TABLE "Project" DROP CONSTRAINT IF EXISTS "Project_roundId_fkey";
|
||||||
|
ALTER TABLE "ProjectFile" DROP CONSTRAINT IF EXISTS "ProjectFile_roundId_fkey";
|
||||||
|
ALTER TABLE "TaggingJob" DROP CONSTRAINT IF EXISTS "TaggingJob_roundId_fkey";
|
||||||
|
ALTER TABLE "Message" DROP CONSTRAINT IF EXISTS "Message_roundId_fkey";
|
||||||
|
|
||||||
|
-- Make TaggingJob.roundId nullable
|
||||||
|
DO $$ BEGIN ALTER TABLE "TaggingJob" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Drop Round and RoundTemplate tables and their enums (models retired in Phase 6)
|
||||||
|
DROP TABLE IF EXISTS "RoundTemplate" CASCADE;
|
||||||
|
DROP TABLE IF EXISTS "Round" CASCADE;
|
||||||
|
DROP TYPE IF EXISTS "RoundStatus";
|
||||||
|
DROP TYPE IF EXISTS "RoundType";
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 4. ALTER EXISTING TABLES: Make roundId nullable, add stageId
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- EvaluationForm: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationForm" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "EvaluationForm" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationForm" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
DROP INDEX IF EXISTS "EvaluationForm_roundId_version_key";
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationForm_stageId_version_key" ON "EvaluationForm"("stageId", "version");
|
||||||
|
CREATE INDEX IF NOT EXISTS "EvaluationForm_stageId_isActive_idx" ON "EvaluationForm"("stageId", "isActive");
|
||||||
|
|
||||||
|
-- Assignment: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "Assignment" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "Assignment" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "Assignment" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
DROP INDEX IF EXISTS "Assignment_userId_projectId_roundId_key";
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Assignment_userId_projectId_stageId_key" ON "Assignment"("userId", "projectId", "stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Assignment_stageId_idx" ON "Assignment"("stageId");
|
||||||
|
|
||||||
|
-- GracePeriod: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "GracePeriod" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "GracePeriod" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "GracePeriod" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
CREATE INDEX IF NOT EXISTS "GracePeriod_stageId_idx" ON "GracePeriod"("stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "GracePeriod_stageId_userId_extendedUntil_idx" ON "GracePeriod"("stageId", "userId", "extendedUntil");
|
||||||
|
|
||||||
|
-- FileRequirement: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "FileRequirement" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "FileRequirement" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "FileRequirement" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
CREATE INDEX IF NOT EXISTS "FileRequirement_stageId_idx" ON "FileRequirement"("stageId");
|
||||||
|
|
||||||
|
-- FilteringRule: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringRule" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "FilteringRule" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringRule" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
CREATE INDEX IF NOT EXISTS "FilteringRule_stageId_idx" ON "FilteringRule"("stageId");
|
||||||
|
|
||||||
|
-- FilteringResult: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringResult" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "FilteringResult" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringResult" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
DROP INDEX IF EXISTS "FilteringResult_roundId_projectId_key";
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "FilteringResult_stageId_projectId_key" ON "FilteringResult"("stageId", "projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "FilteringResult_stageId_idx" ON "FilteringResult"("stageId");
|
||||||
|
|
||||||
|
-- FilteringJob: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringJob" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "FilteringJob" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringJob" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
CREATE INDEX IF NOT EXISTS "FilteringJob_stageId_idx" ON "FilteringJob"("stageId");
|
||||||
|
|
||||||
|
-- AssignmentJob: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "AssignmentJob" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "AssignmentJob" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "AssignmentJob" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentJob_stageId_idx" ON "AssignmentJob"("stageId");
|
||||||
|
|
||||||
|
-- ReminderLog: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "ReminderLog" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "ReminderLog" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "ReminderLog" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
DROP INDEX IF EXISTS "ReminderLog_roundId_userId_type_key";
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "ReminderLog_stageId_userId_type_key" ON "ReminderLog"("stageId", "userId", "type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ReminderLog_stageId_idx" ON "ReminderLog"("stageId");
|
||||||
|
|
||||||
|
-- ConflictOfInterest: roundId NOT NULL → nullable (no stageId on this table)
|
||||||
|
DO $$ BEGIN ALTER TABLE "ConflictOfInterest" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- EvaluationSummary: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationSummary" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "EvaluationSummary" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationSummary" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
DROP INDEX IF EXISTS "EvaluationSummary_projectId_roundId_key";
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationSummary_projectId_stageId_key" ON "EvaluationSummary"("projectId", "stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "EvaluationSummary_stageId_idx" ON "EvaluationSummary"("stageId");
|
||||||
|
|
||||||
|
-- EvaluationDiscussion: roundId NOT NULL → nullable, add stageId NOT NULL
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationDiscussion" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "EvaluationDiscussion" ADD COLUMN IF NOT EXISTS "stageId" TEXT NOT NULL DEFAULT '__placeholder__';
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationDiscussion" ALTER COLUMN "stageId" DROP DEFAULT; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
DROP INDEX IF EXISTS "EvaluationDiscussion_projectId_roundId_key";
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationDiscussion_projectId_stageId_key" ON "EvaluationDiscussion"("projectId", "stageId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "EvaluationDiscussion_stageId_idx" ON "EvaluationDiscussion"("stageId");
|
||||||
|
|
||||||
|
-- Message: add stageId (nullable)
|
||||||
|
ALTER TABLE "Message" ADD COLUMN IF NOT EXISTS "stageId" TEXT;
|
||||||
|
CREATE INDEX IF NOT EXISTS "Message_stageId_idx" ON "Message"("stageId");
|
||||||
|
|
||||||
|
-- LiveVotingSession: roundId NOT NULL → nullable, add stageId (nullable, unique)
|
||||||
|
DO $$ BEGIN ALTER TABLE "LiveVotingSession" ALTER COLUMN "roundId" DROP NOT NULL; EXCEPTION WHEN others THEN NULL; END $$;
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "stageId" TEXT;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "LiveVotingSession_stageId_key" ON "LiveVotingSession"("stageId");
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 5. FOREIGN KEY CONSTRAINTS: New tables
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
DO $$ BEGIN ALTER TABLE "Pipeline" ADD CONSTRAINT "Pipeline_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "Track" ADD CONSTRAINT "Track_pipelineId_fkey" FOREIGN KEY ("pipelineId") REFERENCES "Pipeline"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "Stage" ADD CONSTRAINT "Stage_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "StageTransition" ADD CONSTRAINT "StageTransition_fromStageId_fkey" FOREIGN KEY ("fromStageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "StageTransition" ADD CONSTRAINT "StageTransition_toStageId_fkey" FOREIGN KEY ("toStageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "ProjectStageState" ADD CONSTRAINT "ProjectStageState_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "ProjectStageState" ADD CONSTRAINT "ProjectStageState_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "ProjectStageState" ADD CONSTRAINT "ProjectStageState_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "RoutingRule" ADD CONSTRAINT "RoutingRule_pipelineId_fkey" FOREIGN KEY ("pipelineId") REFERENCES "Pipeline"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "RoutingRule" ADD CONSTRAINT "RoutingRule_sourceTrackId_fkey" FOREIGN KEY ("sourceTrackId") REFERENCES "Track"("id") ON DELETE SET NULL ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "RoutingRule" ADD CONSTRAINT "RoutingRule_destinationTrackId_fkey" FOREIGN KEY ("destinationTrackId") REFERENCES "Track"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "Cohort" ADD CONSTRAINT "Cohort_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "CohortProject" ADD CONSTRAINT "CohortProject_cohortId_fkey" FOREIGN KEY ("cohortId") REFERENCES "Cohort"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "CohortProject" ADD CONSTRAINT "CohortProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "LiveProgressCursor" ADD CONSTRAINT "LiveProgressCursor_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 6. FOREIGN KEY CONSTRAINTS: stageId on altered tables
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationForm" ADD CONSTRAINT "EvaluationForm_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "GracePeriod" ADD CONSTRAINT "GracePeriod_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringRule" ADD CONSTRAINT "FilteringRule_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringResult" ADD CONSTRAINT "FilteringResult_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "FilteringJob" ADD CONSTRAINT "FilteringJob_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "AssignmentJob" ADD CONSTRAINT "AssignmentJob_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "Message" ADD CONSTRAINT "Message_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE SET NULL ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN ALTER TABLE "LiveVotingSession" ADD CONSTRAINT "LiveVotingSession_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- =====================================================
|
||||||
|
-- 7. SpecialAward: add trackId (nullable, unique)
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "trackId" TEXT;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "SpecialAward_trackId_key" ON "SpecialAward"("trackId");
|
||||||
|
DO $$ BEGIN ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE SET NULL ON UPDATE CASCADE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
-- Simplify RoutingMode enum: remove POST_MAIN, rename PARALLEL -> SHARED
|
||||||
|
-- Drop RoutingRule table (routing is now handled via award assignment)
|
||||||
|
|
||||||
|
-- 1. Update existing PARALLEL values to SHARED, POST_MAIN to SHARED
|
||||||
|
-- (safe to run even if no rows match)
|
||||||
|
UPDATE "Track" SET "routingMode" = 'PARALLEL' WHERE "routingMode" = 'POST_MAIN';
|
||||||
|
|
||||||
|
-- 2. Rename PARALLEL -> SHARED in the enum (only if PARALLEL still exists)
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TYPE "RoutingMode" RENAME VALUE 'PARALLEL' TO 'SHARED';
|
||||||
|
EXCEPTION WHEN invalid_parameter_value THEN NULL; WHEN others THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- 3. Remove POST_MAIN from the enum
|
||||||
|
-- PostgreSQL doesn't support DROP VALUE directly, so we recreate the enum
|
||||||
|
-- Since we already converted POST_MAIN values to PARALLEL (now SHARED), this is safe
|
||||||
|
|
||||||
|
-- Only recreate if the old enum still has POST_MAIN (i.e., hasn't been done yet)
|
||||||
|
DO $$ BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_enum
|
||||||
|
WHERE enumlabel = 'POST_MAIN'
|
||||||
|
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'RoutingMode')
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE "RoutingMode_new" AS ENUM ('SHARED', 'EXCLUSIVE');
|
||||||
|
|
||||||
|
ALTER TABLE "Track"
|
||||||
|
ALTER COLUMN "routingMode" TYPE "RoutingMode_new"
|
||||||
|
USING ("routingMode"::text::"RoutingMode_new");
|
||||||
|
|
||||||
|
DROP TYPE "RoutingMode";
|
||||||
|
ALTER TYPE "RoutingMode_new" RENAME TO "RoutingMode";
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 4. Drop the RoutingRule table (no longer needed)
|
||||||
|
DROP TABLE IF EXISTS "RoutingRule";
|
||||||
@@ -0,0 +1,702 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Phase 0+1: Add Competition/Round Architecture (additive -- no breaking changes)
|
||||||
|
-- =============================================================================
|
||||||
|
-- New enums, new tables, new optional columns on existing tables.
|
||||||
|
-- Old Pipeline/Track/Stage tables are untouched.
|
||||||
|
|
||||||
|
-- --- New Enum Types ---
|
||||||
|
|
||||||
|
DO $$ BEGIN CREATE TYPE "CompetitionStatus" AS ENUM ('DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "RoundType" AS ENUM ('INTAKE', 'FILTERING', 'EVALUATION', 'SUBMISSION', 'MENTORING', 'LIVE_FINAL', 'DELIBERATION'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "RoundStatus" AS ENUM ('ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "ProjectRoundStateValue" AS ENUM ('PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "AdvancementRuleType" AS ENUM ('AUTO_ADVANCE', 'SCORE_THRESHOLD', 'TOP_N', 'ADMIN_SELECTION', 'AI_RECOMMENDED'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "CapMode" AS ENUM ('HARD', 'SOFT', 'NONE'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "DeadlinePolicy" AS ENUM ('HARD_DEADLINE', 'FLAG', 'GRACE'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "JuryGroupMemberRole" AS ENUM ('CHAIR', 'MEMBER', 'OBSERVER'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "AssignmentIntentSource" AS ENUM ('INVITE', 'ADMIN', 'SYSTEM'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "AssignmentIntentStatus" AS ENUM ('INTENT_PENDING', 'HONORED', 'OVERRIDDEN', 'EXPIRED', 'CANCELLED'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "MentorMessageRole" AS ENUM ('MENTOR_ROLE', 'APPLICANT_ROLE', 'ADMIN_ROLE'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "SubmissionPromotionSource" AS ENUM ('MENTOR_FILE', 'ADMIN_REPLACEMENT'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "DeliberationMode" AS ENUM ('SINGLE_WINNER_VOTE', 'FULL_RANKING'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "DeliberationStatus" AS ENUM ('DELIB_OPEN', 'VOTING', 'TALLYING', 'RUNOFF', 'DELIB_LOCKED'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "TieBreakMethod" AS ENUM ('TIE_RUNOFF', 'TIE_ADMIN_DECIDES', 'SCORE_FALLBACK'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "DeliberationParticipantStatus" AS ENUM ('REQUIRED', 'ABSENT_EXCUSED', 'REPLACED', 'REPLACEMENT_ACTIVE'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE TYPE "AwardEligibilityMode" AS ENUM ('SEPARATE_POOL', 'STAY_IN_MAIN'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Add FEATURE_FLAGS to SettingCategory enum
|
||||||
|
DO $$ BEGIN ALTER TYPE "SettingCategory" ADD VALUE 'FEATURE_FLAGS'; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- New Tables ---
|
||||||
|
|
||||||
|
-- Competition (replaces Pipeline)
|
||||||
|
CREATE TABLE IF NOT EXISTS "Competition" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"programId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"status" "CompetitionStatus" NOT NULL DEFAULT 'DRAFT',
|
||||||
|
"categoryMode" TEXT NOT NULL DEFAULT 'SHARED',
|
||||||
|
"startupFinalistCount" INTEGER NOT NULL DEFAULT 3,
|
||||||
|
"conceptFinalistCount" INTEGER NOT NULL DEFAULT 3,
|
||||||
|
"notifyOnRoundAdvance" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"notifyOnDeadlineApproach" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"deadlineReminderDays" INTEGER[] DEFAULT ARRAY[7, 3, 1]::INTEGER[],
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Competition_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Round (replaces Stage)
|
||||||
|
CREATE TABLE IF NOT EXISTS "Round" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"competitionId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"roundType" "RoundType" NOT NULL,
|
||||||
|
"status" "RoundStatus" NOT NULL DEFAULT 'ROUND_DRAFT',
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"windowOpenAt" TIMESTAMP(3),
|
||||||
|
"windowCloseAt" TIMESTAMP(3),
|
||||||
|
"configJson" JSONB,
|
||||||
|
"purposeKey" TEXT,
|
||||||
|
"juryGroupId" TEXT,
|
||||||
|
"submissionWindowId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Round_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ProjectRoundState
|
||||||
|
CREATE TABLE IF NOT EXISTS "ProjectRoundState" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"state" "ProjectRoundStateValue" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"enteredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"exitedAt" TIMESTAMP(3),
|
||||||
|
"metadataJson" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ProjectRoundState_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AdvancementRule
|
||||||
|
CREATE TABLE IF NOT EXISTS "AdvancementRule" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"targetRoundId" TEXT,
|
||||||
|
"ruleType" "AdvancementRuleType" NOT NULL,
|
||||||
|
"configJson" JSONB NOT NULL,
|
||||||
|
"isDefault" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AdvancementRule_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- JuryGroup
|
||||||
|
CREATE TABLE IF NOT EXISTS "JuryGroup" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"competitionId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"defaultMaxAssignments" INTEGER NOT NULL DEFAULT 20,
|
||||||
|
"defaultCapMode" "CapMode" NOT NULL DEFAULT 'SOFT',
|
||||||
|
"softCapBuffer" INTEGER NOT NULL DEFAULT 2,
|
||||||
|
"categoryQuotasEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"defaultCategoryQuotas" JSONB,
|
||||||
|
"allowJurorCapAdjustment" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"allowJurorRatioAdjustment" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "JuryGroup_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- JuryGroupMember
|
||||||
|
CREATE TABLE IF NOT EXISTS "JuryGroupMember" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"juryGroupId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"role" "JuryGroupMemberRole" NOT NULL DEFAULT 'MEMBER',
|
||||||
|
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"maxAssignmentsOverride" INTEGER,
|
||||||
|
"capModeOverride" "CapMode",
|
||||||
|
"categoryQuotasOverride" JSONB,
|
||||||
|
"preferredStartupRatio" DOUBLE PRECISION,
|
||||||
|
"availabilityNotes" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "JuryGroupMember_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- SubmissionWindow
|
||||||
|
CREATE TABLE IF NOT EXISTS "SubmissionWindow" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"competitionId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"roundNumber" INTEGER NOT NULL,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"windowOpenAt" TIMESTAMP(3),
|
||||||
|
"windowCloseAt" TIMESTAMP(3),
|
||||||
|
"deadlinePolicy" "DeadlinePolicy" NOT NULL DEFAULT 'FLAG',
|
||||||
|
"graceHours" INTEGER,
|
||||||
|
"lockOnClose" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"isLocked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SubmissionWindow_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- SubmissionFileRequirement
|
||||||
|
CREATE TABLE IF NOT EXISTS "SubmissionFileRequirement" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"submissionWindowId" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"mimeTypes" TEXT[],
|
||||||
|
"maxSizeMb" INTEGER,
|
||||||
|
"required" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SubmissionFileRequirement_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RoundSubmissionVisibility
|
||||||
|
CREATE TABLE IF NOT EXISTS "RoundSubmissionVisibility" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"submissionWindowId" TEXT NOT NULL,
|
||||||
|
"canView" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"displayLabel" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "RoundSubmissionVisibility_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AssignmentIntent
|
||||||
|
CREATE TABLE IF NOT EXISTS "AssignmentIntent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"juryGroupMemberId" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"source" "AssignmentIntentSource" NOT NULL,
|
||||||
|
"status" "AssignmentIntentStatus" NOT NULL DEFAULT 'INTENT_PENDING',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AssignmentIntent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AssignmentException
|
||||||
|
CREATE TABLE IF NOT EXISTS "AssignmentException" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"assignmentId" TEXT NOT NULL,
|
||||||
|
"reason" TEXT NOT NULL,
|
||||||
|
"overCapBy" INTEGER NOT NULL,
|
||||||
|
"approvedById" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AssignmentException_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- MentorFile
|
||||||
|
CREATE TABLE IF NOT EXISTS "MentorFile" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"mentorAssignmentId" TEXT NOT NULL,
|
||||||
|
"uploadedByUserId" TEXT NOT NULL,
|
||||||
|
"fileName" TEXT NOT NULL,
|
||||||
|
"mimeType" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL,
|
||||||
|
"bucket" TEXT NOT NULL,
|
||||||
|
"objectKey" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"isPromoted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"promotedToFileId" TEXT,
|
||||||
|
"promotedAt" TIMESTAMP(3),
|
||||||
|
"promotedByUserId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "MentorFile_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- MentorFileComment
|
||||||
|
CREATE TABLE IF NOT EXISTS "MentorFileComment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"mentorFileId" TEXT NOT NULL,
|
||||||
|
"authorId" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"parentCommentId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "MentorFileComment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- SubmissionPromotionEvent
|
||||||
|
CREATE TABLE IF NOT EXISTS "SubmissionPromotionEvent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"slotKey" TEXT NOT NULL,
|
||||||
|
"sourceType" "SubmissionPromotionSource" NOT NULL,
|
||||||
|
"sourceFileId" TEXT,
|
||||||
|
"promotedById" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "SubmissionPromotionEvent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- DeliberationSession
|
||||||
|
CREATE TABLE IF NOT EXISTS "DeliberationSession" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"competitionId" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"category" "CompetitionCategory" NOT NULL,
|
||||||
|
"mode" "DeliberationMode" NOT NULL,
|
||||||
|
"showCollectiveRankings" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"showPriorJuryData" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"status" "DeliberationStatus" NOT NULL,
|
||||||
|
"tieBreakMethod" "TieBreakMethod" NOT NULL,
|
||||||
|
"adminOverrideResult" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "DeliberationSession_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- DeliberationVote
|
||||||
|
CREATE TABLE IF NOT EXISTS "DeliberationVote" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"juryMemberId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"rank" INTEGER,
|
||||||
|
"isWinnerPick" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"runoffRound" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "DeliberationVote_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- DeliberationResult
|
||||||
|
CREATE TABLE IF NOT EXISTS "DeliberationResult" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"finalRank" INTEGER NOT NULL,
|
||||||
|
"voteCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"isAdminOverridden" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"overrideReason" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "DeliberationResult_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- DeliberationParticipant
|
||||||
|
CREATE TABLE IF NOT EXISTS "DeliberationParticipant" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"status" "DeliberationParticipantStatus" NOT NULL,
|
||||||
|
"replacedById" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "DeliberationParticipant_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ResultLock
|
||||||
|
CREATE TABLE IF NOT EXISTS "ResultLock" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"competitionId" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"category" "CompetitionCategory" NOT NULL,
|
||||||
|
"lockedById" TEXT NOT NULL,
|
||||||
|
"resultSnapshot" JSONB NOT NULL,
|
||||||
|
"lockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ResultLock_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ResultUnlockEvent
|
||||||
|
CREATE TABLE IF NOT EXISTS "ResultUnlockEvent" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"resultLockId" TEXT NOT NULL,
|
||||||
|
"unlockedById" TEXT NOT NULL,
|
||||||
|
"reason" TEXT NOT NULL,
|
||||||
|
"unlockedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ResultUnlockEvent_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- --- Add Columns to Existing Tables ---
|
||||||
|
|
||||||
|
-- Assignment: add juryGroupId
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Assignment" ADD COLUMN "juryGroupId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- SpecialAward: add competition/round architecture fields
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN "competitionId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN "evaluationRoundId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN "juryGroupId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN "eligibilityMode" "AwardEligibilityMode" NOT NULL DEFAULT 'STAY_IN_MAIN';
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD COLUMN "decisionMode" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- MentorAssignment: add workspace fields
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorAssignment" ADD COLUMN "workspaceEnabled" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorAssignment" ADD COLUMN "workspaceOpenAt" TIMESTAMP(3);
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorAssignment" ADD COLUMN "workspaceCloseAt" TIMESTAMP(3);
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- MentorMessage: add workspace fields
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMessage" ADD COLUMN "workspaceId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMessage" ADD COLUMN "senderRole" "MentorMessageRole";
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- ProjectFile: add submission window link
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "submissionWindowId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "submissionFileRequirementId" TEXT;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- Unique Constraints ---
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Competition_slug_key" ON "Competition"("slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Round_competitionId_slug_key" ON "Round"("competitionId", "slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Round_competitionId_sortOrder_key" ON "Round"("competitionId", "sortOrder");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "ProjectRoundState_projectId_roundId_key" ON "ProjectRoundState"("projectId", "roundId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "JuryGroup_competitionId_slug_key" ON "JuryGroup"("competitionId", "slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "JuryGroupMember_juryGroupId_userId_key" ON "JuryGroupMember"("juryGroupId", "userId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "SubmissionWindow_competitionId_slug_key" ON "SubmissionWindow"("competitionId", "slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "SubmissionWindow_competitionId_roundNumber_key" ON "SubmissionWindow"("competitionId", "roundNumber");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "RoundSubmissionVisibility_roundId_submissionWindowId_key" ON "RoundSubmissionVisibility"("roundId", "submissionWindowId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "AssignmentIntent_juryGroupMemberId_roundId_projectId_key" ON "AssignmentIntent"("juryGroupMemberId", "roundId", "projectId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "MentorFile_promotedToFileId_key" ON "MentorFile"("promotedToFileId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "DeliberationVote_sessionId_juryMemberId_projectId_runoffRo_key" ON "DeliberationVote"("sessionId", "juryMemberId", "projectId", "runoffRound");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "DeliberationResult_sessionId_projectId_key" ON "DeliberationResult"("sessionId", "projectId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "DeliberationParticipant_sessionId_userId_key" ON "DeliberationParticipant"("sessionId", "userId");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "SubmissionFileRequirement_submissionWindowId_slug_key" ON "SubmissionFileRequirement"("submissionWindowId", "slug");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "AdvancementRule_roundId_sortOrder_key" ON "AdvancementRule"("roundId", "sortOrder");
|
||||||
|
|
||||||
|
-- --- Indexes ---
|
||||||
|
|
||||||
|
-- Competition
|
||||||
|
CREATE INDEX IF NOT EXISTS "Competition_programId_idx" ON "Competition"("programId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Competition_status_idx" ON "Competition"("status");
|
||||||
|
|
||||||
|
-- Round
|
||||||
|
CREATE INDEX IF NOT EXISTS "Round_competitionId_idx" ON "Round"("competitionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Round_roundType_idx" ON "Round"("roundType");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Round_status_idx" ON "Round"("status");
|
||||||
|
|
||||||
|
-- ProjectRoundState
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectRoundState_projectId_idx" ON "ProjectRoundState"("projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectRoundState_roundId_idx" ON "ProjectRoundState"("roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectRoundState_state_idx" ON "ProjectRoundState"("state");
|
||||||
|
|
||||||
|
-- AdvancementRule
|
||||||
|
CREATE INDEX IF NOT EXISTS "AdvancementRule_roundId_idx" ON "AdvancementRule"("roundId");
|
||||||
|
|
||||||
|
-- JuryGroup
|
||||||
|
CREATE INDEX IF NOT EXISTS "JuryGroup_competitionId_idx" ON "JuryGroup"("competitionId");
|
||||||
|
|
||||||
|
-- JuryGroupMember
|
||||||
|
CREATE INDEX IF NOT EXISTS "JuryGroupMember_juryGroupId_idx" ON "JuryGroupMember"("juryGroupId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "JuryGroupMember_userId_idx" ON "JuryGroupMember"("userId");
|
||||||
|
|
||||||
|
-- SubmissionWindow
|
||||||
|
CREATE INDEX IF NOT EXISTS "SubmissionWindow_competitionId_idx" ON "SubmissionWindow"("competitionId");
|
||||||
|
|
||||||
|
-- SubmissionFileRequirement
|
||||||
|
CREATE INDEX IF NOT EXISTS "SubmissionFileRequirement_submissionWindowId_idx" ON "SubmissionFileRequirement"("submissionWindowId");
|
||||||
|
|
||||||
|
-- RoundSubmissionVisibility
|
||||||
|
CREATE INDEX IF NOT EXISTS "RoundSubmissionVisibility_roundId_idx" ON "RoundSubmissionVisibility"("roundId");
|
||||||
|
|
||||||
|
-- AssignmentIntent
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentIntent_roundId_idx" ON "AssignmentIntent"("roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentIntent_projectId_idx" ON "AssignmentIntent"("projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentIntent_status_idx" ON "AssignmentIntent"("status");
|
||||||
|
|
||||||
|
-- AssignmentException
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentException_assignmentId_idx" ON "AssignmentException"("assignmentId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentException_approvedById_idx" ON "AssignmentException"("approvedById");
|
||||||
|
|
||||||
|
-- MentorFile
|
||||||
|
CREATE INDEX IF NOT EXISTS "MentorFile_mentorAssignmentId_idx" ON "MentorFile"("mentorAssignmentId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MentorFile_uploadedByUserId_idx" ON "MentorFile"("uploadedByUserId");
|
||||||
|
|
||||||
|
-- MentorFileComment
|
||||||
|
CREATE INDEX IF NOT EXISTS "MentorFileComment_mentorFileId_idx" ON "MentorFileComment"("mentorFileId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MentorFileComment_authorId_idx" ON "MentorFileComment"("authorId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MentorFileComment_parentCommentId_idx" ON "MentorFileComment"("parentCommentId");
|
||||||
|
|
||||||
|
-- SubmissionPromotionEvent
|
||||||
|
CREATE INDEX IF NOT EXISTS "SubmissionPromotionEvent_projectId_idx" ON "SubmissionPromotionEvent"("projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "SubmissionPromotionEvent_roundId_idx" ON "SubmissionPromotionEvent"("roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "SubmissionPromotionEvent_sourceFileId_idx" ON "SubmissionPromotionEvent"("sourceFileId");
|
||||||
|
|
||||||
|
-- DeliberationSession
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationSession_competitionId_idx" ON "DeliberationSession"("competitionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationSession_roundId_idx" ON "DeliberationSession"("roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationSession_status_idx" ON "DeliberationSession"("status");
|
||||||
|
|
||||||
|
-- DeliberationVote
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationVote_sessionId_idx" ON "DeliberationVote"("sessionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationVote_juryMemberId_idx" ON "DeliberationVote"("juryMemberId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationVote_projectId_idx" ON "DeliberationVote"("projectId");
|
||||||
|
|
||||||
|
-- DeliberationResult
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationResult_sessionId_idx" ON "DeliberationResult"("sessionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationResult_projectId_idx" ON "DeliberationResult"("projectId");
|
||||||
|
|
||||||
|
-- DeliberationParticipant
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationParticipant_sessionId_idx" ON "DeliberationParticipant"("sessionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "DeliberationParticipant_userId_idx" ON "DeliberationParticipant"("userId");
|
||||||
|
|
||||||
|
-- ResultLock
|
||||||
|
CREATE INDEX IF NOT EXISTS "ResultLock_competitionId_idx" ON "ResultLock"("competitionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ResultLock_roundId_idx" ON "ResultLock"("roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ResultLock_category_idx" ON "ResultLock"("category");
|
||||||
|
|
||||||
|
-- ResultUnlockEvent
|
||||||
|
CREATE INDEX IF NOT EXISTS "ResultUnlockEvent_resultLockId_idx" ON "ResultUnlockEvent"("resultLockId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ResultUnlockEvent_unlockedById_idx" ON "ResultUnlockEvent"("unlockedById");
|
||||||
|
|
||||||
|
-- Indexes on modified existing tables
|
||||||
|
CREATE INDEX IF NOT EXISTS "Assignment_juryGroupId_idx" ON "Assignment"("juryGroupId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "SpecialAward_competitionId_idx" ON "SpecialAward"("competitionId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "SpecialAward_evaluationRoundId_idx" ON "SpecialAward"("evaluationRoundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "MentorMessage_workspaceId_idx" ON "MentorMessage"("workspaceId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectFile_submissionWindowId_idx" ON "ProjectFile"("submissionWindowId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ProjectFile_submissionFileRequirementId_idx" ON "ProjectFile"("submissionFileRequirementId");
|
||||||
|
|
||||||
|
-- --- Foreign Keys ---
|
||||||
|
|
||||||
|
-- Competition
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Competition" ADD CONSTRAINT "Competition_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Round
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Round" ADD CONSTRAINT "Round_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Round" ADD CONSTRAINT "Round_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Round" ADD CONSTRAINT "Round_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- ProjectRoundState
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectRoundState" ADD CONSTRAINT "ProjectRoundState_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectRoundState" ADD CONSTRAINT "ProjectRoundState_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- AdvancementRule
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AdvancementRule" ADD CONSTRAINT "AdvancementRule_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- JuryGroup
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "JuryGroup" ADD CONSTRAINT "JuryGroup_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- JuryGroupMember
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "JuryGroupMember" ADD CONSTRAINT "JuryGroupMember_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "JuryGroupMember" ADD CONSTRAINT "JuryGroupMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- SubmissionWindow
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SubmissionWindow" ADD CONSTRAINT "SubmissionWindow_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- SubmissionFileRequirement
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SubmissionFileRequirement" ADD CONSTRAINT "SubmissionFileRequirement_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- RoundSubmissionVisibility
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "RoundSubmissionVisibility" ADD CONSTRAINT "RoundSubmissionVisibility_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "RoundSubmissionVisibility" ADD CONSTRAINT "RoundSubmissionVisibility_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- AssignmentIntent
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentIntent" ADD CONSTRAINT "AssignmentIntent_juryGroupMemberId_fkey" FOREIGN KEY ("juryGroupMemberId") REFERENCES "JuryGroupMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentIntent" ADD CONSTRAINT "AssignmentIntent_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentIntent" ADD CONSTRAINT "AssignmentIntent_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- AssignmentException
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentException" ADD CONSTRAINT "AssignmentException_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "Assignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentException" ADD CONSTRAINT "AssignmentException_approvedById_fkey" FOREIGN KEY ("approvedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- MentorFile
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_mentorAssignmentId_fkey" FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_uploadedByUserId_fkey" FOREIGN KEY ("uploadedByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_promotedByUserId_fkey" FOREIGN KEY ("promotedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_promotedToFileId_fkey" FOREIGN KEY ("promotedToFileId") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- MentorFileComment
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFileComment" ADD CONSTRAINT "MentorFileComment_mentorFileId_fkey" FOREIGN KEY ("mentorFileId") REFERENCES "MentorFile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFileComment" ADD CONSTRAINT "MentorFileComment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorFileComment" ADD CONSTRAINT "MentorFileComment_parentCommentId_fkey" FOREIGN KEY ("parentCommentId") REFERENCES "MentorFileComment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- SubmissionPromotionEvent
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_sourceFileId_fkey" FOREIGN KEY ("sourceFileId") REFERENCES "MentorFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SubmissionPromotionEvent" ADD CONSTRAINT "SubmissionPromotionEvent_promotedById_fkey" FOREIGN KEY ("promotedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- DeliberationSession
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationSession" ADD CONSTRAINT "DeliberationSession_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationSession" ADD CONSTRAINT "DeliberationSession_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- DeliberationVote
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationVote" ADD CONSTRAINT "DeliberationVote_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "DeliberationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationVote" ADD CONSTRAINT "DeliberationVote_juryMemberId_fkey" FOREIGN KEY ("juryMemberId") REFERENCES "JuryGroupMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationVote" ADD CONSTRAINT "DeliberationVote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- DeliberationResult
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationResult" ADD CONSTRAINT "DeliberationResult_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "DeliberationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationResult" ADD CONSTRAINT "DeliberationResult_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- DeliberationParticipant
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationParticipant" ADD CONSTRAINT "DeliberationParticipant_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "DeliberationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationParticipant" ADD CONSTRAINT "DeliberationParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "JuryGroupMember"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "DeliberationParticipant" ADD CONSTRAINT "DeliberationParticipant_replacedById_fkey" FOREIGN KEY ("replacedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- ResultLock
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ResultLock" ADD CONSTRAINT "ResultLock_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ResultLock" ADD CONSTRAINT "ResultLock_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ResultLock" ADD CONSTRAINT "ResultLock_lockedById_fkey" FOREIGN KEY ("lockedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- ResultUnlockEvent
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ResultUnlockEvent" ADD CONSTRAINT "ResultUnlockEvent_resultLockId_fkey" FOREIGN KEY ("resultLockId") REFERENCES "ResultLock"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ResultUnlockEvent" ADD CONSTRAINT "ResultUnlockEvent_unlockedById_fkey" FOREIGN KEY ("unlockedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- FKs on modified existing tables
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_competitionId_fkey" FOREIGN KEY ("competitionId") REFERENCES "Competition"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_evaluationRoundId_fkey" FOREIGN KEY ("evaluationRoundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_juryGroupId_fkey" FOREIGN KEY ("juryGroupId") REFERENCES "JuryGroup"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "MentorMessage" ADD CONSTRAINT "MentorMessage_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "MentorAssignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_submissionWindowId_fkey" FOREIGN KEY ("submissionWindowId") REFERENCES "SubmissionWindow"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_submissionFileRequirementId_fkey" FOREIGN KEY ("submissionFileRequirementId") REFERENCES "SubmissionFileRequirement"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- AlterTable
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "JuryGroupMember" ADD COLUMN "selfServiceCap" INTEGER;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "JuryGroupMember" ADD COLUMN "selfServiceRatio" DOUBLE PRECISION;
|
||||||
|
EXCEPTION WHEN duplicate_column THEN NULL; END $$;
|
||||||
335
prisma/migrations/20260215200000_phase7_fk_renames/migration.sql
Normal file
335
prisma/migrations/20260215200000_phase7_fk_renames/migration.sql
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Phase 7/8 Migration Part 1: Rename stageId -> roundId on 15 tables
|
||||||
|
-- =============================================================================
|
||||||
|
-- This migration renames stageId columns to roundId and updates FK constraints
|
||||||
|
-- to point to the Round table instead of Stage table.
|
||||||
|
--
|
||||||
|
-- NOTE: After the pipeline migration (20260213), most tables have BOTH a
|
||||||
|
-- nullable roundId column (legacy, no FK) AND a stageId column. We must
|
||||||
|
-- drop the old roundId column before renaming stageId -> roundId.
|
||||||
|
|
||||||
|
-- --- 1. EvaluationForm ---
|
||||||
|
|
||||||
|
-- Drop old roundId column (nullable, no FK since 20260213 migration)
|
||||||
|
ALTER TABLE "EvaluationForm" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
-- Drop FK constraint on stageId
|
||||||
|
ALTER TABLE "EvaluationForm" DROP CONSTRAINT IF EXISTS "EvaluationForm_stageId_fkey";
|
||||||
|
|
||||||
|
-- Drop indexes
|
||||||
|
DROP INDEX IF EXISTS "EvaluationForm_stageId_version_key";
|
||||||
|
DROP INDEX IF EXISTS "EvaluationForm_stageId_isActive_idx";
|
||||||
|
|
||||||
|
-- Rename column (only if stageId exists)
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationForm" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- Recreate indexes with new name
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationForm_roundId_version_key" ON "EvaluationForm"("roundId", "version");
|
||||||
|
CREATE INDEX IF NOT EXISTS "EvaluationForm_roundId_isActive_idx" ON "EvaluationForm"("roundId", "isActive");
|
||||||
|
|
||||||
|
-- Recreate FK pointing to Round
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationForm" ADD CONSTRAINT "EvaluationForm_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 2. FileRequirement ---
|
||||||
|
|
||||||
|
ALTER TABLE "FileRequirement" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "FileRequirement" DROP CONSTRAINT IF EXISTS "FileRequirement_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "FileRequirement_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FileRequirement" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "FileRequirement_roundId_idx" ON "FileRequirement"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 3. Assignment ---
|
||||||
|
|
||||||
|
ALTER TABLE "Assignment" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "Assignment" DROP CONSTRAINT IF EXISTS "Assignment_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "Assignment_userId_projectId_stageId_key";
|
||||||
|
DROP INDEX IF EXISTS "Assignment_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Assignment" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "Assignment_userId_projectId_roundId_key" ON "Assignment"("userId", "projectId", "roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Assignment_roundId_idx" ON "Assignment"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Assignment" ADD CONSTRAINT "Assignment_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 4. GracePeriod ---
|
||||||
|
|
||||||
|
ALTER TABLE "GracePeriod" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "GracePeriod" DROP CONSTRAINT IF EXISTS "GracePeriod_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "GracePeriod_stageId_idx";
|
||||||
|
DROP INDEX IF EXISTS "GracePeriod_stageId_userId_extendedUntil_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "GracePeriod" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "GracePeriod_roundId_idx" ON "GracePeriod"("roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "GracePeriod_roundId_userId_extendedUntil_idx" ON "GracePeriod"("roundId", "userId", "extendedUntil");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "GracePeriod" ADD CONSTRAINT "GracePeriod_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 5. LiveVotingSession ---
|
||||||
|
|
||||||
|
ALTER TABLE "LiveVotingSession" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "LiveVotingSession" DROP CONSTRAINT IF EXISTS "LiveVotingSession_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "LiveVotingSession_stageId_key";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVotingSession" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "LiveVotingSession_roundId_key" ON "LiveVotingSession"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD CONSTRAINT "LiveVotingSession_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 6. FilteringRule ---
|
||||||
|
|
||||||
|
ALTER TABLE "FilteringRule" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "FilteringRule" DROP CONSTRAINT IF EXISTS "FilteringRule_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "FilteringRule_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FilteringRule" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "FilteringRule_roundId_idx" ON "FilteringRule"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FilteringRule" ADD CONSTRAINT "FilteringRule_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 7. FilteringResult ---
|
||||||
|
|
||||||
|
ALTER TABLE "FilteringResult" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "FilteringResult" DROP CONSTRAINT IF EXISTS "FilteringResult_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "FilteringResult_stageId_projectId_key";
|
||||||
|
DROP INDEX IF EXISTS "FilteringResult_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FilteringResult" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "FilteringResult_roundId_projectId_key" ON "FilteringResult"("roundId", "projectId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "FilteringResult_roundId_idx" ON "FilteringResult"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FilteringResult" ADD CONSTRAINT "FilteringResult_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 8. FilteringJob ---
|
||||||
|
|
||||||
|
ALTER TABLE "FilteringJob" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "FilteringJob" DROP CONSTRAINT IF EXISTS "FilteringJob_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "FilteringJob_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FilteringJob" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "FilteringJob_roundId_idx" ON "FilteringJob"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "FilteringJob" ADD CONSTRAINT "FilteringJob_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 9. AssignmentJob ---
|
||||||
|
|
||||||
|
ALTER TABLE "AssignmentJob" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "AssignmentJob" DROP CONSTRAINT IF EXISTS "AssignmentJob_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "AssignmentJob_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentJob" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "AssignmentJob_roundId_idx" ON "AssignmentJob"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "AssignmentJob" ADD CONSTRAINT "AssignmentJob_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 10. ReminderLog ---
|
||||||
|
|
||||||
|
ALTER TABLE "ReminderLog" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "ReminderLog" DROP CONSTRAINT IF EXISTS "ReminderLog_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "ReminderLog_stageId_userId_type_key";
|
||||||
|
DROP INDEX IF EXISTS "ReminderLog_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ReminderLog" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "ReminderLog_roundId_userId_type_key" ON "ReminderLog"("roundId", "userId", "type");
|
||||||
|
CREATE INDEX IF NOT EXISTS "ReminderLog_roundId_idx" ON "ReminderLog"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ReminderLog" ADD CONSTRAINT "ReminderLog_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 11. EvaluationSummary ---
|
||||||
|
|
||||||
|
ALTER TABLE "EvaluationSummary" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "EvaluationSummary" DROP CONSTRAINT IF EXISTS "EvaluationSummary_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "EvaluationSummary_projectId_stageId_key";
|
||||||
|
DROP INDEX IF EXISTS "EvaluationSummary_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationSummary" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationSummary_projectId_roundId_key" ON "EvaluationSummary"("projectId", "roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "EvaluationSummary_roundId_idx" ON "EvaluationSummary"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationSummary" ADD CONSTRAINT "EvaluationSummary_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 12. EvaluationDiscussion ---
|
||||||
|
|
||||||
|
ALTER TABLE "EvaluationDiscussion" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "EvaluationDiscussion" DROP CONSTRAINT IF EXISTS "EvaluationDiscussion_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "EvaluationDiscussion_projectId_stageId_key";
|
||||||
|
DROP INDEX IF EXISTS "EvaluationDiscussion_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationDiscussion" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "EvaluationDiscussion_projectId_roundId_key" ON "EvaluationDiscussion"("projectId", "roundId");
|
||||||
|
CREATE INDEX IF NOT EXISTS "EvaluationDiscussion_roundId_idx" ON "EvaluationDiscussion"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 13. Message ---
|
||||||
|
|
||||||
|
-- Message has roundId (from init, nullable) and stageId (from pipeline, nullable)
|
||||||
|
ALTER TABLE "Message" DROP COLUMN IF EXISTS "roundId";
|
||||||
|
|
||||||
|
ALTER TABLE "Message" DROP CONSTRAINT IF EXISTS "Message_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "Message_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Message" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Message_roundId_idx" ON "Message"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 14. Cohort ---
|
||||||
|
-- Cohort was created in pipeline migration with stageId only (no roundId)
|
||||||
|
|
||||||
|
ALTER TABLE "Cohort" DROP CONSTRAINT IF EXISTS "Cohort_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "Cohort_stageId_idx";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Cohort" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "Cohort_roundId_idx" ON "Cohort"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "Cohort" ADD CONSTRAINT "Cohort_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 15. LiveProgressCursor ---
|
||||||
|
-- LiveProgressCursor was created in pipeline migration with stageId only (no roundId)
|
||||||
|
|
||||||
|
ALTER TABLE "LiveProgressCursor" DROP CONSTRAINT IF EXISTS "LiveProgressCursor_stageId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "LiveProgressCursor_stageId_key";
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveProgressCursor" RENAME COLUMN "stageId" TO "roundId";
|
||||||
|
EXCEPTION WHEN undefined_column THEN NULL; END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "LiveProgressCursor_roundId_key" ON "LiveProgressCursor"("roundId");
|
||||||
|
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "LiveProgressCursor" ADD CONSTRAINT "LiveProgressCursor_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 16. SpecialAward: Drop trackId column ---
|
||||||
|
|
||||||
|
ALTER TABLE "SpecialAward" DROP CONSTRAINT IF EXISTS "SpecialAward_trackId_fkey";
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS "SpecialAward_trackId_key";
|
||||||
|
|
||||||
|
ALTER TABLE "SpecialAward" DROP COLUMN IF EXISTS "trackId";
|
||||||
|
|
||||||
|
-- --- 17. ConflictOfInterest: roundId was made nullable in pipeline migration ---
|
||||||
|
-- It still exists, just restore FK to new Round table
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ConflictOfInterest" ADD CONSTRAINT "ConflictOfInterest_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
-- --- 18. TaggingJob: roundId was made nullable in pipeline migration ---
|
||||||
|
-- Restore FK to new Round table
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "TaggingJob" ADD CONSTRAINT "TaggingJob_roundId_fkey"
|
||||||
|
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Phase 7/8 Migration Part 2: Drop legacy Pipeline/Track/Stage tables and enums
|
||||||
|
-- =============================================================================
|
||||||
|
-- This migration removes the old stage-based architecture tables and enums.
|
||||||
|
-- All data has been migrated to Competition/Round architecture.
|
||||||
|
|
||||||
|
-- ─── Drop Tables in FK-safe order ────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Drop ProjectStageState (references Track and Stage)
|
||||||
|
DROP TABLE IF EXISTS "ProjectStageState" CASCADE;
|
||||||
|
|
||||||
|
-- Drop StageTransition (references Stage)
|
||||||
|
DROP TABLE IF EXISTS "StageTransition" CASCADE;
|
||||||
|
|
||||||
|
-- Drop Stage (references Track)
|
||||||
|
DROP TABLE IF EXISTS "Stage" CASCADE;
|
||||||
|
|
||||||
|
-- Drop Track (references Pipeline)
|
||||||
|
DROP TABLE IF EXISTS "Track" CASCADE;
|
||||||
|
|
||||||
|
-- Drop Pipeline
|
||||||
|
DROP TABLE IF EXISTS "Pipeline" CASCADE;
|
||||||
|
|
||||||
|
-- ─── Drop Enums ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DROP TYPE IF EXISTS "StageType";
|
||||||
|
DROP TYPE IF EXISTS "TrackKind";
|
||||||
|
DROP TYPE IF EXISTS "RoutingMode";
|
||||||
|
DROP TYPE IF EXISTS "StageStatus";
|
||||||
|
DROP TYPE IF EXISTS "ProjectStageStateValue";
|
||||||
|
DROP TYPE IF EXISTS "DecisionMode";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add pageCount column to ProjectFile (was in schema but missing migration)
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN IF NOT EXISTS "pageCount" INTEGER;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- Schema Reconciliation: Fill remaining gaps between migrations and schema.prisma
|
||||||
|
-- =============================================================================
|
||||||
|
-- All statements are idempotent (safe to re-run on any database state).
|
||||||
|
|
||||||
|
-- 1. ConflictOfInterest: add standalone hasConflict index (schema has @@index([hasConflict]))
|
||||||
|
-- Migration 20260205223133 only created composite (roundId, hasConflict) index.
|
||||||
|
CREATE INDEX IF NOT EXISTS "ConflictOfInterest_hasConflict_idx" ON "ConflictOfInterest"("hasConflict");
|
||||||
|
|
||||||
|
-- 2. Ensure ConflictOfInterest.roundId is nullable (schema says String?)
|
||||||
|
-- Pipeline migration (20260213) makes it nullable, but guard for safety.
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ConflictOfInterest" ALTER COLUMN "roundId" DROP NOT NULL;
|
||||||
|
EXCEPTION WHEN others THEN NULL;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 3. Drop stale composite index that no longer matches schema
|
||||||
|
-- Schema only has @@index([hasConflict]) and @@index([userId]), not (roundId, hasConflict).
|
||||||
|
DROP INDEX IF EXISTS "ConflictOfInterest_roundId_hasConflict_idx";
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "textPreview" TEXT;
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "detectedLang" TEXT;
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "langConfidence" DOUBLE PRECISION;
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "analyzedAt" TIMESTAMP(3);
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# Please do not edit this file manually
|
||||||
# It should be added in your version-control system (e.g., Git)
|
# It should be added in your version-control system (e.g., Git)
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
|
|||||||
4415
prisma/schema.prisma
4415
prisma/schema.prisma
File diff suppressed because it is too large
Load Diff
@@ -1,510 +0,0 @@
|
|||||||
import { PrismaClient, CompetitionCategory, OceanIssue, TeamMemberRole } from '@prisma/client'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
import * as path from 'path'
|
|
||||||
import { fileURLToPath } from 'url'
|
|
||||||
import Papa from 'papaparse'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
|
|
||||||
// CSV Column Mapping
|
|
||||||
interface CandidatureRow {
|
|
||||||
'Full name': string
|
|
||||||
'Application status': string
|
|
||||||
'Category': string
|
|
||||||
'Comment ': string // Note the space after 'Comment'
|
|
||||||
'Country': string
|
|
||||||
'Date of creation': string
|
|
||||||
'E-mail': string
|
|
||||||
'How did you hear about MOPC?': string
|
|
||||||
'Issue': string
|
|
||||||
'Jury 1 attribués': string
|
|
||||||
'MOPC team comments': string
|
|
||||||
'Mentorship': string
|
|
||||||
'PHASE 1 - Submission': string
|
|
||||||
'PHASE 2 - Submission': string
|
|
||||||
"Project's name": string
|
|
||||||
'Team members': string
|
|
||||||
'Tri par zone': string
|
|
||||||
'Téléphone': string
|
|
||||||
'University': string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map CSV category strings to enum values
|
|
||||||
function mapCategory(category: string): CompetitionCategory | null {
|
|
||||||
if (!category) return null
|
|
||||||
const lower = category.toLowerCase()
|
|
||||||
if (lower.includes('start-up') || lower.includes('startup')) {
|
|
||||||
return 'STARTUP'
|
|
||||||
}
|
|
||||||
if (lower.includes('business concept')) {
|
|
||||||
return 'BUSINESS_CONCEPT'
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map CSV issue strings to enum values
|
|
||||||
function mapOceanIssue(issue: string): OceanIssue | null {
|
|
||||||
if (!issue) return null
|
|
||||||
const lower = issue.toLowerCase()
|
|
||||||
|
|
||||||
if (lower.includes('pollution')) return 'POLLUTION_REDUCTION'
|
|
||||||
if (lower.includes('climate') || lower.includes('sea-level')) return 'CLIMATE_MITIGATION'
|
|
||||||
if (lower.includes('technology') || lower.includes('innovation')) return 'TECHNOLOGY_INNOVATION'
|
|
||||||
if (lower.includes('shipping') || lower.includes('yachting')) return 'SUSTAINABLE_SHIPPING'
|
|
||||||
if (lower.includes('blue carbon')) return 'BLUE_CARBON'
|
|
||||||
if (lower.includes('habitat') || lower.includes('restoration') || lower.includes('ecosystem')) return 'HABITAT_RESTORATION'
|
|
||||||
if (lower.includes('community') || lower.includes('capacity') || lower.includes('coastal')) return 'COMMUNITY_CAPACITY'
|
|
||||||
if (lower.includes('fishing') || lower.includes('aquaculture') || lower.includes('blue food')) return 'SUSTAINABLE_FISHING'
|
|
||||||
if (lower.includes('awareness') || lower.includes('education') || lower.includes('consumer')) return 'CONSUMER_AWARENESS'
|
|
||||||
if (lower.includes('acidification')) return 'OCEAN_ACIDIFICATION'
|
|
||||||
|
|
||||||
return 'OTHER'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse team members string into array
|
|
||||||
function parseTeamMembers(teamMembersStr: string): { name: string; email?: string }[] {
|
|
||||||
if (!teamMembersStr) return []
|
|
||||||
|
|
||||||
// Split by comma or semicolon
|
|
||||||
const members = teamMembersStr.split(/[,;]/).map((m) => m.trim()).filter(Boolean)
|
|
||||||
|
|
||||||
return members.map((name) => ({
|
|
||||||
name: name.trim(),
|
|
||||||
// No emails in CSV, just names with titles
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract country code from location string or return ISO code directly
|
|
||||||
function extractCountry(location: string): string | null {
|
|
||||||
if (!location) return null
|
|
||||||
|
|
||||||
// If already a 2-letter ISO code, return it directly
|
|
||||||
const trimmed = location.trim()
|
|
||||||
if (/^[A-Z]{2}$/.test(trimmed)) return trimmed
|
|
||||||
|
|
||||||
// Common country mappings from the CSV data
|
|
||||||
const countryMappings: Record<string, string> = {
|
|
||||||
'tunisie': 'TN',
|
|
||||||
'tunisia': 'TN',
|
|
||||||
'royaume-uni': 'GB',
|
|
||||||
'uk': 'GB',
|
|
||||||
'united kingdom': 'GB',
|
|
||||||
'angleterre': 'GB',
|
|
||||||
'england': 'GB',
|
|
||||||
'espagne': 'ES',
|
|
||||||
'spain': 'ES',
|
|
||||||
'inde': 'IN',
|
|
||||||
'india': 'IN',
|
|
||||||
'france': 'FR',
|
|
||||||
'états-unis': 'US',
|
|
||||||
'usa': 'US',
|
|
||||||
'united states': 'US',
|
|
||||||
'allemagne': 'DE',
|
|
||||||
'germany': 'DE',
|
|
||||||
'italie': 'IT',
|
|
||||||
'italy': 'IT',
|
|
||||||
'portugal': 'PT',
|
|
||||||
'monaco': 'MC',
|
|
||||||
'suisse': 'CH',
|
|
||||||
'switzerland': 'CH',
|
|
||||||
'belgique': 'BE',
|
|
||||||
'belgium': 'BE',
|
|
||||||
'pays-bas': 'NL',
|
|
||||||
'netherlands': 'NL',
|
|
||||||
'australia': 'AU',
|
|
||||||
'australie': 'AU',
|
|
||||||
'japon': 'JP',
|
|
||||||
'japan': 'JP',
|
|
||||||
'chine': 'CN',
|
|
||||||
'china': 'CN',
|
|
||||||
'brésil': 'BR',
|
|
||||||
'brazil': 'BR',
|
|
||||||
'mexique': 'MX',
|
|
||||||
'mexico': 'MX',
|
|
||||||
'canada': 'CA',
|
|
||||||
'maroc': 'MA',
|
|
||||||
'morocco': 'MA',
|
|
||||||
'egypte': 'EG',
|
|
||||||
'egypt': 'EG',
|
|
||||||
'afrique du sud': 'ZA',
|
|
||||||
'south africa': 'ZA',
|
|
||||||
'nigeria': 'NG',
|
|
||||||
'kenya': 'KE',
|
|
||||||
'ghana': 'GH',
|
|
||||||
'senegal': 'SN',
|
|
||||||
'sénégal': 'SN',
|
|
||||||
'côte d\'ivoire': 'CI',
|
|
||||||
'ivory coast': 'CI',
|
|
||||||
'indonesia': 'ID',
|
|
||||||
'indonésie': 'ID',
|
|
||||||
'philippines': 'PH',
|
|
||||||
'vietnam': 'VN',
|
|
||||||
'thaïlande': 'TH',
|
|
||||||
'thailand': 'TH',
|
|
||||||
'malaisie': 'MY',
|
|
||||||
'malaysia': 'MY',
|
|
||||||
'singapour': 'SG',
|
|
||||||
'singapore': 'SG',
|
|
||||||
'grèce': 'GR',
|
|
||||||
'greece': 'GR',
|
|
||||||
'turquie': 'TR',
|
|
||||||
'turkey': 'TR',
|
|
||||||
'pologne': 'PL',
|
|
||||||
'poland': 'PL',
|
|
||||||
'norvège': 'NO',
|
|
||||||
'norway': 'NO',
|
|
||||||
'suède': 'SE',
|
|
||||||
'sweden': 'SE',
|
|
||||||
'danemark': 'DK',
|
|
||||||
'denmark': 'DK',
|
|
||||||
'finlande': 'FI',
|
|
||||||
'finland': 'FI',
|
|
||||||
'irlande': 'IE',
|
|
||||||
'ireland': 'IE',
|
|
||||||
'autriche': 'AT',
|
|
||||||
'austria': 'AT',
|
|
||||||
// Additional mappings from CSV data (French names, accented variants)
|
|
||||||
'nigéria': 'NG',
|
|
||||||
'tanzanie': 'TZ',
|
|
||||||
'tanzania': 'TZ',
|
|
||||||
'ouganda': 'UG',
|
|
||||||
'uganda': 'UG',
|
|
||||||
'zambie': 'ZM',
|
|
||||||
'zambia': 'ZM',
|
|
||||||
'somalie': 'SO',
|
|
||||||
'somalia': 'SO',
|
|
||||||
'jordanie': 'JO',
|
|
||||||
'jordan': 'JO',
|
|
||||||
'bulgarie': 'BG',
|
|
||||||
'bulgaria': 'BG',
|
|
||||||
'indonesie': 'ID',
|
|
||||||
'macédoine du nord': 'MK',
|
|
||||||
'north macedonia': 'MK',
|
|
||||||
'jersey': 'JE',
|
|
||||||
'kazakhstan': 'KZ',
|
|
||||||
'cameroun': 'CM',
|
|
||||||
'cameroon': 'CM',
|
|
||||||
'vanuatu': 'VU',
|
|
||||||
'bénin': 'BJ',
|
|
||||||
'benin': 'BJ',
|
|
||||||
'argentine': 'AR',
|
|
||||||
'argentina': 'AR',
|
|
||||||
'srbija': 'RS',
|
|
||||||
'serbia': 'RS',
|
|
||||||
'kraljevo': 'RS',
|
|
||||||
'kosovo': 'XK',
|
|
||||||
'pristina': 'XK',
|
|
||||||
'xinjiang': 'CN',
|
|
||||||
'haïti': 'HT',
|
|
||||||
'haiti': 'HT',
|
|
||||||
'sri lanka': 'LK',
|
|
||||||
'luxembourg': 'LU',
|
|
||||||
'congo': 'CG',
|
|
||||||
'brazzaville': 'CG',
|
|
||||||
'colombie': 'CO',
|
|
||||||
'colombia': 'CO',
|
|
||||||
'bogota': 'CO',
|
|
||||||
'ukraine': 'UA',
|
|
||||||
}
|
|
||||||
|
|
||||||
const lower = location.toLowerCase()
|
|
||||||
|
|
||||||
for (const [key, code] of Object.entries(countryMappings)) {
|
|
||||||
if (lower.includes(key)) {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('Starting candidatures import...\n')
|
|
||||||
|
|
||||||
// Read the CSV file
|
|
||||||
const csvPath = path.join(__dirname, '../docs/candidatures_2026.csv')
|
|
||||||
|
|
||||||
if (!fs.existsSync(csvPath)) {
|
|
||||||
console.error(`CSV file not found at ${csvPath}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvContent = fs.readFileSync(csvPath, 'utf-8')
|
|
||||||
|
|
||||||
// Parse CSV
|
|
||||||
const parseResult = Papa.parse<CandidatureRow>(csvContent, {
|
|
||||||
header: true,
|
|
||||||
skipEmptyLines: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (parseResult.errors.length > 0) {
|
|
||||||
console.warn('CSV parsing warnings:', parseResult.errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows = parseResult.data
|
|
||||||
console.log(`Found ${rows.length} candidatures in CSV\n`)
|
|
||||||
|
|
||||||
// Get or create program
|
|
||||||
let program = await prisma.program.findFirst({
|
|
||||||
where: {
|
|
||||||
name: 'Monaco Ocean Protection Challenge',
|
|
||||||
year: 2026,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!program) {
|
|
||||||
program = await prisma.program.create({
|
|
||||||
data: {
|
|
||||||
name: 'Monaco Ocean Protection Challenge',
|
|
||||||
year: 2026,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
description: 'The Monaco Ocean Protection Challenge is a flagship program promoting innovative solutions for ocean conservation.',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log('Created program:', program.name, program.year)
|
|
||||||
} else {
|
|
||||||
console.log('Using existing program:', program.name, program.year)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create Round 1
|
|
||||||
let round = await prisma.round.findFirst({
|
|
||||||
where: {
|
|
||||||
programId: program.id,
|
|
||||||
slug: 'mopc-2026-round-1',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!round) {
|
|
||||||
round = await prisma.round.create({
|
|
||||||
data: {
|
|
||||||
programId: program.id,
|
|
||||||
name: 'Round 1 - Semi-Finalists Selection',
|
|
||||||
slug: 'mopc-2026-round-1',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
roundType: 'EVALUATION',
|
|
||||||
submissionStartDate: new Date('2025-09-01'),
|
|
||||||
submissionEndDate: new Date('2026-01-31'),
|
|
||||||
votingStartAt: new Date('2026-02-15'),
|
|
||||||
votingEndAt: new Date('2026-02-28'),
|
|
||||||
requiredReviews: 3,
|
|
||||||
settingsJson: {
|
|
||||||
gracePeriod: { hours: 24 },
|
|
||||||
allowLateSubmissions: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log('Created round:', round.name)
|
|
||||||
} else {
|
|
||||||
console.log('Using existing round:', round.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nImporting candidatures...\n')
|
|
||||||
|
|
||||||
let imported = 0
|
|
||||||
let skipped = 0
|
|
||||||
let errors = 0
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
try {
|
|
||||||
const projectName = row["Project's name"]?.trim()
|
|
||||||
const email = row['E-mail']?.trim()
|
|
||||||
|
|
||||||
if (!projectName || !email) {
|
|
||||||
console.log(`Skipping row: missing project name or email`)
|
|
||||||
skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if project already exists
|
|
||||||
const existingProject = await prisma.project.findFirst({
|
|
||||||
where: {
|
|
||||||
roundId: round.id,
|
|
||||||
OR: [
|
|
||||||
{ title: projectName },
|
|
||||||
{ submittedByEmail: email },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (existingProject) {
|
|
||||||
console.log(`Skipping duplicate: ${projectName} (${email})`)
|
|
||||||
skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get or create user
|
|
||||||
let user = await prisma.user.findUnique({
|
|
||||||
where: { email },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email,
|
|
||||||
name: row['Full name']?.trim() || 'Unknown',
|
|
||||||
role: 'APPLICANT',
|
|
||||||
status: 'INVITED',
|
|
||||||
phoneNumber: row['Téléphone']?.trim() || null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse date
|
|
||||||
let submittedAt: Date | null = null
|
|
||||||
if (row['Date of creation']) {
|
|
||||||
const dateStr = row['Date of creation'].trim()
|
|
||||||
const parsed = new Date(dateStr)
|
|
||||||
if (!isNaN(parsed.getTime())) {
|
|
||||||
submittedAt = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create project
|
|
||||||
const project = await prisma.project.create({
|
|
||||||
data: {
|
|
||||||
programId: round.programId,
|
|
||||||
roundId: round.id,
|
|
||||||
title: projectName,
|
|
||||||
description: row['Comment ']?.trim() || null,
|
|
||||||
competitionCategory: mapCategory(row['Category']),
|
|
||||||
oceanIssue: mapOceanIssue(row['Issue']),
|
|
||||||
country: extractCountry(row['Country']),
|
|
||||||
geographicZone: row['Tri par zone']?.trim() || null,
|
|
||||||
institution: row['University']?.trim() || null,
|
|
||||||
wantsMentorship: row['Mentorship']?.toLowerCase() === 'true',
|
|
||||||
phase1SubmissionUrl: row['PHASE 1 - Submission']?.trim() || null,
|
|
||||||
phase2SubmissionUrl: row['PHASE 2 - Submission']?.trim() || null,
|
|
||||||
referralSource: row['How did you hear about MOPC?']?.trim() || null,
|
|
||||||
applicationStatus: row['Application status']?.trim() || 'Received',
|
|
||||||
internalComments: row['MOPC team comments']?.trim() || null,
|
|
||||||
submissionSource: 'CSV',
|
|
||||||
submittedByEmail: email,
|
|
||||||
submittedByUserId: user.id,
|
|
||||||
submittedAt: submittedAt || new Date(),
|
|
||||||
metadataJson: {
|
|
||||||
importedFrom: 'candidatures_2026.csv',
|
|
||||||
importedAt: new Date().toISOString(),
|
|
||||||
originalPhone: row['Téléphone']?.trim(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create team lead membership
|
|
||||||
await prisma.teamMember.create({
|
|
||||||
data: {
|
|
||||||
projectId: project.id,
|
|
||||||
userId: user.id,
|
|
||||||
role: 'LEAD',
|
|
||||||
title: 'Team Lead',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Parse and create team members
|
|
||||||
const teamMembers = parseTeamMembers(row['Team members'])
|
|
||||||
const leadName = row['Full name']?.trim().toLowerCase()
|
|
||||||
|
|
||||||
for (const member of teamMembers) {
|
|
||||||
// Skip if it's the lead (already added)
|
|
||||||
if (member.name.toLowerCase() === leadName) continue
|
|
||||||
|
|
||||||
// Since we don't have emails for team members, we create placeholder accounts
|
|
||||||
// They can claim their accounts later
|
|
||||||
const memberEmail = `${member.name.toLowerCase().replace(/[^a-z0-9]/g, '.')}@pending.mopc.local`
|
|
||||||
|
|
||||||
let memberUser = await prisma.user.findUnique({
|
|
||||||
where: { email: memberEmail },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!memberUser) {
|
|
||||||
memberUser = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
email: memberEmail,
|
|
||||||
name: member.name,
|
|
||||||
role: 'APPLICANT',
|
|
||||||
status: 'INVITED',
|
|
||||||
metadataJson: {
|
|
||||||
isPendingEmailVerification: true,
|
|
||||||
originalName: member.name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if membership already exists
|
|
||||||
const existingMembership = await prisma.teamMember.findUnique({
|
|
||||||
where: {
|
|
||||||
projectId_userId: {
|
|
||||||
projectId: project.id,
|
|
||||||
userId: memberUser.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!existingMembership) {
|
|
||||||
await prisma.teamMember.create({
|
|
||||||
data: {
|
|
||||||
projectId: project.id,
|
|
||||||
userId: memberUser.id,
|
|
||||||
role: 'MEMBER',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Imported: ${projectName} (${email}) - ${teamMembers.length} team members`)
|
|
||||||
imported++
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Error importing row:`, err)
|
|
||||||
errors++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backfill: update any existing projects with null country
|
|
||||||
console.log('\nBackfilling missing country codes...\n')
|
|
||||||
let backfilled = 0
|
|
||||||
const nullCountryProjects = await prisma.project.findMany({
|
|
||||||
where: { roundId: round.id, country: null },
|
|
||||||
select: { id: true, submittedByEmail: true, title: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const project of nullCountryProjects) {
|
|
||||||
// Find the matching CSV row by email or title
|
|
||||||
const matchingRow = rows.find(
|
|
||||||
(r) =>
|
|
||||||
r['E-mail']?.trim() === project.submittedByEmail ||
|
|
||||||
r["Project's name"]?.trim() === project.title
|
|
||||||
)
|
|
||||||
if (matchingRow?.['Country']) {
|
|
||||||
const countryCode = extractCountry(matchingRow['Country'])
|
|
||||||
if (countryCode) {
|
|
||||||
await prisma.project.update({
|
|
||||||
where: { id: project.id },
|
|
||||||
data: { country: countryCode },
|
|
||||||
})
|
|
||||||
console.log(` Updated: ${project.title} → ${countryCode}`)
|
|
||||||
backfilled++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(` Backfilled: ${backfilled} projects\n`)
|
|
||||||
|
|
||||||
console.log('\n========================================')
|
|
||||||
console.log(`Import complete!`)
|
|
||||||
console.log(` Imported: ${imported}`)
|
|
||||||
console.log(` Skipped: ${skipped}`)
|
|
||||||
console.log(` Errors: ${errors}`)
|
|
||||||
console.log(` Backfilled: ${backfilled}`)
|
|
||||||
console.log('========================================\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect()
|
|
||||||
})
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client'
|
|
||||||
import bcrypt from 'bcryptjs'
|
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('Setting up demo jury member...\n')
|
|
||||||
|
|
||||||
// Hash a password for the demo jury account
|
|
||||||
const password = 'JuryDemo2026!'
|
|
||||||
const passwordHash = await bcrypt.hash(password, 12)
|
|
||||||
|
|
||||||
// Create or update jury member
|
|
||||||
const juryUser = await prisma.user.upsert({
|
|
||||||
where: { email: 'jury.demo@monaco-opc.com' },
|
|
||||||
update: {
|
|
||||||
passwordHash,
|
|
||||||
mustSetPassword: false,
|
|
||||||
status: 'ACTIVE',
|
|
||||||
onboardingCompletedAt: new Date(),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
email: 'jury.demo@monaco-opc.com',
|
|
||||||
name: 'Dr. Marie Laurent',
|
|
||||||
role: 'JURY_MEMBER',
|
|
||||||
status: 'ACTIVE',
|
|
||||||
passwordHash,
|
|
||||||
mustSetPassword: false,
|
|
||||||
passwordSetAt: new Date(),
|
|
||||||
onboardingCompletedAt: new Date(),
|
|
||||||
expertiseTags: ['Marine Biology', 'Ocean Conservation', 'Sustainable Innovation'],
|
|
||||||
notificationPreference: 'EMAIL',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`Jury user: ${juryUser.email} (${juryUser.id})`)
|
|
||||||
console.log(`Password: ${password}\n`)
|
|
||||||
|
|
||||||
// Find the round
|
|
||||||
const round = await prisma.round.findFirst({
|
|
||||||
where: { slug: 'mopc-2026-round-1' },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!round) {
|
|
||||||
console.error('Round not found! Run seed-candidatures first.')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Round: ${round.name} (${round.id})`)
|
|
||||||
|
|
||||||
// Ensure voting window is open
|
|
||||||
const now = new Date()
|
|
||||||
const votingStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) // 7 days ago
|
|
||||||
const votingEnd = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000) // 30 days from now
|
|
||||||
|
|
||||||
await prisma.round.update({
|
|
||||||
where: { id: round.id },
|
|
||||||
data: {
|
|
||||||
status: 'ACTIVE',
|
|
||||||
votingStartAt: votingStart,
|
|
||||||
votingEndAt: votingEnd,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log(`Voting window: ${votingStart.toISOString()} → ${votingEnd.toISOString()}\n`)
|
|
||||||
|
|
||||||
// Get some projects to assign
|
|
||||||
const projects = await prisma.project.findMany({
|
|
||||||
where: { roundId: round.id },
|
|
||||||
take: 8,
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
select: { id: true, title: true },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (projects.length === 0) {
|
|
||||||
console.error('No projects found! Run seed-candidatures first.')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${projects.length} projects to assign\n`)
|
|
||||||
|
|
||||||
// Create assignments
|
|
||||||
let created = 0
|
|
||||||
let skipped = 0
|
|
||||||
|
|
||||||
for (const project of projects) {
|
|
||||||
try {
|
|
||||||
await prisma.assignment.upsert({
|
|
||||||
where: {
|
|
||||||
userId_projectId_roundId: {
|
|
||||||
userId: juryUser.id,
|
|
||||||
projectId: project.id,
|
|
||||||
roundId: round.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
create: {
|
|
||||||
userId: juryUser.id,
|
|
||||||
projectId: project.id,
|
|
||||||
roundId: round.id,
|
|
||||||
method: 'MANUAL',
|
|
||||||
isRequired: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log(` Assigned: ${project.title}`)
|
|
||||||
created++
|
|
||||||
} catch {
|
|
||||||
skipped++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure evaluation criteria exist for this round
|
|
||||||
const existingForm = await prisma.evaluationForm.findFirst({
|
|
||||||
where: { roundId: round.id },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!existingForm) {
|
|
||||||
await prisma.evaluationForm.create({
|
|
||||||
data: {
|
|
||||||
roundId: round.id,
|
|
||||||
isActive: true,
|
|
||||||
criteriaJson: [
|
|
||||||
{
|
|
||||||
id: 'innovation',
|
|
||||||
label: 'Innovation & Originality',
|
|
||||||
description: 'How innovative is the proposed solution? Does it bring a new approach to ocean conservation?',
|
|
||||||
scale: 10,
|
|
||||||
weight: 25,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'feasibility',
|
|
||||||
label: 'Technical Feasibility',
|
|
||||||
description: 'Is the solution technically viable? Can it be realistically implemented?',
|
|
||||||
scale: 10,
|
|
||||||
weight: 25,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'impact',
|
|
||||||
label: 'Environmental Impact',
|
|
||||||
description: 'What is the potential positive impact on ocean health and marine ecosystems?',
|
|
||||||
scale: 10,
|
|
||||||
weight: 30,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'team',
|
|
||||||
label: 'Team Capability',
|
|
||||||
description: 'Does the team have the skills, experience, and commitment to execute?',
|
|
||||||
scale: 10,
|
|
||||||
weight: 20,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
console.log('\nCreated evaluation form with 4 criteria')
|
|
||||||
} else {
|
|
||||||
console.log('\nEvaluation form already exists')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n========================================')
|
|
||||||
console.log('Demo jury member setup complete!')
|
|
||||||
console.log(` Email: jury.demo@monaco-opc.com`)
|
|
||||||
console.log(` Password: ${password}`)
|
|
||||||
console.log(` Assignments: ${created} created, ${skipped} skipped`)
|
|
||||||
console.log(` Voting: OPEN (${round.name})`)
|
|
||||||
console.log('========================================\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
main()
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
.finally(async () => {
|
|
||||||
await prisma.$disconnect()
|
|
||||||
})
|
|
||||||
1404
prisma/seed.ts
1404
prisma/seed.ts
File diff suppressed because it is too large
Load Diff
@@ -58,12 +58,9 @@ sudo mkdir -p /data/mopc/postgres
|
|||||||
sudo chown -R 1000:1000 /data/mopc
|
sudo chown -R 1000:1000 /data/mopc
|
||||||
|
|
||||||
# 6. Pull and start
|
# 6. Pull and start
|
||||||
echo "==> Pulling latest images..."
|
echo "==> Pulling latest images and starting services..."
|
||||||
cd "$DOCKER_DIR"
|
cd "$DOCKER_DIR"
|
||||||
docker compose pull app
|
docker compose up -d --pull always
|
||||||
|
|
||||||
echo "==> Starting services..."
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# 7. Wait for health check
|
# 7. Wait for health check
|
||||||
echo "==> Waiting for application to start..."
|
echo "==> Waiting for application to start..."
|
||||||
|
|||||||
@@ -17,16 +17,12 @@ echo " MOPC Platform - Update"
|
|||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# 1. Pull latest image from registry
|
# 1. Pull and recreate app only (postgres stays running)
|
||||||
echo "==> Pulling latest image..."
|
echo "==> Pulling latest image and recreating app..."
|
||||||
cd "$DOCKER_DIR"
|
cd "$DOCKER_DIR"
|
||||||
docker compose pull app
|
docker compose up -d --pull always --force-recreate app
|
||||||
|
|
||||||
# 2. Restart app only (postgres stays running)
|
# 2. Wait for health check
|
||||||
echo "==> Restarting app..."
|
|
||||||
docker compose up -d app
|
|
||||||
|
|
||||||
# 3. Wait for health check
|
|
||||||
echo "==> Waiting for application to start..."
|
echo "==> Waiting for application to start..."
|
||||||
MAX_WAIT=120
|
MAX_WAIT=120
|
||||||
WAITED=0
|
WAITED=0
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,291 +1,537 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { use, useState, useEffect } from 'react'
|
import { use, useState, useEffect } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { trpc } from '@/lib/trpc/client'
|
import { trpc } from '@/lib/trpc/client'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { ArrowLeft, Save, Loader2 } from 'lucide-react'
|
import { ArrowLeft, Save, Loader2, Plus, X, Info } from 'lucide-react'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
export default function EditAwardPage({
|
|
||||||
params,
|
type AutoTagRule = {
|
||||||
}: {
|
id: string
|
||||||
params: Promise<{ id: string }>
|
field: 'competitionCategory' | 'country' | 'geographicZone' | 'tags' | 'oceanIssue'
|
||||||
}) {
|
operator: 'equals' | 'contains' | 'in'
|
||||||
const { id: awardId } = use(params)
|
value: string
|
||||||
const router = useRouter()
|
}
|
||||||
|
|
||||||
const utils = trpc.useUtils()
|
export default function EditAwardPage({
|
||||||
const { data: award, isLoading } = trpc.specialAward.get.useQuery({ id: awardId })
|
params,
|
||||||
const updateAward = trpc.specialAward.update.useMutation({
|
}: {
|
||||||
onSuccess: () => {
|
params: Promise<{ id: string }>
|
||||||
utils.specialAward.get.invalidate({ id: awardId })
|
}) {
|
||||||
utils.specialAward.list.invalidate()
|
const { id: awardId } = use(params)
|
||||||
},
|
const router = useRouter()
|
||||||
})
|
|
||||||
|
const utils = trpc.useUtils()
|
||||||
const [name, setName] = useState('')
|
const { data: award, isLoading } = trpc.specialAward.get.useQuery({ id: awardId })
|
||||||
const [description, setDescription] = useState('')
|
|
||||||
const [criteriaText, setCriteriaText] = useState('')
|
// Fetch competition rounds for source round selector
|
||||||
const [scoringMode, setScoringMode] = useState<'PICK_WINNER' | 'RANKED' | 'SCORED'>('PICK_WINNER')
|
const competitionId = award?.competitionId
|
||||||
const [useAiEligibility, setUseAiEligibility] = useState(true)
|
const { data: competition } = trpc.competition.getById.useQuery(
|
||||||
const [maxRankedPicks, setMaxRankedPicks] = useState('3')
|
{ id: competitionId! },
|
||||||
const [votingStartAt, setVotingStartAt] = useState('')
|
{ enabled: !!competitionId }
|
||||||
const [votingEndAt, setVotingEndAt] = useState('')
|
)
|
||||||
|
|
||||||
// Helper to format date for datetime-local input
|
const updateAward = trpc.specialAward.update.useMutation({
|
||||||
const formatDateForInput = (date: Date | string | null | undefined): string => {
|
onSuccess: () => {
|
||||||
if (!date) return ''
|
utils.specialAward.get.invalidate({ id: awardId })
|
||||||
const d = new Date(date)
|
utils.specialAward.list.invalidate()
|
||||||
// Format: YYYY-MM-DDTHH:mm
|
},
|
||||||
return d.toISOString().slice(0, 16)
|
})
|
||||||
}
|
|
||||||
|
const [name, setName] = useState('')
|
||||||
// Load existing values when award data arrives
|
const [description, setDescription] = useState('')
|
||||||
useEffect(() => {
|
const [criteriaText, setCriteriaText] = useState('')
|
||||||
if (award) {
|
const [scoringMode, setScoringMode] = useState<'PICK_WINNER' | 'RANKED' | 'SCORED'>('PICK_WINNER')
|
||||||
setName(award.name)
|
const [useAiEligibility, setUseAiEligibility] = useState(true)
|
||||||
setDescription(award.description || '')
|
const [maxRankedPicks, setMaxRankedPicks] = useState('3')
|
||||||
setCriteriaText(award.criteriaText || '')
|
const [votingStartAt, setVotingStartAt] = useState('')
|
||||||
setScoringMode(award.scoringMode as 'PICK_WINNER' | 'RANKED' | 'SCORED')
|
const [votingEndAt, setVotingEndAt] = useState('')
|
||||||
setUseAiEligibility(award.useAiEligibility)
|
const [evaluationRoundId, setEvaluationRoundId] = useState('')
|
||||||
setMaxRankedPicks(String(award.maxRankedPicks || 3))
|
const [eligibilityMode, setEligibilityMode] = useState<'STAY_IN_MAIN' | 'SEPARATE_POOL'>('STAY_IN_MAIN')
|
||||||
setVotingStartAt(formatDateForInput(award.votingStartAt))
|
const [autoTagRules, setAutoTagRules] = useState<AutoTagRule[]>([])
|
||||||
setVotingEndAt(formatDateForInput(award.votingEndAt))
|
|
||||||
}
|
// Helper to format date for datetime-local input
|
||||||
}, [award])
|
const formatDateForInput = (date: Date | string | null | undefined): string => {
|
||||||
|
if (!date) return ''
|
||||||
const handleSubmit = async () => {
|
const d = new Date(date)
|
||||||
if (!name.trim()) return
|
// Format: YYYY-MM-DDTHH:mm
|
||||||
try {
|
return d.toISOString().slice(0, 16)
|
||||||
await updateAward.mutateAsync({
|
}
|
||||||
id: awardId,
|
|
||||||
name: name.trim(),
|
// Load existing values when award data arrives
|
||||||
description: description.trim() || undefined,
|
useEffect(() => {
|
||||||
criteriaText: criteriaText.trim() || undefined,
|
if (award) {
|
||||||
useAiEligibility,
|
setName(award.name)
|
||||||
scoringMode,
|
setDescription(award.description || '')
|
||||||
maxRankedPicks: scoringMode === 'RANKED' ? parseInt(maxRankedPicks) : undefined,
|
setCriteriaText(award.criteriaText || '')
|
||||||
votingStartAt: votingStartAt ? new Date(votingStartAt) : undefined,
|
setScoringMode(award.scoringMode as 'PICK_WINNER' | 'RANKED' | 'SCORED')
|
||||||
votingEndAt: votingEndAt ? new Date(votingEndAt) : undefined,
|
setUseAiEligibility(award.useAiEligibility)
|
||||||
})
|
setMaxRankedPicks(String(award.maxRankedPicks || 3))
|
||||||
toast.success('Award updated')
|
setVotingStartAt(formatDateForInput(award.votingStartAt))
|
||||||
router.push(`/admin/awards/${awardId}`)
|
setVotingEndAt(formatDateForInput(award.votingEndAt))
|
||||||
} catch (error) {
|
setEvaluationRoundId(award.evaluationRoundId || '')
|
||||||
toast.error(
|
setEligibilityMode(award.eligibilityMode as 'STAY_IN_MAIN' | 'SEPARATE_POOL')
|
||||||
error instanceof Error ? error.message : 'Failed to update award'
|
|
||||||
)
|
// Parse autoTagRulesJson
|
||||||
}
|
if (award.autoTagRulesJson && typeof award.autoTagRulesJson === 'object') {
|
||||||
}
|
const rules = award.autoTagRulesJson as { rules?: AutoTagRule[] }
|
||||||
|
setAutoTagRules(rules.rules || [])
|
||||||
if (isLoading) {
|
} else {
|
||||||
return (
|
setAutoTagRules([])
|
||||||
<div className="space-y-6">
|
}
|
||||||
<Skeleton className="h-9 w-48" />
|
}
|
||||||
<Skeleton className="h-[400px] w-full" />
|
}, [award])
|
||||||
</div>
|
|
||||||
)
|
const handleSubmit = async () => {
|
||||||
}
|
if (!name.trim()) return
|
||||||
|
try {
|
||||||
if (!award) return null
|
await updateAward.mutateAsync({
|
||||||
|
id: awardId,
|
||||||
return (
|
name: name.trim(),
|
||||||
<div className="space-y-6">
|
description: description.trim() || undefined,
|
||||||
<div className="flex items-center gap-4">
|
criteriaText: criteriaText.trim() || undefined,
|
||||||
<Button variant="ghost" asChild className="-ml-4">
|
useAiEligibility,
|
||||||
<Link href={`/admin/awards/${awardId}`}>
|
scoringMode,
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
maxRankedPicks: scoringMode === 'RANKED' ? parseInt(maxRankedPicks) : undefined,
|
||||||
Back to Award
|
votingStartAt: votingStartAt ? new Date(votingStartAt) : undefined,
|
||||||
</Link>
|
votingEndAt: votingEndAt ? new Date(votingEndAt) : undefined,
|
||||||
</Button>
|
evaluationRoundId: evaluationRoundId || undefined,
|
||||||
</div>
|
eligibilityMode,
|
||||||
|
autoTagRulesJson: autoTagRules.length > 0 ? { rules: autoTagRules } : undefined,
|
||||||
<div>
|
})
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
toast.success('Award updated')
|
||||||
Edit Award
|
router.push(`/admin/awards/${awardId}`)
|
||||||
</h1>
|
} catch (error) {
|
||||||
<p className="text-muted-foreground">
|
toast.error(
|
||||||
Update award settings and eligibility criteria
|
error instanceof Error ? error.message : 'Failed to update award'
|
||||||
</p>
|
)
|
||||||
</div>
|
}
|
||||||
|
}
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
const addRule = () => {
|
||||||
<CardTitle>Award Details</CardTitle>
|
setAutoTagRules([
|
||||||
<CardDescription>
|
...autoTagRules,
|
||||||
Configure the award name, criteria, and scoring mode
|
{
|
||||||
</CardDescription>
|
id: `rule-${Date.now()}`,
|
||||||
</CardHeader>
|
field: 'competitionCategory',
|
||||||
<CardContent className="space-y-4">
|
operator: 'equals',
|
||||||
<div className="space-y-2">
|
value: '',
|
||||||
<Label htmlFor="name">Award Name</Label>
|
},
|
||||||
<Input
|
])
|
||||||
id="name"
|
}
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
const removeRule = (id: string) => {
|
||||||
placeholder="e.g., Mediterranean Entrepreneurship Award"
|
setAutoTagRules(autoTagRules.filter((r) => r.id !== id))
|
||||||
/>
|
}
|
||||||
</div>
|
|
||||||
|
const updateRule = (id: string, updates: Partial<AutoTagRule>) => {
|
||||||
<div className="space-y-2">
|
setAutoTagRules(
|
||||||
<Label htmlFor="description">Description</Label>
|
autoTagRules.map((r) => (r.id === id ? { ...r, ...updates } : r))
|
||||||
<Textarea
|
)
|
||||||
id="description"
|
}
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
if (isLoading) {
|
||||||
placeholder="Brief description of this award"
|
return (
|
||||||
rows={3}
|
<div className="space-y-6">
|
||||||
/>
|
<Skeleton className="h-9 w-48" />
|
||||||
</div>
|
<Skeleton className="h-[400px] w-full" />
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
)
|
||||||
<Label htmlFor="criteria">Eligibility Criteria</Label>
|
}
|
||||||
<Textarea
|
|
||||||
id="criteria"
|
if (!award) return null
|
||||||
value={criteriaText}
|
|
||||||
onChange={(e) => setCriteriaText(e.target.value)}
|
return (
|
||||||
placeholder="Describe the criteria in plain language. AI will interpret this to evaluate project eligibility."
|
<div className="space-y-6">
|
||||||
rows={4}
|
<div className="flex items-center gap-4">
|
||||||
/>
|
<Button variant="ghost" asChild className="-ml-4">
|
||||||
<p className="text-xs text-muted-foreground">
|
<Link href={`/admin/awards/${awardId}`}>
|
||||||
This text will be used by AI to determine which projects are
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
eligible for this award.
|
Back to Award
|
||||||
</p>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
<div>
|
||||||
<Label htmlFor="ai-toggle">AI Eligibility</Label>
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
<p className="text-xs text-muted-foreground">
|
Edit Award
|
||||||
Use AI to automatically evaluate project eligibility based on the criteria above.
|
</h1>
|
||||||
Turn off for awards decided by feeling or manual selection.
|
<p className="text-muted-foreground">
|
||||||
</p>
|
Update award settings and eligibility criteria
|
||||||
</div>
|
</p>
|
||||||
<Switch
|
</div>
|
||||||
id="ai-toggle"
|
|
||||||
checked={useAiEligibility}
|
<Card>
|
||||||
onCheckedChange={setUseAiEligibility}
|
<CardHeader>
|
||||||
/>
|
<CardTitle>Award Details</CardTitle>
|
||||||
</div>
|
<CardDescription>
|
||||||
|
Configure the award name, criteria, and scoring mode
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
</CardDescription>
|
||||||
<div className="space-y-2">
|
</CardHeader>
|
||||||
<Label htmlFor="scoring">Scoring Mode</Label>
|
<CardContent className="space-y-4">
|
||||||
<Select
|
<div className="space-y-2">
|
||||||
value={scoringMode}
|
<Label htmlFor="name">Award Name</Label>
|
||||||
onValueChange={(v) =>
|
<Input
|
||||||
setScoringMode(v as 'PICK_WINNER' | 'RANKED' | 'SCORED')
|
id="name"
|
||||||
}
|
value={name}
|
||||||
>
|
onChange={(e) => setName(e.target.value)}
|
||||||
<SelectTrigger id="scoring">
|
placeholder="e.g., Mediterranean Entrepreneurship Award"
|
||||||
<SelectValue />
|
/>
|
||||||
</SelectTrigger>
|
</div>
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="PICK_WINNER">
|
<div className="space-y-2">
|
||||||
Pick Winner — Each juror picks 1
|
<Label htmlFor="description">Description</Label>
|
||||||
</SelectItem>
|
<Textarea
|
||||||
<SelectItem value="RANKED">
|
id="description"
|
||||||
Ranked — Each juror ranks top N
|
value={description}
|
||||||
</SelectItem>
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
<SelectItem value="SCORED">
|
placeholder="Brief description of this award"
|
||||||
Scored — Use evaluation form
|
rows={3}
|
||||||
</SelectItem>
|
/>
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
|
||||||
</div>
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="criteria">Eligibility Criteria</Label>
|
||||||
{scoringMode === 'RANKED' && (
|
<Textarea
|
||||||
<div className="space-y-2">
|
id="criteria"
|
||||||
<Label htmlFor="maxPicks">Max Ranked Picks</Label>
|
value={criteriaText}
|
||||||
<Input
|
onChange={(e) => setCriteriaText(e.target.value)}
|
||||||
id="maxPicks"
|
placeholder="Describe the criteria in plain language. AI will interpret this to evaluate project eligibility."
|
||||||
type="number"
|
rows={4}
|
||||||
min="1"
|
/>
|
||||||
max="20"
|
<p className="text-xs text-muted-foreground">
|
||||||
value={maxRankedPicks}
|
This text will be used by AI to determine which projects are
|
||||||
onChange={(e) => setMaxRankedPicks(e.target.value)}
|
eligible for this award.
|
||||||
/>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
</CardContent>
|
<div className="space-y-0.5">
|
||||||
</Card>
|
<Label htmlFor="ai-toggle">AI Eligibility</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
{/* Voting Window Card */}
|
Use AI to automatically evaluate project eligibility based on the criteria above.
|
||||||
<Card>
|
Turn off for awards decided by feeling or manual selection.
|
||||||
<CardHeader>
|
</p>
|
||||||
<CardTitle>Voting Window</CardTitle>
|
</div>
|
||||||
<CardDescription>
|
<Switch
|
||||||
Set the time period during which jurors can submit their votes
|
id="ai-toggle"
|
||||||
</CardDescription>
|
checked={useAiEligibility}
|
||||||
</CardHeader>
|
onCheckedChange={setUseAiEligibility}
|
||||||
<CardContent className="space-y-4">
|
/>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="votingStart">Voting Opens</Label>
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Input
|
<div className="space-y-2">
|
||||||
id="votingStart"
|
<Label htmlFor="scoring">Scoring Mode</Label>
|
||||||
type="datetime-local"
|
<Select
|
||||||
value={votingStartAt}
|
value={scoringMode}
|
||||||
onChange={(e) => setVotingStartAt(e.target.value)}
|
onValueChange={(v) =>
|
||||||
/>
|
setScoringMode(v as 'PICK_WINNER' | 'RANKED' | 'SCORED')
|
||||||
<p className="text-xs text-muted-foreground">
|
}
|
||||||
When jurors can start voting (leave empty to set when opening voting)
|
>
|
||||||
</p>
|
<SelectTrigger id="scoring">
|
||||||
</div>
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
<div className="space-y-2">
|
<SelectContent>
|
||||||
<Label htmlFor="votingEnd">Voting Closes</Label>
|
<SelectItem value="PICK_WINNER">
|
||||||
<Input
|
Pick Winner — Each juror picks 1
|
||||||
id="votingEnd"
|
</SelectItem>
|
||||||
type="datetime-local"
|
<SelectItem value="RANKED">
|
||||||
value={votingEndAt}
|
Ranked — Each juror ranks top N
|
||||||
onChange={(e) => setVotingEndAt(e.target.value)}
|
</SelectItem>
|
||||||
/>
|
<SelectItem value="SCORED">
|
||||||
<p className="text-xs text-muted-foreground">
|
Scored — Use evaluation form
|
||||||
Deadline for juror votes
|
</SelectItem>
|
||||||
</p>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
{scoringMode === 'RANKED' && (
|
||||||
|
<div className="space-y-2">
|
||||||
<div className="flex justify-end gap-4">
|
<Label htmlFor="maxPicks">Max Ranked Picks</Label>
|
||||||
<Button variant="outline" asChild>
|
<Input
|
||||||
<Link href={`/admin/awards/${awardId}`}>Cancel</Link>
|
id="maxPicks"
|
||||||
</Button>
|
type="number"
|
||||||
<Button
|
min="1"
|
||||||
onClick={handleSubmit}
|
max="20"
|
||||||
disabled={updateAward.isPending || !name.trim()}
|
value={maxRankedPicks}
|
||||||
>
|
onChange={(e) => setMaxRankedPicks(e.target.value)}
|
||||||
{updateAward.isPending ? (
|
/>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
</div>
|
||||||
) : (
|
)}
|
||||||
<Save className="mr-2 h-4 w-4" />
|
</div>
|
||||||
)}
|
</CardContent>
|
||||||
Save Changes
|
</Card>
|
||||||
</Button>
|
|
||||||
</div>
|
{/* Source Round & Eligibility */}
|
||||||
</div>
|
<Card>
|
||||||
)
|
<CardHeader>
|
||||||
}
|
<CardTitle>Source Round & Pool</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Define which round feeds projects into this award and how they interact with the main competition
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="sourceRound">Source Round</Label>
|
||||||
|
<Select
|
||||||
|
value={evaluationRoundId || 'none'}
|
||||||
|
onValueChange={(v) => setEvaluationRoundId(v === 'none' ? '' : v)}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="sourceRound">
|
||||||
|
<SelectValue placeholder="Select round..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">No source round</SelectItem>
|
||||||
|
{competition?.rounds
|
||||||
|
?.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
.map((round) => (
|
||||||
|
<SelectItem key={round.id} value={round.id}>
|
||||||
|
{round.name} ({round.roundType})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Projects from this round will be considered for award eligibility
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="eligibilityMode">Eligibility Mode</Label>
|
||||||
|
<Select
|
||||||
|
value={eligibilityMode}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
setEligibilityMode(v as 'STAY_IN_MAIN' | 'SEPARATE_POOL')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="eligibilityMode">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="STAY_IN_MAIN">
|
||||||
|
Stay in Main — Projects remain in competition
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="SEPARATE_POOL">
|
||||||
|
Separate Pool — Projects exit to award track
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Whether award-eligible projects continue in the main competition or move to a separate track
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Auto-Tag Rules */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Auto-Tag Rules</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Deterministic eligibility rules based on project metadata
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onClick={addRule}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Rule
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{autoTagRules.length === 0 ? (
|
||||||
|
<div className="flex items-start gap-2 rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
|
||||||
|
<Info className="h-4 w-4 mt-0.5 shrink-0" />
|
||||||
|
<p>
|
||||||
|
No rules defined. Add rules to automatically filter projects based on category, location, tags, or ocean issues.
|
||||||
|
Rules work together with the source round setting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{autoTagRules.map((rule, index) => (
|
||||||
|
<div
|
||||||
|
key={rule.id}
|
||||||
|
className="flex items-start gap-3 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div className="flex-1 grid gap-3 sm:grid-cols-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Field</Label>
|
||||||
|
<Select
|
||||||
|
value={rule.field}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
updateRule(rule.id, {
|
||||||
|
field: v as AutoTagRule['field'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="competitionCategory">
|
||||||
|
Competition Category
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="country">Country</SelectItem>
|
||||||
|
<SelectItem value="geographicZone">
|
||||||
|
Geographic Zone
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tags">Tags</SelectItem>
|
||||||
|
<SelectItem value="oceanIssue">Ocean Issue</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Operator</Label>
|
||||||
|
<Select
|
||||||
|
value={rule.operator}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
updateRule(rule.id, {
|
||||||
|
operator: v as AutoTagRule['operator'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-9">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="equals">Equals</SelectItem>
|
||||||
|
<SelectItem value="contains">Contains</SelectItem>
|
||||||
|
<SelectItem value="in">In (comma-separated)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label className="text-xs">Value</Label>
|
||||||
|
<Input
|
||||||
|
className="h-9"
|
||||||
|
value={rule.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateRule(rule.id, { value: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
rule.operator === 'in'
|
||||||
|
? 'value1,value2,value3'
|
||||||
|
: 'Enter value...'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 shrink-0"
|
||||||
|
onClick={() => removeRule(rule.id)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{autoTagRules.length > 0 && (
|
||||||
|
<div className="flex items-start gap-2 rounded-lg bg-muted p-3 text-xs text-muted-foreground">
|
||||||
|
<Info className="h-3 w-3 mt-0.5 shrink-0" />
|
||||||
|
<p>
|
||||||
|
<strong>How it works:</strong> Filter from{' '}
|
||||||
|
<Badge variant="outline" className="mx-1">
|
||||||
|
{evaluationRoundId
|
||||||
|
? competition?.rounds?.find((r) => r.id === evaluationRoundId)
|
||||||
|
?.name || 'Selected Round'
|
||||||
|
: 'All Projects'}
|
||||||
|
</Badge>
|
||||||
|
, where ALL rules match (AND logic). Projects matching these deterministic rules will be marked eligible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Voting Window Card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Voting Window</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Set the time period during which jurors can submit their votes
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="votingStart">Voting Opens</Label>
|
||||||
|
<Input
|
||||||
|
id="votingStart"
|
||||||
|
type="datetime-local"
|
||||||
|
value={votingStartAt}
|
||||||
|
onChange={(e) => setVotingStartAt(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
When jurors can start voting (leave empty to set when opening voting)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="votingEnd">Voting Closes</Label>
|
||||||
|
<Input
|
||||||
|
id="votingEnd"
|
||||||
|
type="datetime-local"
|
||||||
|
value={votingEndAt}
|
||||||
|
onChange={(e) => setVotingEndAt(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Deadline for juror votes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link href={`/admin/awards/${awardId}`}>Cancel</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={updateAward.isPending || !name.trim()}
|
||||||
|
>
|
||||||
|
{updateAward.isPending ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,227 +1,227 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { trpc } from '@/lib/trpc/client'
|
import { trpc } from '@/lib/trpc/client'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { ArrowLeft, Save, Loader2 } from 'lucide-react'
|
import { ArrowLeft, Save, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
export default function CreateAwardPage() {
|
export default function CreateAwardPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [criteriaText, setCriteriaText] = useState('')
|
const [criteriaText, setCriteriaText] = useState('')
|
||||||
const [scoringMode, setScoringMode] = useState<
|
const [scoringMode, setScoringMode] = useState<
|
||||||
'PICK_WINNER' | 'RANKED' | 'SCORED'
|
'PICK_WINNER' | 'RANKED' | 'SCORED'
|
||||||
>('PICK_WINNER')
|
>('PICK_WINNER')
|
||||||
const [useAiEligibility, setUseAiEligibility] = useState(true)
|
const [useAiEligibility, setUseAiEligibility] = useState(true)
|
||||||
const [maxRankedPicks, setMaxRankedPicks] = useState('3')
|
const [maxRankedPicks, setMaxRankedPicks] = useState('3')
|
||||||
const [programId, setProgramId] = useState('')
|
const [programId, setProgramId] = useState('')
|
||||||
|
|
||||||
const utils = trpc.useUtils()
|
const utils = trpc.useUtils()
|
||||||
const { data: programs } = trpc.program.list.useQuery()
|
const { data: programs } = trpc.program.list.useQuery()
|
||||||
const createAward = trpc.specialAward.create.useMutation({
|
const createAward = trpc.specialAward.create.useMutation({
|
||||||
onSuccess: () => utils.specialAward.list.invalidate(),
|
onSuccess: () => utils.specialAward.list.invalidate(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!name.trim() || !programId) return
|
if (!name.trim() || !programId) return
|
||||||
try {
|
try {
|
||||||
const award = await createAward.mutateAsync({
|
const award = await createAward.mutateAsync({
|
||||||
programId,
|
programId,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
criteriaText: criteriaText.trim() || undefined,
|
criteriaText: criteriaText.trim() || undefined,
|
||||||
useAiEligibility,
|
useAiEligibility,
|
||||||
scoringMode,
|
scoringMode,
|
||||||
maxRankedPicks:
|
maxRankedPicks:
|
||||||
scoringMode === 'RANKED' ? parseInt(maxRankedPicks) : undefined,
|
scoringMode === 'RANKED' ? parseInt(maxRankedPicks) : undefined,
|
||||||
})
|
})
|
||||||
toast.success('Award created')
|
toast.success('Award created')
|
||||||
router.push(`/admin/awards/${award.id}`)
|
router.push(`/admin/awards/${award.id}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
toast.error(
|
||||||
error instanceof Error ? error.message : 'Failed to create award'
|
error instanceof Error ? error.message : 'Failed to create award'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="ghost" asChild className="-ml-4">
|
<Button variant="ghost" asChild className="-ml-4">
|
||||||
<Link href="/admin/awards">
|
<Link href="/admin/awards">
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
Back to Awards
|
Back to Awards
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
Create Special Award
|
Create Special Award
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Define a new award with eligibility criteria and voting rules
|
Define a new award with eligibility criteria and voting rules
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Award Details</CardTitle>
|
<CardTitle>Award Details</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure the award name, criteria, and scoring mode
|
Configure the award name, criteria, and scoring mode
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="program">Edition</Label>
|
<Label htmlFor="program">Edition</Label>
|
||||||
<Select value={programId} onValueChange={setProgramId}>
|
<Select value={programId} onValueChange={setProgramId}>
|
||||||
<SelectTrigger id="program">
|
<SelectTrigger id="program">
|
||||||
<SelectValue placeholder="Select an edition" />
|
<SelectValue placeholder="Select an edition" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{programs?.map((p) => (
|
{programs?.map((p) => (
|
||||||
<SelectItem key={p.id} value={p.id}>
|
<SelectItem key={p.id} value={p.id}>
|
||||||
{p.year} Edition
|
{p.year} Edition
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name">Award Name</Label>
|
<Label htmlFor="name">Award Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="e.g., Mediterranean Entrepreneurship Award"
|
placeholder="e.g., Mediterranean Entrepreneurship Award"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Description</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Brief description of this award"
|
placeholder="Brief description of this award"
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="criteria">Eligibility Criteria</Label>
|
<Label htmlFor="criteria">Eligibility Criteria</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="criteria"
|
id="criteria"
|
||||||
value={criteriaText}
|
value={criteriaText}
|
||||||
onChange={(e) => setCriteriaText(e.target.value)}
|
onChange={(e) => setCriteriaText(e.target.value)}
|
||||||
placeholder="Describe the criteria in plain language. AI will interpret this to evaluate project eligibility."
|
placeholder="Describe the criteria in plain language. AI will interpret this to evaluate project eligibility."
|
||||||
rows={4}
|
rows={4}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
This text will be used by AI to determine which projects are
|
This text will be used by AI to determine which projects are
|
||||||
eligible for this award.
|
eligible for this award.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="ai-toggle">AI Eligibility</Label>
|
<Label htmlFor="ai-toggle">AI Eligibility</Label>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Use AI to automatically evaluate project eligibility based on the criteria above.
|
Use AI to automatically evaluate project eligibility based on the criteria above.
|
||||||
Turn off for awards decided by feeling or manual selection.
|
Turn off for awards decided by feeling or manual selection.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
id="ai-toggle"
|
id="ai-toggle"
|
||||||
checked={useAiEligibility}
|
checked={useAiEligibility}
|
||||||
onCheckedChange={setUseAiEligibility}
|
onCheckedChange={setUseAiEligibility}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="scoring">Scoring Mode</Label>
|
<Label htmlFor="scoring">Scoring Mode</Label>
|
||||||
<Select
|
<Select
|
||||||
value={scoringMode}
|
value={scoringMode}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
setScoringMode(
|
setScoringMode(
|
||||||
v as 'PICK_WINNER' | 'RANKED' | 'SCORED'
|
v as 'PICK_WINNER' | 'RANKED' | 'SCORED'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="scoring">
|
<SelectTrigger id="scoring">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="PICK_WINNER">
|
<SelectItem value="PICK_WINNER">
|
||||||
Pick Winner — Each juror picks 1
|
Pick Winner — Each juror picks 1
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="RANKED">
|
<SelectItem value="RANKED">
|
||||||
Ranked — Each juror ranks top N
|
Ranked — Each juror ranks top N
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="SCORED">
|
<SelectItem value="SCORED">
|
||||||
Scored — Use evaluation form
|
Scored — Use evaluation form
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{scoringMode === 'RANKED' && (
|
{scoringMode === 'RANKED' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="maxPicks">Max Ranked Picks</Label>
|
<Label htmlFor="maxPicks">Max Ranked Picks</Label>
|
||||||
<Input
|
<Input
|
||||||
id="maxPicks"
|
id="maxPicks"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
value={maxRankedPicks}
|
value={maxRankedPicks}
|
||||||
onChange={(e) => setMaxRankedPicks(e.target.value)}
|
onChange={(e) => setMaxRankedPicks(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<Button variant="outline" asChild>
|
<Button variant="outline" asChild>
|
||||||
<Link href="/admin/awards">Cancel</Link>
|
<Link href="/admin/awards">Cancel</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={createAward.isPending || !name.trim() || !programId}
|
disabled={createAward.isPending || !name.trim() || !programId}
|
||||||
>
|
>
|
||||||
{createAward.isPending ? (
|
{createAward.isPending ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-2 h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
Create Award
|
Create Award
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user