Round system redesign: Phases 1-7 complete

Full pipeline/track/stage architecture replacing the legacy round system.

Schema: 11 new models (Pipeline, Track, Stage, StageTransition,
ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor,
OverrideAction, AudienceVoter) + 8 new enums.

Backend: 9 new routers (pipeline, stage, routing, stageFiltering,
stageAssignment, cohort, live, decision, award) + 6 new services
(stage-engine, routing-engine, stage-filtering, stage-assignment,
stage-notifications, live-control).

Frontend: Pipeline wizard (17 components), jury stage pages (7),
applicant pipeline pages (3), public stage pages (2), admin pipeline
pages (5), shared stage components (3), SSE route, live hook.

Phase 6 refit: 23 routers/services migrated from roundId to stageId,
all frontend components refitted. Deleted round.ts (985 lines),
roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx,
10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs.

Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing,
TypeScript 0 errors, Next.js build succeeds, 13 integrity checks,
legacy symbol sweep clean, auto-seed on first Docker startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 13:57:09 +01:00
parent 8a328357e3
commit 331b67dae0
256 changed files with 29117 additions and 21424 deletions

View File

@@ -0,0 +1,216 @@
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',
},
})
}