Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user