Environment
@@ -134,3 +281,35 @@ function StudentDetail({ studentId, onBack }) {
>
)
}
+
+function ConvertForm({ studentId, onDone }) {
+ const [email, setEmail] = useState('')
+ const [loginIdentifier, setLoginIdentifier] = useState('')
+ const [busy, setBusy] = useState(false)
+ const [error, setError] = useState(null)
+
+ const submit = async (e) => {
+ e.preventDefault()
+ setBusy(true); setError(null)
+ try {
+ const { activation_token } = await adminApi.convertStudent(studentId, {
+ email: email.trim() || undefined, login_identifier: loginIdentifier.trim() || undefined,
+ })
+ onDone(`Converted. Activation link (shown once): ${window.location.origin}/?activate=${activation_token}`)
+ } catch (e) { setError(e.message) } finally { setBusy(false) }
+ }
+
+ return (
+
+ )
+}
diff --git a/frontend/src/admin/api.js b/frontend/src/admin/api.js
index 4df2ace..cc0715e 100644
--- a/frontend/src/admin/api.js
+++ b/frontend/src/admin/api.js
@@ -16,7 +16,9 @@ function getCookie(name) {
async function json(res) {
if (!res.ok) {
const body = await res.json().catch(() => ({}))
- throw new Error(body.message || body.error || `Request failed (${res.status})`)
+ const err = new Error(body.message || body.error || `Request failed (${res.status})`)
+ if (body.row_errors) err.row_errors = body.row_errors // Phase 5A §29 — CSV import row-level errors
+ throw err
}
return res.json()
}
@@ -49,8 +51,14 @@ export const adminApi = {
dashboard: () => get('/api/admin/dashboard'),
students: () => get('/api/admin/students'),
- createStudent: (display_name, cohort_id) => post('/api/admin/students', { display_name, cohort_id }),
+ createStudent: (fields) => post('/api/admin/students', fields),
updateStudent: (id, fields) => patch(`/api/admin/students/${id}`, fields),
+ // ── Phase 5A §4-7/§29: credential lifecycle + CSV import ────────────────────────────────
+ convertStudent: (id, fields) => post(`/api/admin/students/${id}/convert`, fields),
+ resetStudentPassword: (id) => post(`/api/admin/students/${id}/reset-password`),
+ revokeStudentSessions: (id) => post(`/api/admin/students/${id}/revoke-sessions`),
+ studentSessions: (id) => get(`/api/admin/students/${id}/sessions`),
+ importStudents: (csv) => post('/api/admin/students/import', { csv }),
studentProgress: (id) => get(`/api/admin/students/${id}/progress`),
studentAttempts: (id, page = 1) => get(`/api/admin/students/${id}/attempts?page=${page}`), // Phase 4.1 §16 — paginated, see pagination.js
studentEnvironment: (id) => get(`/api/admin/students/${id}/environment`),
@@ -95,4 +103,17 @@ export const adminApi = {
assignAssessment: (id, cohort_id) => post(`/api/admin/assessments/${id}/assign`, { cohort_id }),
assessmentSessions: (id, page = 1) => get(`/api/admin/assessments/${id}/sessions?page=${page}`), // Phase 4.1 §16 — paginated, see pagination.js
cancelAssessmentSession: (sessionId) => post(`/api/admin/assessment-sessions/${sessionId}/cancel`),
+
+ // ── Phase 5A §14/§21: remote classroom control center ───────────────────────────────────
+ classrooms: (cohortId) => get(`/api/admin/cohorts/${cohortId}/classroom-sessions`),
+ createClassroom: (cohortId, fields) => post(`/api/admin/cohorts/${cohortId}/classroom-sessions`, fields),
+ classroomDashboard: (id) => get(`/api/admin/classroom-sessions/${id}`),
+ startClassroom: (id) => post(`/api/admin/classroom-sessions/${id}/start`),
+ endClassroom: (id) => post(`/api/admin/classroom-sessions/${id}/end`),
+ cancelClassroom: (id) => post(`/api/admin/classroom-sessions/${id}/cancel`),
+ prepareClassroomLabs: (id) => post(`/api/admin/classroom-sessions/${id}/prepare-labs`),
+ setClassroomActivity: (id, scenario_id) => post(`/api/admin/classroom-sessions/${id}/activity`, { scenario_id }),
+ postClassroomAnnouncement: (id, message) => post(`/api/admin/classroom-sessions/${id}/announcements`, { message }),
+ acknowledgeHelpRequest: (id) => post(`/api/admin/help-requests/${id}/acknowledge`),
+ resolveHelpRequest: (id, note) => post(`/api/admin/help-requests/${id}/resolve`, { note }),
}