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:
@@ -1,216 +0,0 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ sessionId: string }> }
|
||||
) {
|
||||
const { sessionId } = await params
|
||||
|
||||
// Validate session exists
|
||||
const cursor = await prisma.liveProgressCursor.findUnique({
|
||||
where: { sessionId },
|
||||
})
|
||||
|
||||
if (!cursor) {
|
||||
return new Response('Session not found', { status: 404 })
|
||||
}
|
||||
|
||||
// Manually fetch related data since LiveProgressCursor doesn't have these relations
|
||||
let activeProject = null
|
||||
if (cursor.activeProjectId) {
|
||||
activeProject = await prisma.project.findUnique({
|
||||
where: { id: cursor.activeProjectId },
|
||||
select: { id: true, title: true, teamName: true, description: true },
|
||||
})
|
||||
}
|
||||
|
||||
const stageInfo = await prisma.stage.findUnique({
|
||||
where: { id: cursor.stageId },
|
||||
select: { id: true, name: true },
|
||||
})
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send initial state
|
||||
type CohortWithProjects = Awaited<ReturnType<typeof prisma.cohort.findMany<{
|
||||
where: { stageId: string }
|
||||
include: { projects: { select: { projectId: true } } }
|
||||
}>>>
|
||||
|
||||
const cohortPromise: Promise<CohortWithProjects> = prisma.cohort
|
||||
.findMany({
|
||||
where: { stageId: cursor.stageId },
|
||||
include: {
|
||||
projects: {
|
||||
select: { projectId: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((cohorts) => {
|
||||
const initData = {
|
||||
activeProject,
|
||||
isPaused: cursor.isPaused,
|
||||
stageInfo,
|
||||
openCohorts: cohorts.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
isOpen: c.isOpen,
|
||||
projectIds: c.projects.map((p) => p.projectId),
|
||||
})),
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(`event: init\ndata: ${JSON.stringify(initData)}\n\n`)
|
||||
)
|
||||
|
||||
return cohorts
|
||||
})
|
||||
.catch((): CohortWithProjects => {
|
||||
// Ignore errors on init
|
||||
return []
|
||||
})
|
||||
|
||||
cohortPromise.then((initialCohorts: CohortWithProjects) => {
|
||||
// Poll for updates
|
||||
let lastActiveProjectId = cursor.activeProjectId
|
||||
let lastIsPaused = cursor.isPaused
|
||||
let lastCohortState = JSON.stringify(
|
||||
(initialCohorts ?? []).map((c: { id: string; isOpen: boolean; windowOpenAt: Date | null; windowCloseAt: Date | null }) => ({
|
||||
id: c.id,
|
||||
isOpen: c.isOpen,
|
||||
windowOpenAt: c.windowOpenAt?.toISOString() ?? null,
|
||||
windowCloseAt: c.windowCloseAt?.toISOString() ?? null,
|
||||
}))
|
||||
)
|
||||
|
||||
intervalId = setInterval(async () => {
|
||||
try {
|
||||
const updated = await prisma.liveProgressCursor.findUnique({
|
||||
where: { sessionId },
|
||||
})
|
||||
|
||||
if (!updated) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`event: session.ended\ndata: ${JSON.stringify({ reason: 'Session removed' })}\n\n`
|
||||
)
|
||||
)
|
||||
controller.close()
|
||||
if (intervalId) clearInterval(intervalId)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for cursor changes
|
||||
if (
|
||||
updated.activeProjectId !== lastActiveProjectId ||
|
||||
updated.isPaused !== lastIsPaused
|
||||
) {
|
||||
// Fetch updated active project if changed
|
||||
let updatedActiveProject = null
|
||||
if (updated.activeProjectId) {
|
||||
updatedActiveProject = await prisma.project.findUnique({
|
||||
where: { id: updated.activeProjectId },
|
||||
select: { id: true, title: true, teamName: true, description: true },
|
||||
})
|
||||
}
|
||||
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`event: cursor.updated\ndata: ${JSON.stringify({
|
||||
activeProject: updatedActiveProject,
|
||||
isPaused: updated.isPaused,
|
||||
})}\n\n`
|
||||
)
|
||||
)
|
||||
|
||||
// Check pause/resume transitions
|
||||
if (updated.isPaused && !lastIsPaused) {
|
||||
controller.enqueue(encoder.encode(`event: session.paused\ndata: {}\n\n`))
|
||||
} else if (!updated.isPaused && lastIsPaused) {
|
||||
controller.enqueue(encoder.encode(`event: session.resumed\ndata: {}\n\n`))
|
||||
}
|
||||
|
||||
lastActiveProjectId = updated.activeProjectId
|
||||
lastIsPaused = updated.isPaused
|
||||
}
|
||||
|
||||
// Poll cohort changes
|
||||
const currentCohorts = await prisma.cohort.findMany({
|
||||
where: { stageId: cursor.stageId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
isOpen: true,
|
||||
windowOpenAt: true,
|
||||
windowCloseAt: true,
|
||||
projects: { select: { projectId: true } },
|
||||
},
|
||||
})
|
||||
|
||||
const currentCohortState = JSON.stringify(
|
||||
currentCohorts.map((c) => ({
|
||||
id: c.id,
|
||||
isOpen: c.isOpen,
|
||||
windowOpenAt: c.windowOpenAt?.toISOString() ?? null,
|
||||
windowCloseAt: c.windowCloseAt?.toISOString() ?? null,
|
||||
}))
|
||||
)
|
||||
|
||||
if (currentCohortState !== lastCohortState) {
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
`event: cohort.window.changed\ndata: ${JSON.stringify({
|
||||
openCohorts: currentCohorts.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
isOpen: c.isOpen,
|
||||
projectIds: c.projects.map((p) => p.projectId),
|
||||
})),
|
||||
})}\n\n`
|
||||
)
|
||||
)
|
||||
lastCohortState = currentCohortState
|
||||
}
|
||||
|
||||
// Send heartbeat to keep connection alive
|
||||
controller.enqueue(encoder.encode(`: heartbeat\n\n`))
|
||||
} catch {
|
||||
// Connection may be closed, ignore errors
|
||||
}
|
||||
}, POLL_INTERVAL_MS)
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Check if client disconnected
|
||||
request.signal.addEventListener('abort', () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
intervalId = null
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
Connection: 'keep-alive',
|
||||
'X-Accel-Buffering': 'no',
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user