- classroom.js: create/start/end/cancel (DB-level partial unique index guarantees at most one live classroom per cohort; app layer catches 23505 and returns a clean ALREADY_LIVE), join/ leave/heartbeat, derivePresence (pure, derived from last_seen_at at read time — no stored status to go stale), help-request create/acknowledge/resolve, announcements, current-activity (guidance only, never redirects a browser or marks work complete), append-only event log. - admin-classroom-routes.js: instructor-facing classroom API, RBAC via rbac.js's requireClassroomAccess/requireClassroomWriteAccess/requireHelpRequestWriteAccess (resolves through to the classroom's cohort, same owner/admin/instructor/viewer rules as everywhere else cohort-scoped). The dashboard route (GET /api/admin/classroom-sessions/:id) combines real Provisioner Lab state and real academic records with classroom.js's own data — no second Lab-state or academic-state system. - classroom-routes.js: student-facing API (live classroom, join/leave/heartbeat, help-request, announcements), requireEnrolledClassroom middleware. - prepare-labs.js: shared prepareLabsForCohort, extracted so the existing cohort-level and new classroom-level Prepare Labs routes share one implementation; maps Phase 4.1 admission control's CAPACITY_EXCEEDED into a capacity_waiting per-student result.
28 lines
1.8 KiB
JavaScript
28 lines
1.8 KiB
JavaScript
// Shared "Prepare Labs for a cohort's enrolled students" logic — used by both the existing
|
|
// cohort-level admin route and the Phase 5A classroom-scoped one (a classroom's Prepare Labs is
|
|
// exactly this, just entered from the classroom dashboard instead of the cohort page — see
|
|
// docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md §21). Extracted so there's one place this
|
|
// logic lives, not two copies that could drift.
|
|
|
|
async function prepareLabsForCohort(pool, provisioner, cohortId) {
|
|
const { rows: students } = await pool.query(
|
|
`SELECT s.student_id, s.display_name FROM cohort_enrollments e JOIN students s ON s.student_id = e.student_id
|
|
WHERE e.cohort_id = $1 AND e.enrollment_status = 'enrolled' AND s.status = 'active'`,
|
|
[cohortId]
|
|
);
|
|
const results = await Promise.allSettled(students.map(async (s) => {
|
|
const existing = await provisioner.listForStudent(s.student_id);
|
|
const active = existing.body?.find((e) => ['requested', 'provisioning', 'running', 'resetting'].includes(e.status));
|
|
if (active) return { student_id: s.student_id, display_name: s.display_name, status: active.status };
|
|
const { status, body } = await provisioner.provision(s.student_id);
|
|
if (status === 503 && body?.code === 'CAPACITY_EXCEEDED') {
|
|
return { student_id: s.student_id, display_name: s.display_name, status: 'capacity_waiting' };
|
|
}
|
|
if (status !== 201) return { student_id: s.student_id, display_name: s.display_name, status: 'error', error: body?.error };
|
|
return { student_id: s.student_id, display_name: s.display_name, status: body.status };
|
|
}));
|
|
return results.map((r, i) => r.status === 'fulfilled' ? r.value : { student_id: students[i].student_id, display_name: students[i].display_name, status: 'error', error: r.reason?.message });
|
|
}
|
|
|
|
module.exports = { prepareLabsForCohort };
|