From 896c230142b418720f77e80a960d8dbeb8573892 Mon Sep 17 00:00:00 2001 From: ced4568 Date: Tue, 25 Aug 2026 01:20:13 -0500 Subject: [PATCH] docs(phase5a): student identity and remote classroom documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md: the full design doc. - RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md, RUNBOOK_REMOTE_CLASSROOM.md: operational runbooks. - REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the 'no real student authentication' gap this doc previously flagged as the one item blocking self-paced delivery. - ENGINEERING_SOP.md §5 rewritten (was describing a pre-Phase-2A state no longer true of any deployed version of this codebase) plus a new §14; roadmap item 3 updated. - LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md: cross-references and a Phase 5A production-validation section. --- README.md | 22 +- docs/ENGINEERING_SOP.md | 88 +++++--- docs/LAB_ARCHITECTURE.md | 9 + ...ASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md | 195 ++++++++++++++++++ docs/PRODUCTION_VALIDATION_CHECKLIST.md | 13 ++ docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md | 22 +- docs/RUNBOOK_REMOTE_CLASSROOM.md | 53 +++++ docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md | 44 ++++ platform/README.md | 12 +- 9 files changed, 402 insertions(+), 56 deletions(-) create mode 100644 docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md create mode 100644 docs/RUNBOOK_REMOTE_CLASSROOM.md create mode 100644 docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md diff --git a/README.md b/README.md index 739a22d..3a8018a 100644 --- a/README.md +++ b/README.md @@ -150,12 +150,19 @@ its core: the browser (`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, session model in `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` §3, hint gating in `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` §3). -- The student flow (roster login → curriculum dashboard → Launch Lab → practice/assessments/ - capstone → achievements) lives in `frontend/src/platform/`; the instructor/admin portal - (per-instructor login, cohorts, students, instructor accounts, audit log, classroom - "Prepare Labs," environment controls, learning analytics, assessment/capstone management) - lives in `frontend/src/admin/` at the `/admin` route — both additions alongside the existing - Lab UI, not a rewrite of it. +- The student flow (real login (email or instructor-assigned login ID) or legacy pilot roster + entry → curriculum dashboard → Launch Lab → practice/assessments/capstone → achievements → + live classroom → profile) lives in `frontend/src/platform/`; the instructor/admin portal + (per-instructor login, cohorts, students with credential/CSV-import management, a remote + classroom control center, instructor accounts, audit log, environment controls, learning + analytics, assessment/capstone management) lives in `frontend/src/admin/` at the `/admin` + route — both additions alongside the existing Lab UI, not a rewrite of it. +- Students authenticate with real credentials as of Phase 5A (email+password or + login-identifier+password, first-login/reset via a single-use token an instructor relays) — + see `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`. Instructors run live, remote-capable + classes from `/admin` (presence, Lab/learning status, help-request queue, announcements, + guidance-only current-activity) — RootNode owns identity/coordination, an external service + (Meet/Zoom/Teams/etc.) owns the actual video/voice. **Never publish the raw privileged container** — front it with isolation (that's what the Gateway is). The one-container `docker run --privileged -p 7554:80 ...` command above stays @@ -192,7 +199,8 @@ Key properties of the platform (not this image): Production topology and what's built vs. not yet: `docs/LAB_ARCHITECTURE.md`, `docs/PHASE1_PLATFORM_FOUNDATION.md`, `docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, -`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md`, `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md`. +`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md`, `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md`, +`docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`. The production infrastructure package (Proxmox/Ansible/Terraform/firewall/monitoring/backup/CI) that runs all of this on real hardware lives in `infra/` — see diff --git a/docs/ENGINEERING_SOP.md b/docs/ENGINEERING_SOP.md index 56241c0..f3b57e5 100644 --- a/docs/ENGINEERING_SOP.md +++ b/docs/ENGINEERING_SOP.md @@ -90,35 +90,34 @@ To add/reorder a stage: edit `rootnode-path.json` directly, referencing existing ## 5. Student identity — what's built, what isn't -**Built (backend only, as of this doc):** -- `students` table: `student_id` (`student-NNN`, sequential), `display_name`, `cohort`, - `created_at`, `last_active_at`. -- Login model: **instructor roster, pick-your-name, no password.** `POST /api/roster` adds - a student (instructor-only action, currently unauthenticated at the API level — see - "not built" below). `GET /api/roster?cohort=X` lists names for a dropdown. - `POST /api/login` just validates the id exists and returns it; the frontend is expected to - hold onto `student_id` and send it back as an `X-Student-Id` header on every request. -- `progress` table now keyed on `(student_id, scenario_id)` instead of `scenario_id` alone — - each student's completion/attempts/hints are isolated. Verified by smoke test: two students - answering the same scenario don't affect each other's state, and pre-migration data was - preserved under `student_id = 'unassigned'`. -- Exam `sessions` now carry `student_id` (set at session start), so exam history and the - timer-reset-on-submit logic are correctly scoped per student instead of leaking across - whoever's browser hits the API next. +**Current state (Phase 5A — see `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md` for the +full design):** +- `students.student_id` (`student-NNN`, sequential) is still the FK every other table hangs off + of and never changes across an account's lifecycle, including conversion. +- Real credentials: `email` and `login_identifier` (independently optional/unique — email for + older/self-paced students, an instructor-assigned login ID for younger ones), `password_hash` + reusing `instructors`' exact `scrypt` machinery (`src/auth/password.js`). A student with + neither field set is a plain **pilot** account, unchanged since Phase 2A: `POST /api/login + { student_id }`, no password, instructor-roster-only. +- First login/reset is one shared, single-use, hashed, time-limited token mechanism + (`students-auth.js`) — instructors never see or set a student's actual password. +- Sessions are the same Postgres-backed `sessions` table instructors use (opaque, HttpOnly, + hashed, revocable, CSRF-protected); login is rate-limited the same Postgres-backed way as + instructor login (Phase 4.1), now also applied to students. +- Per-student `progress`/`scenario_attempts`/exam `sessions` isolation (below) and real + concurrent multi-student Lab isolation (Phase 1's Provisioner/k3s-per-student model) were both + already in place well before this — Phase 5A only changed how a student proves who they are, + not how their data or Labs are isolated. -**Not built yet — do not assume these exist:** -- **No frontend UI.** No login screen, no name picker, no roster-add form. The API works; - nothing calls it yet. This is the next slice. -- **No auth on `/api/roster` POST.** Anyone who can reach the API can add a "student." - Fine for a dev instance, not fine once this is reachable by actual students — needs an - instructor-only gate before that happens (ties into the instructor dashboard, roadmap - item 3). -- **No concurrency / isolation.** Exam sessions are still global-single-active - (`WHERE status='active' LIMIT 1` — see `server.js`), and there is one shared k3s cluster - for the whole instance. Per-student identity ≠ per-student environment. **Multiple students - cannot yet use this platform at the same time without stepping on each other.** That's - roadmap item 4 (isolation) and it gates real classroom/minors use, same as the plan called - out — this SOP is not claiming that gate is cleared. +**Not built — deliberate non-goals, not gaps (Phase 5A §37):** +- No public self-registration — accounts are still instructor-created/converted/imported only. +- No email delivery infrastructure — password reset works via an instructor-issued token today; + self-service reset returns a real token only in non-production, by design. +- No Parent/Guardian portal — the schema doesn't block adding one later, but none exists. + +**Historical note, kept for context:** earlier drafts of this section described a pre-Phase-2A +state (`X-Student-Id` header, unauthenticated roster POST, one shared cluster) that no longer +reflects any deployed version of this codebase; that content has been superseded by the above. ## 6. Schema migration pattern @@ -146,9 +145,11 @@ Order agreed 2026-08-17/18. Status as of this doc: (§12), student-facing tabs (My Learning/Practice/Assessments/Capstone/Achievements) at `frontend/src/platform/`. 3. Student accounts / persistent progress — **done**, Postgres-authoritative as of Phase 2B - (§9), sessions persistent as of Phase 3A (§10). Roster name-picking is still classroom/pilot - identification, not strong student auth — documented, not silently glossed over - (docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §7). + (§9), sessions persistent as of Phase 3A (§10). Real student credentials (email or + login-identifier + password, first-login/reset token flow) closed the remaining "pick your + name" gap as of Phase 5A — see §5 above and + docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md. Pilot (no-login) accounts still work + unchanged for anyone not yet converted. 4. Instructor dashboard — **done** (§9/§10). Full admin portal at `/admin`: dashboard, cohort and student management, classroom Prepare/Destroy Labs, per-instructor accounts with owner/admin/instructor roles, audit log. @@ -332,4 +333,27 @@ Full design: `docs/PHASE4_1_PREPRODUCTION_HARDENING.md`, plus - **Known limitation carried forward:** real remote/self-paced delivery needs actual student authentication (today's `student_id`-only login relies on instructors controlling who gets a valid id) — see `docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md` for the full accounting of what - already works vs. what's genuinely Phase 5 scope. + already works vs. what's genuinely Phase 5 scope. **Closed in Phase 5A, below.** + +## 14. Student Identity & Remote Classroom (Phase 5A) + +Full design: `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`, plus +`docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md` and `docs/RUNBOOK_REMOTE_CLASSROOM.md`. + +- **What it is:** two deliverables built on the Phase 4.1 milestone. (A) Real student accounts — + email-or-login-identifier + password, reusing `instructors`' exact credential machinery, a + single first-login/reset token mechanism, deactivation enforced per-request — replacing pilot + "pick your name" identification without touching a single existing pilot student's progress. + (B) A remote classroom control center — scheduled/live/ended sessions, derived (not stored) + presence via heartbeat, a live instructor dashboard combining real Provisioner Lab state and + real academic records, a help-request queue, guidance-only announcements/current-activity, and + a generic external meeting-URL field. Explicitly **not** video/voice/chat/shell-takeover — + RootNode owns identity and coordination, an external service owns the call. +- **Tested, not just designed:** `platform/test/e2e-phase5a.js` (21 checks: the two-student RBAC + matrix and full password-security checklist, including a real Platform API restart proving + rate-limit state survives it) and `infra/scripts/drills/test-phase5a-rehearsal.js` (15 checks: + 5 pilot students with seeded real history, a live classroom, help requests, **a real + mid-rehearsal API restart** proving classroom/session/presence recovery, then converting two + students and confirming zero orphaned data) — both against a live `docker-compose.dev.yml` + stack, not mocks. The full pre-existing regression suite was re-run given how much shared code + this phase touched (`server.js`, `admin-routes.js`, `sessions.js`, `rbac.js`). diff --git a/docs/LAB_ARCHITECTURE.md b/docs/LAB_ARCHITECTURE.md index 6d8bc08..bb94d86 100644 --- a/docs/LAB_ARCHITECTURE.md +++ b/docs/LAB_ARCHITECTURE.md @@ -498,3 +498,12 @@ mechanism for reaching a Lab container (it assumes Gateway and Lab share a Docke breaks once they're genuinely on separate VMs); Phase 4.1 (`docs/PHASE4_1_PREPRODUCTION_HARDENING.md`) built and simulation-tested the fix, a Lab Host Router (`platform/lab-router/`). + +## Phase 5A: reused, not modified + +Phase 5A (`docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`) added real student accounts and a +remote classroom control center on top of everything this document describes, without changing +any of it — the Provisioner, the `environments` state machine, the Lab Host Router, disposable +Lab lifecycle, and the Platform/Lab-Host VM boundary are all untouched. The classroom dashboard +reads Lab status the exact same way every other admin view does (`provisioner.listForStudent`, +the same state enum) rather than inventing a second one. diff --git a/docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md b/docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md new file mode 100644 index 0000000..96e6e35 --- /dev/null +++ b/docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md @@ -0,0 +1,195 @@ +# Phase 5A — Student Identity & Remote Classroom + +**Status: PHASE 5A COMPLETE. STUDENT IDENTITY COMPLETE. REMOTE CLASSROOM FOUNDATION COMPLETE.** + +Phase 4.1 closed the last pre-production infrastructure gap without real hardware. Phase 5A's +job — still without real hardware — was the last real *product* gap flagged along the way: pilot +students identify themselves by picking a name from a list, and there was no way for an +instructor to run a live, remote-capable class through the platform itself. This phase replaces +pilot identification with real accounts and adds a remote classroom control center. It does +**not** add video, voice, chat, or any A/V — those stay Meet/Zoom/Teams's job; RootNode owns +identity, presence, curriculum guidance, Lab status, help requests, and instructor control. + +## 1. Student identity model + +`students` (migration `017_student_credentials.sql`) gained `email`, `login_identifier`, +`password_hash`, `last_login_at`, and a single credential-token triple +(`credential_token_hash`/`_purpose`/`_expires_at`) — all nullable, all additive. `email` and +`login_identifier` are independently unique (partial unique indexes, `WHERE ... IS NOT NULL`) +and both optional: an older/self-paced student gets an email, a younger instructor-managed +student gets an instructor-assigned login ID, neither is required, and nothing here blocks a +future `guardian_id` FK for a Parent Portal — deliberately not built this phase (§37). + +A student with **neither** field set is unchanged from before Phase 5A: a plain pilot row, +identified with `POST /api/login { student_id }`. Setting either field (via account creation or +`/convert`) is what makes an account "real." `password_hash` reuses `platform/api/src/auth/ +password.js` (scrypt, `salt:hash`) verbatim — the same function instructors use, no second +hashing scheme. + +## 2. Credential lifecycle — one token mechanism, not two + +Activation ("first login") and password reset are the same problem: a time-limited, single-use +secret that lets someone set a new password. `students-auth.js`'s `issueCredentialToken`/ +`consumeCredentialToken` implement it once (SHA-256-hashed at rest, plaintext returned exactly +once in the API response, never logged), with `purpose` (`activation` | `reset`) distinguishing +the two callers. Activation tokens live 7 days, reset tokens 1 hour. `consumeCredentialToken` is +atomic: token verification, password hashing, `password_hash` write, and token-clearing happen +in one statement — there is no window where a token is "spent" but the password isn't set. + +Instructors never see or choose a student's password (§5's "instructor creates account → +temporary credential" flow): `POST /api/admin/students`, `/convert`, and `/reset-password` all +return a token (activation or reset), which the instructor relays out of band (printed, read +aloud, handed over). The admin UI surfaces the resulting `?activate=`/`?reset=` +link once and never again — there is no "view credentials" screen anywhere. + +## 3. Password reset without email delivery + +No email-sending infrastructure exists yet, and this phase deliberately doesn't fake one. +`POST /api/student/request-password-reset` always returns an identical generic response +regardless of whether the identifier exists (anti-enumeration) and never echoes the token in +production. In non-production (`isProduction` false, i.e. `COOKIE_SECURE !== '1'`) it also +returns `dev_reset_token` so local/CI flows can complete the loop without a real inbox — a +documented dev/test path, not a production feature. The realistic production path today is +instructor-triggered: `POST /api/admin/students/:id/reset-password`. Real email delivery is a +named, not-yet-built follow-up, not something silently promised. + +## 4. Session and login security — reused, not duplicated + +Students and instructors share the exact same `sessions` table and mechanism +(`sessions.js`: opaque tokens, HttpOnly, SameSite, hashed at rest, revocable), differentiated +only by `session_type`/`student_id` vs `instructor_id`. `studentSession` middleware now +re-verifies `status = 'active'` on **every** request (previously only checked session +existence) — deactivating a student takes effect immediately, matching `instructorSession`'s +existing behavior, not just at next login (verified in `e2e-phase5a.js`). + +Rate limiting reuses Phase 4.1's Postgres-backed `tooManyAttempts` (`security.js`) with a +`student:` key prefix — 8 attempts / 5 minutes, survives a Platform API restart because it's a +table, not an in-memory Map (verified with a real restart in both `e2e-phase5a.js` and the +rehearsal drill). CSRF reuses the existing double-submit cookie pattern (`rn_csrf_s`). + +**A real bug this closed:** the legacy `POST /api/login` (bare `student_id`, no password) only +checked `password_hash` to decide whether an account had "moved on" from pilot mode. An account +mid-conversion — instructor has issued an activation token, student hasn't set a password yet — +has `credential_token_hash` set but `password_hash` still `NULL`, and was reachable through the +old insecure path for that whole window. Found via testing, fixed by checking +`student.password_hash || student.credential_token_hash` instead (`server.js`). + +## 5. Pilot migration + +No destructive migration. `017_student_credentials.sql` only adds nullable columns — every +existing pilot row, its progress, attempts, achievements, and cohort membership are completely +untouched at migration time. `POST /api/admin/students/:id/convert` assigns email/login_ +identifier and issues an activation token without touching `student_id` (the FK every other +table hangs off of) or anything downstream of it. Verified directly, not just asserted: the +Phase 5A rehearsal drill (`infra/scripts/drills/test-phase5a-rehearsal.js`) seeds real pre- +existing progress/attempt/achievement rows for a pilot student, converts them, activates them, +logs in the new way, and confirms `/api/progress` shows the exact same completed scenario — +nothing orphaned, nothing re-created. + +## 6. Remote classroom — a fourth session concept + +Distinct from cohort membership, assessment sessions, and Lab environments: a scheduled, +bounded teaching event. `018_classroom_sessions.sql` adds `classroom_sessions` (lifecycle +`scheduled → live → ended`, or `cancelled`), `classroom_participants`, `classroom_events` +(append-only audit), `classroom_announcements`, and `help_requests`. A partial unique index +(`WHERE status = 'live'`) enforces at most one live classroom per cohort at the database level — +the app layer catches the resulting `23505` and returns a clean `ALREADY_LIVE` (409), not a +raw constraint error (verified in `e2e-phase5a.js`). + +## 7. Presence — derived, not stored + +`classroom.derivePresence(lastSeenAt, leftAt)` is a pure function computing `online` / +`recently_active` / `offline` from a timestamp at read time — there's no status column that can +go stale or need a background job to sweep. Heartbeats (`POST /api/classroom/:id/heartbeat`, +client fires every 20-30s while the Classroom tab is open and joined) are throttled server-side +(`WHERE last_seen_at IS NULL OR last_seen_at < now() - interval`), never depend on Lab terminal +activity, and stop the moment the tab is closed or the student leaves — no invasive tracking. + +## 8. The classroom dashboard — real data, not a second state system + +`GET /api/admin/classroom-sessions/:id` is the one place that reaches beyond `classroom.js`: it +joins participant/presence data with the Provisioner's actual `environments` state (same enum +every other Lab-status view uses — `none`/`requested`/`provisioning`/`running`/`resetting`/ +`error`/`destroying`/`destroyed`) and a lightweight academic snapshot from the existing +`progress`/`scenario_attempts` tables (current stage, most recent scenario, validation-fail +count, hints used). Never inspects terminal command history. Nothing here is a second Lab-state +or academic-state system — it's the existing ones, read the same way every other admin view +already reads them. + +## 9. Help requests — a queue, not a chat + +`POST /api/classroom/:id/help-request { category, message }` → `open` → (instructor) +`acknowledge` → `resolve`. That's the entire surface — explicitly no chat, no video, no remote +shell takeover (§37). The instructor dashboard's help queue polls the same dashboard endpoint +everything else does. + +## 10. Guidance, never control + +`setCurrentActivity` (current scenario) and announcements are exactly that — a link the student +can choose to open, a message they can read. Nothing server-side redirects a student's browser +or marks work complete on their behalf. The external meeting URL (`classroom_sessions. +meeting_url`) is a generic field the student sees as a link — RootNode never proxies or embeds +Meet/Zoom/Teams/anything else, so it isn't coupled to a single vendor. + +## 11. CSV roster import — whole-file, transactional + +`student-csv.js` hand-parses (no CSV dependency — the file format here is small and simple +enough that pulling in a library would just be one more thing to audit) and validates the +**entire** file before any database write. `POST /api/admin/students/import` then does every +insert inside one Postgres transaction — one bad row rolls back the whole import rather than +leaving a half-imported roster, which is the more dangerous failure mode of the two. Never +accepts a `password`/`password_hash`/`credential_token_hash` column — activation tokens are +always generated server-side, never taken from input. + +## 12. Observability + +`GET /metrics` is a hand-rolled Prometheus text-exposition endpoint (no `prom-client` +dependency — a handful of counters/gauges don't need one) with real in-process counters for +login success/failure and on-demand gauges for live-classroom/help-request/Lab-state counts. +No student or instructor ID ever appears as a label value — every label is a bounded enum +(status, presence, result), per the explicit no-PII/no-high-cardinality-labels instruction. The +Prometheus scrape job for the Platform API (`infra/monitoring/prometheus/prometheus.yml.j2`), +previously commented out, is now active. + +## 13. Frontend + +**Student side** (`frontend/src/platform/`): `LoginScreen` (credentialed, now the default entry +point) with links to the legacy pilot entry and forgot-password; `SetPasswordScreen` (shared +activate/reset UI, driven by `?activate=`/`?reset=` query params — no router library, matching +this app's existing state-machine-per-shell convention); `ForgotPasswordScreen`; a `Classroom` +tab (live-class banner, join, guidance-only current-activity link, own Lab status/launch reusing +the My Learning tab's exact API, announcements, Need Help form); a `Profile` tab (identity, +cohort, progress, achievements, active-session count, change-password). + +**Admin side** (`frontend/src/admin/`): `StudentsView` gained credential status in the roster +table, a create-student form (email/login-ID/cohort), convert/reset-password/revoke-sessions +controls per student, and a bulk CSV import form. A new `ClassroomView` is the control center: +cohort picker → schedule/list classes → a live dashboard (participants with presence/Lab-status/ +current-work, help queue with acknowledge/resolve, current-activity setter, announcement +poster, start/end/cancel, Prepare Labs). + +## 14. Testing — real, not asserted + +- `platform/test/e2e-phase5a.js` (21 checks): activation single-use, password security (§32 — + correct/wrong/unknown-identifier/deactivated/immediate-deactivation/rate-limit/rate-limit- + survives-restart/change-password/session-revocation/forgot-password anti-enumeration/admin + reset/CSRF), and the two-student RBAC matrix (§31 — student can't reach `/api/admin/*`, + assigned/unassigned/viewer instructor access), plus a classroom lifecycle smoke pass. +- `infra/scripts/drills/test-phase5a-rehearsal.js` (15 checks): 5 pilot students, real seeded + progress/attempt/achievement history, a live classroom, help requests, announcements, **a real + Platform API restart mid-rehearsal** proving classroom/session/presence recovery, then + converting two students mid-rehearsal and confirming every bit of their prior history survived + under the new login. +- Both run against a live `docker-compose.dev.yml` stack over real HTTP, not mocks. +- The full pre-existing regression suite (Phase 1/2A/2B/3A/3B/4.1) was re-run unmodified given + how much shared code (`server.js`, `admin-routes.js`, `sessions.js`, `rbac.js`) this phase + touched — see the Phase 5A completion report for results. + +## 15. What was deliberately not built + +Per §37: video/voice conferencing, student-to-student chat, remote instructor shell takeover, +screen recording or proctoring, public registration or course catalog, payments/Stripe, a Parent +Portal, school/SIS integrations, real email delivery infrastructure, certificates, an AI tutor, +a mobile app, a multi-Lab-host scheduler, and production deployment itself. Each is a plausible +"while we're in here" addition and each was left alone on purpose — the schema and API surface +built this phase don't block any of them later. diff --git a/docs/PRODUCTION_VALIDATION_CHECKLIST.md b/docs/PRODUCTION_VALIDATION_CHECKLIST.md index 1947dda..bc6d394 100644 --- a/docs/PRODUCTION_VALIDATION_CHECKLIST.md +++ b/docs/PRODUCTION_VALIDATION_CHECKLIST.md @@ -54,5 +54,18 @@ can prove in advance. full" message, not a 500 — confirms the real host's admission control matches the local test - [ ] `docs/RUNBOOK_LAB_HOST_RESOURCE_PRESSURE.md`'s steps still make sense once capacity numbers are real +## Student identity & remote classroom (Phase 5A — see docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md) +- [ ] `COOKIE_SECURE=1` in the real production environment — confirms `/api/student/ + request-password-reset` never returns `dev_reset_token` outside of dev/CI +- [ ] A real activation/reset link (`?activate=`/`?reset=`) round-trips against the production + domain, not just `localhost` — query-param parsing, no router library in play +- [ ] Student login rate limiting still 429s after a restart against the real production + Postgres, same as the local `e2e-phase5a.js` check +- [ ] `GET /metrics` is reachable by the real Prometheus instance (`infra/monitoring/ + prometheus/prometheus.yml.j2`'s `rootnode-platform-api` job) and no student/instructor ID + appears in any label value +- [ ] `docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md` and `docs/RUNBOOK_REMOTE_CLASSROOM.md`'s steps + still make sense walked against the real admin portal + Only once every box above is checked against the real server does the deployment graduate from "infrastructure package built" to "actually in production." diff --git a/docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md b/docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md index d3cdfc5..bbb1f98 100644 --- a/docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md +++ b/docs/REMOTE_AND_SELF_PACED_ARCHITECTURE.md @@ -51,15 +51,13 @@ this flow specifically — see the Phase 4.1 report for the full run results. product/business decision (course commerce, is it free, who's allowed to enroll), not an infrastructure gap. -2. **No real student authentication.** A student session is created from a bare `student_id` - (`POST /api/login`, no password) — deliberately, since today every `student_id` is issued and - controlled by an instructor via the roster, not chosen or known by the public. A genuinely - remote/self-paced product where students access their account without an instructor in the - loop needs real credentials (email+password, or a magic link) — reusing the same - `password_hash`/`scrypt` machinery `instructors` already has (`src/auth/password.js`) would - be the natural extension, not a new auth system. **This is the one item on this list that - would actually block self-paced delivery today** — everything else already works or is a - product decision, not an infrastructure one. +2. ~~**No real student authentication.**~~ **Closed in Phase 5A.** Students now have real + credentials (email+password or login-identifier+password, `password_hash`/`scrypt` reusing + `instructors`' exact `src/auth/password.js`), a first-login/reset token flow, and Postgres- + backed rate-limited/CSRF-protected sessions on the same `sessions` table instructors use — see + `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`. The old bare-`student_id` `POST + /api/login` still works for not-yet-converted pilot rows only; it's rejected (409) the moment + an account has real credentials or a pending activation token. 3. **`curriculum_assignments.student_id` is schema-ready but unwired.** The table has supported a direct per-student curriculum assignment (independent of any cohort) since Phase 2B @@ -80,6 +78,6 @@ this flow specifically — see the Phase 4.1 report for the full run results. No signup flow, no student password/email auth implementation, no per-student curriculum assignment route, no payments, no public course catalog, no held/premium scenario content. All -explicit non-goals per this phase's authorization. This document exists so Phase 5 planning -starts from an accurate picture of the gap (one real item: student authentication) instead of a -guess. +explicit non-goals per this phase's authorization. Student authentication (item 2, above) was +closed in Phase 5A; the rest remain open, unmodeled by design (YAGNI) until a real product +reason exists. diff --git a/docs/RUNBOOK_REMOTE_CLASSROOM.md b/docs/RUNBOOK_REMOTE_CLASSROOM.md new file mode 100644 index 0000000..44a4757 --- /dev/null +++ b/docs/RUNBOOK_REMOTE_CLASSROOM.md @@ -0,0 +1,53 @@ +# Runbook: Running a Remote Classroom + +Covers scheduling, running, and recovering a live class through `/admin` → Classroom. See +`docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md` for the underlying design. RootNode does +**not** provide video/voice — the meeting URL field just links out to Meet/Zoom/Teams/whatever +you already use for that. + +**Before class: schedule it** + +`/admin` → Classroom → pick the cohort → Schedule a class. Title is required; scheduled start, +description, and a meeting URL are optional. Only one class can be `live` per cohort at a time — +scheduling a second one is fine, starting it while another is live is rejected. + +**Right before class: Prepare Labs** + +Open the classroom → Prepare Labs. This provisions/warms every enrolled student's Lab ahead of +time (same underlying flow as the cohort-level Prepare Labs button) so nobody is waiting on a +cold start once class begins. If a Lab host is at capacity, that student shows as +`capacity_waiting` in the results rather than failing silently — check back or ease off other +concurrent load. + +**Start class** + +Open the classroom → Start class. Status moves to `live`; students now see a "LIVE NOW" banner +on their Classroom tab and can join. + +**During class** + +- **Participants table**: presence (`online`/`recently_active`/`offline`, derived from each + student's last heartbeat — not a hard real-time signal, allow ~30s of lag), Lab status, and a + one-line current-work snapshot (stage, scenario, validation failures, hints used). +- **Set current activity**: enter a scenario ID and Set. This only shows students a link — it + never redirects their browser or marks anything complete. Use it to say "everyone open this + next," not to force navigation. +- **Post an announcement**: short messages, shown newest-first on the student side. +- **Help queue**: students flag `lab_problem` / `dont_understand` / `validation_failing` / + `other` with an optional note. Acknowledge when you've seen it, Resolve when it's handled. + There is no chat here — if it needs back-and-forth, use your actual video call. + +**End of class** + +Open the classroom → End class. Status moves to `ended`; this does not touch Labs or student +progress — destroy Labs separately from the cohort view if you're done with them for the day. + +**If you need to cancel a scheduled (not yet started) class:** Cancel, from the classroom +detail view. A `live` class can only be ended, not cancelled. + +**If the Platform API restarts mid-class** (deploy, crash, host reboot): nothing to do. Sessions, +the live classroom row, and participant presence all live in Postgres, not in the API process's +memory — students' browsers keep their existing session and heartbeat right through it, and the +dashboard shows the same state a few seconds after the API comes back. This is exercised for +real in `infra/scripts/drills/test-phase5a-rehearsal.js`, which restarts the API mid-rehearsal +and asserts on exactly this. diff --git a/docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md b/docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md new file mode 100644 index 0000000..8dfaa18 --- /dev/null +++ b/docs/RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md @@ -0,0 +1,44 @@ +# Runbook: Student Account Lifecycle + +Covers creating, converting, resetting, and revoking student accounts. See +`docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md` for the underlying design. + +**Create a new credentialed student** (`/admin` → Students → Add student): + +Fill in display name plus **either** email (older/self-paced) or login ID (younger/instructor- +managed) — leave both blank for a plain pilot account instead. Submitting shows an activation +link **once**: `https:///?activate=`. Copy it immediately (print it, read it +aloud, hand it over) — it is never shown again and never stored in plaintext anywhere. The +student opens the link and sets their own password; the instructor never sees it. + +**Convert an existing pilot student to a real account** (Students → \[student\] → Convert): + +Same as creation, but for a student who already exists with progress/attempts/achievements — +none of that is touched. Give an email or login ID, get a one-time activation link the same way. + +**Bulk import via CSV** (Students → Bulk import): + +Columns: `display_name, email, login_identifier, cohort_id` (only `display_name` is required per +row). The whole file is validated before anything is written — one bad row rejects the entire +file with a list of row errors, rather than importing some rows and skipping others. Never +include a password column; it will always be rejected if present. Activation links for every +created row are shown once after a successful import. + +**Reset a student's password** (Students → \[student\] → Reset password): + +Only for already-credentialed accounts. Issues a fresh reset link (`?reset=`, valid 1 +hour) and immediately revokes every existing session for that student — the instructor still +never sees or sets the actual password. + +**Revoke a student's sessions** (Students → \[student\] → Revoke all sessions): + +Immediately invalidates every active login for that student without touching their password. +Use this if a device is lost/shared or a session looks compromised. + +**Deactivate a student** (Students → \[student\] → PATCH status: inactive, via the existing +students list — deactivation takes effect on their very next request, not just their next +login, because `studentSession` middleware re-checks `status='active'` every time. + +**If a student says their activation/reset link "doesn't work":** tokens are single-use and +time-limited (activation 7 days, reset 1 hour). Just issue a new one via Convert or Reset +password — there's no way to "resend" the same token, and that's intentional. diff --git a/platform/README.md b/platform/README.md index 5a3a0f8..8a728d4 100644 --- a/platform/README.md +++ b/platform/README.md @@ -10,13 +10,15 @@ entrypoint/Dockerfile unmodified; frontend bundle gained Phase 3B learning UX, s - `api/` — the lightweight Platform API: students, cohorts, enrollments, curriculum, detailed scenario attempt telemetry, progressive hints, curriculum completion rules, the exam + capstone assessment engine, achievements, per-instructor accounts, persistent student/ - instructor sessions, RBAC, audit logging, and the instructor/admin control plane. No k3s, no - `--privileged` — this replaced Phase 2A's "identity" instance, which was a full Lab image - booting an unused k3s cluster just to serve these APIs. Design: + instructor sessions, RBAC, audit logging, real student credentials + remote classrooms + (Phase 5A), and the instructor/admin control plane. No k3s, no `--privileged` — this replaced + Phase 2A's "identity" instance, which was a full Lab image booting an unused k3s cluster just + to serve these APIs. Design: `docs/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` (split + Postgres migration), `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` (sessions, accounts, RBAC, audit), - `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` (learning/assessment engine). Operational - reference: `api/README.md`. + `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` (learning/assessment engine), + `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md` (student credentials, remote classroom). + Operational reference: `api/README.md`. - `gateway/` — the only student-facing path to a Lab (HTTP + WebSocket proxy, environment-aware routing derived from a server-controlled session — not a client-suppliable header/param — and, as of Phase 3B, server-side gating of progressive hint reveal). Design: