Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s

Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -311,7 +311,7 @@ export default function ApplySettingsPage() {
// Initialize local state from server data
useEffect(() => {
if (serverConfig && !initialized) {
setConfig(serverConfig)
setConfig({...DEFAULT_WIZARD_CONFIG, ...serverConfig})
setInitialized(true)
}
}, [serverConfig, initialized])

View File

@@ -1,5 +1,6 @@
import { notFound } from 'next/navigation'
import Link from 'next/link'
import type { Route } from 'next'
import { api } from '@/lib/trpc/server'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
@@ -86,22 +87,22 @@ export default async function ProgramDetailPage({ params }: ProgramDetailPagePro
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Stages</CardTitle>
<CardTitle>Rounds</CardTitle>
<CardDescription>
Pipeline stages for this program
Competition rounds for this program
</CardDescription>
</div>
<Button asChild>
<Link href={`/admin/rounds/pipelines?programId=${id}`}>
<Link href={`/admin/competitions?programId=${id}` as Route}>
<Plus className="mr-2 h-4 w-4" />
Manage Pipeline
Manage Competitions
</Link>
</Button>
</CardHeader>
<CardContent>
{(program.stages as Array<{ id: string; name: string; status: string; _count: { projects: number; assignments: number }; createdAt?: Date }>).length === 0 ? (
<div className="py-8 text-center text-muted-foreground">
No stages created yet. Set up a pipeline to get started.
No rounds created yet. Set up a competition to get started.
</div>
) : (
<Table>

View File

@@ -42,14 +42,10 @@ async function ProgramsContent() {
const programs = await prisma.program.findMany({
// Note: PROGRAM_ADMIN filtering should be handled via middleware or a separate relation
include: {
pipelines: {
competitions: {
include: {
tracks: {
include: {
stages: {
select: { id: true, status: true },
},
},
rounds: {
select: { id: true, status: true },
},
},
},
@@ -57,16 +53,14 @@ async function ProgramsContent() {
orderBy: { createdAt: 'desc' },
})
// Flatten stages per program for convenience
const programsWithStageCounts = programs.map((p) => {
const allStages = p.pipelines.flatMap((pl) =>
pl.tracks.flatMap((t) => t.stages)
)
const activeStages = allStages.filter((s) => s.status === 'STAGE_ACTIVE')
return { ...p, stageCount: allStages.length, activeStageCount: activeStages.length }
// Flatten rounds per program for convenience
const programsWithRoundCounts = programs.map((p) => {
const allRounds = p.competitions.flatMap((c) => c.rounds)
const activeRounds = allRounds.filter((r) => r.status === 'ROUND_ACTIVE')
return { ...p, roundCount: allRounds.length, activeRoundCount: activeRounds.length }
})
if (programsWithStageCounts.length === 0) {
if (programsWithRoundCounts.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
@@ -102,14 +96,14 @@ async function ProgramsContent() {
<TableRow>
<TableHead>Program</TableHead>
<TableHead>Year</TableHead>
<TableHead>Stages</TableHead>
<TableHead>Rounds</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{programsWithStageCounts.map((program) => (
{programsWithRoundCounts.map((program) => (
<TableRow key={program.id}>
<TableCell>
<div>
@@ -124,10 +118,10 @@ async function ProgramsContent() {
<TableCell>{program.year}</TableCell>
<TableCell>
<div>
<p>{program.stageCount} total</p>
{program.activeStageCount > 0 && (
<p>{program.roundCount} total</p>
{program.activeRoundCount > 0 && (
<p className="text-sm text-muted-foreground">
{program.activeStageCount} active
{program.activeRoundCount} active
</p>
)}
</div>
@@ -176,7 +170,7 @@ async function ProgramsContent() {
{/* Mobile card view */}
<div className="space-y-4 md:hidden">
{programsWithStageCounts.map((program) => (
{programsWithRoundCounts.map((program) => (
<Card key={program.id}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
@@ -193,7 +187,7 @@ async function ProgramsContent() {
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Stages</span>
<span>
{program.stageCount} ({program.activeStageCount} active)
{program.roundCount} ({program.activeRoundCount} active)
</span>
</div>
<div className="flex items-center justify-between text-sm">