diff --git a/app/api/exercises/[id]/session/route.ts b/app/api/exercises/[id]/session/route.ts index c6863f4..5e83cd7 100644 --- a/app/api/exercises/[id]/session/route.ts +++ b/app/api/exercises/[id]/session/route.ts @@ -9,7 +9,6 @@ export async function GET( { params }: { params: { id: string } } ) { const session = await getServerSession(authOptions); - if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -17,9 +16,27 @@ export async function GET( const userId = session.user.id; const exerciseId = params.id; - // Look up or create the session for this exercise + user + // Check exercise-level start_time BEFORE creating a session + // This way participants who haven't started yet are blocked at the exercise level + const exerciseRows = await sql` + SELECT start_time, end_time, duration_limit, question_count + FROM exercises WHERE id = ${exerciseId} LIMIT 1 + `; + if (exerciseRows.length === 0) { + return NextResponse.json({ error: 'Exercise not found' }, { status: 404 }); + } + const exercise = exerciseRows[0]; + + if (exercise.start_time && new Date() < new Date(exercise.start_time as string)) { + return NextResponse.json( + { error: 'Session not yet open', opens_at: exercise.start_time }, + { status: 423 } + ); + } + + // Look up or create the session let rows = await sql` - SELECT s.id, s.start_time, s.end_time, s.duration_limit, + SELECT s.id, s.end_time, s.duration_limit, s.started_at, s.closed_at, s.current_question_index, e.question_count FROM sessions s @@ -30,43 +47,30 @@ export async function GET( `; if (rows.length === 0) { - // Create a new session, inheriting timing from the exercise + // Create session — inherit end_time and duration_limit but NOT start_time + // (start_time is checked at the exercise level above) const created = await sql` - INSERT INTO sessions (exercise_id, user_id, started_at, current_question_index, start_time, end_time, duration_limit) + INSERT INTO sessions (exercise_id, user_id, started_at, current_question_index, end_time, duration_limit) SELECT ${exerciseId}, ${userId}, now(), 0, - e.start_time, e.end_time, e.duration_limit FROM exercises e WHERE e.id = ${exerciseId} - RETURNING id, start_time, end_time, duration_limit, - started_at, closed_at, current_question_index - `; - - const exerciseRows = await sql` - SELECT question_count FROM exercises WHERE id = ${exerciseId} + RETURNING id, end_time, duration_limit, started_at, closed_at, current_question_index `; rows = created.map((r) => ({ ...r, - question_count: exerciseRows[0]?.question_count ?? 0, + question_count: exercise.question_count ?? 0, })); } const row = rows[0]; - // 423 if start_time is set and now() < start_time - if (row.start_time && new Date() < new Date(row.start_time as string)) { - return NextResponse.json( - { error: 'Session not yet open', opens_at: row.start_time }, - { status: 423 } - ); - } - // 410 if session is closed if (row.closed_at) { return NextResponse.json({ error: 'Session closed' }, { status: 410 }); @@ -74,25 +78,19 @@ export async function GET( const now = new Date(); - // Auto-close if end_time has passed - if (row.end_time && now > new Date(row.end_time as string)) { - await sql` - UPDATE sessions SET closed_at = now() - WHERE id = ${row.id} AND closed_at IS NULL - `; + // Auto-close if end_time has passed (check both session and exercise end_time) + const effectiveEndTime = (row.end_time || exercise.end_time) as string | null; + if (effectiveEndTime && now > new Date(effectiveEndTime)) { + await sql`UPDATE sessions SET closed_at = now() WHERE id = ${row.id} AND closed_at IS NULL`; return NextResponse.json({ error: 'Session closed' }, { status: 410 }); } - // Auto-close if duration_limit has been exceeded + // Auto-close if duration_limit exceeded if (row.duration_limit && row.started_at) { const durationSeconds = parseIntervalToSeconds(row.duration_limit); const startedAt = new Date(row.started_at as string).getTime(); - const expiresAt = startedAt + durationSeconds * 1000; - if (now.getTime() > expiresAt) { - await sql` - UPDATE sessions SET closed_at = now() - WHERE id = ${row.id} AND closed_at IS NULL - `; + if (now.getTime() > startedAt + durationSeconds * 1000) { + await sql`UPDATE sessions SET closed_at = now() WHERE id = ${row.id} AND closed_at IS NULL`; return NextResponse.json({ error: 'Session closed' }, { status: 410 }); } } @@ -103,41 +101,30 @@ export async function GET( if (row.duration_limit && row.started_at) { const durationSeconds = parseIntervalToSeconds(row.duration_limit); const startedAt = new Date(row.started_at as string).getTime(); - const expiresAt = startedAt + durationSeconds * 1000; - remainingSeconds = Math.floor((expiresAt - Date.now()) / 1000); + remainingSeconds = Math.floor((startedAt + durationSeconds * 1000 - Date.now()) / 1000); } - if (row.end_time) { - const fromEndTime = Math.floor( - (new Date(row.end_time as string).getTime() - Date.now()) / 1000 - ); + if (effectiveEndTime) { + const fromEndTime = Math.floor((new Date(effectiveEndTime).getTime() - Date.now()) / 1000); remainingSeconds = remainingSeconds === null ? fromEndTime : Math.min(remainingSeconds, fromEndTime); } - const warningLowTime = - remainingSeconds !== null && remainingSeconds < 300; - - // Query per-question submission statuses const submissions = await sql` SELECT question_index, is_final, (response_text IS NOT NULL AND response_text <> '') AS has_draft - FROM submissions - WHERE session_id = ${row.id} + FROM submissions WHERE session_id = ${row.id} `; - const questionStatuses = submissions.map((s) => ({ - question_index: s.question_index, - has_draft: Boolean(s.has_draft), - is_final: Boolean(s.is_final), - })); - return NextResponse.json({ session_id: row.id, - current_question_index: row.current_question_index, - question_count: row.question_count, + current_question_index: row.current_question_index as number, + question_count: row.question_count as number, remaining_seconds: remainingSeconds, - warning_low_time: warningLowTime, - question_statuses: questionStatuses, + warning_low_time: remainingSeconds !== null && remainingSeconds < 300, + question_statuses: submissions.map((s) => ({ + question_index: s.question_index, + has_draft: Boolean(s.has_draft), + is_final: Boolean(s.is_final), + })), }); } - diff --git a/app/globals.css b/app/globals.css index 8968f0e..ecb2143 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,6 +5,7 @@ --bg2: #0c0f14; --bg3: #111520; --bg4: #161b28; + --bg5: #535353; --border: rgba(255,255,255,0.06); --border2: rgba(255,255,255,0.1); --accent: #6366f1; diff --git a/app/instructor/exercises/[id]/ExerciseManager.tsx b/app/instructor/exercises/[id]/ExerciseManager.tsx index 1f6d695..1658a3c 100644 --- a/app/instructor/exercises/[id]/ExerciseManager.tsx +++ b/app/instructor/exercises/[id]/ExerciseManager.tsx @@ -124,7 +124,10 @@ export default function ExerciseManager({ exercise: initial, sessions, assignedU