diff --git a/app/api/interview/[id]/chaos-timeout/route.ts b/app/api/interview/[id]/chaos-timeout/route.ts index 8d6e920..f795abe 100644 --- a/app/api/interview/[id]/chaos-timeout/route.ts +++ b/app/api/interview/[id]/chaos-timeout/route.ts @@ -1,20 +1,51 @@ import { NextRequest, NextResponse } from 'next/server'; import dbConnect from '@/src/lib/db/mongoose'; import InterviewSession, { IConstraintChange } from '@/src/lib/db/models/InterviewSession'; +import User from '@/src/lib/db/models/User'; +import { getAuthenticatedUser } from '@/src/lib/firebase/firebaseAdmin'; export async function POST( req: NextRequest, context: { params: Promise<{ id: string }> } ) { try { - await dbConnect(); const { id } = await context.params; - const { type, constraintId } = await req.json(); + const body = await req.json(); + + // Validate request body + const { type, constraintId } = body; + if (!type || (type !== 'warning' && type !== 'penalty')) { + return new Response(JSON.stringify({ error: 'Invalid type. Must be "warning" or "penalty".' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + if (!constraintId || (typeof constraintId !== 'string' && typeof constraintId !== 'number') || constraintId === '') { + return new Response(JSON.stringify({ error: 'Invalid constraintId. Must be a non-empty string or number.' }), { + status: 400, + headers: { 'Content-Type': 'application/json' } + }); + } + + // Authenticate user + const authHeader = req.headers.get('Authorization'); + const authenticatedUser = await getAuthenticatedUser(authHeader); + + if (!authenticatedUser) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + await dbConnect(); + + const user = await User.findOne({ firebaseUid: authenticatedUser.uid }); + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } - // 1. Fetch Session - const session = await InterviewSession.findOne({ id }); + // 1. Fetch Session with ownership check + const session = await InterviewSession.findOne({ id, userId: user._id }); if (!session) { - return NextResponse.json({ error: 'Session not found' }, { status: 404 }); + return NextResponse.json({ error: 'Session not found or access denied' }, { status: 403 }); } if (session.status !== 'in_progress') { return NextResponse.json({ success: true, warning: 'Session is no longer in progress' }); @@ -66,4 +97,4 @@ export async function POST( console.error('Chaos Timeout Error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/components/canvas/DesignCanvas.tsx b/components/canvas/DesignCanvas.tsx index d7178f1..14dd672 100644 --- a/components/canvas/DesignCanvas.tsx +++ b/components/canvas/DesignCanvas.tsx @@ -187,6 +187,13 @@ export function DesignCanvas({ const [targetRps, setTargetRps] = useState(10000); const simulationMetrics = useSimulationEngine(nodes, connections, targetRps, isSimulationRunning); + // Stop simulation when canvas becomes read-only + useEffect(() => { + if (readOnly && isSimulationRunning) { + setIsSimulationRunning(false); + } + }, [readOnly, isSimulationRunning]); + // Selection state const [selectedNodeId, setSelectedNodeId] = useState(null); const [selectedConnectionId, setSelectedConnectionId] = useState(null); @@ -1190,4 +1197,4 @@ export function DesignCanvas({ ); -} +} \ No newline at end of file diff --git a/components/interview/ChaosTimer.tsx b/components/interview/ChaosTimer.tsx index fbe2ea7..8f4e6bd 100644 --- a/components/interview/ChaosTimer.tsx +++ b/components/interview/ChaosTimer.tsx @@ -45,8 +45,10 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) { const hasPendingPenaltyRequest = useRef(false); useEffect(() => { - // Reset timers on DB fields updates have been removed (unnecessary toggles) - + // Reset pending request flags for each new active constraint + hasPendingWarningRequest.current = false; + hasPendingPenaltyRequest.current = false; + const interval = setInterval(() => { const current = calculateRemaining(); setState(current); @@ -93,4 +95,4 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) { ); -} +} \ No newline at end of file diff --git a/src/lib/simulation/engine.ts b/src/lib/simulation/engine.ts index 9461b4b..4f0743a 100644 --- a/src/lib/simulation/engine.ts +++ b/src/lib/simulation/engine.ts @@ -55,10 +55,9 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target if (inDegree[e.to] !== undefined) inDegree[e.to]++; }); - // We will do a modified iterative propagation to handle cycles softly. - // Instead of strict topological sort, we do a fixed number of iterations (e.g. 5 passes) - // For pure DAGs, it propagates cleanly. For cycles, it stabilizes. - + // Iterative propagation with convergence detection to handle cycles properly + // We iterate until traffic distribution stabilizes or we hit maxIterations + // Initial Load sources.forEach(s => { if (nodeMetrics[s.id]) { @@ -66,30 +65,40 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target } }); - // 10 passes is enough for most UI architectures - for (let pass = 0; pass < 10; pass++) { - // Reset edge flows before recalculating this pass's spread + const maxIterations = 100; + const epsilon = 0.01; // Convergence threshold: stop when total change < epsilon + const sourceIds = new Set(sources.map(s => s.id)); + + for (let iteration = 0; iteration < maxIterations; iteration++) { + // Store previous trafficIn values to measure convergence + const prevTrafficIn: Record = {}; + Object.keys(nodeMetrics).forEach(id => { + prevTrafficIn[id] = nodeMetrics[id].trafficIn; + }); + + // Reset edge flows before recalculating this iteration's spread edges.forEach(e => { edgeMetrics[e.id].trafficFlow = 0; }); - - // We need a snapshot of trafficIn for the current frame to distribute it properly - const sourceIds = new Set(sources.map(s => s.id)); - const currentTrafficIn = Object.keys(nodeMetrics).reduce((acc, id) => { - acc[id] = nodeMetrics[id].trafficIn; - // Clear trafficIn for non-sources so they can receive the fresh wave - if (!sourceIds.has(id)) { - nodeMetrics[id].trafficIn = 0; + + // Create next iteration's trafficIn map, starting with sources + const nextTrafficIn: Record = {}; + sourceIds.forEach(id => { + nextTrafficIn[id] = targetRps / sources.length; + }); + nodes.forEach(n => { + if (!sourceIds.has(n.id)) { + nextTrafficIn[n.id] = 0; } - return acc; - }, {} as Record); + }); + // Process each node: compute outFlow and distribute to children nodes.forEach(node => { const metrics = nodeMetrics[node.id]; - const processingTraffic = currentTrafficIn[node.id]; - + const processingTraffic = prevTrafficIn[node.id]; + // Cap out at capacity const outFlow = Math.min(processingTraffic, metrics.capacity); metrics.trafficOut = outFlow; - + // Update status if (processingTraffic > metrics.capacity) { metrics.status = 'bottlenecked'; @@ -105,12 +114,28 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target const flowPerEdge = outFlow / outgoingEdges.length; outgoingEdges.forEach(edge => { edgeMetrics[edge.id].trafficFlow += flowPerEdge; - if (nodeMetrics[edge.to]) { - nodeMetrics[edge.to].trafficIn += flowPerEdge; + if (nextTrafficIn[edge.to] !== undefined) { + nextTrafficIn[edge.to] += flowPerEdge; } }); } }); + + // Update nodeMetrics with nextTrafficIn + Object.keys(nextTrafficIn).forEach(id => { + nodeMetrics[id].trafficIn = nextTrafficIn[id]; + }); + + // Measure total delta to check for convergence + let totalDelta = 0; + Object.keys(nodeMetrics).forEach(id => { + totalDelta += Math.abs(nodeMetrics[id].trafficIn - prevTrafficIn[id]); + }); + + // Stop if converged + if (totalDelta < epsilon) { + break; + } } // Evaluate Global Status @@ -120,4 +145,4 @@ export function runSimulation(nodes: ICanvasNode[], edges: IConnection[], target else if (bottleneckCount > 0) globalStatus = 'degraded'; return { nodeMetrics, edgeMetrics, globalStatus }; -} +} \ No newline at end of file