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

@@ -0,0 +1,181 @@
'use client'
import { useState, useEffect } from 'react'
import { AlertTriangle, Bot, CheckCircle2 } from 'lucide-react'
import { toast } from 'sonner'
import { trpc } from '@/lib/trpc/client'
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
interface AssignmentPreviewSheetProps {
roundId: string
open: boolean
onOpenChange: (open: boolean) => void
}
export function AssignmentPreviewSheet({
roundId,
open,
onOpenChange,
}: AssignmentPreviewSheetProps) {
const utils = trpc.useUtils()
const {
data: preview,
isLoading,
refetch,
} = trpc.roundAssignment.preview.useQuery(
{ roundId, honorIntents: true, requiredReviews: 3 },
{ enabled: open }
)
const { mutate: execute, isPending: isExecuting } = trpc.roundAssignment.execute.useMutation({
onSuccess: (result) => {
toast.success(`Created ${result.created} assignments`)
utils.roundAssignment.coverageReport.invalidate({ roundId })
utils.roundAssignment.unassignedQueue.invalidate({ roundId })
onOpenChange(false)
},
onError: (err) => {
toast.error(err.message)
},
})
useEffect(() => {
if (open) {
refetch()
}
}, [open, refetch])
const handleExecute = () => {
if (!preview?.assignments || preview.assignments.length === 0) {
toast.error('No assignments to execute')
return
}
execute({
roundId,
assignments: preview.assignments.map((a: any) => ({
userId: a.userId,
projectId: a.projectId,
})),
})
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-full sm:max-w-xl">
<SheetHeader>
<SheetTitle>Assignment Preview</SheetTitle>
<SheetDescription className="flex items-center gap-2">
<Badge variant="outline" className="text-xs gap-1 shrink-0">
<Bot className="h-3 w-3" />
AI Suggested
</Badge>
Review the proposed assignments before executing. All assignments are admin-approved on execute.
</SheetDescription>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-200px)] mt-6">
{isLoading ? (
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-20 w-full" />
))}
</div>
) : preview ? (
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-sm flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600" />
{preview.stats.assignmentsGenerated || 0} Assignments Proposed
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{preview.stats.totalJurors || 0} jurors will receive assignments
</p>
</CardContent>
</Card>
{preview.warnings && preview.warnings.length > 0 && (
<Card className="border-amber-500">
<CardHeader>
<CardTitle className="text-sm flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-amber-600" />
Warnings ({preview.warnings.length})
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2 text-sm">
{preview.warnings.map((warning: string, idx: number) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-amber-600"></span>
<span>{warning}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)}
{preview.assignments && preview.assignments.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-sm">Assignment Summary</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Total assignments:</span>
<span className="font-medium">{preview.assignments.length}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Unique projects:</span>
<span className="font-medium">
{new Set(preview.assignments.map((a: any) => a.projectId)).size}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Unique jurors:</span>
<span className="font-medium">
{new Set(preview.assignments.map((a: any) => a.userId)).size}
</span>
</div>
</div>
</CardContent>
</Card>
)}
</div>
) : (
<p className="text-sm text-muted-foreground">No preview data available</p>
)}
</ScrollArea>
<SheetFooter className="mt-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleExecute}
disabled={isExecuting || !preview?.assignments || preview.assignments.length === 0}
>
{isExecuting ? 'Executing...' : 'Execute Assignments'}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}

View File

@@ -0,0 +1,98 @@
'use client'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { AlertCircle, CheckCircle2, Users } from 'lucide-react'
interface CoverageReportProps {
roundId: string
}
export function CoverageReport({ roundId }: CoverageReportProps) {
const { data: coverage, isLoading } = trpc.roundAssignment.coverageReport.useQuery({
roundId,
requiredReviews: 3,
})
if (isLoading) {
return (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-32" />
))}
</div>
)
}
if (!coverage) {
return <p className="text-muted-foreground">No coverage data available</p>
}
const totalAssigned = coverage.fullyAssigned || 0
const totalProjects = coverage.totalProjects || 0
const avgPerJuror = coverage.avgReviewsPerProject?.toFixed(1) || '0'
const unassignedCount = coverage.unassigned || 0
return (
<div className="space-y-4">
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Assigned</CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalAssigned}</div>
<p className="text-xs text-muted-foreground">
{totalProjects > 0
? `${((totalAssigned / totalProjects) * 100).toFixed(1)}% coverage`
: 'No projects'}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Avg Per Juror</CardTitle>
<Users className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{avgPerJuror}</div>
<p className="text-xs text-muted-foreground">Assignments per juror</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Unassigned</CardTitle>
<AlertCircle className="h-4 w-4 text-amber-600" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{unassignedCount}</div>
<p className="text-xs text-muted-foreground">
Projects below 3 reviews
</p>
</CardContent>
</Card>
</div>
{coverage.unassigned > 0 && (
<Card className="border-amber-500">
<CardHeader>
<CardTitle className="text-sm">Coverage Warnings</CardTitle>
<CardDescription>Issues detected in assignment coverage</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-1 text-sm">
<li className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
<span>{coverage.unassigned} projects have insufficient coverage</span>
</li>
</ul>
</CardContent>
</Card>
)}
</div>
)
}