import { useState, useEffect, useCallback } from 'react' import { adminApi } from './api' import { StatusPill } from './CohortsView' import styles from './Admin.module.css' export default function StudentsView() { const [students, setStudents] = useState(null) const [selected, setSelected] = useState(null) const [error, setError] = useState(null) const reload = useCallback(() => { adminApi.students().then(setStudents).catch((e) => setError(e.message)) }, []) useEffect(reload, [reload]) if (selected) return { setSelected(null); reload() }} /> return (

Students ({students?.length ?? '…'})

{error &&
{error}
} {students && ( {students.map((s) => ( ))}
NameIDStatusLast active
{s.display_name} {s.student_id} {s.status} {s.last_active_at ? new Date(s.last_active_at).toLocaleString() : 'never'}
)}
) } function StudentDetail({ studentId, onBack }) { const [progress, setProgress] = useState(null) const [attempts, setAttempts] = useState(null) const [env, setEnv] = useState(null) const [events, setEvents] = useState(null) const [error, setError] = useState(null) const [busy, setBusy] = useState(false) const reload = useCallback(() => { adminApi.studentProgress(studentId).then(setProgress).catch(() => {}) // Phase 4.1 §16 — studentAttempts is now paginated; this panel only ever shows the most // recent page (labeled "Recent attempts," not a full history browser — see // /api/admin/students/:id/attempts's own pagination for the underlying capability). adminApi.studentAttempts(studentId).then((r) => setAttempts(r.items)).catch(() => {}) adminApi.studentEnvironment(studentId).then((rows) => setEnv(rows?.find((e) => e.status !== 'destroyed') || null)).catch(() => {}) }, [studentId]) useEffect(reload, [reload]) const withBusy = (fn) => async () => { setBusy(true); setError(null) try { await fn(); reload() } catch (e) { setError(e.message) } finally { setBusy(false) } } const loadEvents = async () => { if (!env) return try { setEvents(await adminApi.environmentEvents(env.environment_id)) } catch (e) { setError(e.message) } } return ( <>

{studentId}

{error &&
{error}
}

Environment

{!env ?

No Lab provisioned.

: ( <>

Status: — Lab host: {env.lab_host_id} — created {new Date(env.created_at).toLocaleString()}

{env.last_active_at &&

Last active: {new Date(env.last_active_at).toLocaleString()}

} )}
{!env && } {env && } {env && ( )} {env && }
{events && ( {events.map((e) => )}
EventActorWhen
{e.event_type}{e.actor}{new Date(e.created_at).toLocaleString()}
)}

Curriculum progress

{!progress ?

Loading…

: progress.stages.map((s) => (
{s.name}
{s.scenarios.filter((sc) => sc.status === 'completed').length}/{s.scenarios.length} complete
))}

Recent attempts

{!attempts ?

Loading…

: attempts.length === 0 ?

None yet.

: ( {attempts.map((a) => ( ))}
ScenarioResultChecksWhen
{a.scenario_id} {a.result} {a.validation_count ?? '—'} {new Date(a.created_at).toLocaleString()}
)}
) }