CourseView.jsx already special-cased 'capstone' activities with a pointer to
the Capstone tab (since only capstone-type activities existed before this
phase). Phase 6C's new rootnode-security-assessment introduces the first
non-capstone 'assessment'-type module activity — without a similar note, a
student would see it in the course tree with no way to discover it needs to
be started from the Assessments tab. Adds the same pattern for 'assessment'
type, naming the assessment by its own title.
No other frontend change was needed: CurriculumView.jsx (instructor view)
already renders activity_type labels, scenario_id/assessment_definition_id,
is_required, instructor_notes availability, and READY/PARTIAL/PLANNED status
pills generically — Phase 6C's new module/activity shapes render correctly
under Phase 6B's existing generic logic, verified by inspection.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
CourseView.jsx now shows an '(optional)' label on any activity marked
is_required=false, both in the module activity list and the activity
detail header — the only genuine UX need Phase 6B's new optional/
advanced practice content exposed. No other changes to the existing
Course/Module/Activity navigation, Launch Lab flow, or Practice tab.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
CourseView.jsx — My Programs -> ROOT_ -> Course -> Module -> Activity ->
Launch Lab -> back -> continue, nested inside the existing self-paced
ProgramsTab rather than a second portal. Renders lesson/demonstration/
knowledge-check/guided-exercise/reflection content via the existing
react-markdown+remark-gfm stack (already used by ScenarioPanel.jsx — no
new dependency). Locked modules/courses are inert, not hidden — the
server is still the actual gate (course-routes.js).
ProgramsTab.jsx gains a 'View Courses' button alongside the existing
'Continue Learning' button — the original direct-to-Lab flow is
byte-for-byte unchanged; both paths call the identical launch sequence
underneath.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
- ProgramsTab.jsx: Active/Completed programs, real progress, deterministic Continue Learning
(reuses the same launch/poll pattern MyLearningTab already established), entitlement-blocked
states shown with a real reason, never a silent dead end.
- StudentDashboard.jsx: new My Programs tab. api.js: programs/program/launchProgram.
- LoginScreen (credentialed, now the default entry point) with links to 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 (anti-enumeration, dev-only
reset-token surface).
- ClassroomTab: LIVE NOW banner, join, heartbeat only while joined and the tab is mounted,
guidance-only current-activity link, own Lab status/launch (reuses the My Learning tab's
exact API), announcements, Need Help form. No chat, no video, no shell access.
- ProfileTab: identity, cohort, progress, achievements, active-session count,
change-password (gated on has_credentials).
- PlatformShell rewritten as an explicit view state machine; StudentDashboard gained the new
tabs; api.js/Platform.module.css extended to match.
StudentDashboard is now tabbed: My Learning (the original Phase 2A/2B flow, extracted
unchanged into MyLearningTab and extended with an overall percent-complete bar), Practice,
Assessments (available/active-with-live-countdown/history), Capstone (locked checklist ->
unlocked -> start/resume -> completed), Achievements (a flat dated list, not a leaderboard).
Inside the Lab UI: ProgressiveHints replaces the static hint array render when the Gateway has
stripped hints (detected via scenario.hints being undefined but hint_count present - standalone
dev mode never sees this, so it keeps the original behavior); a new top-level AssessmentBanner
shows a live countdown and completion count whenever a Platform assessment is running,
rendered alongside (not replacing) the pre-existing Lab-local exam-mode UI, which is untouched.
Non-HttpOnly rn_csrf_s/rn_csrf_i cookies, set on login, echoed back as X-CSRF-Token on every
state-changing request — defense in depth on top of (not instead of) SameSite=Lax. In-memory
sliding-window rate limiting on instructor login (8 attempts / 5 min per ip:email, no new
dependency).
Real bug found while wiring this up: the Gateway's own progress-relay call (triggered when a
student solves a scenario) is a server-to-server POST to /api/progress/record relaying the
browser's session — it needs to forward the CSRF token too, and needs to forward BOTH the
rn_csrf_s cookie and a matching X-CSRF-Token header (forwarding only the header still 403'd,
since the double-submit check needs both on the request Platform API actually receives).
Also tightens the Gateway's session-resolution cache from 5000ms to 2000ms, so a
revoked/expired session stops working at the Gateway within a tighter, tested bound.
Frontend (admin + student API clients) now reads the relevant rn_csrf_* cookie via
document.cookie and attaches it on every POST/PATCH/DELETE.
Replaces the Phase 2A pilot ?sid= query param with an HttpOnly,
SameSite=Lax session cookie. Gateway now derives student identity from
the session (via Platform API's /api/session/whoami) instead of trusting
a client-suppliable id, and checks ownership on every proxied HTTP and
WebSocket Lab request, not just the initial navigation.
Fixes a routing bug found during testing: the fallback route was using
'does this session own a running environment' to decide whether an
unprefixed /api/* call belonged to the Lab UI or the dashboard — true in
both places. Replaced with an explicit LAB_OWNED_PATHS allowlist of the
exact unprefixed paths the existing Lab UI actually calls.
e2e-phase2a.js rewritten to use a cookie jar instead of headers/query
params, with explicit impersonation-rejection tests added. 10/10 passing.
StudentDashboard.jsx — "Up Next" curriculum view + Launch Lab flow, in one
component since they share state (environment status affects what curriculum
actions make sense).
- Renders GET /api/curriculum/path as-is: the existing RootNode path
(Foundation -> Workloads -> Networking -> Configuration -> Troubleshooting
-> Real Ops -> Capstone), no new structure invented. Capstone stays the
locked placeholder from the earlier slice.
- Launch Lab -> POST /api/environment/launch, polls status every 2.5s while
provisioning/resetting, transitions through none -> provisioning -> running
-> error with a distinct UI state for each (EnvironmentPanel). On running,
navigates to launch_path with the student's own id as ?sid= — a
defense-in-depth ownership check the Gateway verifies on that specific
navigation (see the gateway commit).
- One "Recreate Lab" control, calling POST /api/environment/reset — no raw
destroy-without-recreate exposed to students, matching "don't expose raw
destroy/admin controls beyond what is safe for a student." A visible note
states progress is saved separately and survives.
Platform.module.css reuses the existing design tokens from index.css
(--green, --surface, --text, etc.) so this reads as the same product as the
Lab UI, not a bolted-on separate app.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
New Platform Shell entry point (frontend/src/platform/) alongside the
existing, completely unmodified Lab UI (App.jsx and everything under
frontend/src/components/). main.jsx now splits on the current URL path —
/lab/<token>/ renders the existing <App/> exactly as before; anything else
renders the new <PlatformShell/>. Both live in the same built bundle (it's
served both by the persistent identity instance and by every ephemeral Lab
container), split purely at runtime.
- EntryScreen.jsx — cohort dropdown -> name dropdown (existing
GET /api/roster/cohorts, GET /api/roster?cohort=) -> POST /api/login. No
password. Visibly labeled pilot identity, not production auth, per the
governing requirement that this be stated explicitly, not just true in the
architecture docs.
- api.js — student_id lives in localStorage only, sent as X-Student-Id on
every authenticated request. Every authenticated call throws if no student
is stored rather than silently going out anonymously — this is what
"prevent accidental anonymous progress writes" means in practice, not just
a comment.
- PlatformShell.jsx — entry screen if no stored session, dashboard otherwise.
Co-Authored-By: Claude Sonnet 5 <[email protected]>