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,145 @@
'use client'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { FileText, Download, ExternalLink } from 'lucide-react'
import { toast } from 'sonner'
interface MultiWindowDocViewerProps {
roundId: string
projectId: string
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
function getFileIcon(mimeType: string) {
if (mimeType.startsWith('image/')) return '🖼️'
if (mimeType.startsWith('video/')) return '🎥'
if (mimeType.includes('pdf')) return '📄'
if (mimeType.includes('word') || mimeType.includes('document')) return '📝'
if (mimeType.includes('sheet') || mimeType.includes('excel')) return '📊'
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return '📊'
return '📎'
}
export function MultiWindowDocViewer({ roundId, projectId }: MultiWindowDocViewerProps) {
const { data: windows, isLoading } = trpc.round.getVisibleWindows.useQuery(
{ roundId },
{ enabled: !!roundId }
)
if (isLoading) {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
</CardHeader>
<CardContent>
<Skeleton className="h-64" />
</CardContent>
</Card>
)
}
if (!windows || windows.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Documents</CardTitle>
<CardDescription>Submission windows and uploaded files</CardDescription>
</CardHeader>
<CardContent className="text-center py-8">
<FileText className="h-12 w-12 text-muted-foreground/50 mx-auto mb-3" />
<p className="text-sm text-muted-foreground">No submission windows available</p>
</CardContent>
</Card>
)
}
return (
<Card>
<CardHeader>
<CardTitle>Documents</CardTitle>
<CardDescription>Files submitted across all windows</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue={windows[0]?.id || ''} className="w-full">
<TabsList className="w-full flex-wrap justify-start h-auto gap-1 bg-transparent p-0 mb-4">
{windows.map((window: any) => (
<TabsTrigger
key={window.id}
value={window.id}
className="data-[state=active]:bg-brand-blue data-[state=active]:text-white px-4 py-2 rounded-md text-sm"
>
{window.name}
{window.files && window.files.length > 0 && (
<Badge variant="secondary" className="ml-2 text-xs">
{window.files.length}
</Badge>
)}
</TabsTrigger>
))}
</TabsList>
{windows.map((window: any) => (
<TabsContent key={window.id} value={window.id} className="mt-0">
{!window.files || window.files.length === 0 ? (
<div className="text-center py-8 border border-dashed rounded-lg">
<FileText className="h-10 w-10 text-muted-foreground/50 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">No files uploaded</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2">
{window.files.map((file: any) => (
<Card key={file.id} className="overflow-hidden">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="text-2xl">{getFileIcon(file.mimeType || '')}</div>
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate" title={file.filename}>
{file.filename}
</p>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="text-xs">
{file.mimeType?.split('/')[1]?.toUpperCase() || 'FILE'}
</Badge>
{file.size && (
<span className="text-xs text-muted-foreground">
{formatFileSize(file.size)}
</span>
)}
</div>
<div className="flex gap-2 mt-3">
<Button size="sm" variant="outline" className="h-7 text-xs">
<Download className="mr-1 h-3 w-3" />
Download
</Button>
<Button size="sm" variant="ghost" className="h-7 text-xs">
<ExternalLink className="mr-1 h-3 w-3" />
Preview
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
))}
</Tabs>
</CardContent>
</Card>
)
}