docs(phase5a): student identity and remote classroom documentation
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s

- 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.
This commit is contained in:
2026-08-25 01:20:13 -05:00
parent bfa16d9162
commit 896c230142
9 changed files with 402 additions and 56 deletions
+15 -7
View File
@@ -150,12 +150,19 @@ its core:
the browser (`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, session model in the browser (`docs/PHASE2A_GATEWAY_AND_STUDENT_FLOW.md`, session model in
`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` §3, hint gating in `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` §3, hint gating in
`docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` §3). `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` §3).
- The student flow (roster login → curriculum dashboard → Launch Lab → practice/assessments/ - The student flow (real login (email or instructor-assigned login ID) or legacy pilot roster
capstone → achievements) lives in `frontend/src/platform/`; the instructor/admin portal entry → curriculum dashboard → Launch Lab → practice/assessments/capstone → achievements →
(per-instructor login, cohorts, students, instructor accounts, audit log, classroom live classroom → profile) lives in `frontend/src/platform/`; the instructor/admin portal
"Prepare Labs," environment controls, learning analytics, assessment/capstone management) (per-instructor login, cohorts, students with credential/CSV-import management, a remote
lives in `frontend/src/admin/` at the `/admin` route — both additions alongside the existing classroom control center, instructor accounts, audit log, environment controls, learning
Lab UI, not a rewrite of it. 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 **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 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`, 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/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) The production infrastructure package (Proxmox/Ansible/Terraform/firewall/monitoring/backup/CI)
that runs all of this on real hardware lives in `infra/` — see that runs all of this on real hardware lives in `infra/` — see
+56 -32
View File
@@ -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 ## 5. Student identity — what's built, what isn't
**Built (backend only, as of this doc):** **Current state (Phase 5A — see `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md` for the
- `students` table: `student_id` (`student-NNN`, sequential), `display_name`, `cohort`, full design):**
`created_at`, `last_active_at`. - `students.student_id` (`student-NNN`, sequential) is still the FK every other table hangs off
- Login model: **instructor roster, pick-your-name, no password.** `POST /api/roster` adds of and never changes across an account's lifecycle, including conversion.
a student (instructor-only action, currently unauthenticated at the API level — see - Real credentials: `email` and `login_identifier` (independently optional/unique — email for
"not built" below). `GET /api/roster?cohort=X` lists names for a dropdown. older/self-paced students, an instructor-assigned login ID for younger ones), `password_hash`
`POST /api/login` just validates the id exists and returns it; the frontend is expected to reusing `instructors`' exact `scrypt` machinery (`src/auth/password.js`). A student with
hold onto `student_id` and send it back as an `X-Student-Id` header on every request. neither field set is a plain **pilot** account, unchanged since Phase 2A: `POST /api/login
- `progress` table now keyed on `(student_id, scenario_id)` instead of `scenario_id` alone — { student_id }`, no password, instructor-roster-only.
each student's completion/attempts/hints are isolated. Verified by smoke test: two students - First login/reset is one shared, single-use, hashed, time-limited token mechanism
answering the same scenario don't affect each other's state, and pre-migration data was (`students-auth.js`) — instructors never see or set a student's actual password.
preserved under `student_id = 'unassigned'`. - Sessions are the same Postgres-backed `sessions` table instructors use (opaque, HttpOnly,
- Exam `sessions` now carry `student_id` (set at session start), so exam history and the hashed, revocable, CSRF-protected); login is rate-limited the same Postgres-backed way as
timer-reset-on-submit logic are correctly scoped per student instead of leaking across instructor login (Phase 4.1), now also applied to students.
whoever's browser hits the API next. - 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:** **Not built — deliberate non-goals, not gaps (Phase 5A §37):**
- **No frontend UI.** No login screen, no name picker, no roster-add form. The API works; - No public self-registration — accounts are still instructor-created/converted/imported only.
nothing calls it yet. This is the next slice. - No email delivery infrastructure — password reset works via an instructor-issued token today;
- **No auth on `/api/roster` POST.** Anyone who can reach the API can add a "student." self-service reset returns a real token only in non-production, by design.
Fine for a dev instance, not fine once this is reachable by actual students — needs an - No Parent/Guardian portal — the schema doesn't block adding one later, but none exists.
instructor-only gate before that happens (ties into the instructor dashboard, roadmap
item 3). **Historical note, kept for context:** earlier drafts of this section described a pre-Phase-2A
- **No concurrency / isolation.** Exam sessions are still global-single-active state (`X-Student-Id` header, unauthenticated roster POST, one shared cluster) that no longer
(`WHERE status='active' LIMIT 1` — see `server.js`), and there is one shared k3s cluster reflects any deployed version of this codebase; that content has been superseded by the above.
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.
## 6. Schema migration pattern ## 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 (§12), student-facing tabs (My Learning/Practice/Assessments/Capstone/Achievements) at
`frontend/src/platform/`. `frontend/src/platform/`.
3. Student accounts / persistent progress — **done**, Postgres-authoritative as of Phase 2B 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 (§9), sessions persistent as of Phase 3A (§10). Real student credentials (email or
identification, not strong student auth — documented, not silently glossed over login-identifier + password, first-login/reset token flow) closed the remaining "pick your
(docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md §7). 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 4. Instructor dashboard — **done** (§9/§10). Full admin portal at `/admin`: dashboard, cohort
and student management, classroom Prepare/Destroy Labs, per-instructor accounts with and student management, classroom Prepare/Destroy Labs, per-instructor accounts with
owner/admin/instructor roles, audit log. 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 - **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 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 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`).
+9
View File
@@ -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 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 (`docs/PHASE4_1_PREPRODUCTION_HARDENING.md`) built and simulation-tested the fix, a Lab Host
Router (`platform/lab-router/`). 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.
@@ -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=<token>`/`?reset=<token>`
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.
+13
View File
@@ -54,5 +54,18 @@ can prove in advance.
full" message, not a 500 — confirms the real host's admission control matches the local test 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 - [ ] `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 Only once every box above is checked against the real server does the deployment graduate from
"infrastructure package built" to "actually in production." "infrastructure package built" to "actually in production."
+10 -12
View File
@@ -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 product/business decision (course commerce, is it free, who's allowed to enroll), not an
infrastructure gap. infrastructure gap.
2. **No real student authentication.** A student session is created from a bare `student_id` 2. ~~**No real student authentication.**~~ **Closed in Phase 5A.** Students now have real
(`POST /api/login`, no password) — deliberately, since today every `student_id` is issued and credentials (email+password or login-identifier+password, `password_hash`/`scrypt` reusing
controlled by an instructor via the roster, not chosen or known by the public. A genuinely `instructors`' exact `src/auth/password.js`), a first-login/reset token flow, and Postgres-
remote/self-paced product where students access their account without an instructor in the backed rate-limited/CSRF-protected sessions on the same `sessions` table instructors use — see
loop needs real credentials (email+password, or a magic link) — reusing the same `docs/PHASE5A_STUDENT_IDENTITY_REMOTE_CLASSROOM.md`. The old bare-`student_id` `POST
`password_hash`/`scrypt` machinery `instructors` already has (`src/auth/password.js`) would /api/login` still works for not-yet-converted pilot rows only; it's rejected (409) the moment
be the natural extension, not a new auth system. **This is the one item on this list that an account has real credentials or a pending activation token.
would actually block self-paced delivery today** — everything else already works or is a
product decision, not an infrastructure one.
3. **`curriculum_assignments.student_id` is schema-ready but unwired.** The table has supported 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 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 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 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 explicit non-goals per this phase's authorization. Student authentication (item 2, above) was
starts from an accurate picture of the gap (one real item: student authentication) instead of a closed in Phase 5A; the rest remain open, unmodeled by design (YAGNI) until a real product
guess. reason exists.
+53
View File
@@ -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.
+44
View File
@@ -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://<domain>/?activate=<token>`. 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=<token>`, 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.
+7 -5
View File
@@ -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 - `api/` — the lightweight Platform API: students, cohorts, enrollments, curriculum, detailed
scenario attempt telemetry, progressive hints, curriculum completion rules, the exam + scenario attempt telemetry, progressive hints, curriculum completion rules, the exam +
capstone assessment engine, achievements, per-instructor accounts, persistent student/ capstone assessment engine, achievements, per-instructor accounts, persistent student/
instructor sessions, RBAC, audit logging, and the instructor/admin control plane. No k3s, no instructor sessions, RBAC, audit logging, real student credentials + remote classrooms
`--privileged` — this replaced Phase 2A's "identity" instance, which was a full Lab image (Phase 5A), and the instructor/admin control plane. No k3s, no `--privileged` — this replaced
booting an unused k3s cluster just to serve these APIs. Design: 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/PHASE2B_ADMIN_AND_PLATFORM_DATA.md` (split + Postgres migration),
`docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` (sessions, accounts, RBAC, audit), `docs/PHASE3A_IDENTITY_AND_INSTRUCTOR_ACCESS.md` (sessions, accounts, RBAC, audit),
`docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` (learning/assessment engine). Operational `docs/PHASE3B_LEARNING_AND_ASSESSMENT.md` (learning/assessment engine),
reference: `api/README.md`. `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 - `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, 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: as of Phase 3B, server-side gating of progressive hint reveal). Design: