docs(phase5a): student identity and remote classroom documentation
- 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:
@@ -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
@@ -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`).
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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."
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user