import { useState, useEffect, useCallback } from 'react' import { adminApi } from './api' import styles from './Admin.module.css' const DASHBOARD_POLL_MS = 5000 // Phase 5A §14/§21 — remote classroom control center. Cohort picker -> that cohort's // classroom sessions -> a live dashboard once one is open. No video/chat/shell-takeover here // by design (§37) — this is identity/presence/Lab-status/help-queue/announcements only. export default function ClassroomView() { const [cohorts, setCohorts] = useState(null) const [cohortId, setCohortId] = useState('') const [classrooms, setClassrooms] = useState(null) const [selected, setSelected] = useState(null) const [error, setError] = useState(null) useEffect(() => { adminApi.cohorts().then((rows) => { setCohorts(rows); if (rows.length) setCohortId(rows[0].cohort_id) }).catch((e) => setError(e.message)) }, []) const reload = useCallback(() => { if (!cohortId) return adminApi.classrooms(cohortId).then(setClassrooms).catch((e) => setError(e.message)) }, [cohortId]) useEffect(reload, [reload]) if (selected) return { setSelected(null); reload() }} /> return ( <> {error &&
{error}
}

Classroom

{cohortId && (

Schedule a class

)}

Classes

{!classrooms ?

Loading…

: classrooms.length === 0 ?

None yet.

: ( {classrooms.map((c) => ( ))}
TitleStatusScheduled
{c.title} {c.status} {c.scheduled_start ? new Date(c.scheduled_start).toLocaleString() : '—'}
)}
) } function CreateClassroomForm({ cohortId, onCreated }) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [scheduledStart, setScheduledStart] = useState('') const [meetingUrl, setMeetingUrl] = useState('') const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const submit = async (e) => { e.preventDefault() setBusy(true); setError(null) try { await adminApi.createClassroom(cohortId, { title: title.trim(), description: description.trim() || undefined, scheduled_start: scheduledStart || undefined, meeting_url: meetingUrl.trim() || undefined, }) setTitle(''); setDescription(''); setScheduledStart(''); setMeetingUrl('') onCreated() } catch (e) { setError(e.message) } finally { setBusy(false) } } return (
{error &&
{error}
}
setTitle(e.target.value)} required />
setScheduledStart(e.target.value)} />
setMeetingUrl(e.target.value)} />
setDescription(e.target.value)} />
) } function ClassroomDashboard({ classroomId, onBack }) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [notice, setNotice] = useState(null) const [busy, setBusy] = useState(false) const [announcement, setAnnouncement] = useState('') const [activityScenario, setActivityScenario] = useState('') const reload = useCallback(() => { adminApi.classroomDashboard(classroomId).then(setData).catch((e) => setError(e.message)) }, [classroomId]) useEffect(() => { reload() const t = setInterval(reload, DASHBOARD_POLL_MS) // presence/help-queue/Lab-status all move live — poll rather than a second push channel return () => clearInterval(t) }, [reload]) const withBusy = (fn) => async () => { setBusy(true); setError(null); setNotice(null) try { await fn(); reload() } catch (e) { setError(e.message) } finally { setBusy(false) } } if (!data) return <>{error ?
{error}
:

Loading…

} const { classroom, students, announcements, help_requests: helpRequests } = data const openHelp = helpRequests.filter((h) => h.status !== 'resolved') return ( <>

{classroom.title} {classroom.status}

{error &&
{error}
} {notice &&

{notice}

}
{classroom.status === 'scheduled' && } {classroom.status === 'live' && } {classroom.status === 'scheduled' && }

Participants ({students.length})

{students.length === 0 ?

No one enrolled in this cohort yet.

: ( {students.map((s) => ( ))}
StudentPresenceLabCurrent workHints
{s.display_name} {s.presence} {s.lab_status} {s.current_scenario || '—'} {s.attempt_status ? `(${s.attempt_status}, ${s.validation_failures} fails)` : ''} {s.hints_used}
)}

Help queue {openHelp.length > 0 && {openHelp.length} open}

{helpRequests.length === 0 ?

No help requests.

: ( {helpRequests.map((h) => ( ))}
StudentCategoryMessageStatus
{h.display_name || h.student_id} {h.category} {h.message || '—'} {h.status} {h.status === 'open' && } {h.status !== 'resolved' && }
)}

Current activity

Guidance only — students see this as a link, nothing auto-redirects them (§20).

setActivityScenario(e.target.value)} />

Announcements

{announcements.length === 0 ?

None yet.

: ( )}
setAnnouncement(e.target.value)} />
) }