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,153 @@
'use client'
import { useState, useEffect, useRef, useCallback } from 'react'
interface StageliveSseState {
isConnected: boolean
activeProject: {
id: string
title: string
teamName: string | null
description: string | null
} | null
openCohorts: Array<{
id: string
name: string
isOpen: boolean
projectIds: string[]
}>
isPaused: boolean
error: string | null
}
interface UseStageliveSseOptions {
enabled?: boolean
}
export function useStageliveSse(
sessionId: string | null,
options: UseStageliveSseOptions = {}
) {
const { enabled = true } = options
const [state, setState] = useState<StageliveSseState>({
isConnected: false,
activeProject: null,
openCohorts: [],
isPaused: false,
error: null,
})
const eventSourceRef = useRef<EventSource | null>(null)
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const retryCountRef = useRef(0)
const maxRetries = 10
const connect = useCallback(() => {
if (!sessionId || !enabled) return
// Clean up existing connection
if (eventSourceRef.current) {
eventSourceRef.current.close()
}
const url = `/api/sse/stage-live/${sessionId}`
const es = new EventSource(url)
eventSourceRef.current = es
es.onopen = () => {
setState((prev) => ({ ...prev, isConnected: true, error: null }))
retryCountRef.current = 0
}
es.addEventListener('cursor.updated', (event) => {
try {
const data = JSON.parse(event.data)
setState((prev) => ({
...prev,
activeProject: data.activeProject ?? prev.activeProject,
isPaused: data.isPaused ?? prev.isPaused,
}))
} catch {
// Ignore malformed events
}
})
es.addEventListener('cohort.window.changed', (event) => {
try {
const data = JSON.parse(event.data)
setState((prev) => ({
...prev,
openCohorts: data.cohorts ?? prev.openCohorts,
}))
} catch {
// Ignore
}
})
es.addEventListener('vote.received', () => {
// Used for UI feedback (e.g. flash animation)
// No state change needed
})
es.addEventListener('session.paused', () => {
setState((prev) => ({ ...prev, isPaused: true }))
})
es.addEventListener('session.resumed', () => {
setState((prev) => ({ ...prev, isPaused: false }))
})
es.addEventListener('init', (event) => {
try {
const data = JSON.parse(event.data)
setState({
isConnected: true,
activeProject: data.activeProject ?? null,
openCohorts: data.openCohorts ?? [],
isPaused: data.isPaused ?? false,
error: null,
})
} catch {
// Ignore
}
})
es.onerror = () => {
es.close()
setState((prev) => ({ ...prev, isConnected: false }))
if (retryCountRef.current < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retryCountRef.current), 30000)
retryCountRef.current++
reconnectTimeoutRef.current = setTimeout(connect, delay)
} else {
setState((prev) => ({
...prev,
error: 'Connection lost. Please refresh the page.',
}))
}
}
}, [sessionId, enabled])
useEffect(() => {
connect()
return () => {
if (eventSourceRef.current) {
eventSourceRef.current.close()
eventSourceRef.current = null
}
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current)
reconnectTimeoutRef.current = null
}
}
}, [connect])
const reconnect = useCallback(() => {
retryCountRef.current = 0
connect()
}, [connect])
return { ...state, reconnect }
}