91 KiB
MOPC Platform Review
Status Legend: Each finding is marked with its implementation status:
- DONE - Implemented and verified
- DEFERRED - Cannot be done now (requires infrastructure, design decisions, or is too risky)
- N/A - Not actionable (positive observation or informational)
- SKIPPED - Intentionally not addressed (low impact or not worth the risk)
1. UI/UX Design Review
Reviewer: UI/UX Design Reviewer Date: 2026-02-05 Scope: All pages, components, design patterns, mobile responsiveness, UX states, information architecture, interaction patterns, accessibility, visual polish
Overall Assessment
The MOPC platform demonstrates strong foundational UI/UX work. shadcn/ui provides a consistent base, skeleton loading states are implemented throughout, and the evaluation form offers excellent UX with autosave and slider+button scoring. However, there are meaningful gaps in mobile responsiveness, navigation consistency, typography adherence, and error states that should be addressed before a public-facing launch.
1.1 Design System & Theming
DS-1: Public layout uses Inter font instead of Montserrat (Medium) — DONE
File: src/app/(public)/layout.tsx:4-5
The public layout imports Google's Inter font and applies it to the entire public section. The brand identity specifies Montserrat as the platform typeface (defined in globals.css via @font-face). Every other layout uses the root font (Montserrat via font-sans), but public pages render in Inter.
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
Impact: Brand inconsistency on the most externally-visible pages.
Recommendation: Remove the Inter import and use the existing Montserrat font from the root layout. Apply className="font-sans" if needed.
DS-2: Public layout uses placeholder logo instead of MOPC logo component (Low) — DONE
File: src/app/(public)/layout.tsx:20-22
The public layout renders a hardcoded <div> with "M" text instead of using the existing Logo component at src/components/shared/logo.tsx.
<div className="h-8 w-8 rounded-lg bg-primary flex items-center justify-center">
<span className="text-sm font-bold text-white">M</span>
</div>
Recommendation: Import and use the Logo component with variant="small".
DS-3: Dark mode CSS defined but no toggle exposed (Low) — DONE
File: src/app/globals.css (dark mode variables block)
Full dark mode color tokens are defined in globals.css under @media (prefers-color-scheme: dark), but there is no theme toggle anywhere in the UI. Users with dark system preferences will see the dark theme, but cannot switch. This creates an untested UX path.
Recommendation: Either add a theme toggle in settings/nav (using next-themes), or remove the dark mode definitions to ensure consistent appearance. If dark mode is deferred, add class="light" to the <html> tag to force light mode.
DS-4: Card CardTitle default size is too large and always overridden (Low) — DONE
File: src/components/ui/card.tsx
CardTitle defaults to text-2xl tracking-tight, but nearly every usage in the codebase adds custom sizing classes. This suggests the default is not useful.
Recommendation: Change the default to text-lg font-semibold which matches the most common override pattern.
1.2 Navigation & Information Architecture
NAV-1: Inconsistent active state styling across role navigation components (Medium) — DONE
Files:
src/components/layouts/jury-nav.tsx- Usesbg-primary/10 text-primarysrc/components/layouts/mentor-nav.tsx- Usesbg-primary/10 text-primarysrc/components/layouts/observer-nav.tsx- Usesbg-primary text-primary-foreground(solid background)
The observer nav uses a completely different active state style (solid filled background) compared to jury and mentor navs (subtle tinted background). This inconsistency is jarring for users who switch between roles.
Recommendation: Unify active state styling. Better yet, extract all three into a shared RoleNav component as noted in the code quality review (4.1.4).
NAV-2: Notification bell links are hardcoded to admin paths (Medium) — DONE
File: src/components/shared/notification-bell.tsx
The notification bell component links to /admin/settings and /admin/notifications regardless of the user's role. Jury members, mentors, and observers will be redirected or see 403 errors when clicking these links.
Recommendation: Use role-aware paths (e.g., /${role}/notifications) or a role-neutral /notifications route.
NAV-3: Jury nav is missing Awards link (Low) — DONE
File: src/components/layouts/jury-nav.tsx
The jury navigation includes Dashboard, My Assignments, and Learning Hub. There is no link to special awards even though jury members may need to view award-eligible projects. The admin sidebar includes awards management, but there is no jury-facing awards view link.
Recommendation: Add an Awards link to the jury navigation if jury members should be able to view special award results.
NAV-4: Admin sidebar user section uses hardcoded colors (Low) — DONE
File: src/components/layouts/admin-sidebar.tsx:247-267
The user info section at the bottom of the admin sidebar uses hardcoded slate-200, slate-300, slate-700, slate-400 colors instead of semantic tokens like muted, muted-foreground, border.
Recommendation: Replace with semantic color tokens for consistency with the rest of the design system and proper dark mode support.
1.3 Mobile Responsiveness
MOB-1: Admin rounds table is not responsive (High) — DONE
File: src/app/(admin)/admin/rounds/page.tsx
The rounds management page uses a fixed 7-column grid (grid-cols-7) that does not adapt to mobile or tablet screens. On narrow viewports, content will overflow or become illegible. Unlike the projects page (which has separate table/card views), rounds has no mobile layout.
Recommendation: Implement a mobile card layout for rounds similar to the project list's mobile view. Show the full table only on lg: screens.
MOB-2: Pagination component is not mobile-optimized (Medium) — DONE
File: src/components/shared/pagination.tsx
The pagination component renders all page numbers without truncation. For datasets with many pages, this creates a long horizontal row that overflows on mobile.
Recommendation: Show abbreviated pagination on mobile: first, previous, current, next, last. Use hidden sm:flex to show the full pagination only on larger screens.
MOB-3: Admin dashboard stat cards lack stacking on small screens (Low) — DONE
File: src/app/(admin)/admin/page.tsx
The admin dashboard uses grid-cols-2 md:grid-cols-4 for the stats grid. On very narrow phones (< 360px), two columns may still be too wide for the stat cards.
Recommendation: Consider grid-cols-1 sm:grid-cols-2 md:grid-cols-4 for the narrowest phones.
1.4 UX States & Feedback
UX-1: No global 404 or error pages (Medium) — DONE
Files: Only src/app/(auth)/error/page.tsx exists (auth-specific errors)
There is no src/app/not-found.tsx or src/app/error.tsx. Users who navigate to non-existent routes will see Next.js's default 404 page, which has no MOPC branding, navigation, or helpful direction.
Recommendation: Create src/app/not-found.tsx with MOPC branding, a message, and a link back to the dashboard. Create src/app/error.tsx for runtime errors.
UX-2: Profile settings page has three identical save buttons (Medium) — DONE
File: src/app/(settings)/settings/profile/page.tsx:240-252, 286-298, 321-333
The profile page has three separate "Save" buttons (Save Changes, Save Preferences, Save Expertise) that all call the same handleSaveProfile function. This means clicking "Save Expertise" also saves any pending name/bio changes, which may confuse users.
Recommendation: Either use a single save button at the bottom of the page for all profile fields, or make each section save independently by splitting the mutation into separate API calls.
UX-3: Form state initialized via conditional in render body (Low) — DONE
File: src/app/(settings)/settings/profile/page.tsx:76-84
if (user && !profileLoaded) {
setName(user.name || '')
// ...
setProfileLoaded(true)
}
This pattern of calling setState during render is fragile. React may batch these calls unpredictably, and the profileLoaded guard can fail during concurrent rendering.
Recommendation: Use useEffect to populate form state when user data loads, or use React Hook Form with defaultValues from the query data.
UX-4: Mentor dashboard uses client-side loading instead of Suspense (Low) — DONE
File: src/app/(mentor)/mentor/page.tsx
The mentor dashboard conditionally renders a loading spinner based on isLoading from tRPC queries, rather than using React Suspense boundaries. Other dashboards (admin) use server components. This creates inconsistency in the loading experience.
Recommendation: Convert to a server component with Suspense, or at minimum use skeleton loading states (as the admin dashboard does) instead of a centered spinner.
1.5 Interaction Patterns
INT-1: Evaluation form is excellent (Positive) — N/A
File: src/components/forms/evaluation-form.tsx
The evaluation form demonstrates outstanding UX:
- Autosave with debounce and visual status indicator
- Dual input for scores: slider drag + direct button click
- Score label changes dynamically based on value
- Sticky status bar showing save state
- Confirmation dialog before final submission
- Read-only mode for past evaluations
- Character counter on text fields
This should be the UX benchmark for all forms in the platform.
INT-2: Button press feedback is well-implemented (Positive) — N/A
File: src/components/ui/button.tsx
All buttons include active:scale-[0.98] for tactile press feedback. Combined with proper disabled states and loading spinners (Loader2 with animate-spin), the button interactions feel responsive and polished.
INT-3: File upload component is well-designed (Positive) — N/A
File: src/components/shared/file-upload.tsx
The file upload supports drag-and-drop with visual hover state, progress bar during upload, file type and size validation with clear error messages, and a preview of uploaded files. This is production-ready UX.
INT-4: Onboarding wizard has strong UX (Positive) — N/A
File: src/app/(auth)/onboarding/page.tsx
The multi-step onboarding uses:
- Brand gradient background for visual distinction
- Step indicator with progress
- Form validation per step
- Expertise tag selection with TagInput component
- Smooth transitions between steps
1.6 Accessibility
A11Y-1: Muted foreground color may have insufficient contrast (Medium) — DONE
File: src/app/globals.css
The --muted-foreground color (used for secondary text, placeholders, descriptions) is defined as hsl(215.4 16.3% 46.9%) in light mode. This corresponds to approximately #6b7280 which provides ~4.6:1 contrast against white. While this passes WCAG AA for normal text (4.5:1), it fails for small text and is very close to the threshold.
Many description texts and helper texts throughout the platform use text-muted-foreground at small sizes (text-xs, text-sm).
Recommendation: Darken --muted-foreground slightly to ensure comfortable reading, especially for text-xs usage. Aim for at least 5:1 contrast ratio.
A11Y-2: Focus styles are properly implemented (Positive) — N/A
File: src/app/globals.css, src/components/ui/button.tsx
The design system includes focus-visible:ring-2 focus-visible:ring-ring on interactive elements. This provides clear keyboard navigation indicators without affecting mouse users. This is correctly implemented across all shadcn/ui base components.
A11Y-3: Skeleton loading states provide good screen reader context (Positive) — N/A
Files: Multiple pages using <Skeleton> components
Loading states use semantic skeleton placeholders instead of spinners, which gives screen readers (via aria-busy on parent containers) context about what is loading.
1.7 Visual Polish
VP-1: Landing page is minimal and unbranded (Medium) — DONE
File: src/app/page.tsx
The landing/home page is very minimal. For a platform representing the Monaco Ocean Protection Challenge, the entry point should convey the mission and brand more strongly.
Recommendation: Enhance the landing page with the MOPC logo, brand colors, a brief mission statement, and a clear CTA to login or learn more. Use the brand gradient seen in the onboarding flow.
VP-2: Auth error page is generic (Low) — DONE
File: src/app/(auth)/error/page.tsx
The auth error page shows a basic card with the error message. It does not use MOPC branding or provide helpful navigation (e.g., "Return to Login" or "Contact Support").
Recommendation: Add MOPC branding, a supportive error message, and navigation options.
VP-3: Observer dashboard "read-only" notice could be more prominent (Low) — DONE
File: src/app/(observer)/observer/page.tsx
The observer dashboard shows a subtle info card noting read-only access. Given that observers might expect interactivity, this notice could be more visually distinct.
Recommendation: Use a persistent banner or badge-style indicator near the page header.
1.8 Summary Table
| Category | Findings | High | Medium | Low | Positive |
|---|---|---|---|---|---|
| Design System & Theming | 4 | 0 | 1 | 3 | 0 |
| Navigation & IA | 4 | 0 | 2 | 2 | 0 |
| Mobile Responsiveness | 3 | 1 | 1 | 1 | 0 |
| UX States & Feedback | 4 | 0 | 2 | 2 | 0 |
| Interaction Patterns | 4 | 0 | 0 | 0 | 4 |
| Accessibility | 3 | 0 | 1 | 0 | 2 |
| Visual Polish | 3 | 0 | 1 | 2 | 0 |
| Total | 25 | 1 | 8 | 10 | 6 |
Quick Wins (< 1 hour each)
- DS-1: Replace Inter with Montserrat in public layout
- DS-2: Use Logo component in public layout
- NAV-4: Replace hardcoded slate colors with semantic tokens
- UX-3: Move form state initialization to useEffect
- VP-2: Add branding and navigation to auth error page
- DS-4: Adjust CardTitle default size
- NAV-3: Add Awards link to jury nav (if applicable)
Longer-Term Improvements
- MOB-1: Build responsive card layout for rounds table (High priority)
- UX-1: Create global 404 and error pages
- NAV-1/NAV-2: Unify navigation components and fix notification paths
- A11Y-1: Audit and improve muted-foreground contrast
- VP-1: Redesign landing page with full MOPC branding
- MOB-2: Implement truncated mobile pagination
- UX-2: Fix profile settings save button behavior
- DS-3: Either implement dark mode toggle or force light mode
- UX-4: Standardize loading patterns across dashboards
3. Database Schema & Implementation Review
Reviewer: Database & Backend Reviewer Date: 2026-02-05 Scope: Prisma schema, tRPC routers, services, indexing, query patterns, data integrity
Overall Assessment
The schema is well-structured for its domain with good use of Prisma features. The codebase demonstrates solid TypeScript/Prisma patterns, proper use of enums, composite unique constraints, and consistent audit logging. However, there are several areas where improvements in indexing, query efficiency, transaction usage, and data integrity would enhance performance and reliability, particularly as the platform scales beyond Phase 1.
Schema Design
SD-1: Project.roundId is required but treated as optional (High) — DEFERRED (requires design decision on pool project architecture)
Files: prisma/schema.prisma:417, src/server/routers/round.ts:631-639
The Project model declares roundId String as a required (non-nullable) field and has a required relation round Round @relation(...). However, in round.removeProjects, the code sets roundId: null as unknown as string, which is a type-unsafe workaround to force a null into a non-nullable field. Similarly, project.listPool queries for roundId: null, which would never match if the schema is enforced.
Impact: Data integrity violation; Prisma may not enforce this at the DB level with @default() but PostgreSQL will reject nulls on a NOT NULL column.
Recommendation: Either make roundId optional (String?) with an optional relation if projects can be unassigned from rounds, or implement a "pool" round concept. The current workaround is fragile and could fail at the database level.
SD-2: Evaluation versioning is declared but not implemented (Medium) — DONE (documented as deferred in schema with TODO comment)
Files: prisma/schema.prisma:570, src/server/routers/evaluation.ts
The Evaluation model has a version Int @default(1) field and the CLAUDE.md states "Evaluations are versioned - edits create new versions." However, the evaluation router's autosave and submit mutations update in-place and never create new versioned rows. The version field is never incremented.
Impact: The stated requirement for version history is not fulfilled. If an admin or auditor needs to see how an evaluation changed over time, that data is lost.
Recommendation: Either implement proper versioning (create new Evaluation rows with incremented version on re-submission) or remove the version field to avoid confusion. If versioning is deferred, document it clearly.
SD-3: Soft deletes not implemented (Medium) — DEFERRED (massive schema change, requires careful planning)
Files: All routers using .delete(), prisma/schema.prisma
The CLAUDE.md architecture decisions (ADR #7) state "Soft deletes: Audit trail, recovery, referential integrity." However, no model has a deletedAt field, and all delete operations use hard deletes (e.g., project.delete, user.delete, round.delete). The User.delete cascades through assignments, evaluations, and other relations.
Impact: Once deleted, data cannot be recovered. For a jury voting platform where audit trails are critical, this is a significant gap. Deleting a user cascades through assignments and evaluations, potentially destroying evaluation data.
Recommendation: Add deletedAt DateTime? to critical models (User, Project, Evaluation, Assignment), implement soft delete middleware via Prisma's $extends, and ensure all queries filter out soft-deleted records.
SD-4: LiveVotingSession.status is a String, not an Enum (Low) — DONE
Files: prisma/schema.prisma:918
The LiveVotingSession.status field is typed as String @default("NOT_STARTED") with valid values documented in a comment. Other status fields use proper enums.
Impact: No database-level validation of valid status values.
Recommendation: Create a LiveVotingSessionStatus enum with values NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED.
SD-5: InAppNotification.priority and type are Strings, not Enums (Low) — DONE
Files: prisma/schema.prisma:737-738
Similar to SD-4, priority and type on InAppNotification are strings but have a fixed set of known values. The service defines NotificationPriority as a TypeScript type but it's not enforced at the database level.
Recommendation: Create enums for priority (low/normal/high/urgent). The type field has many values that expand over time, so keeping it as a string is acceptable, but priority should be an enum.
SD-6: Redundant date fields on Round (Low) — DEFERRED (fields still actively used in application/round routers)
Files: prisma/schema.prisma:348-353
The Round model has both submissionDeadline and submissionEndDate with a comment "replaces submissionDeadline if set". Additionally, phase1Deadline and phase2Deadline exist but appear unused in any router. This creates confusion about which fields are authoritative.
Recommendation: Remove deprecated fields (submissionDeadline if replaced by submissionEndDate) and either implement or remove phase1Deadline/phase2Deadline.
Indexing
IX-1: Missing composite index on Evaluation for round-based queries (High) — DONE
Files: prisma/schema.prisma:554-584
Many queries filter evaluations by assignment.roundId (e.g., evaluation.listByRound, all analytics queries, export.evaluations). This requires a join through the Assignment table. There is no index on Evaluation.formId paired with status, and more critically, the join through assignment.roundId means these queries perform a nested lookup.
Impact: As evaluation counts grow (130+ projects x 3+ reviews = 390+ evaluations per round), these queries will slow down.
Recommendation: Consider denormalizing roundId onto the Evaluation table or adding a database view. At minimum, ensure the existing @@index([status]) and @@index([formId]) are sufficient with the join pattern.
IX-2: AuditLog table will grow unbounded (Medium) — DEFERRED (requires DBA decision on partitioning strategy)
Files: prisma/schema.prisma:638-663
The AuditLog table has good indexes (userId, action, entityType+entityId, timestamp) but no partitioning or archival strategy. With every CRUD operation, status change, login, and export generating audit entries, this table will grow rapidly.
Impact: Query performance degradation over time, especially for audit.getStats which does groupBy operations across the entire table.
Recommendation: Implement time-based partitioning on timestamp, or add an archival job that moves old audit logs to a separate table. Consider adding a composite index on (entityType, entityId, timestamp) for the getByEntity query.
IX-3: User.email has both @unique and @@index (Low) — DONE
Files: prisma/schema.prisma:192, 262
email String @unique already creates a unique index. The additional @@index([email]) is redundant.
Impact: Extra write overhead on every user insert/update for maintaining a duplicate index.
Recommendation: Remove @@index([email]).
IX-4: Missing index on GracePeriod for combined lookups (Low) — DONE
Files: prisma/schema.prisma:590-612
The evaluation.submit mutation queries grace periods with roundId + userId + extendedUntil + projectId. Individual indexes exist but no composite index covers this query pattern.
Recommendation: Add @@index([roundId, userId, extendedUntil]) for the common grace period lookup pattern.
Query Optimization
QO-1: Analytics getProjectRankings loads all project data including all assignments and evaluations (High) — DONE
Files: src/server/routers/analytics.ts:148-207
The getProjectRankings query does include: { assignments: { include: { evaluation: ... } } } on ALL projects in a round, fetching complete project records with nested assignments and evaluations. For a round with 130 projects and 3+ evaluations each, this fetches 390+ evaluation records with full project data.
Impact: Significant memory usage and query time. The response payload is very large even though only scores are needed for ranking.
Recommendation: Use a raw SQL aggregate query or Prisma's select to only fetch the fields needed (project id, title, evaluation scores). Consider pre-computing rankings or caching results.
QO-2: getTags fetches all projects to extract unique tags (Medium) — DONE
Files: src/server/routers/project.ts:580-599
The getTags query fetches ALL projects with select: { tags: true } then deduplicates in JavaScript. For 100+ projects, this is inefficient.
Impact: Unnecessary data transfer and memory usage.
Recommendation: Use Prisma's $queryRaw with SELECT DISTINCT unnest(tags) FROM "Project" or PostgreSQL's array_agg(DISTINCT ...) to get unique tags at the database level.
QO-3: Project.list includes full files array for every project (Medium) — DONE
Files: src/server/routers/project.ts:133-134
The project list query includes files: true, fetching all file records for every project in the paginated list. Most list views only need to know whether files exist, not the full file details.
Impact: Over-fetching data, larger payloads, slower queries.
Recommendation: Use _count: { select: { files: true } } or files: { select: { id: true, fileType: true } } instead of files: true.
QO-4: Sequential notification sends in bulk operations (Medium) — DONE
Files: src/server/routers/assignment.ts:476-490, src/server/routers/user.ts:559-596
In assignment.bulkCreate and user.bulkCreate, notifications are sent sequentially in a for loop with await createNotification(...). Each createNotification call also queries the database for email settings and user preferences.
Impact: Bulk operations are O(n) in database queries for notifications, significantly slowing down bulk assignment/invitation flows.
Recommendation: Use createBulkNotifications (which already exists and uses createMany) instead of looping with createNotification. Batch the email checks as well.
QO-5: Export queries fetch unbounded data (Medium) — DONE
Files: src/server/routers/export.ts:16-34, 106-118
Export queries for evaluations and project scores have no pagination and no limit (except audit logs with take: 10000). For large rounds, this could return thousands of records in a single response.
Impact: Memory pressure on the server, potential timeout for large exports.
Recommendation: Implement streaming or chunked export, or add a reasonable limit with warning to the user.
QO-6: N+1 in mentor.getSuggestions (Medium) — DONE
Files: src/server/routers/mentor.ts:50-78
After getting AI suggestions, each mentor is fetched individually in a Promise.all(suggestions.map(...)) loop. Each iteration makes a separate user.findUnique query.
Impact: If there are 5 suggestions, this makes 5 additional database queries.
Recommendation: Fetch all needed mentors in a single user.findMany({ where: { id: { in: mentorIds } } }) query, then map results.
QO-7: Audit log creation not batched or fire-and-forget (Low) — DONE (migrated to logAudit utility with try-catch)
Files: All routers
Every mutation awaits the audit log creation, meaning the user's response is delayed by audit log write time. Audit logs are non-critical for the operation's success.
Recommendation: Consider making audit log writes fire-and-forget (don't await) or use a background queue. Alternatively, batch audit logs within transactions where possible.
Data Integrity
DI-1: Evaluation submission and assignment update are not in a transaction (High) — DONE
Files: src/server/routers/evaluation.ts:200-213
The evaluation.submit mutation updates the evaluation status to SUBMITTED and then separately updates the assignment's isCompleted to true. These two operations should be atomic - if the second fails, the evaluation is marked submitted but the assignment still shows as incomplete.
Impact: Inconsistent state between evaluation and assignment records.
Recommendation: Wrap both updates in a $transaction:
await ctx.prisma.$transaction([
ctx.prisma.evaluation.update({ where: { id }, data: { ...data, status: 'SUBMITTED', submittedAt: now } }),
ctx.prisma.assignment.update({ where: { id: evaluation.assignmentId }, data: { isCompleted: true } }),
])
DI-2: Project create + audit log not in a transaction (Medium) — DONE
Files: src/server/routers/project.ts:299-323, and similar patterns across all routers
Across all routers, the pattern is: create/update entity, then create audit log as a separate operation. If the audit log creation fails, the entity operation succeeds but the audit trail has a gap.
Impact: Missing audit entries for successful operations.
Recommendation: For critical operations (especially ones involving evaluation submissions, status changes, and user modifications), wrap entity changes and audit log creation in a single transaction. For less critical operations, fire-and-forget the audit log.
DI-3: bulkUpdateStatus does not check project ownership before notification (Medium) — DONE
Files: src/server/routers/project.ts:620-742
The bulkUpdateStatus mutation uses updateMany (which doesn't return updated records) and then separately fetches projects by ID to send notifications. If some IDs don't match the roundId filter, the notification list may not match the actually-updated projects.
Impact: Notifications could be sent for projects that weren't actually updated.
Recommendation: Fetch the matching projects before the update to ensure consistency between updates and notifications, or use the updated.count to validate.
DI-4: Assignment uniqueness constraint may be bypassed by skipDuplicates (Low) — DONE
Files: src/server/routers/assignment.ts:427-434
The bulkCreate mutation uses createMany({ skipDuplicates: true }). While this prevents constraint violations, it silently drops assignments that already exist. The returned count may not reflect what the user intended.
Impact: Users may not realize some assignments were skipped.
Recommendation: Before createMany, check which assignments already exist and report the skip count to the user (similar to how user.bulkCreate handles it).
DI-5: No foreign key constraint on SpecialAward.winnerOverriddenBy (Low) — DONE
Files: prisma/schema.prisma:1213
The winnerProjectId field has a relation to Project with onDelete: SetNull, which is appropriate. However, winnerOverriddenBy is just a String? with no relation to User, meaning there's no referential integrity check.
Recommendation: Add a relation from winnerOverriddenBy to User to ensure referential integrity.
Performance
PF-1: Large transaction in filtering.executeRules (High) — DONE
Files: src/server/routers/filtering.ts:503-531
The executeRules mutation creates a transaction with one upsert per project. For 130 projects, this is 130 sequential upserts in a single transaction, holding database locks for the duration.
Impact: Long-running transaction that blocks other operations on the FilteringResult table.
Recommendation: Batch the upserts into groups of 20-30, or use createMany/updateMany where possible. Alternatively, use $executeRawUnsafe with a bulk INSERT ... ON CONFLICT statement.
PF-2: Description field text search uses ILIKE without text index (Medium) — DEFERRED (requires PostgreSQL full-text search extension)
Files: src/server/routers/project.ts:111-117
Project search uses { description: { contains: search, mode: 'insensitive' } } which translates to ILIKE '%search%'. The description field is @db.Text (unlimited length). Without a text index, this performs a full table scan on potentially large text fields.
Impact: Slow search queries as project count grows, especially with long descriptions.
Recommendation: Add PostgreSQL full-text search (tsvector/tsquery) via a generated column and GIN index, or use Prisma's full-text search extension. Alternatively, limit search to title and teamName only for list views.
PF-3: Assignment stats query makes 5 separate database calls (Low) — DONE
Files: src/server/routers/assignment.ts:530-554
The getStats query runs 4 parallel queries via Promise.all plus a separate findUniqueOrThrow. While parallel, this is still 5 database round-trips.
Recommendation: Consolidate into fewer queries. The projectCoverage query already returns project data that could provide the total projects count, eliminating one query.
Positive Patterns Observed
-
Consistent use of
@@uniquecomposite constraints: The schema properly uses composite unique constraints (@@unique([userId, projectId, roundId])on Assignment,@@unique([awardId, projectId])on AwardEligibility, etc.) to prevent duplicate records at the database level. -
Good use of
selectto limit data fetching: Many queries useselectclauses to fetch only needed fields (e.g.,user.me,user.list), reducing payload sizes. -
Promise.allfor parallel queries: List queries consistently usePromise.all([findMany, count])for pagination, reducing latency. -
RBAC enforced at query level: Jury members are restricted to their assigned projects through
whereclause filtering, not just middleware checks. -
Proper Zod validation: All tRPC inputs use comprehensive Zod schemas with min/max constraints, enums, and proper typing.
-
Consistent audit logging: Nearly every mutation creates an audit log entry with context (userId, action, entityType, entityId, detailsJson).
-
Background job pattern: Long-running AI operations (filtering, assignment) use a job table with status tracking and progress callbacks, with fire-and-forget execution.
-
Cascade deletes on relations: The schema consistently uses
onDelete: Cascadefor owned relations andonDelete: SetNullfor references, which is appropriate. -
Good use of JsonB for flexible data: The use of
@db.JsonBfor criteria, settings, and metadata allows schema flexibility without migrations. -
Pagination implemented consistently: List endpoints consistently implement cursor or offset-based pagination with
page/perPageparameters and return total counts.
Priority Summary
| Priority | Count | Key Issues |
|---|---|---|
| High | 4 | Missing transaction on evaluation submit (DI-1), large filtering transaction (PF-1), analytics over-fetching (QO-1), project.roundId nullability mismatch (SD-1) |
| Medium | 9 | Soft deletes not implemented (SD-3), evaluation versioning missing (SD-2), N+1 queries (QO-6), sequential notifications (QO-4), audit log growth (IX-2), export unbounded (QO-5), text search without index (PF-2), DI-2, DI-3 |
| Low | 7 | Redundant email index (IX-3), string status fields (SD-4, SD-5), deprecated date fields (SD-6), grace period composite index (IX-4), DI-4, DI-5, PF-3 |
4. Code Quality Review
4.1 Duplicate Code
4.1.1 toProjectWithRelations duplicated across 3 AI services — DONE
Files:
src/server/services/ai-tagging.ts:144src/server/services/ai-filtering.ts:281src/server/services/ai-award-eligibility.ts:137
All three files contain nearly identical toProjectWithRelations() functions that convert a project object to the ProjectWithRelations type for anonymization. The function body is the same each time: mapping fields, using as any casts, providing defaults for missing fields.
Recommendation: Extract into a shared function in src/server/services/anonymization.ts (which already defines ProjectWithRelations).
Effort: Quick Fix
4.1.2 AI service batch processing boilerplate — DEFERRED (large refactor, high risk for no behavioral change)
Files:
src/server/services/ai-assignment.ts:273-377src/server/services/ai-filtering.ts:425-565src/server/services/ai-award-eligibility.ts:278-368src/server/services/mentor-matching.ts:202-337
All four AI services follow an identical pattern:
- Get OpenAI client, check if null -> fallback
- Get configured model
- Anonymize data
- Validate anonymization
- Loop over batches, call
processXxxBatch() - Track total tokens
- Catch errors, classify with
classifyAIError, log withlogAIError, log usage withlogAIUsage - Fallback to algorithm
Each batch processor also repeats: build params -> call API -> extract usage -> log usage -> parse JSON -> deanonymize -> handle parse errors with identical error logging.
Recommendation: Create a generic processAIBatches() utility that handles the shared orchestration, accepting callbacks for prompt building and result parsing.
Effort: Large Refactor
4.1.3 Avatar and Logo routers are structural clones — DONE
Files:
src/server/routers/avatar.ts(187 lines)src/server/routers/logo.ts(196 lines)
Both routers have identical structure with 4 endpoints each (getUploadUrl, confirmUpload, getUrl, delete) that follow the same pattern:
- Validate image type
- Generate storage key
- Get provider
- Upload/download/delete with provider
- Update database record
- Create audit log
The only differences are: entity type (User vs Project), field names (profileImageKey vs logoKey), and auth level (protectedProcedure vs adminProcedure).
Recommendation: Create a generic createImageUploadRouter() factory that accepts entity config.
Effort: Medium
4.1.4 Navigation components (jury, mentor, observer) are near-duplicates — DONE
Files:
src/components/layouts/jury-nav.tsxsrc/components/layouts/mentor-nav.tsxsrc/components/layouts/observer-nav.tsx
All three components share the same structure: mobile hamburger menu, desktop nav bar, user avatar dropdown with settings/sign-out, notification bell. They differ only in the navigation links array and the user prop interface name.
Recommendation: Create a shared RoleNav component that accepts a navigation array and role-specific config as props.
Effort: Medium
4.1.5 Expertise matching logic duplicated between AI assignment fallback and smart assignment — SKIPPED (different scoring ranges 0-1 vs 0-40, consolidating would change algorithm behavior)
Files:
src/server/services/ai-assignment.ts:514-534(calculateExpertiseScore)src/server/services/smart-assignment.ts:140-163(calculateTagOverlapScore)
Both implement tag-based expertise matching with slight variations. The fallback in ai-assignment.ts uses set intersection, while smart-assignment.ts uses weighted confidence scores. The mentor-matching algorithmic fallback (src/server/services/mentor-matching.ts:374-444) also has its own keyword matching implementation.
Recommendation: Consolidate expertise scoring into a single utility with configurable weighting. Effort: Medium
4.1.6 Audit logging: two competing patterns — DONE
Files:
src/server/utils/audit.ts- SharedlogAudit()utility (wraps in try-catch)- Used in:
src/server/routers/filtering.ts,src/server/routers/specialAward.ts - 19 other router files use inline
ctx.prisma.auditLog.create()directly (79 total occurrences)
Most routers create audit logs inline without try-catch, meaning an audit failure could break the operation. The logAudit utility exists specifically to prevent this but is only used in 2 of 21 routers.
Recommendation: Migrate all 79 inline audit log calls to use the logAudit utility for consistent error handling. Or add audit logging as tRPC middleware.
Effort: Medium
4.1.7 Batch tagging logic duplicated between service and router — DONE (dead service functions removed)
Files:
src/server/services/ai-tagging.ts:456-551(batchTagProjects)src/server/services/ai-tagging.ts:558-655(batchTagProgramProjects)src/server/routers/tag.ts:20-170(runTaggingJob)
batchTagProjects and batchTagProgramProjects in the service are nearly identical (differ only in the Prisma where clause). The runTaggingJob in the router reimplements the same loop with progress tracking.
Recommendation: The service functions (batchTagProjects, batchTagProgramProjects) appear to be dead code since the router uses runTaggingJob directly. Remove the unused service functions and consolidate into a single batch function with a flexible query parameter.
Effort: Medium
4.2 Dead Code
4.2.1 getObjectInfo function in minio.ts is never called — DONE
File: src/lib/minio.ts:115-120
The getObjectInfo function is exported but never imported or used anywhere in the codebase.
Recommendation: Remove. Effort: Quick Fix
4.2.2 isValidImageSize function in storage/index.ts is never called — DONE
File: src/lib/storage/index.ts:136-138
Exported but never imported anywhere. Image size validation is not performed server-side.
Recommendation: Remove, or actually use it in the avatar/logo upload flows. Effort: Quick Fix
4.2.3 clearStorageProviderCache is never called — DONE
File: src/lib/storage/index.ts:85-88
This function is defined to clear the cached storage provider when settings change, but it is never called from the settings router or anywhere else. This means changing the storage provider in settings has no effect until the server restarts.
Recommendation: Call this function from the settings router when storage_provider is updated.
Effort: Quick Fix
4.2.4 deleteObject in minio.ts is only used indirectly — DONE
File: src/lib/minio.ts:93-98
Only referenced via the S3 storage provider (src/lib/storage/s3-provider.ts). The direct export from minio.ts is never imported directly by any router or service.
Recommendation: Consider making it non-exported (internal to the module) since it's only used by the S3 provider. Effort: Quick Fix
4.2.5 Deprecated batch tag endpoints still exposed — DONE
File: src/server/routers/tag.ts:763-814
Two endpoints are marked @deprecated (batchTagProjects, batchTagProgramProjects) but still included in the router. They duplicate the logic of startTaggingJob.
Recommendation: Check if any frontend code calls these deprecated endpoints. If not, remove them. Effort: Quick Fix
4.2.6 batchTagProjects and batchTagProgramProjects service functions likely unused — DONE
Files:
src/server/services/ai-tagging.ts:456-551src/server/services/ai-tagging.ts:558-655
These two exported functions are no longer called by the tag router (which uses runTaggingJob instead). No other files import them.
Recommendation: Remove both functions. Effort: Quick Fix
4.2.7 twilio npm package is never imported — DONE
File: package.json:85
The twilio package (v5.4.0) is listed as a dependency, but the Twilio WhatsApp provider (src/lib/whatsapp/twilio-provider.ts) uses raw fetch() calls to the Twilio API instead of the SDK. The SDK is never imported anywhere.
Recommendation: Remove twilio from dependencies to reduce bundle size and install footprint.
Effort: Quick Fix
4.3 Redundant Code
4.3.1 Two storage abstraction layers: minio.ts and storage/ — DONE
Files:
src/lib/minio.ts- Direct MinIO client with helper functionssrc/lib/storage/- Provider-based abstraction (S3Provider wraps minio.ts, LocalProvider)
Some routers use minio.ts directly (file.ts, partner.ts, learningResource.ts, applicant.ts) while others use the storage abstraction (avatar.ts, logo.ts). This creates inconsistency: files uploaded through direct minio calls won't work if the admin switches to local storage.
Recommendation: Migrate all direct @/lib/minio imports to use the @/lib/storage abstraction for consistent provider-agnostic file handling.
Effort: Medium
4.3.2 Debug console.log statements in user router — DONE
File: src/server/routers/user.ts:246,256
Debug logging for a simple user fetch query:
console.log('[user.get] Fetching user:', input.id)
console.log('[user.get] Found user:', user.email)
These appear to be leftover development debug statements, not structured logging.
Recommendation: Remove. Effort: Quick Fix
4.3.3 Excessive console.log in AI services — DONE (structured logger implemented)
Files:
src/server/services/ai-tagging.ts- 22 console.log statementssrc/server/services/ai-filtering.ts- 5 console.log statementssrc/server/services/ai-assignment.ts- 5 console.log statementssrc/server/services/ai-award-eligibility.ts- 5 console.log statementssrc/server/services/mentor-matching.ts- 4 console.log statementssrc/server/routers/tag.ts- 10 console.log statements
Total: ~51 console.log/warn/error statements across AI services. While useful for debugging, production code should use structured logging with configurable log levels.
Recommendation: Implement a simple logger utility with configurable levels (debug, info, warn, error) and replace all console.log calls. Effort: Medium
4.4 Unfinished Code
4.4.1 TODO: Send invitation email to new team member — DONE
File: src/server/routers/applicant.ts:659
// TODO: Send invitation email to the new team member
The team member addition flow creates the team member record but never sends an invitation email.
Recommendation: Implement email notification using the existing notification service. Effort: Medium
4.4.2 aiBoost field always returns 0 — DONE (removed unused field)
File: src/server/services/smart-assignment.ts:27,370
The ScoreBreakdown type includes an aiBoost field (described as "Reserved: 0-5 points (future AI boost)" in the comment), but it's always set to 0 in all score calculations.
Recommendation: Either implement the AI boost feature or remove the field to avoid confusion. Effort: Quick Fix (to remove) / Large Refactor (to implement)
4.5 Unused Dependencies — ALL DONE
| Package | Evidence | Recommendation | Status |
|---|---|---|---|
twilio (v5.4.0) |
Never imported; Twilio provider uses raw fetch() |
Remove from package.json | DONE |
autoprefixer (v10.4.20) |
Only referenced in postcss config; Tailwind v4 includes autoprefixer | Verify and potentially remove | DONE |
@types/leaflet (v1.9.21) |
Listed in dependencies not devDependencies |
Move to devDependencies | DONE |
4.6 Summary
| Category | Count | Quick Fix | Medium | Large |
|---|---|---|---|---|
| Duplicate Code | 7 | 0 | 4 | 3 |
| Dead Code | 7 | 7 | 0 | 0 |
| Redundant Code | 3 | 2 | 1 | 0 |
| Unfinished Code | 2 | 1 | 1 | 0 |
| Unused Dependencies | 3 | 3 | 0 | 0 |
| Total | 22 | 13 | 6 | 3 |
Priority recommendations (highest impact, lowest effort):
- Remove unused
twiliodependency (saves ~10MB in node_modules) - Remove dead code:
getObjectInfo,isValidImageSize, deprecated batch endpoints, unused batch service functions - Call
clearStorageProviderCachewhen storage settings change (bug fix) - Extract shared
toProjectWithRelationsto eliminate 3-way duplication - Migrate direct
@/lib/minioimports to@/lib/storagefor consistency
2. Security Review
Reviewer: Security Reviewer Agent Date: 2026-02-05 Scope: Authentication, authorization, input validation, file security, rate limiting, CSRF, data privacy, password security, API routes, injection prevention, XSS, audit logging, session security, environment variables, account lockout
Critical Findings
CRIT-1: Path Traversal Bypass in Local Storage Provider — DONE
Affected file: src/lib/storage/local-provider.ts:65-68
The path traversal sanitization is insufficient. It only strips literal .. sequences but does not account for encoded variants or alternative traversal techniques on Windows. Additionally, path.join() can resolve .. segments inserted via other mechanisms.
private getFilePath(key: string): string {
const sanitizedKey = key.replace(/\.\./g, '').replace(/^\//, '')
return path.join(this.basePath, sanitizedKey)
}
Impact: An attacker with a valid signed URL (or who can manipulate the key parameter) could potentially read arbitrary files from the server filesystem.
Recommendation: After constructing the full path with path.resolve(), verify it still starts with the expected base directory:
const resolved = path.resolve(this.basePath, sanitizedKey)
if (!resolved.startsWith(path.resolve(this.basePath))) {
throw new Error('Invalid file path')
}
CRIT-2: Timing-Unsafe Signature Comparison — DONE
Affected file: src/lib/storage/local-provider.ts:56-63
The HMAC signature verification uses === string comparison, which is vulnerable to timing attacks. An attacker could theoretically determine the correct HMAC byte-by-byte by measuring response times.
return signature === expectedSignature && expiresAt > Date.now() / 1000
Impact: In theory, an attacker could forge valid pre-signed URLs by progressively guessing the HMAC signature. While practical exploitation is difficult over the network, it remains a cryptographic best-practice violation.
Recommendation: Use crypto.timingSafeEqual() for HMAC comparison:
import { timingSafeEqual } from 'crypto'
const sigBuffer = Buffer.from(signature, 'hex')
const expectedBuffer = Buffer.from(expectedSignature, 'hex')
return sigBuffer.length === expectedBuffer.length
&& timingSafeEqual(sigBuffer, expectedBuffer)
&& expiresAt > Date.now() / 1000
CRIT-3: Email/Password Change API Routes Lack Proper Authentication — DONE
Affected files: src/app/api/email/verify-credentials/route.ts, src/app/api/email/change-password/route.ts
These API routes accept POST requests with email and password credentials but have no session-based authentication verifying the caller. While the middleware authorized callback blocks unauthenticated users from non-public paths, these routes do not verify the authenticated user matches the email being acted upon. An authenticated user could verify credentials or change the Poste.io mailbox password of any @monaco-opc.com email address.
The change-password route at src/app/api/email/change-password/route.ts:97-109 sends the new password in plaintext to the Poste.io admin API using hardcoded admin credentials.
Impact: An authenticated user could potentially change another user's email password. Rate limiting (3-5 attempts per 15 minutes) slows but does not prevent this.
Recommendation: Add NextAuth session checks to both API routes. Verify that the authenticated user's email matches the email being acted upon.
High Severity Findings
HIGH-1: In-Memory Rate Limiting Does Not Scale — DEFERRED (requires Redis infrastructure)
Affected files: src/lib/rate-limit.ts, src/lib/auth.ts:12
Both the rate limiter and the account lockout tracker use in-memory Map objects. In a multi-instance deployment (e.g., multiple Docker replicas behind a load balancer), rate limits and lockouts are per-instance, not global.
const failedAttempts = new Map<string, { count: number; lockedUntil: number }>()
Impact: An attacker could bypass rate limiting by having requests routed to different instances. Account lockout after 5 failed attempts would not work reliably across instances. This is acknowledged in the rate-limit.ts comment but still represents a deployment risk.
Recommendation: Move to Redis-based rate limiting and lockout tracking before deploying with multiple instances. If running single-instance only, document this constraint clearly.
HIGH-2: Health Check Endpoint Leaks Database Error Details — DONE
Affected file: src/app/api/health/route.ts:29
The health endpoint returns the actual error message when the database is disconnected:
error: error instanceof Error ? error.message : 'Unknown error',
Impact: Database connection errors may contain hostnames, ports, connection strings, or other internal infrastructure details that aid attackers in reconnaissance.
Recommendation: Log the full error server-side but return only a generic status to the client. If the health endpoint is meant for internal monitoring only, restrict access via network configuration or authentication.
HIGH-3: Local Storage Fallback Secret Key — DONE
Affected file: src/lib/storage/local-provider.ts:6
const SECRET_KEY = process.env.NEXTAUTH_SECRET || 'local-storage-secret'
If NEXTAUTH_SECRET is not set, the signing key falls back to a hardcoded value. While NEXTAUTH_SECRET should always be set in production, this fallback is dangerous if accidentally deployed without it.
Impact: An attacker who knows the fallback value can forge pre-signed file download/upload URLs, bypassing all file access controls.
Recommendation: Remove the fallback or throw an error at startup if NEXTAUTH_SECRET is not set. Never use hardcoded cryptographic secrets.
HIGH-4: MinIO Client Defaults to Insecure Credentials — DONE
Affected file: src/lib/minio.ts:23-24
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
Impact: If environment variables are not set, the client uses default MinIO credentials (minioadmin/minioadmin). Combined with a misconfigured or exposed MinIO instance, this could grant full access to all stored files including jury evaluation documents and project submissions.
Recommendation: Fail loudly in production when these environment variables are not set instead of falling back to defaults.
HIGH-5: Application Submission Creates Users Without Verification — DONE (added rate limiting)
Affected file: src/server/routers/application.ts:148-341
The application.submit route uses publicProcedure (no authentication required) and creates new User records with the APPLICANT role, plus TeamMember records, based entirely on user-supplied email addresses. Any external user can:
- Create arbitrary user accounts with the
APPLICANTrole - Create team member records linking arbitrary email addresses
- Pollute the user table with fake data
While the APPLICANT role has limited permissions, there is no CAPTCHA, no email verification, and the only rate limit is the global tRPC limit of 100 requests/minute per IP.
Impact: User table pollution, spam applications, potential abuse of the notification system (applicants receive in-app notifications).
Recommendation: Add CAPTCHA/reCAPTCHA verification to the submission endpoint. Implement a stricter rate limit specific to application submissions. Consider requiring email verification before creating user accounts and team memberships.
Medium Severity Findings
MED-1: Password Policy Does Not Require Special Characters — DONE
Affected file: src/lib/password.ts:34-56
The password validation requires only 8 characters, one uppercase, one lowercase, and one digit. Special characters are not required.
Impact: Passwords without special characters are more susceptible to dictionary and brute-force attacks.
Recommendation: Consider requiring at least one special character, or increase the minimum length to 12 characters. Also consider checking against common password lists (e.g., "Have I Been Pwned" API or a local blocklist of the top 10,000 passwords).
MED-2: Audit Logging Failures Are Silently Swallowed — DONE
Affected files: src/server/utils/audit.ts:29-32, src/lib/auth.ts:91,147, and many router files
Audit log failures are caught and silently ignored throughout the codebase:
await prisma.auditLog.create({ ... }).catch(() => {})
Impact: If the audit system fails (e.g., database connection issues, disk full), critical security events (login attempts, role changes, data exports) will not be recorded. This violates the stated requirement of an "immutable audit log."
Recommendation: At minimum, log audit failures to a secondary logging system (e.g., file-based logger or error monitoring service like Sentry). Consider implementing a write-ahead buffer or retry mechanism for audit entries.
MED-3: File Upload Has No Content-Type or Extension Validation — DONE
Affected file: src/server/routers/file.ts:75-125
The getUploadUrl procedure validates the fileType enum (EXEC_SUMMARY, PRESENTATION, VIDEO, OTHER) and accepts a mimeType string, but does not verify that the MIME type matches the file extension, nor does it block dangerous file types (e.g., .exe, .sh, .php, .html).
The local storage upload route at src/app/api/storage/local/route.ts:111-124 accepts any content-type from the request header without validation.
Impact: An admin user could upload executable or HTML files. If these files are later served with their original content-type, this could lead to stored XSS or other browser-based attacks when other users download them.
Recommendation: Implement server-side MIME type validation and file extension allowlisting. Serve uploaded files with Content-Disposition: attachment headers and sanitized content types.
MED-4: File Deletion Does Not Remove Storage Objects — DONE
Affected file: src/server/routers/file.ts:143-171
When a file record is deleted, only the database entry is removed. The actual file in MinIO or local storage is not deleted:
// Note: Actual MinIO deletion could be done here or via background job
// For now, we just delete the database record
Impact: Orphaned files accumulate in storage. Previously-issued pre-signed URLs remain valid until their expiry. This has data retention/GDPR compliance implications.
Recommendation: Implement storage object deletion alongside database record deletion, or create a background cleanup job for orphaned files.
MED-5: IP Address Extraction Relies on Spoofable Header — DEFERRED (requires Nginx proxy configuration)
Affected files: src/server/context.ts:13, src/app/api/trpc/[trpc]/route.ts:13-18, src/app/api/auth/[...nextauth]/route.ts:8-12
Client IP is extracted from the x-forwarded-for header:
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
Impact: If the application is not behind a properly configured reverse proxy, clients can spoof their IP address by setting the x-forwarded-for header. This bypasses IP-based rate limiting and poisons audit log entries with false IP addresses.
Recommendation: Configure the application to only trust x-forwarded-for from known proxy IPs. In production with Nginx, ensure the proxy overwrites (not appends to) this header and strips any client-supplied values.
MED-6: Public Email Availability Check Enables Enumeration — DONE
Affected file: src/server/routers/application.ts:346-367
The checkEmailAvailability endpoint is a public procedure that reveals whether a specific email has already submitted an application for a given round:
return { available: !existing, message: existing ? 'An application with this email...' : null }
Impact: An attacker can enumerate which emails have submitted applications for specific rounds, potentially revealing participation in the ocean protection challenge.
Recommendation: Consider removing this endpoint or requiring authentication. If it is needed for UX, implement stricter rate limiting and CAPTCHA.
MED-7: Assignment Job Status Accessible to All Authenticated Users — DONE
Affected file: src/server/routers/assignment.ts:1041-1061
The getAIAssignmentJobStatus endpoint uses protectedProcedure instead of adminProcedure:
getAIAssignmentJobStatus: protectedProcedure
.input(z.object({ jobId: z.string() }))
Impact: Any authenticated user (including jury members, observers, applicants) can query the status of AI assignment jobs if they know or guess a job ID. While job IDs are UUIDs, this exposes internal platform operations unnecessarily.
Recommendation: Change to adminProcedure to restrict to admin users only.
MED-8: PII Logging in User Router — DONE
Affected file: src/server/routers/user.ts:246,256
Debug logging statements expose user data in server logs:
console.log('[user.get] Fetching user:', input.id)
console.log('[user.get] Found user:', user.email)
Impact: User IDs and email addresses are written to server logs. Under GDPR, log files containing PII must be treated as personal data and properly managed (retention, access controls, etc.).
Recommendation: Remove debug console.log statements or replace with a structured logger that respects log levels and redacts PII.
Low Severity Findings
LOW-1: Session Configuration Could Be Hardened — DONE
Affected file: src/lib/auth.config.ts:88-90
The session uses JWT strategy with a default 24-hour maxAge. No explicit cookie configuration is visible.
session: {
strategy: 'jwt',
maxAge: parseInt(process.env.SESSION_MAX_AGE || '86400'),
},
Impact: NextAuth v5 sets secure cookie defaults, but explicit configuration is better for defense-in-depth. A 24-hour session may be too long for admin users.
Recommendation: Explicitly set cookie options: httpOnly: true, secure: true (production), sameSite: 'lax'. Consider shorter session lifetimes for admin roles.
LOW-2: Export Endpoint Allows Large Data Fetches — DONE (reduced to 5000)
Affected file: src/server/routers/export.ts:266
take: 10000, // Limit export to 10k records
Impact: Large exports could cause memory pressure or slow response times. Admin-only access mitigates the risk.
Recommendation: Consider pagination or streaming exports for large datasets.
LOW-3: Project List Allows Up to 5000 Items Per Page — DONE (reduced to 200)
Affected file: src/server/routers/project.ts:58
perPage: z.number().int().min(1).max(5000).default(20),
Impact: A request for 5000 projects with full includes could cause significant memory usage and slow responses.
Recommendation: Reduce the maximum to a reasonable value (e.g., 200) or implement cursor-based pagination for large result sets.
LOW-4: No CSRF Protection on Custom API Routes — DONE
Affected files: src/app/api/email/verify-credentials/route.ts, src/app/api/email/change-password/route.ts, src/app/api/storage/local/route.ts
These custom API routes (outside of tRPC and NextAuth) accept POST/PUT requests without explicit CSRF token validation. While the Same-Origin Policy and Content-Type restrictions provide some protection, explicit CSRF tokens would add defense-in-depth.
Impact: Low risk due to browser same-origin restrictions, but a defense-in-depth concern.
Recommendation: Consider adding CSRF token validation for custom API routes, especially those that modify state (password change, file upload).
LOW-5: Invite Token Validation Has No Specific Rate Limit — N/A (token entropy sufficient, no action needed)
Affected file: src/server/routers/user.ts:45-69
The validateInviteToken is a public procedure protected only by the global tRPC rate limit (100/min per IP). With 256 bits of entropy in the tokens, brute-force is computationally infeasible.
Impact: Negligible due to the token entropy. Listed for completeness.
Recommendation: No immediate action needed. The existing token entropy is sufficient.
Informational Findings (Positive Security Practices)
INFO-1: Comprehensive Audit Logging — N/A
The platform implements thorough audit logging across all critical operations: user management, authentication, evaluations, assignments, file access, exports, and settings changes. Each audit entry includes userId, action, entityType, entityId, details, IP address, and user agent.
INFO-2: Well-Designed RBAC Architecture — N/A
The tRPC middleware-based RBAC system is well-designed with clear role hierarchies (publicProcedure, protectedProcedure, adminProcedure, superAdminProcedure, juryProcedure, mentorProcedure). Role checks are enforced at the procedure level, with additional ownership checks in individual handlers.
INFO-3: No SQL Injection Risk — N/A
The codebase exclusively uses Prisma ORM for database access. The only raw query found is the health check's SELECT 1. No user input is interpolated into raw SQL anywhere in the application.
INFO-4: No XSS via dangerouslySetInnerHTML — N/A
No usage of dangerouslySetInnerHTML was found in the codebase. React's default escaping provides XSS protection for rendered content.
INFO-5: Environment Variables Properly Gitignored — N/A
All .env variants (.env, .env.local, .env.development.local, .env.test.local, .env.production.local, docker/.env) are listed in .gitignore. No hardcoded secrets were found in the source code beyond the noted fallback defaults.
INFO-6: Thorough Data Anonymization for AI — N/A
The anonymization service (src/server/services/anonymization.ts) comprehensively strips PII (emails, phones, URLs, SSNs, IP addresses) before sending data to OpenAI. GDPR compliance validation is enforced before every AI call. Both basic (assignment) and enhanced (filtering/awards) anonymization paths are implemented with validation.
INFO-7: CSRF Protection via tRPC Content-Type — N/A
tRPC uses application/json content type which triggers CORS preflight for cross-origin requests, providing natural CSRF protection. NextAuth routes have built-in CSRF token protection. No permissive CORS headers are configured.
INFO-8: Strong Password Hashing — N/A
bcrypt with 12 salt rounds provides strong password hashing. Account lockout after 5 failed attempts with a 15-minute lockout is implemented. Password reset does not reveal whether an email exists (anti-enumeration pattern).
INFO-9: Jury Data Isolation — N/A
Jury members can only see projects they are assigned to. This is enforced both at the query level (project list endpoint adds assignments: { some: { userId: ctx.user.id } }) and via explicit assignment checks in get/evaluation endpoints.
INFO-10: Rate Limiting Coverage — N/A
Rate limiting is applied to: all tRPC endpoints (100 requests/min), auth POST endpoints (10/min), email verification (5 per 15 min), and email password change (3 per 15 min). Headers expose rate limit status for client-side handling.
Security Review Summary
| Severity | Count | Key Areas |
|---|---|---|
| Critical | 3 | Path traversal, timing-unsafe HMAC, unauthenticated email routes |
| High | 5 | In-memory rate limiting, error info leaks, hardcoded secrets, user creation without verification |
| Medium | 8 | Password policy, audit reliability, file validation, IP spoofing, access control gaps |
| Low | 5 | Session config, large exports, CSRF on custom routes |
| Informational | 10 | Positive practices observed |
Overall Assessment: The platform demonstrates strong security fundamentals with comprehensive RBAC, audit logging, proper ORM usage (no SQL injection), XSS prevention, and data anonymization for AI services. The critical issues center around the local file storage provider (path traversal bypass, timing-unsafe HMAC comparison) and the email API routes lacking proper authorization checks. The high-severity items relate to deployment concerns (in-memory rate limiting, hardcoded fallback secrets) and the unauthenticated application submission endpoint's ability to create user records without verification. Addressing the critical and high-severity items should be prioritized before production deployment with external exposure.
5. Feature Proposals & Improvements — PARTIAL (10 of 26 features implemented)
Reviewed by: Feature Proposer Agent Date: 2026-02-05
This section proposes new features and improvements based on a thorough review of the platform's codebase, including all routers, services, pages, components, and data model. Proposals are organized by implementation effort and prioritized by impact.
Note: This section contains feature proposals for future development. None of these are bugs or issues — they are enhancement ideas preserved as a roadmap reference.
Quick Wins (Low Effort, High Impact)
5.1.1 Jury Evaluation Reminders & Deadline Countdown — DONE
What: Add automated reminder notifications at configurable intervals before voting deadlines (e.g., 72h, 24h, 1h). Show a prominent countdown timer on the jury dashboard and assignment pages when deadline is approaching.
Why: The platform already has REMINDER_24H and REMINDER_1H notification types defined in the email templates, but there is no scheduled job or cron mechanism to actually trigger these reminders. Jury members may forget deadlines, leading to incomplete evaluations.
Who benefits: Jury Members, Program Admins (higher completion rates)
Scope: Add a scheduled task (cron job or database-polling endpoint) that checks upcoming deadlines and sends notifications. Add countdown UI component to jury dashboard. ~2-3 days.
5.1.2 Evaluation Progress Indicator Per Project — DONE
What: On the evaluation form page (/jury/projects/[id]/evaluate), show a visual progress indicator (e.g., "3 of 5 criteria scored") so jury members know how far along they are before submitting.
Why: The current autosave system tracks partial scores, but there is no visual indicator showing the jury member how many criteria they have completed. This causes friction especially for forms with many criteria.
Who benefits: Jury Members
Scope: Frontend-only change. Add a progress bar or step indicator to the evaluation form component. ~0.5 day.
5.1.3 Bulk Status Update for Projects — DONE
What: Allow admins to select multiple projects from the project list and change their status in bulk (e.g., mark 20 projects as SEMIFINALIST at once).
Why: Currently, the project router has individual update capabilities but no bulk status change. After reviewing evaluations, admins need to advance multiple projects, which is tedious one-by-one. The filtering router has finalizeResults for automated advancement, but there is no manual bulk option outside of filtering.
Who benefits: Program Admins, Super Admins
Scope: Add bulkUpdateStatus mutation to project router, add checkbox selection UI to project list page. ~1-2 days.
5.1.4 Export Filtering Results as CSV — DONE
What: Add CSV export capability for filtering results, similar to the existing evaluation and project score exports.
Why: The export router currently supports evaluations, project scores, assignments, and audit logs, but not filtering results. Admins reviewing AI filtering outcomes may want to share or archive these results externally.
Who benefits: Program Admins
Scope: Add a new endpoint to the export router for filtering results. ~0.5 day.
5.1.5 Observer Access to Reports & Analytics — DONE
What: Allow observers to view the Reports page (analytics charts and exports) in read-only mode.
Why: The observer dashboard (/observer) shows basic stats (program count, project count, juror count, evaluation completion), but observers cannot access the detailed analytics charts (score distribution, evaluation timeline, juror workload, project rankings, geographic distribution) that are on the admin Reports page. The observer role is designed for stakeholders who need visibility without edit access.
Who benefits: Observers (stakeholders, board members)
Scope: Create an /observer/reports page that mirrors the admin analytics tab using the existing chart components. Requires adding observer-accessible query variants for the analytics router. ~1-2 days.
5.1.6 Conflict of Interest Declaration — DONE
What: When a jury member starts an evaluation, prompt them to declare any conflicts of interest with the project. Store the declaration and allow admins to flag/reassign if needed.
Why: In academic and grant judging, conflict of interest management is a standard requirement. Currently there is no mechanism for jurors to declare COI, which could undermine the integrity of the evaluation process.
Who benefits: Jury Members, Program Admins, platform credibility
Scope: Add a conflictOfInterest field to the Assignment or Evaluation model, add a COI declaration dialog before evaluation starts, add admin view for flagged COI. ~2-3 days.
Medium-Term (Moderate Effort)
5.2.1 Email Digest / Summary Notifications
What: Implement a daily or weekly email digest that summarizes a user's pending actions: unfinished evaluations, upcoming deadlines, new assignments, new notifications. Configurable per user (daily/weekly/off).
Why: The notification system sends individual emails per event, which can be noisy. A digest approach reduces email fatigue while ensuring users stay informed. The NotificationEmailSetting model exists but only controls per-type settings, not digest frequency.
Who benefits: All roles (Jury Members, Mentors, Admins)
Scope: Add user preference for digest frequency, create digest aggregation service, implement scheduled job to send digests. ~3-5 days.
5.2.2 Evaluation Calibration / Norming Session Tool
What: Create a calibration mode where all jurors evaluate the same 2-3 "test" projects, then admins can view the variance in scores and use this to identify outlier jurors or adjust scoring guidance.
Why: In large jury panels (handling 130+ projects), inter-rater reliability is a common challenge. Some jurors may be consistently lenient or harsh. A calibration round helps ensure scoring consistency and is standard practice in grant review and academic conferences.
Who benefits: Program Admins (quality assurance), Jury Members (alignment)
Scope: Add a "calibration round" type, mark specific projects as calibration, create comparison analytics showing juror variance on calibration projects. ~5-7 days.
5.2.3 Multi-Language Support (i18n)
What: Add internationalization support, starting with French and English. The platform is for Monaco, which is French-speaking, but the jury may be international.
Why: Monaco is a French-speaking principality, and many stakeholders and applicants may prefer French. The current platform is English-only. This is especially important for the public application form and applicant-facing pages.
Who benefits: Applicants, Jury Members, all French-speaking users
Scope: Integrate next-intl or similar i18n library, extract all user-facing strings, translate to French. Start with public-facing pages (application form, login, applicant portal). ~5-10 days for initial setup + French translation.
5.2.4 Project Comparison View for Jury
What: Allow jury members to view 2-3 assigned projects side-by-side in a comparison view, showing key details (title, description, team, country, tags) next to each other.
Why: When evaluating 15-20 projects, jurors often want to compare similar projects before assigning scores. Currently they must navigate between individual project pages, losing context.
Who benefits: Jury Members
Scope: Create a new comparison page that loads multiple projects, side-by-side layout. ~3-4 days.
5.2.5 Applicant Portal Enhancements — DONE
What: Expand the applicant portal (/my-submission) with:
- Application status tracking with timeline (submitted -> under review -> semifinalist -> finalist)
- Ability to upload supplementary documents after initial submission (within a deadline)
- Team member management (add/remove members, update roles)
- Communication channel with mentors (if assigned)
Why: The current applicant portal shows submission details and team management, but lacks a status timeline and post-submission document upload. These are standard features in grant/competition platforms. The team management page exists but could be enriched.
Who benefits: Applicants
Scope: Enhance the existing /my-submission/[id] pages with status timeline component, add supplementary upload endpoint, enrich team management. ~5-7 days.
5.2.6 Round Templates
What: Allow admins to create round templates that pre-configure evaluation criteria, assignment constraints, voting windows, and filtering rules. When creating a new round, they can select a template instead of configuring everything from scratch.
Why: If the MOPC runs annually, admins will repeat similar configurations each year. Templates reduce setup time and ensure consistency across editions. Currently, every new round requires manual configuration of all parameters.
Who benefits: Program Admins, Super Admins
Scope: Add a RoundTemplate model, create template management UI, add "Create from template" option in round creation. ~3-5 days.
5.2.7 Jury Member Availability & Workload Preferences
What: Allow jury members to set their availability (date ranges when they can evaluate) and preferred workload (number of projects they are comfortable reviewing). Surface this to admins during assignment.
Why: The current system has maxAssignments per user but no availability dates. Jurors may have travel or work conflicts during part of the voting window. Knowing availability helps admins assign projects more effectively.
Who benefits: Jury Members, Program Admins
Scope: Add availability fields to user profile, surface in assignment UI, factor into smart assignment algorithm. ~3-4 days.
5.2.8 Real-Time Voting Dashboard for Live Events
What: Enhance the live voting feature with a real-time scoreboard using Server-Sent Events or WebSocket polling. Currently, the live voting uses standard HTTP polling. Add a large-screen "presentation mode" showing live score animations as votes come in.
Why: The live voting feature (/admin/rounds/[id]/live-voting) and public scores page (/live-scores/[sessionId]) exist but use standard queries with manual refresh. For a live event with an audience, real-time updates with animations create a much more engaging experience.
Who benefits: Event organizers, audience, Jury Members at live events
Scope: Add SSE or WebSocket endpoint for vote updates, create presentation mode component with animations, add audience count display. ~5-7 days.
5.2.9 AI-Powered Evaluation Summary — DONE
What: After all evaluations are submitted for a project, use AI to generate a summary of the jurors' collective feedback. Group common themes, highlight strengths and weaknesses mentioned by multiple jurors.
Why: Admins currently see individual evaluation scores and feedback text. Synthesizing feedback from 3-5 jurors manually is time-consuming. An AI summary helps admins quickly understand the consensus view, and could be shared with applicants as constructive feedback.
Who benefits: Program Admins, Applicants (if feedback is shared)
Scope: Add AI summary service using existing OpenAI integration, create summary generation endpoint (on-demand, not automatic), add summary display in project detail view. ~3-5 days.
Long-Term (Ambitious, High Impact)
5.3.1 Public Website Module
What: Build the public-facing website for monaco-opc.com, including:
- Homepage with program overview, timeline, and partner logos
- "Apply" landing page with program details and application form link
- Past winners gallery with project descriptions and photos/videos
- News/blog section for announcements
- Partner showcase page
Why: Listed as a "Future" feature in CLAUDE.md. The platform currently has no public website -- only the internal portal. A public site is essential for program visibility, applicant recruitment, and sponsor recognition.
Who benefits: Applicants (discovery), Partners (visibility), Program (credibility)
Scope: Build Next.js pages under (public) route group, CMS-like content management for news/blog, integrate existing partner data. ~15-25 days.
5.3.2 Communication Hub
What: Add an integrated messaging system within the platform:
- Admin-to-jury broadcast messages
- Admin-to-team (applicant) messages with templates
- Mentor-to-team direct messaging
- Message templates for common communications (congratulations, not selected, next steps)
- Integration with existing email notification system
Why: Currently, communications happen outside the platform (email, potentially WhatsApp). The WhatsApp integration library exists (src/lib/whatsapp/) but is not wired to any router. An in-platform communication hub creates a single source of truth for all program communications and enables better audit trails.
Who benefits: All roles
Scope: Add Message model, create messaging router, build inbox/compose UI, integrate with email and potentially WhatsApp providers. ~10-15 days.
5.3.3 Advanced Analytics & Reporting Dashboard
What: Expand analytics with:
- Cross-round comparison (how did scores change between Round 1 and Round 2 for advancing projects?)
- Juror consistency analysis (standard deviation per juror, score correlation between jurors)
- Diversity metrics (geographic distribution of advancing projects, category balance)
- Year-over-year trends (if multi-edition data exists)
- Exportable PDF reports with charts for board presentations
- Custom report builder (select metrics, date ranges, filters)
Why: The current analytics router provides good per-round views (score distribution, timeline, workload, rankings, criteria scores, geographic distribution), but lacks cross-round and historical analysis. For a program that runs annually, trend analysis is valuable for demonstrating impact to stakeholders.
Who benefits: Program Admins, Super Admins, Observers, Board Members
Scope: Add cross-round query endpoints, create new chart components, build PDF export using a library like @react-pdf/renderer. ~10-15 days.
5.3.4 Applicant Self-Service with Draft Saving
What: Enhance the public application form to support:
- Save as draft and resume later (using magic link or email verification)
- Multi-step form with progress saving between sessions
- File uploads during application (pitch deck, business plan, video)
- Application preview before final submission
Why: The current application form (/apply/[slug]) submits everything in one go. For complex applications requiring a business plan and team details, applicants may need multiple sessions to complete their submission. Draft saving is standard in grant application platforms.
Who benefits: Applicants
Scope: Add draft application model, magic link authentication for applicants, file upload integration in application form, preview component. ~7-10 days.
5.3.5 Webhooks & API Integration Layer
What: Add a webhook system that fires events when key actions occur:
- Application submitted
- Project status changed (advanced, rejected)
- Evaluation submitted
- Round status changed
- Award winner selected
Allow admins to configure webhook URLs for external integrations (CRM, Slack, custom dashboards).
Why: The platform is currently self-contained. Organizations often need to integrate with external systems (CRM for applicant tracking, Slack for admin notifications, donor databases). A webhook system enables this without custom code for each integration.
Who benefits: Program Admins, Super Admins, external system integrations
Scope: Add Webhook model (url, events, secret), create webhook dispatch service, add admin webhook configuration UI. ~5-7 days.
5.3.6 Peer Review / Collaborative Evaluation Notes
What: After individual evaluations are submitted (and locked), allow jurors assigned to the same project to see anonymized summaries of other jurors' scores and feedback. Enable optional discussion threads for projects where scores diverge significantly.
Why: In academic peer review, after independent evaluation, reviewers often discuss divergent opinions to reach consensus. Currently, jurors submit in isolation with no visibility into other evaluations. For borderline projects, discussion can improve decision quality.
Who benefits: Jury Members, Program Admins (better decisions)
Scope: Add post-evaluation discussion model, create discussion thread UI, implement anonymization for jury comments, add divergence detection logic. ~7-10 days.
Improvements to Existing Features
5.4.1 Smart Assignment Algorithm Improvements — DONE
Current state: The smart assignment algorithm in assignment.ts:getSuggestions uses expertise matching (35%), load balancing (20%), and under-min-target bonus (15%). The AI assignment uses GPT for more nuanced matching.
Improvement: Add the following factors to the algorithm:
- Geographic diversity: Ensure projects from the same country are not all assigned to the same juror
- Previous round familiarity: If a juror evaluated a project in Round 1, prioritize assigning them the same project in Round 2 for continuity (or explicitly avoid it for fresh perspective, configurable)
- Conflict avoidance: If COI declarations are implemented, auto-exclude conflicted jurors
Scope: Modify the scoring algorithm in the getSuggestions and AI assignment services. ~2-3 days.
5.4.2 Evaluation Form Flexibility — DONE
Current state: Evaluation criteria are configurable per round with labels, descriptions, scales, and weights. Criteria support numeric scoring (1-10).
Improvement:
- Add support for text-only criteria (qualitative feedback without a numeric score)
- Add support for yes/no criteria (boolean questions like "Does this project have a viable business model?")
- Add conditional criteria (show criterion B only if criterion A score is above 7)
- Add criterion grouping (organize criteria into sections like "Innovation", "Impact", "Feasibility")
Scope: Extend the criteria JSON schema, update the evaluation form component to render different input types. ~3-5 days.
5.4.3 Audit Log Enhancements
Current state: Comprehensive audit logging exists for most actions. The audit page shows logs with filtering by user, action type, entity type, and date range.
Improvement:
- Add before/after diff for update actions (store previous values alongside new values)
- Add session tracking (group actions from the same login session)
- Add anomaly detection (flag unusual patterns like bulk deletes or rapid score changes)
- Add audit log retention policy (auto-archive logs older than X months)
Scope: Enhance audit log creation to capture previous state, add session ID tracking, build anomaly detection service. ~5-7 days.
5.4.4 File Management Improvements
Current state: Files are uploaded to MinIO with pre-signed URLs. Files are associated with projects and have types (exec summary, presentation, video, etc.).
Improvement:
- Add file versioning (track file replacements, keep history)
- Add file preview (inline PDF viewer, video player, image thumbnails) instead of download-only
- Add file size and type validation on upload (client-side + server-side)
- Add virus scanning integration for uploaded files
- Add bulk file download (zip multiple project files for offline review)
Scope: Add file version tracking, integrate a PDF viewer component, add validation middleware. ~5-7 days.
5.4.5 Live Voting UX Improvements
Current state: Live voting allows admin to control sessions, set project order, start/stop voting windows. Jury members vote on the current project.
Improvement:
- Add QR code generation for the voting URL so audience members can scan and vote on their phones
- Add countdown timer with visual animations when time is running low
- Add score reveal animation after voting closes for each project
- Add tie-breaking mechanism configuration
- Add audience vote option (separate from jury vote, displayed alongside but not counted in official scoring)
Scope: QR code generation, animation components, tie-breaking logic. ~5-7 days.
5.4.6 Mentor Dashboard Enhancements
Current state: Mentors can view their assigned projects, project details, team members, and files. The mentor dashboard shows a list of assignments.
Improvement:
- Add mentoring milestones/checklist (configurable per program: "Initial meeting", "Business plan review", "Pitch preparation")
- Add mentor notes on each project (private notes visible only to the mentor and admins)
- Add mentor-to-team communication within the platform
- Add mentor activity tracking (when did the mentor last view the project?)
- Add mentorship completion status tracking
Scope: Add MentorNote model, milestone checklist model, enhance mentor project detail page. ~5-7 days.
5.4.7 Application Form Builder (Admin Configurable)
Current state: The application form has a fixed schema defined in application.ts with hard-coded fields (category, contact info, project details, team members, additional info).
Improvement: Allow admins to configure the application form fields per round, similar to how evaluation criteria are configurable. Support:
- Custom text fields, dropdowns, checkboxes
- Required vs optional fields
- Conditional fields (show field B if field A has value X)
- Custom validation rules
- Field ordering via drag-and-drop
Why: Different programs or editions may require different application information. The current hard-coded form works for MOPC but limits reusability.
Scope: Create form builder model (similar to EvaluationForm's criteriaJson), build form builder UI, dynamic form renderer. ~7-10 days.
Priority Matrix
| Priority | Feature | Impact | Effort | Status |
|---|---|---|---|---|
| P0 | 5.1.1 Jury Evaluation Reminders | High | Low | DONE |
| P0 | 5.1.2 Evaluation Progress Indicator | High | Very Low | DONE |
| P0 | 5.1.3 Bulk Project Status Update | High | Low | DONE |
| P0 | 5.1.6 Conflict of Interest Declaration | High | Low | DONE |
| P1 | 5.1.4 Export Filtering Results | Medium | Very Low | DONE |
| P1 | 5.1.5 Observer Access to Reports | Medium | Low | DONE |
| P1 | 5.2.1 Email Digest Notifications | High | Medium | |
| P1 | 5.2.5 Applicant Portal Enhancements | High | Medium | DONE |
| P1 | 5.2.9 AI Evaluation Summary | High | Medium | DONE |
| P1 | 5.4.2 Evaluation Form Flexibility | High | Medium | DONE |
| P2 | 5.2.2 Evaluation Calibration Tool | Medium | Medium | |
| P2 | 5.2.4 Project Comparison View | Medium | Medium | |
| P2 | 5.2.6 Round Templates | Medium | Medium | |
| P2 | 5.2.7 Jury Availability Preferences | Medium | Medium | |
| P2 | 5.2.8 Real-Time Live Voting | Medium | Medium | |
| P2 | 5.4.1 Smart Assignment Improvements | Medium | Low | DONE |
| P2 | 5.4.4 File Management Improvements | Medium | Medium | |
| P2 | 5.4.5 Live Voting UX Improvements | Medium | Medium | |
| P2 | 5.4.6 Mentor Dashboard Enhancements | Medium | Medium | |
| P3 | 5.2.3 Multi-Language Support | High | High | |
| P3 | 5.3.1 Public Website Module | High | High | |
| P3 | 5.3.2 Communication Hub | High | High | |
| P3 | 5.3.3 Advanced Analytics Dashboard | Medium | High | |
| P3 | 5.3.4 Applicant Self-Service Drafts | Medium | Medium | |
| P3 | 5.3.5 Webhooks & API Integration | Medium | Medium | |
| P3 | 5.3.6 Peer Review / Collaborative Notes | Medium | High | |
| P3 | 5.4.3 Audit Log Enhancements | Low | Medium | |
| P3 | 5.4.7 Application Form Builder | Medium | High |