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

@@ -48,7 +48,7 @@ import { useDebouncedCallback } from 'use-debounce'
const PER_PAGE_OPTIONS = [10, 20, 50]
export function ObserverDashboardContent({ userName }: { userName?: string }) {
const [selectedStageId, setSelectedStageId] = useState<string>('all')
const [selectedRoundId, setSelectedRoundId] = useState<string>('all')
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<string>('all')
@@ -65,8 +65,8 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
debouncedSetSearch(value)
}
const handleStageChange = (value: string) => {
setSelectedStageId(value)
const handleRoundChange = (value: string) => {
setSelectedRoundId(value)
setPage(1)
}
@@ -75,38 +75,38 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
setPage(1)
}
// Fetch programs/stages for the filter dropdown
const { data: programs } = trpc.program.list.useQuery({ includeStages: true })
// Fetch programs/rounds for the filter dropdown
const { data: programs } = trpc.program.list.useQuery({})
const stages = programs?.flatMap((p) =>
(p.stages as { id: string; name: string; status: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map((s) => ({
id: s.id,
name: s.name,
const rounds = programs?.flatMap((p) =>
(p.rounds ?? []).map((r: { id: string; name: string; status: string }) => ({
id: r.id,
name: r.name,
programName: `${p.year} Edition`,
status: s.status,
status: r.status,
}))
) || []
// Fetch dashboard stats
const stageIdParam = selectedStageId !== 'all' ? selectedStageId : undefined
const roundIdParam = selectedRoundId !== 'all' ? selectedRoundId : undefined
const { data: stats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery(
{ stageId: stageIdParam }
{ roundId: roundIdParam }
)
// Fetch projects
const { data: projectsData, isLoading: projectsLoading } = trpc.analytics.getAllProjects.useQuery({
stageId: stageIdParam,
roundId: roundIdParam,
search: debouncedSearch || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
page,
perPage,
})
// Fetch recent stages for jury completion
const { data: recentStagesData } = trpc.program.list.useQuery({ includeStages: true })
const recentStages = recentStagesData?.flatMap((p) =>
(p.stages as { id: string; name: string; status: string; windowCloseAt: Date | null; _count: { projects: number; assignments: number } }[]).map((s) => ({
...s,
// Fetch recent rounds for jury completion
const { data: recentRoundsData } = trpc.program.list.useQuery({})
const recentRounds = recentRoundsData?.flatMap((p) =>
(p.rounds ?? []).map((r: { id: string; name: string; status: string }) => ({
...r,
programName: `${p.year} Edition`,
}))
)?.slice(0, 5) || []
@@ -141,18 +141,18 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
</div>
</div>
{/* Stage Filter */}
{/* Round Filter */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<label className="text-sm font-medium">Filter by Stage:</label>
<Select value={selectedStageId} onValueChange={handleStageChange}>
<label className="text-sm font-medium">Filter by Round:</label>
<Select value={selectedRoundId} onValueChange={handleRoundChange}>
<SelectTrigger className="w-full sm:w-[300px]">
<SelectValue placeholder="All Stages" />
<SelectValue placeholder="All Rounds" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Stages</SelectItem>
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.programName} - {stage.name}
<SelectItem value="all">All Rounds</SelectItem>
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
</SelectItem>
))}
</SelectContent>
@@ -184,7 +184,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<p className="text-sm font-medium text-muted-foreground">Programs</p>
<p className="text-2xl font-bold mt-1">{stats.programCount}</p>
<p className="text-xs text-muted-foreground mt-1">
{stats.activeStageCount} active round{stats.activeStageCount !== 1 ? 's' : ''}
{stats.activeRoundCount} active round{stats.activeRoundCount !== 1 ? 's' : ''}
</p>
</div>
<div className="rounded-xl bg-blue-50 p-3">
@@ -203,7 +203,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<p className="text-sm font-medium text-muted-foreground">Projects</p>
<p className="text-2xl font-bold mt-1">{stats.projectCount}</p>
<p className="text-xs text-muted-foreground mt-1">
{selectedStageId !== 'all' ? 'In selected stage' : 'Across all stages'}
{selectedRoundId !== 'all' ? 'In selected round' : 'Across all rounds'}
</p>
</div>
<div className="rounded-xl bg-emerald-50 p-3">
@@ -341,7 +341,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<TableCell className="max-w-[150px] truncate">{project.teamName || '-'}</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs whitespace-nowrap">
{project.stageName}
{project.roundName}
</Badge>
</TableCell>
<TableCell>
@@ -375,7 +375,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
)}
<div className="flex items-center justify-between text-xs text-muted-foreground">
<Badge variant="outline" className="text-xs">
{project.stageName}
{project.roundName}
</Badge>
<div className="flex gap-3">
<span>Score: {project.averageScore !== null ? project.averageScore.toFixed(2) : '-'}</span>
@@ -465,8 +465,8 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
</AnimatedCard>
)}
{/* Recent Stages */}
{recentStages.length > 0 && (
{/* Recent Rounds */}
{recentRounds.length > 0 && (
<AnimatedCard index={6}>
<Card>
<CardHeader>
@@ -474,40 +474,40 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
<div className="rounded-lg bg-violet-500/10 p-1.5">
<BarChart3 className="h-4 w-4 text-violet-500" />
</div>
Recent Stages
Recent Rounds
</CardTitle>
<CardDescription>Overview of the latest evaluation stages</CardDescription>
<CardDescription>Overview of the latest evaluation rounds</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentStages.map((stage) => (
{recentRounds.map((round) => (
<div
key={stage.id}
key={round.id}
className="flex items-center justify-between rounded-lg border p-4 transition-all hover:shadow-sm"
>
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">{stage.name}</p>
<p className="font-medium">{round.name}</p>
<Badge
variant={
stage.status === 'STAGE_ACTIVE'
round.status === 'ROUND_ACTIVE'
? 'default'
: stage.status === 'STAGE_CLOSED'
: round.status === 'ROUND_CLOSED'
? 'secondary'
: 'outline'
}
>
{stage.status === 'STAGE_ACTIVE' ? 'Active' : stage.status === 'STAGE_CLOSED' ? 'Closed' : stage.status}
{round.status === 'ROUND_ACTIVE' ? 'Active' : round.status === 'ROUND_CLOSED' ? 'Closed' : round.status}
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{stage.programName}
{round.programName}
</p>
</div>
<div className="text-right text-sm">
<p>{stage._count?.projects || 0} projects</p>
<p>Round details</p>
<p className="text-muted-foreground">
{stage._count?.assignments || 0} assignments
View analytics
</p>
</div>
</div>