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,149 @@
'use client';
import { use } from 'react';
import { trpc } from '@/lib/trpc/client';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { DeliberationRankingForm } from '@/components/jury/deliberation-ranking-form';
import { CheckCircle2 } from 'lucide-react';
import { toast } from 'sonner';
export default function JuryDeliberationPage({ params: paramsPromise }: { params: Promise<{ sessionId: string }> }) {
const params = use(paramsPromise);
const utils = trpc.useUtils();
const { data: session, isLoading } = trpc.deliberation.getSession.useQuery({
sessionId: params.sessionId
});
const submitVoteMutation = trpc.deliberation.submitVote.useMutation({
onSuccess: () => {
utils.deliberation.getSession.invalidate();
toast.success('Vote submitted successfully');
},
onError: (err) => {
toast.error(err.message);
}
});
const handleSubmitVote = (votes: Array<{ projectId: string; rank?: number; isWinnerPick?: boolean }>) => {
votes.forEach((vote) => {
submitVoteMutation.mutate({
sessionId: params.sessionId,
juryMemberId: session?.currentUser?.id || '',
projectId: vote.projectId,
rank: vote.rank,
isWinnerPick: vote.isWinnerPick
});
});
};
if (isLoading) {
return (
<div className="space-y-6">
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">Loading session...</p>
</CardContent>
</Card>
</div>
);
}
if (!session) {
return (
<div className="space-y-6">
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">Session not found</p>
</CardContent>
</Card>
</div>
);
}
const hasVoted = session.currentUser?.hasVoted;
if (session.status !== 'DELIB_VOTING') {
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Deliberation Session</CardTitle>
<CardDescription>
{session.round?.name} - {session.category}
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center justify-center py-12">
<p className="text-muted-foreground">
{session.status === 'DELIB_OPEN'
? 'Voting has not started yet. Please wait for the admin to open voting.'
: session.status === 'DELIB_TALLYING'
? 'Voting is closed. Results are being tallied.'
: 'This session is locked.'}
</p>
</CardContent>
</Card>
</div>
);
}
if (hasVoted) {
return (
<div className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>Deliberation Session</CardTitle>
<CardDescription className="mt-1">
{session.round?.name} - {session.category}
</CardDescription>
</div>
<Badge>{session.status}</Badge>
</div>
</CardHeader>
<CardContent className="flex flex-col items-center justify-center py-12">
<CheckCircle2 className="mb-4 h-12 w-12 text-green-600" />
<p className="font-medium">Vote Submitted</p>
<p className="mt-1 text-sm text-muted-foreground">
Thank you for your participation in this deliberation
</p>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle>Deliberation Session</CardTitle>
<CardDescription className="mt-1">
{session.round?.name} - {session.category}
</CardDescription>
</div>
<Badge>{session.status}</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
{session.mode === 'SINGLE_WINNER_VOTE'
? 'Select your top choice for this category.'
: 'Rank all projects from best to least preferred.'}
</p>
</CardContent>
</Card>
<DeliberationRankingForm
projects={session.projects || []}
mode={session.mode}
onSubmit={handleSubmitVote}
disabled={submitVoteMutation.isPending}
/>
</div>
);
}