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

@@ -54,11 +54,11 @@ import {
import { ExportPdfButton } from '@/components/shared/export-pdf-button'
import { AnimatedCard } from '@/components/shared/animated-container'
// Parse selection value: "all:programId" for edition-wide, or stageId
function parseSelection(value: string | null): { stageId?: string; programId?: string } {
// Parse selection value: "all:programId" for edition-wide, or roundId
function parseSelection(value: string | null): { roundId?: string; programId?: string } {
if (!value) return {}
if (value.startsWith('all:')) return { programId: value.slice(4) }
return { stageId: value }
return { roundId: value }
}
function OverviewTab({ selectedValue }: { selectedValue: string | null }) {
@@ -72,7 +72,7 @@ function OverviewTab({ selectedValue }: { selectedValue: string | null }) {
) || []
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.stageId || !!queryInput.programId
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: overviewStats, isLoading: statsLoading } =
trpc.analytics.getOverviewStats.useQuery(
@@ -101,7 +101,7 @@ function OverviewTab({ selectedValue }: { selectedValue: string | null }) {
}
const totalProjects = stages.reduce((acc, s) => acc + (s._count?.projects || 0), 0)
const activeStages = stages.filter((s) => s.status === 'STAGE_ACTIVE').length
const activeStages = stages.filter((s) => s.status === 'ROUND_ACTIVE').length
const totalPrograms = programs?.length || 0
return (
@@ -341,7 +341,7 @@ function OverviewTab({ selectedValue }: { selectedValue: string | null }) {
function AnalyticsTab({ selectedValue }: { selectedValue: string }) {
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.stageId || !!queryInput.programId
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: scoreDistribution, isLoading: scoreLoading } =
trpc.analytics.getScoreDistribution.useQuery(
@@ -462,19 +462,19 @@ function CrossStageTab() {
((p.stages || []) as Array<{ id: string; name: string }>).map(s => ({ id: s.id, name: s.name, programName: `${p.year} Edition` }))
) || []
const [selectedStageIds, setSelectedStageIds] = useState<string[]>([])
const [selectedRoundIds, setSelectedRoundIds] = useState<string[]>([])
const { data: comparison, isLoading: comparisonLoading } =
trpc.analytics.getCrossStageComparison.useQuery(
{ stageIds: selectedStageIds },
{ enabled: selectedStageIds.length >= 2 }
trpc.analytics.getCrossRoundComparison.useQuery(
{ roundIds: selectedRoundIds },
{ enabled: selectedRoundIds.length >= 2 }
)
const toggleStage = (stageId: string) => {
setSelectedStageIds((prev) =>
prev.includes(stageId)
? prev.filter((id) => id !== stageId)
: [...prev, stageId]
const toggleRound = (roundId: string) => {
setSelectedRoundIds((prev) =>
prev.includes(roundId)
? prev.filter((id) => id !== roundId)
: [...prev, roundId]
)
}
@@ -492,15 +492,15 @@ function CrossStageTab() {
{stages.map((stage) => (
<Badge
key={stage.id}
variant={selectedStageIds.includes(stage.id) ? 'default' : 'outline'}
variant={selectedRoundIds.includes(stage.id) ? 'default' : 'outline'}
className="cursor-pointer text-sm py-1.5 px-3"
onClick={() => toggleStage(stage.id)}
onClick={() => toggleRound(stage.id)}
>
{stage.programName} - {stage.name}
</Badge>
))}
</div>
{selectedStageIds.length < 2 && (
{selectedRoundIds.length < 2 && (
<p className="text-sm text-muted-foreground mt-3">
Select at least 2 stages to enable comparison
</p>
@@ -508,11 +508,11 @@ function CrossStageTab() {
</CardContent>
</Card>
{comparisonLoading && selectedStageIds.length >= 2 && <Skeleton className="h-[350px]" />}
{comparisonLoading && selectedRoundIds.length >= 2 && <Skeleton className="h-[350px]" />}
{comparison && (
<CrossStageComparisonChart data={comparison as Array<{
stageId: string; stageName: string; projectCount: number; evaluationCount: number
roundId: string; roundName: string; projectCount: number; evaluationCount: number
completionRate: number; averageScore: number | null
scoreDistribution: { score: number; count: number }[]
}>} />
@@ -523,7 +523,7 @@ function CrossStageTab() {
function JurorConsistencyTab({ selectedValue }: { selectedValue: string }) {
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.stageId || !!queryInput.programId
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: consistency, isLoading } =
trpc.analytics.getJurorConsistency.useQuery(
@@ -551,7 +551,7 @@ function JurorConsistencyTab({ selectedValue }: { selectedValue: string }) {
function DiversityTab({ selectedValue }: { selectedValue: string }) {
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.stageId || !!queryInput.programId
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: diversity, isLoading } =
trpc.analytics.getDiversityMetrics.useQuery(
@@ -595,7 +595,7 @@ export default function ObserverReportsPage() {
}
const hasSelection = !!selectedValue
const selectedStage = stages.find((s) => s.id === selectedValue)
const selectedRound = stages.find((s) => s.id === selectedValue)
return (
<div className="space-y-6">
@@ -662,9 +662,9 @@ export default function ObserverReportsPage() {
</TabsList>
{selectedValue && !selectedValue.startsWith('all:') && (
<ExportPdfButton
stageId={selectedValue}
roundName={selectedStage?.name}
programName={selectedStage?.programName}
roundId={selectedValue}
roundName={selectedRound?.name}
programName={selectedRound?.programName}
/>
)}
</div>