- enrollment.js: server-only status transitions (pending->active->completed, cancel),
deriveEnrollmentAccess (pure, derived-at-read-time like classroom.js's derivePresence —
'expired' is never stored, computed from access_ends_at), maybeCompleteEnrollment
(idempotent, server-derived from the existing capstone achievement).
- environment-view.js: publicEnvView/launchEnvironmentForStudent extracted out of server.js's
original /api/environment/launch route body so the new entitlement-gated self-paced launch
reuses the identical disposable-Lab sequence instead of a second copy — behavior for the
existing cohort/pilot route is completely unchanged.
- program-status.js: combines an enrollment with real curriculum/progress state into one view
(access, percent-complete, current stage, deterministic next-activity/resume target) — shared
by the student and admin surfaces so both show identical information.
- selfpaced-routes.js: GET /api/student/programs(/:id), POST .../launch — entitlement enforced
server-side before ever touching the Provisioner, never just hidden in the UI.
- rbac.js: requireEnrollmentAccess — a plain instructor may manage an enrollment they created
or that belongs to a student sharing one of their cohorts; a self-paced enrollment can have
no cohort at all, so the existing cohort-only checks can't cover it.
- server.js: found and fixed a real bug — POST /api/progress/record's handler predated the
wrap() convention, so an unexpected internal error (a malformed environment_id) crashed the
entire process instead of just 500ing that request. Now wrapped + input-validated.