Commit Graph
100 Commits
Author SHA1 Message Date
ced4568 bfae2aafe0 docs(business): canonicalize RootNode V1 pricing matrix
docs/ROOTNODE_PRICING_AND_OFFERS_V1.md — the founder-approved, locked V1 pricing matrix (seasonal
camps, instructor-led programs, self-paced, mentoring/workshops, sibling policy, Winter Break
launch terms) recovered from synced conversation history after the original Phase 9.5 commit was
confirmed absent from canonical Git history. Cross-validated against the independently-prepared
RootNode Academy WHM+WHMCS Agent Handoff's own price guardrail table — identical figures.

Does not resolve or invent dates, venue, capacity, tax treatment, refund/cancellation policy, or
legal agreement language — those remain separate, open launch gates.

PHASE10_CANONICAL_STATE_AUDIT.md updated to reflect the recovery — the original Phase 9.5 commit
is still confirmed missing from history, but the pricing decision itself is now canonically
recorded rather than resting only on verbal confirmation. PC #2 inspection remains useful only to
check for an earlier local copy to compare against, not to locate the pricing itself anymore.
2026-08-28 14:06:03 -05:00
ced4568 74033b4cb8 docs(audit): reconcile Phase 10 findings on Phase 9.5, Infisical, and website pricing
Phase 9.5: reconfirmed no canonical commit exists (broader content search across full history —
still zero hits outside this session's own commits); Chase confirmed the real document lives on
PC #2, unpushed. Not duplicated here per Chase's explicit instruction — he owns bringing it to
canonical Gitea.

Infisical: Phase 10's 'project does not exist' finding was incomplete, not wrong about what it
checked — a proper org-scoped API enumeration (8 projects) still shows no RootNode Academy
project visible to the claude-code-migration machine identity, even though Chase confirms it
exists. Root cause narrowed to a missing per-project access grant (Infisical scopes identity
visibility per-project, separate from org membership) rather than the project not existing.
Corrected docs/SECRETS_AND_INFISICAL.md and docs/PHASE10_CANONICAL_STATE_AUDIT.md accordingly,
with the exact one-step fix (grant the identity access via Project Settings -> Access) and the
identity id needed to do it. No duplicate project was created.

Website pricing: a read-only fetch of rootnodeacademy.com found stale founding-price RANGES that
don't match the exact fixed V1 prices Chase gave directly this phase; Chase confirmed the fixed
prices in this repo's docs are correct and current. Noted as a real discrepancy for his own
awareness, not something this repo's scope covers fixing.

Updated the Phase 10 launch matrix with a Category column (TECHNICAL READY / OWNER DECISION
REQUIRED / EXTERNAL BLOCKER / HARDWARE BLOCKER) distinguishing who needs to act on each
remaining gate. No application code changed — audit/documentation reconciliation only.
2026-08-28 13:09:05 -05:00
ced4568 0b2303a3e2 docs(phase10): canonical state audit, secrets inventory, production/security/launch readiness
Gate 1 — canonical state audit: this workstation's HEAD/origin/main and every claimed milestone
SHA (Phase 2A through 9.1) verified consistent; no stranded branches/tags on Gitea; full-history
secret scan clean (no rotation required); Phase 9.5 confirmed absent from canonical history
(pricing was given directly by Chase and used without it); second-PC reconciliation commands
provided since this session cannot see that machine.

Gate 3 — secrets: real inventory derived from process.env greps across every service (not
guessed); Infisical auth fixed (wrong domain was the actual root cause of Phase 9.1's failures —
the identity belongs to infisical.cedshomelab.com, not app.infisical.com) but no RootNode
Academy project exists there yet, contradicting this phase's own authorization — flagged for
Chase to create.

Gates 4-9/15-16 — production: verified, not assumed, that no dedicated server/Proxmox VM has
been provisioned (empty terraform.tfvars, no state file, placeholder group_vars/production.yml)
— IaC validated (tofu validate, ansible --syntax-check both clean) and left unexecuted per this
phase's own 'do not fake production deployment' instruction. rootnodeacademy.com confirmed live
(Cloudflare, real HTTPS) — the separate, not-RootNode-owned website; no Platform API subdomain
exists yet.

Gate 18 — security audit: full review against every regression suite rerun this phase; no
launch-critical defect found.

Gates 19/20 — full regression (131 checks: e2e-phase5b/5c/6f/7/8/9, content validation,
migrations, frontend build) and the Winter Break commerce rehearsal, both rerun clean against
current main.

Gates 10-14/21 — website integration, Winter Break pricing (99/49, real, Stripe-validated),
agreements/legal, tax, and refund policy all reviewed; legal text/tax/refund policy/dates/venue/
capacity correctly left as open owner decisions, not invented.

No application code changed this phase — audit/launch-prep only.
2026-08-28 12:07:25 -05:00
ced4568 a4c0058f8c docs(stripe): document real Stripe TEST-mode validation results
PHASE9_1_STRIPE_TEST_VALIDATION.md rewritten from BLOCKED to the real, proven results: a real
Stripe TEST Checkout, a real TEST payment at the founder-approved $399 Founding Cohort price, a
genuine signed webhook, the existing registration->enrollment conversion, a real
duplicate-delivery proof, a real automated abandoned-checkout proof, a real refund with the
non-cascade rule reconfirmed, full Stripe<->RootNode reconciliation IDs, and the one real defect
found and fixed. STRIPE_INTEGRATION_RUNBOOK.md updated (real-Stripe local testing is no longer
'not yet exercised'; notes the no-sudo Stripe CLI install path and --api-key/--print-secret
usage). WINTER_BREAK_LAUNCH_CHECKLIST.md: Stripe TEST validation checked, price checked ($399
launch / $449 retail per Chase directly), live-mode items correctly still unchecked.
PHASE9_COMMERCE_AND_LAUNCH.md's Phase 9.1 addendum updated to reflect completion.
2026-08-28 09:47:11 -05:00
ced4568 fbf52d8334 test(stripe): add real-Stripe regression coverage and complete the opt-in TEST-mode check
e2e-phase9.js: one new deterministic regression test proving the webhook-correlation fix — a
test event referencing a transaction's own UUID (the exact shape a real Stripe
payment_intent.payment_failed/charge.refunded event now produces) is still matched. 20/20.

stripe-integration-test.js: configurable price/currency (STRIPE_TEST_PRICE_CENTS/
STRIPE_TEST_CURRENCY) and a STRIPE_TEST_MINOR=true guardian+minor path. Once a real payment
lands, now fully automates deep post-payment verification: exactly-one payment_transactions
record, registration/enrollment/cohort/guardian reconciliation, a genuine duplicate-delivery
proof (re-signing the REAL event fetched via stripe.events.retrieve — never a fabricated one),
real student activation + program-visibility, and a real refund with the non-cascade rule
reconfirmed — plus a fully automated, browser-free abandoned-checkout proof via Stripe's own
sessions.expire() API. Still opt-in, still a guaranteed no-op by default, still refuses
live-mode keys.
2026-08-28 09:47:01 -05:00
ced4568 c86b3de611 fix(payment): correlate PaymentIntent/Charge-referencing webhooks to the right transaction
Real Stripe TEST traffic (Phase 9.1) exposed a genuine defect the deterministic TestProvider
never could: payment_intent.payment_failed and charge.refunded events reference Stripe's
PaymentIntent/Charge object, never the Checkout Session, but payment_transactions.
provider_reference only ever stored the Session id. A real failure or refund webhook would have
silently failed to match any transaction (UNKNOWN_TRANSACTION).

- payment-provider.js: session creation now also sets payment_intent_data.metadata (the same
  metadata already carrying our transaction_id), so it propagates onto the PaymentIntent Stripe
  creates. normalizeEvent() (now async) reads transaction_id back from the PaymentIntent's own
  metadata directly for payment_intent.payment_failed, and via one extra paymentIntents.retrieve
  call for charge.refunded (Charges don't inherit PaymentIntent metadata automatically).
- commerce.js: the transaction lookup now matches provider_reference OR transaction_id — one
  additional OR clause, no schema/migration change.
- webhook-routes.js: added the now-required await on verifyAndParseWebhook (TestProvider's
  synchronous return is unaffected).
- docker-compose.dev.yml: wires PAYMENT_PROVIDER/STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET through
  to the api container (previously entirely absent — Phase 9 never needed it against the
  deterministic provider), empty/unset by default matching EMAIL_PROVIDER's own convention.

Verified fixed against a real Stripe TEST refund (Phase 9.1); e2e-phase9.js 20/20 and the Winter
Break commerce rehearsal 13/13 rerun clean after the fix.
2026-08-28 09:46:51 -05:00
ced4568 77fd1869db docs(stripe): document Phase 9.1 blocked status and live-mode readiness checklist
New: PHASE9_1_STRIPE_TEST_VALIDATION.md — baseline verification, the exact Stripe-credential
search performed (none found), the Stripe architecture review (dynamic price_data, unchanged),
what was proven vs. blocked per the mission's own step list, and the live-mode readiness
checklist. Updated: STRIPE_INTEGRATION_RUNBOOK.md and WINTER_BREAK_LAUNCH_CHECKLIST.md to point
at the new opt-in test and the blocked status. PHASE9_COMMERCE_AND_LAUNCH.md gets a short
addendum. No application code changed this phase — no genuine defect was exposed without real
Stripe traffic, so no regression rerun was required.
2026-08-27 10:40:53 -05:00
ced4568 f0ff14af38 test(stripe): add opt-in real Stripe TEST-mode integration check
RUN_STRIPE_INTEGRATION_TEST=true (default: no-op, zero Stripe network traffic). Refuses to run
against a live-mode secret key. Automates what's provable via pure API alone — real Checkout
Session creation verified against Stripe's own retrieve API, server-authoritative pricing proven
against Stripe's own recorded amount even under a tampered caller amount, invalid-signature
webhook rejection with no domain side effect — then polls RootNode's status endpoint for a
human/Stripe-CLI-completed real payment rather than fabricating a signed success event. Never
runs as part of normal regression/CI.
2026-08-27 10:40:42 -05:00
ced4568 80a5f6e457 docs(phase9): document commerce integration and Winter Break launch readiness
New: PHASE9_COMMERCE_AND_LAUNCH.md, WEBSITE_REGISTRATION_AND_CHECKOUT.md,
STRIPE_INTEGRATION_RUNBOOK.md, WINTER_BREAK_LAUNCH_CHECKLIST.md. Updated:
ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (commerce boundary + PHASE 9 BUILT rows),
OFFERING_AND_REGISTRATION_MODEL.md (pricing, the payment/commerce boundary, three-status
principle), WEBSITE_ENROLLMENT_INTEGRATION.md (price fields, real agreement/checkout/webhook
sections replacing Phase 8 placeholders), WINTER_BREAK_CAMP_OPERATIONS.md (real payment step
replacing the manual placeholder).
2026-08-27 09:06:04 -05:00
ced4568 281d60bc2d test(camp): add Winter Break commerce rehearsal
13/13 against a real, freshly-built stack, PAYMENT_PROVIDER=test (no live money): admin
configuration including price/payment-provider readiness, minor + adult registration through
real checkout and a real webhook call, duplicate-webhook idempotency, failed-payment seat
retention, abandoned-checkout seat release, a last-seat checkout race, a real classroom session,
and a real Lab task completed by the newly-activated minor student.
2026-08-27 09:05:54 -05:00
ced4568 8b3e56e402 test(phase9): add commerce integration acceptance suite
19/19: pricing in the public serializer, checkout gated on agreements, server-authoritative
amount, webhook signature verification and idempotency, payment success/failure/abandonment,
last-seat checkout race, reservation reaper, manual refund with no cascade, minor guardian
routing, admin/RBAC payment-data restrictions, audit/metric hygiene, the status-lookup endpoint,
and confirmation that ROOT_ V1/free-offering/classroom flows are unaffected.
2026-08-27 09:05:46 -05:00
ced4568 f0dde9b84a feat(admin): add offering pricing and payment/refund UI
- admin-offerings-routes.js: PATCH /api/admin/offerings/:id (allowlisted fields), GET
  /api/admin/registrations/:id/payments, POST /api/admin/payments/:id/refund
- frontend: Pricing card on offering detail, Payments card (transaction history + record-refund
  action) on registration detail — admin/owner-gated, reusing existing admin UI conventions
2026-08-27 09:05:36 -05:00
ced4568 16622bd9ab feat(webhook): add verified payment webhook route and website checkout/agreement contract
- webhook-routes.js: POST /api/webhooks/payment — signature-verified, idempotent, always 200
  once verified (a forged/invalid signature is a clean quiet 400)
- integration-routes.js: POST /api/integrations/registrations/:id/agreements (closes Phase 8's
  gap — agreement acceptance now on the public registration contract), POST .../:id/checkout
  (server-to-server, amount always server-side), GET .../:id/status (minimal safe status lookup
  for the website's success page — no PII, no amount, no provider reference)
- public-offerings-routes.js: price_amount/price_currency/requires_payment in the public serializer
- metrics.js: checkout/payment/webhook/conversion/seat-released counters, all low-cardinality
- notifications.js: listForRegistration now unions by the registration's own enrollment_id too,
  so the post-conversion activation notification shows up in the registration's communication
  history (previously only pre-conversion notifications were visible)
2026-08-27 09:05:29 -05:00
ced4568 b0e07bb17f feat(commerce): add payment-provider abstraction, commerce layer, and pricing schema
- migration 037: offerings.price_amount/price_currency, offering_registrations.reservation_expires_at,
  payment_transactions, payment_webhook_events (webhook_event_id as real PK — the idempotency mechanism)
- payment-provider.js: StripeProvider (real SDK, HMAC webhook verification) + TestProvider
  (deterministic, in-process, no network) behind one interface, selected via PAYMENT_PROVIDER
- commerce.js: createCheckoutSession (server-side price, never caller-supplied), processWebhookEvent
  (idempotent), handlePaymentSuccess/Failure/CheckoutAbandoned/Refund, reservation reaper support
- offerings.js: computeReservationExpiry, updateOffering (allowlisted PATCH), readiness gained
  price/payment_provider checks, convertRegistrationToEnrollment now fires the activation email
  every other enrollment path already triggers, and links the registration's guardian_id into
  student_guardians on conversion (real bug: a converted minor had no guardian link, so the new
  activation email had nowhere to resolve a recipient to)
- server.js: raw-body middleware scoped to /api/webhooks/payment, reservation-reaper poller
2026-08-27 09:05:19 -05:00
ced4568andClaude Sonnet 5 788fb197e0 docs(phase8): document enrollment operations
New: PHASE8_ENROLLMENT_OPERATIONS.md (phase summary), WINTER_BREAK_CAMP_OPERATIONS.md
(the real operational walkthrough), REGISTRATION_AND_GUARDIAN_COMMUNICATIONS.md,
OFFERING_ADMIN_GUIDE.md.

Updated: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (BUILT NOW/PHASE 8 BUILT
table), OFFERING_AND_REGISTRATION_MODEL.md (both ordering bugs, launch readiness,
registration enforcement), STUDENT_GUARDIAN_DATA_MODEL.md (guardian communications now
wired), WEBSITE_ENROLLMENT_INTEGRATION.md (the real, live registration contract with
worked examples, replacing the old speculative future-flow section), platform/README.md
(test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:42 -05:00
ced4568andClaude Sonnet 5 aa63d09fb8 test(phase8): add enrollment operations coverage and Winter Break registration rehearsal
- e2e-phase8.js (16/16): offering CRUD/transitions/readiness, registration open/closed
  enforcement, capacity + concurrent final-seat race through the real registration
  endpoints, the public serializer's safety, website integration idempotency (sequential +
  concurrent), adult/minor paths, required-agreement conversion blocking with version
  snapshotting, offering->cohort membership, guardian recipient resolution, RBAC, and
  confirmation ROOT_ V1/self-paced/classroom flows are unaffected.
- test-phase8-winter-camp-registration.js (12/12): the full commercial-path story with no
  payment code — website submits a real minor registration (real guardian created/matched),
  guardian agreement acceptance, a simulated admin-side payment confirmation, conversion to
  real enrollment/cohort, real account activation, an independent adult path, two students
  genuinely competing for the final seat, and a REAL Lab task completed by the minor
  student with the instructor then seeing the real progress.
- e2e-phase7.js: fixed two now-stale test assumptions the new server-authoritative
  registration-window enforcement broke (an offering left in draft that Phase 7's own test
  never transitioned before registering against it) and widened a file-allowlist check for
  the new, legitimate public-offerings-routes.js.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32, e2e-phase5c.js 29/29, all fresh-stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:32 -05:00
ced4568andClaude Sonnet 5 40362ec0c9 feat(admin): add Offerings operations UI
Offerings list + create; offering detail with overview (valid-transitions-only buttons),
server-computed launch readiness, required agreements, and registrations/roster;
registration detail with lifecycle transitions, agreement status, communication history
(masked recipients), and convert-to-enrollment. Reuses the existing Admin.module.css/
adminApi conventions exactly (ProgramsView.jsx's own list+detail pattern) — no new UI
framework or pattern introduced.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:20 -05:00
ced4568andClaude Sonnet 5 24eee80354 feat(offering): launch readiness, agreement requirements, guardian communications, website registration API
- Registration is now server-authoritative: draft/cancelled/completed/full/window-closed
  offerings reject registration attempts with distinct error codes (assertRegistrationOpen).
- offering_agreement_requirements + conversion-time blocking on missing required
  agreements; accepted_by_type gained 'registrant' for an adult self-accepting before
  their student_id exists.
- computeOfferingReadiness() — a server-derived launch checklist (curriculum, instructor,
  schedule, location, capacity, registration window, agreements, curriculum package).
- Guardian communications wired into the existing Phase 5C notification system: a guardian
  email fallback in resolveRecipient(), a parallel enqueueForRegistration()/
  resolveRegistrationRecipient() pair for pre-account registration-stage notifications
  (registration_received/confirmed/needs_action, agreement_required, camp_operational_update),
  five new RootNode-branded templates. Same table, same worker, same masked admin view —
  no second notification system.
- public-offerings-routes.js: the unauthenticated, explicitly-allowlisted offering read API.
- POST /api/integrations/registrations: the website's idempotent registration submission
  contract, same shared-secret boundary as the existing enrollment integration endpoint.

Two real bugs found and fixed during this phase's own testing before any commit:
(1) the five new notification_type values were wired into the worker before the DB's own
CHECK constraint was widened to accept them — every registration-stage notification was
silently failing until a comprehensive smoke test caught it (036 fixes the constraint;
every previously-silent notification catch now at least logs).
(2) firing a notification right after a registration's insert, before its seat reservation
could still fail and roll the row back, created a real foreign-key violation on rollback —
fixed by reordering createRegistration() to validate -> reserve -> insert -> notify.

See docs/OFFERING_AND_REGISTRATION_MODEL.md and docs/PHASE8_ENROLLMENT_OPERATIONS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 23:59:11 -05:00
ced4568andClaude Sonnet 5 df5e39f767 docs(phase7): document the business/product architecture foundation
New: ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md (canonical business/platform
bridge — course families, delivery modes, BUILT NOW/PHASE 7 BUILT/FUTURE), OFFERING_AND_
REGISTRATION_MODEL.md (lifecycle diagrams, capacity mechanics), STUDENT_GUARDIAN_DATA_
MODEL.md (data minimization, RBAC, legal-review items), SCHOOL_AND_COMMUNITY_DELIVERY_
ARCHITECTURE.md (organization/location mapping, explicitly not multi-tenancy),
PHASE7_PRODUCT_ARCHITECTURE.md (phase summary).

Updated: WEBSITE_ENROLLMENT_INTEGRATION.md (documented future offering/registration flow,
current live contract unchanged), platform/README.md (test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:44:13 -05:00
ced4568andClaude Sonnet 5 7d870d8e2b test(phase7): add business/product architecture acceptance suite and Winter Camp rehearsal
- e2e-phase7.js (17/17): organizations/locations, guardians, curriculum packages
  (reference-only), offering capacity/transitions, registration lifecycle, agreements,
  registration-to-enrollment conversion, RBAC, age-guidance-independence, and confirmation
  that existing ROOT_ V1 self-paced/classroom flows are unaffected.
- test-phase7-winter-camp.js (18/18): the full Winter Break Camp operational path with NO
  payment code invoked — organization/location/curriculum-package/cohort/offering setup,
  guardian + adult registration, agreement acceptance, a simulated admin-side commerce
  confirmation, conversion to real enrollments and real cohort membership, real account
  activation via real one-time tokens, a real classroom session, and a REAL Lab task
  (linux-inspect-hardware) completed by the minor student inside a real disposable Lab
  container, confirmed to survive a real logout/login.

Regression: e2e-phase6f.js 17/17, e2e-phase5b.js 32/32 (including its own embedded Phase 5A
checks) on independent fresh stacks.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:44:02 -05:00
ced4568andClaude Sonnet 5 3510979196 feat(platform): business/product architecture — organizations, guardians, offerings, registrations
- organizations, locations, guardians, student_guardians (033) — additive, no existing
  table's meaning changes. Widens programs.family to accept 'bootcamp'.
- curriculum_packages/curriculum_package_items, offerings, instructor_offerings,
  offering_registrations, agreement_definitions/agreement_acceptances (034) — the full
  purchasable-delivery model. Server-defined status transitions only (no client-controlled
  offering/registration status). Capacity uses a single atomic UPDATE (reserved_seats),
  never a SELECT-count-then-INSERT race.
- offerings.js — business logic: reserveSeat/releaseSeat, transitionOffering/
  transitionRegistration (fixed transition tables), convertRegistrationToEnrollment
  (reuses enrollment.js's existing resolveOrCreateStudent/createManualEnrollment rather
  than duplicating student-matching logic).
- enrollment.js — resolveOrCreateStudent gains an optional, default-off returnToken param
  (used only by offerings.js's admin-only conversion route; the public website integration
  endpoint's contract is completely unchanged, per docs/WEBSITE_ENROLLMENT_INTEGRATION.md).
- admin-offerings-routes.js — the full admin/owner API surface. Registration/guardian
  detail is admin/owner only; offering/location listing is any instructor (access_notes
  stripped for a plain instructor).
- A real bug found and fixed during this phase's own smoke testing before any commit: the
  registration route originally reserved a seat BEFORE validating the registration's own
  fields, letting a validation failure consume a real seat and get masked behind a false
  OFFERING_FULL under a full offering. Fixed: validate (create) first, reserve second, roll
  back the registration row if reservation then genuinely fails.

See docs/ROOTNODE_ACADEMY_BUSINESS_AND_PRODUCT_ARCHITECTURE.md and
docs/OFFERING_AND_REGISTRATION_MODEL.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 22:43:52 -05:00
ced4568andClaude Sonnet 5 d066410451 docs(phase6f): document ROOT_ V1 pilot readiness
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
New: PHASE6F_ROOT_V1_PILOT_READINESS.md (phase summary), ROOT_V1_CURRICULUM_MANIFEST.md
(canonical V1 scope), ROOT_V1_PREREQUISITE_MAP.md, ROOT_V1_ASSESSMENT_MAP.md,
ROOT_V1_SYLLABUS.md, ROOT_V1_INSTRUCTOR_GUIDE.md, ROOT_V1_STUDENT_REQUIREMENTS.md,
ROOT_V1_INSTRUCTOR_LED_PILOT.md, WINTER_BREAK_CAMP_CURRICULUM_OPTIONS.md.

Updated: ROOT_PROGRAM_ARCHITECTURE.md (course-sequence table reflects real V1 statuses),
ROOT_PILOT_CURRICULUM.md (self-paced-readiness correction), ROOT_PROGRAM_DURATION_MODEL.md
(real post-integration totals: 64.2 V1 hours / 68.3 program-wide), ROOT_CAPSTONE_READINESS.md
(§1b, the new root-09 course-layer gate), platform/README.md (test list).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:46 -05:00
ced4568andClaude Sonnet 5 bd55b74b4f test(pilot): add multi-student and end-to-end ROOT_ V1 rehearsals
- test-phase6f-root-v1-pilot.js (22/22): a real cohort + live classroom session with three
  students in genuinely different states — on track, a real Lab validation failure/help-
  request/fix cycle, and a real admin Lab-reap/recreate with academic-state persistence
  proven, not assumed. Instructor visibility distinguishes all three.
- test-phase6f-root-v1-end-to-end.js (14/14): one student, entirely through the real
  student API, ROOT 01 through Capstone-eligible, including a real nested-Docker
  container-networking fix and an explicit proof that the real rootnode-capstone assessment
  is genuinely startable at the end — not just that the course tree says complete. Found and
  documented (not fixed) a real, pre-existing gap: the legacy six-stage Capstone unlock_rule
  requires two scenario sets (root-07-m01/m02) that are not course-layer prerequisites of
  anything on the path to root-capstone.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:35 -05:00
ced4568andClaude Sonnet 5 2864fe03dc test(phase6f): add V1 pilot acceptance suite; fix regression from real curriculum changes
- New platform/test/e2e-phase6f.js (17 tests): V1 manifest/exclusion correctness, the full
  V1 prerequisite graph, ROOT 01-04 re-verification, the orientation module's unlock
  behavior, optional-activity handling, the strengthened Capstone gate in both directions,
  instructor-notes RBAC on locked (not just draft) content, isolation, and an explicit
  age-independence check.
- foundations-helper.js: completeThroughRoot06/completeThroughRoot10 updated for the new
  practice-bank intro-lesson activities; new completeThroughCapstoneEligible() helper
  (ROOT 07-m03/m04 + full ROOT 09 + ROOT 12) shared by e2e-phase6f.js and both new drills.
- e2e-phase6a/6b/6d/6e.js: resume-pointer assertions updated for the new orientation
  module; e2e-phase6a.js's and e2e-phase6b.js's own inline ROOT 06/07/09/10/12 completion
  sequences updated for the new intro-lesson activities and the new root-09 Capstone
  dependency; e2e-phase6b.js's stale ROOT 09 status assertion and its draft-activity test
  (previously targeting the now fully-excluded ROOT 08) corrected.
- test-phase6d/6e-foundations-rehearsal.js: same orientation-module fix applied to the
  real-Lab dress rehearsals.

All of 6A (23/23) / 6B (26/26) / 6C (17/17) / 6D (21/21) / 6E (23/23) / 6F (17/17) verified
green on independent fresh stacks. See docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:21 -05:00
ced4568andClaude Sonnet 5 f695b7c95d feat(content): author the ROOT_ V1 orientation module and three practice-bank intro lessons
- root-01-m00-welcome-to-root: program overview + Lab-lifecycle orientation (disposable
  Labs, permanent progress, hints, reset/recreate, help-requests).
- One short intro-lesson activity for each of the three bare practice-bank modules
  (root-06-m01, root-07-m01, root-10-m02), explaining what the bank is and why it has no
  per-scenario teaching of its own — the minimum real authoring needed for genuine
  self-paced readiness without reclassifying them optional (see the curriculum migrations).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:09 -05:00
ced4568andClaude Sonnet 5 e48710a0be feat(curriculum): integrate ROOT_ V1 scope — status corrections, ROOT 08 exclusion, Capstone/ROOT 09 gate, orientation
- Correct stale courses.status for ROOT 05/06/07/09/10/12/Capstone (all modules were
  already ready; the course-level field was never bumped in an earlier phase).
- Exclude ROOT 08 (Storage & Scheduling) from ROOT_ V1 via visibility=draft — nothing
  downstream requires it, and leaving it visible mid-sequence would look like a skipped course.
- Self-correct a same-phase mistake: reclassifying the three instructor-led-only practice
  banks (root-06-m01/root-07-m01/root-10-m02) as optional would have broken Capstone
  reachability, since their scenarios are the literal legacy stage-gate content. Kept them
  required and gave each a real intro-lesson activity instead.
- Strengthen root-capstone's prerequisites to require ROOT 09 (Security) alongside ROOT 12
  (Real Ops) — the Capstone's own scenario pool has zero security overlap, and ROOT 09 only
  reached genuine READY status this phase.
- Add a required orientation module (root-01-m00-welcome-to-root) explaining the program,
  the Learn/Practice/Lab/Troubleshoot/Validate loop, and Lab-lifecycle expectations — no
  orientation content existed anywhere before this phase.

See docs/ROOT_V1_CURRICULUM_MANIFEST.md and docs/PHASE6F_ROOT_V1_PILOT_READINESS.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 18:33:00 -05:00
ced4568andClaude Sonnet 5 87ad1251ce docs(phase6e): document the ROOT Foundations Completion milestone
Adds PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md (the phase's own record: what was authored, the
dash-vs-bash scenario bug found and fixed, the pre-existing lock-enforcement gap found and
fixed, the regression fallout that fix revealed in earlier suites and why each was a stale test
assumption rather than a platform bug, and honest test/rehearsal/regression results).

Updates ROOT_FOUNDATIONS_PILOT_READINESS.md (full re-audit: all 4 ROOT Foundations courses now
READY, not inherited blindly from Phase 6D), ROOT_PROGRAM_DURATION_MODEL.md (real ROOT 02-04
duration data: 24.7 combined hours, landing inside Phase 6D's own estimated range; 67.9 hours
across the full authored program), ROOT_PILOT_CURRICULUM.md (ROOT 02-04 move from NOT IN PILOT
to IN PILOT with full module-level detail; the pilot is now genuinely self-paced-capable for the
entire foundational half, not just instructor-led), ROOT_PROGRAM_ARCHITECTURE.md and
ROOT_CURRICULUM_COVERAGE_MATRIX.md (course/module/scenario totals), and
ROOT_FOUNDATIONS_ARCHITECTURE.md (marked as-built, not just as-designed).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:09:02 -05:00
ced4568andClaude Sonnet 5 59edceea1f test(phase6e): acceptance suite, real-Lab rehearsal, and regression fixes for the lock fix
Adds platform/test/e2e-phase6e.js (23 checks): full ROOT 02/03/04 hierarchy/content/assessment-
pool verification, the complete ROOT 01->02->03->04->05 chain proven on one student (each
course's completion unlocking exactly the next), the direct-API lock bypass proven blocked on
both a Phase 6E course pair and a pre-existing unrelated pair, existing ROOT 06+/Capstone/
is_required behavior confirmed unaffected, self-paced no-instructor-deferral scan,
instructor-notes exclusion, RBAC, persistence, isolation, version stability, content validation.
23/23 pass against both a warm and a freshly-reset stack.

Adds platform/test/foundations-helper.js — a shared completeRootFoundations01Through04/
completeThroughRoot06/completeThroughRoot10 helper, extracted once multiple suites needed the
identical real-completion sequence as test setup, rather than duplicating it four times.

Adds infra/scripts/drills/{run,test}-phase6e-foundations-rehearsal — the real Platform API/
Gateway/Provisioner/disposable-Lab/nested-Docker/Git/k3s dress rehearsal: one real Lab task per
new course (nginx inspection, a real merge conflict, a real container-networking fix) plus the
full chain to a genuine ROOT 05 unlock, with k3s reconfirmed healthy and untouched at the end.
13/13 pass against a real, freshly-built stack.

Fixes the regression fallout from the (correct) course-routes.js lock fix: several earlier
suites' own historical shortcuts had accumulated small ordering/scenario gaps the missing
enforcement had been silently covering for — none were platform bugs. e2e-phase6a/6b/6c/6d.js
now inject the real ROOT 01-04 (and where needed ROOT 05/06/10) completion sequence via
foundations-helper.js before touching ROOT 05+ content; e2e-phase6b.js's bypass-student flow now
completes deployment-rollback/job-task/cronjob-task/container-logging-mcq explicitly (real
scenarios added in later phases that its original ALL_STAGES list never included); e2e-phase6a/
6d.js's own ROOT 01/05 completion tests now complete each module fully (through its lab and
reflection) before moving to the next, since those courses' own modules carry real intra-course
prerequisites. Two now-obsolete Phase-6D-era assertions in e2e-phase6d.js (ROOT 02-04 stay
PLANNED / stay vacuously unlocked) are updated to assert the opposite, with a comment explaining
Phase 6E intentionally changed that fact. All four suites re-verified green fresh.

Also fixes a real, reproducible dash-vs-bash bug caught while testing the new ROOT 01
rehearsal's ordering fix: `&>file` misparses under this image's actual /bin/sh (dash), breaking
a later `$!`-based kill — the rehearsal script now writes multi-line completion loops with
explicit status assertions, matching the discipline the other suites already use.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:48 -05:00
ced4568andClaude Sonnet 5 a09b7aaa53 fix(curriculum): enforce course/module lock on manual-marker completion
POST /api/student/activities/:activityId/complete previously checked only that the activity
exists and the student holds an active PROGRAM enrollment — never whether the activity's own
course/module was actually unlocked for that student. A direct POST could mark any
manual_marker activity (lesson/demonstration/knowledge_check/guided_exercise/reflection)
complete regardless of the real prerequisite chain, the exact class of gap Phase 6B already
fixed once for the capstone's own start route, just never closed for this more general one.

Found while proving the real ROOT 01->02->03->04->05 chain via direct API (this phase's own
explicit requirement) — and confirmed to predate Phase 6E entirely by reproducing it against an
unrelated, pre-existing ROOT 05/06 pair before touching any Phase 6E content.

Fix: the route now resolves the activity's own course/module through the exact same
buildCourseTree() lock computation every other route already trusts, and returns 403 { error:
'locked' } if either is locked — reusing the existing mechanism, not a second, duplicated check.
Verified live: both a ROOT 02 bypass attempt and a ROOT 06 bypass attempt are now correctly
rejected; legitimate unlocked completions are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:28 -05:00
ced4568andClaude Sonnet 5 682ef2de47 feat(root04): author Containers & Docker
Adds all 8 ROOT 04 modules (why containers exist, images & containers, Docker CLI, container
networking, container storage, Dockerfile & image build, Docker Compose, containers →
Kubernetes) — 43 activities, using the nested Docker Engine Phase 6D built and tested. Modules
1, 2, and 8 are conceptual (no lab by design) — the honest bridge module (M8) explicitly names
what Kubernetes adds beyond what this course already taught, without re-teaching Kubernetes
itself.

Adds 5 new hands-on Docker scenarios plus 2 MCQs, all against the real nested Docker Engine:
docker-cli-basics (run/exec/inspect a real container), docker-container-networking (a genuine
"works internally, unreachable from host" port-publishing failure, diagnosed and fixed by
recreating the container with the correct `-p` flag), docker-container-storage (a real
destroy/recreate cycle proving named-volume persistence, not just described), docker-build-image
(a real Dockerfile, a real build, a real custom image run), and docker-compose-multiservice (a
real two-service Compose application with a real published port). All tested end-to-end via the
real scenario-validation API, including negative tests.

Wires rootnode-containers-assessment (2 required + 4 random-draw scenarios) using the existing
Phase 3B assessment engine.

This completes the entire ROOT_ Foundations sequence (ROOT 01-04) — see
docs/PHASE6E_ROOT_FOUNDATIONS_COMPLETION.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:14 -05:00
ced4568andClaude Sonnet 5 e68b6f445a feat(root03): author Git & Collaborative Development
Adds all 6 ROOT 03 modules (why version control exists, the Git mental model, everyday Git,
branching & merging, remote repositories, engineering Git workflow) — 31 activities. Uses only
real LOCAL Git repositories throughout — no Gitea account, no network access, and no
RootNode-canonical-repo credential is ever needed or exposed in a student Lab
(docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §4). Modules 1, 2, and 6 are conceptual bookends with no
lab, matching the flexible readiness standard already used in ROOT 01/02.

Adds 3 new hands-on Git scenarios plus 2 MCQs: git-everyday-basics (init/add/commit, validated
by inspecting real `git log`/`git show` state, not just command output strings — Step 6's
explicit requirement), git-branching-and-merge (a REAL merge conflict caused on purpose by two
branches editing the same line, resolved by the student, validated by confirming the conflict
markers are gone and a real two-parent merge commit exists), and git-remote-clone-push (a real
local bare repository standing in for a remote — clone/commit/push validated by inspecting the
REMOTE side directly, not just the student's own clone). All tested end-to-end via the real
scenario-validation API, including negative tests.

Wires rootnode-git-fundamentals-assessment (2 required + 3 random-draw scenarios) using the
existing Phase 3B assessment engine.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:08:00 -05:00
ced4568andClaude Sonnet 5 e007bcf000 feat(root02): author Linux Administration & Networking
Adds all 6 ROOT 02 modules (Linux system administration, storage & filesystems, the
Networking Mental Model, Linux network inspection, DNS & HTTP, Linux troubleshooting) — 37
activities, each with a real lesson, demonstration/guided exercise, knowledge check, hands-on
Lab, and reflection. The Networking Mental Model module (M3) gets deliberately extra depth per
its "critical module" designation, with no lab by design — conceptual mastery via knowledge
check and guided exercise, matching root-05-m00's own precedent.

Adds 6 new hands-on Linux/networking scenarios plus 2 MCQs: linux-service-inspection (inspects
the platform's own real nginx without touching it — a genuinely safe, real service-inspection
target that avoids ever risking the Lab's own connectivity), linux-service-lifecycle (start/
verify/stop a fully controllable service), linux-storage-inspection, linux-network-inspection
(ip/ss/getent/curl, each framed as answering one specific diagnostic question),
linux-dns-http-diagnosis (a genuine DNS-resolves-but-wrong-port failure, diagnosed and fixed),
and linux-troubleshooting-framework (a real missing-config-file failure, resolved via the
RootNode Observe/Evidence/Hypothesis/Test/Repair/Validate/Prevent framework). All tested
end-to-end via the real scenario-validation API, including negative tests confirming they fail
when unsolved.

Wires rootnode-linux-networking-assessment (2 required + 4 random-draw scenarios) using the
existing Phase 3B assessment engine.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 17:07:46 -05:00
ced4568andClaude Sonnet 5 b6516a74db docs(phase6d): document the ROOT Foundations milestone
Adds PHASE6D_ROOT_FOUNDATIONS.md (the phase's own record: the generic-scenario-engine finding,
the tested container-runtime decision, what was authored, the bugs found and fixed, and honest
test/regression/rehearsal results) and ROOT_FOUNDATIONS_PILOT_READINESS.md (module-by-module
READY/DEFERRED classification for ROOT 01-04 against the unchanged Phase 6C readiness standard —
ROOT 01 READY, ROOT 02-04 DEFERRED by design).

Adds ROOT_PROGRAM_DURATION_MODEL.md — real per-course instructional-hour totals computed from
actual estimated_duration_minutes values (2,931 minutes / 48.9 hours across all authored
content), not invented, with an explicit note on why instructor-led timing isn't a separate
summable field yet.

Updates ROOT_PROGRAM_ARCHITECTURE.md (ROOT 01's new READY status and the real 01-04 prerequisite
chain), ROOT_PILOT_CURRICULUM.md (ROOT 01 added to the pilot, module-level detail), and
ROOT_CURRICULUM_COVERAGE_MATRIX.md (updated module/scenario totals) to reflect what Phase 6D
actually built, without restating Phase 6C's own Kubernetes-scenario tables.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 13:00:41 -05:00
ced4568andClaude Sonnet 5 f5379afd5c test(phase6d): acceptance suite, real-Lab rehearsal, and 6A/6B resume-point fixes
Adds platform/test/e2e-phase6d.js (20 checks): ROOT 01 hierarchy, ROOT 02-04 honestly empty
(not padded), activity ordering, lab-cannot-be-manually-completed, full course completion via
lesson/lab/assessment, the prerequisite chain proven on two students (one who completed ROOT 01,
one fresh), existing ROOT 05+/Capstone state unaffected, is_required unaffected, self-paced
no-instructor-deferral check, instructor-notes exclusion, RBAC, logout/login persistence,
cross-student isolation, version-bump stability, and content validation. 20/20 pass against both
a warm and a freshly-reset dev stack.

Adds infra/scripts/drills/{run,test}-phase6d-foundations-rehearsal — the real disposable-Lab
dress rehearsal: login -> enroll -> lesson/guided-exercise -> real Lab launch -> real bash
hardware inspection (nproc/free/df, written to a real file) -> real validation -> a genuine
permission denial (a second Linux user actually denied via `su -`) then a real chmod fix ->
remaining modules -> assessment -> course completion -> ROOT 02 unlock -> logout/login
persistence -> instructor visibility of the resulting learning state. 11/11 pass against a real
Lab container, no mocking. (First run caught a real triple-escaping bug in the hardware-report
step's inline heredoc — fixed by writing the script to a file and docker-cp-ing it in instead.)

Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js that this phase's own
ROOT 01 addition broke: both suites asserted `resume` landed on ROOT 05's gold module as "the
first real content," which was true before this phase and is now ROOT 01. Both suites' original
intent (resume must never land on a bare-lab bank) is preserved, just against the current front
door. Both suites re-verified green (21/21 and 24/24) against a fresh stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 13:00:17 -05:00
ced4568andClaude Sonnet 5 15b616274b feat(root01): author Computing & Linux Foundations, and the real 01->05 prerequisite chain
Adds all 6 ROOT 01 modules (how computers run software, the shell, filesystem navigation,
files & text, processes, users & permissions foundations), each with a real lesson,
demonstration/guided exercise, knowledge check where warranted, a hands-on Lab, and a
reflection — 35 activities, 29 new content files.

Adds 6 new hands-on Linux scenarios (linux-inspect-hardware, linux-shell-basics,
linux-filesystem-navigation, linux-files-and-text, linux-process-management,
linux-permissions-foundations) plus 2 MCQ scenarios, using the existing generic scenario/
validation engine (docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §1) — every one tested end-to-end via
the real scenario setup/validate API, including negative tests confirming they correctly FAIL
when not actually solved (e.g. the process-management scenario fails while the target process
is still running; the permissions scenario is tested against a real `su - labuser` denial).

Wires rootnode-linux-foundations-assessment (2 required + 4 random-draw scenarios) using the
existing Phase 3B assessment engine.

Adds a real, server-side ROOT 01->02->03->04->05 course prerequisite chain
(courses.prerequisites) — verified live on two students: one who completed ROOT 01 sees ROOT 02
unlock; a fresh student sees ROOT 02 correctly locked while 03/04/05 stay vacuously satisfied
(their own prerequisite courses are still empty). ROOT 06+ and the Capstone are unaffected.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 12:59:52 -05:00
ced4568andClaude Sonnet 5 87526bcf7f feat(labs): add a nested Docker Engine to the Lab image for ROOT 04
Installs docker.io + a static docker-compose v2 binary (same "curl a static binary" pattern
already used for k3s) inside the same already-Privileged Lab container that runs k3s.
scripts/entrypoint.sh starts dockerd (vfs storage driver, to avoid nesting overlay-on-overlay)
non-blockingly right after k3s's own API server is confirmed up — a Docker daemon failure only
logs a warning and never breaks the Kubernetes-only Labs that are the vast majority of usage.

This does not weaken the security boundary: the container was already Privileged (required for
k3s, the existing accepted threat model), so a second runtime inside it grants no new host
capability, and the Lab Host's own real Docker socket is never touched — the nested dockerd
listens on its own /var/run/docker.sock entirely inside the guest's filesystem namespace.

Tested empirically against the real production resource caps (2 vCPU, 4096MB, 512 PIDs,
Privileged: true) before merging: k3s + dockerd + a running nginx:alpine container (with a
published port, curled successfully) + a real docker build all together used 884MB/4096MB and
160/512 PIDs — comfortable headroom in every dimension. See docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md
§3 for the full test log.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 12:59:23 -05:00
ced4568andClaude Sonnet 5 3c815cfeea docs(foundations): define ROOT 01-04 architecture and Lab strategy
ROOT_FOUNDATIONS_ARCHITECTURE.md designs all 26 modules across ROOT 01 (Computing & Linux
Foundations), 02 (Linux Administration & Networking), 03 (Git & Collaborative Development), and
04 (Containers & Docker), using the existing course/module/activity model — outcomes,
prerequisites, activities, and Real Ops connections for each, per docs/COURSE_AUTHORING_STANDARD.md.

ROOT_FOUNDATIONS_LAB_STRATEGY.md documents the central finding this phase is built on: the
existing scenario engine (backend/server.js's runCommand()) executes an arbitrary shell command
string and checks its output — nothing about it is Kubernetes-specific. The existing `lab`
activity type and scenario JSON format already fully support Linux and Git exercises with real,
deterministic validation. No shell_lab/git_lab/container_lab activity type is built or needed.

Also documents the container-runtime decision for ROOT 04, evaluated and tested empirically
(not assumed) against the real Lab image and production resource caps — see the following
commit for the implementation.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 12:59:00 -05:00
ced4568andClaude Sonnet 5 bd0a10fbfb docs(phase6c): pilot readiness standard, pilot curriculum manifest, ROOT 11 scope
Adds the four required Phase 6C documents:

- ROOT_PILOT_READINESS_STANDARD.md — the explicit 8-point sufficiency bar
  used to judge every module this phase, replacing any fixed activity-count
  assumption.
- ROOT11_DEVOPS_PLATFORM_SCOPE.md — evaluates all 7 proposed ROOT 11 modules
  individually (READY TO AUTHOR NOW / PARTIAL-LESSON-ONLY / REQUIRES FUTURE
  LAB ENVIRONMENT), with what current infra supports vs. what GitOps/CI-CD/
  IaC/observability would each require.
- ROOT_PILOT_CURRICULUM.md — the authoritative pilot manifest: instructor-led
  as the first pilot's delivery mode, honest READY/PARTIAL/DEFERRED/NOT IN
  PILOT status per course and module, and what is intentionally excluded
  (ROOT 01-04 never authored, ROOT 08 non-capstone-gating and deferred, ROOT
  11 modules 2-7, three practice-bank modules instructor-led-only this pilot).
- PHASE6C_CURRICULUM_COMPLETION.md — the phase's own record of what was
  upgraded, why, the bug found and fixed, and the pilot readiness conclusion.

Updates ROOT_CURRICULUM_COVERAGE_MATRIX.md with the verified before/after
module counts (21->22 modules, 6->16 READY) and per-module upgrade rationale,
and ROOT_CAPSTONE_READINESS.md with a re-review confirming the unlock gate's
6 stages, and ROOT 08/09's non-gating status, are unchanged by this phase's
curriculum expansion.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:21:09 -05:00
ced4568andClaude Sonnet 5 01d3764138 feat(frontend): visibility note for standalone assessment-type activities
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]>
2026-08-26 11:20:51 -05:00
ced4568andClaude Sonnet 5 903b0ead43 test(phase6c): acceptance suite, real-k3s rehearsal, and 6A/6B staleness fixes
Adds platform/test/e2e-phase6c.js (17 checks): upgraded-module activity
chains, documented-exception promotions, optional-lab non-blocking behavior,
no-fake-completion-from-viewing, idempotent completion, instructor-notes
exclusion and RBAC on new content, security-assessment pool/draw/cross-student
isolation/completion, the capstone gate's documented indifference to ROOT 09,
ROOT 11's honest partial/lesson-only rendering, and version-bump content
stability — plus running validate-course-content.js as part of the suite.
All 17 pass against both a warm and a freshly-reset dev stack.

Adds infra/scripts/drills/{run,test}-phase6c-security-config-rehearsal —
Section 22's real-k3s dress rehearsal: login -> program -> course -> lesson ->
demonstration -> knowledge-check -> guided-exercise -> real Lab -> real
kubectl (create Secret, apply Pod, wait for Ready) -> validation -> progress
persistence across logout/login -> instructor visibility of the resulting
learning state, walking root-07-m02-configuration-and-secrets (upgraded from
PARTIAL this phase). 10/10 pass against a real k3s Lab, no mocking.

Fixes two stale hardcoded assumptions in e2e-phase6a.js and e2e-phase6b.js
that Phase 6C's own legitimate content changes broke: the gold module
(root-10-m01) now has 7 activities (6 required + 1 new optional lab, not 6),
and root-05-m01 now has 2 required manual-marker activities the course-
completion path must also complete (its new bookending lesson/reflection).
Both suites are back to green (21/21 and 24/24) against a fresh stack.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:20:37 -05:00
ced4568andClaude Sonnet 5 7f6e1857c0 feat(curriculum): Phase 6C module upgrades, security assessment bank, ROOT 11 orientation
Upgrades six PARTIAL modules to the gold-module standard (lesson/demonstration/
knowledge-check/guided-exercise/lab(s)/reflection), preserving every existing lab
activity_id and scenario_id: root-06-m03 (jobs/cronjobs/daemonsets), root-07-m04
(ingress/networkpolicy), root-07-m02 (configuration/secrets, converted in place
from a bare 5-lab bank), root-09-m02 (serviceaccounts/rbac), root-09-m03
(network/supply-chain security), root-10-m03 (troubleshooting framework).

root-05-m01 (20-task kubectl-fluency bank) gets a minimal bookending lesson +
reflection rather than a full redesign, since each task already carries its own
explanation. root-12-m01, root-capstone-m01, and root-09-m04 are promoted to
READY via documented exceptions (no new activities) where adding one would
misrepresent their intentional design.

Wires a new rootnode-security-assessment (exam, 60min, hint_policy disabled)
using the existing Phase 3B assessment engine: 3 required hands-on CORE
scenarios + 4 random-draw pools covering SecurityContext, RBAC, network
policy, and the 7 previously-unwired CKS MCQs. Wires the previously-unwired
readiness-probe-task scenario as an OPTIONAL practice lab in root-10-m01,
using the existing is_required model.

Adds root-11-m01-devops-platform-thinking — an honest, lesson-only ROOT 11
orientation module naming DevOps/platform engineering practices already
exercised in ROOT 05-10/12, explicit that GitOps/CI-CD/IaC/observability need
lab infrastructure this platform doesn't yet provide. No fake labs.

Adds infra/scripts/validate-course-content.js: cross-checks the DB against
scenario/content files for duplicate ids, orphans, invalid references, cycles,
sequence collisions, instructor-notes leakage, and (new) self-paced content
deferring explanation to a live instructor. 0 errors against the resulting
22 courses/modules, 163 activities, 98 scenario files.

No application code changed — every gap was satisfiable with existing tables
(course_modules, module_activities, assessment_definitions) and the existing
content-delivery pipeline.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 11:20:17 -05:00
ced4568andClaude Sonnet 5 2db136875f docs(phase6b): document curriculum expansion
PHASE6B_ROOT_CURRICULUM_EXPANSION.md — the phase summary: what was
authored per course, the is_required extension, the capstone unlock
fix, verified scenario coverage before/after (44 -> 88 of 98, 89.8%,
by direct query not estimate), and honest known limitations/Phase 6C
recommendations.

ROOT_CAPSTONE_READINESS.md — the readiness module and the server-side
unlock fix in full detail, including why the module's completion and
the real assessment's unlock gate are deliberately independent systems.

Updated ROOT_CURRICULUM_INVENTORY.md (Phase 6B coverage numbers),
ROOT_PROGRAM_ARCHITECTURE.md (ROOT 08/09 PARTIAL, new module lists per
course), COURSE_CONTENT_MODEL.md (is_required completion rule, a new
content-versioning section per this phase's own requirement),
COURSE_AUTHORING_STANDARD.md (classifying near-duplicate/advanced
scenarios), ENGINEERING_SOP.md (new Phase 6B section),
REMOTE_AND_SELF_PACED_ARCHITECTURE.md (cross-reference), and
platform/README.md (e2e-phase6b.js, the Phase 6B rehearsal, isolation-
per-suite regression guidance).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:09:47 -05:00
ced4568andClaude Sonnet 5 505783e5f7 test(phase6b): add ROOT curriculum acceptance coverage + real-Lab rehearsal
e2e-phase6b.js (24 checks): hierarchy for the 13 new modules across
ROOT 05-10/12/Capstone, prerequisite chains through the newly-inserted
front-door modules, lesson/knowledge-check completion in brand-new
modules, a newly-wired scenario (job-task) completing correctly, the
optional-vs-required (is_required) completion rule, persistence/
isolation/RBAC/draft-hiding proven against the new content, and the
capstone server-side unlock fix in both directions (a direct bypass
attempt rejected with blocking_stage_ids, an ordinary exam unaffected,
a legitimately unlocked attempt still accepted and its completion
reflected in the course content model).

test-phase6b-root-course-rehearsal.js + wrapper (11 checks, real k3s):
one continuous student narrative through a brand-new module (ROOT 07
Services & Service Discovery, not the Phase 6A gold module) — real Lab
launch, the Lab's own setup_commands, a real 'kubectl expose' against
the live cluster, real validate, logout/login/Resume Learning, Lab
destroy/recreate with academic progress intact — plus an instructor
narrative proving Curriculum view, Prepare Labs, and classroom/learning
tooling all still work against the expanded curriculum.

e2e-phase6a.js updated for two of this phase's own intentional,
documented behavior changes (not defects): resume now points at
root-05's new mental-model module instead of the old bare-lab bank, and
root-10's gold module now unlocks the new troubleshooting-framework
module rather than the bare-lab bank directly.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:09:20 -05:00
ced4568andClaude Sonnet 5 e0f954e714 feat(instructor): expand curriculum teaching visibility
CurriculumView.jsx's per-module activity table gains a Required column
(required/optional) so an instructor can see at a glance which of
Phase 6B's new CORE vs. GUIDED/INDEPENDENT/ADVANCED activities a
student needs to complete a module. Reuses the existing Classroom/
Prepare Labs tooling unchanged — no second classroom system.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:08:54 -05:00
ced4568andClaude Sonnet 5 54b35e8ad7 feat(student): polish structured ROOT learning flow
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]>
2026-08-26 09:08:28 -05:00
ced4568andClaude Sonnet 5 de6c5b3286 feat(root): build Real Ops and capstone readiness content
root-12-real-ops gains a Detect -> Triage -> Diagnose -> Mitigate ->
Recover -> Verify -> Learn incident-response framing lesson at the
front of the existing 4-incident module, and a post-incident reflection
at the end — the incidents themselves are unchanged, still deliberately
un-guided ('you are responsible for this environment,' not 'complete
this exercise'). No fixes revealed; instructor notes carry the deeper
solution guidance the student-facing lesson intentionally withholds.

root-capstone-m02-capstone-readiness — a new READY orientation module:
what the Capstone measures, how the timed/randomized draw works, time
management and troubleshooting strategy under a clock, and a genuine
self-assessment checklist. Never publishes the current draw or answers.
See docs/ROOT_CAPSTONE_READINESS.md for why this module's completion is
deliberately independent of the real capstone's server-side unlock gate.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:08:01 -05:00
ced4568andClaude Sonnet 5 831486d664 feat(root): expand troubleshooting and observability curriculum
The Phase 6A gold-standard Health Probes module is preserved byte-for-
byte unchanged — it's the template, not a page to rewrite for
consistency.

root-10-m03-troubleshooting-framework — a new PARTIAL module
introducing a repeatable Observe -> Gather Evidence -> Form Hypothesis
-> Test -> Repair -> Validate -> Prevent Recurrence framework, applied
to a CrashLoopBackOff and a broken Service selector. Lesson,
demonstration (the full framework walked through a real
CrashLoopBackOff, command by command), knowledge check, and real Labs
(crashloop-fix, endpoint-fix-task). Sequenced between the gold module
and the existing bare-lab bank, which now sits behind it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:07:34 -05:00
ced4568andClaude Sonnet 5 960a638ec9 feat(root): add Kubernetes security curriculum
Four new modules, moving ROOT 09 off PLANNED and building the
22-scenario CKS-style bank into a coherent course instead of
disconnected exam tasks — see docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md
for the CORE/GUIDED/INDEPENDENT/ADVANCED/ASSESSMENT-BANK classification
behind every placement.

root-09-m01-security-model-and-securitycontext — READY, full
gold-standard pattern: defense in depth, least privilege, hardening a
Pod with SecurityContext. Lesson, demonstration (a Pod before/after
hardening), knowledge check, guided exercise (a hardening checklist),
real Labs (CORE: non-root/drop-capabilities/read-only-fs/
pod-security-context; GUIDED: seccomp; ADVANCED: AppArmor; INDEPENDENT:
the generic pod-security-context duplicate), and a reflection.

root-09-m02-serviceaccounts-and-rbac — PARTIAL: workload identity and
least-privilege permissions. Lesson + knowledge check + real Labs.

root-09-m03-network-and-supplychain-security — PARTIAL: NetworkPolicy
as a security control (default-deny), image/registry trust, TLS
ingress. Lesson + knowledge check + real Labs.

root-09-m04-cluster-hardening-and-runtime — PARTIAL, deliberately
survey-depth: Pod Security Admission and a lighter treatment of
audit-policy/runtime-sandboxing topics that don't belong in the core
daily-driver path.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:07:07 -05:00
ced4568andClaude Sonnet 5 73cea1ba1d feat(root): add storage and scheduling curriculum
Two new PARTIAL modules, moving ROOT 08 off PLANNED for the first time:

root-08-m01-storage-fundamentals — ephemeral vs. persistent storage,
PV/PVC/StorageClass. Lesson + knowledge check + real Labs (emptydir-pod,
pv-pvc-mount, pvc-dynamic-task, storageclass-mcq).

root-08-m02-scheduling-resource-governance — how the scheduler places
Pods and how quotas/limits/priority/disruption budgets shape that
placement. Lesson + knowledge check + real Labs (node-label-selector,
node-taint-toleration, pod-affinity-mcq, priority-class-task,
pod-disruption-budget-task, resource-quota-ns, limitrange-task).

cluster-upgrade-mcq and etcd-backup-mcq deliberately left unwired —
ROOT 11/DevOps territory, out of this phase's scope, not overlooked.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:06:43 -05:00
ced4568andClaude Sonnet 5 dbcc210889 feat(root): author networking and configuration curriculum
root-07-m03-services-and-service-discovery — a new, full gold-standard-
pattern module: the Service/Endpoints/DNS request path, choosing the
right Service type. Lesson, demonstration (a Service with zero
Endpoints — the single fastest diagnostic check for this failure
class), knowledge check, guided exercise (ClusterIP vs. NodePort vs.
LoadBalancer for three real components), a real Lab
(expose-service-basics), and a reflection.

root-07-m04-ingress-networkpolicy-foundations — a new PARTIAL module:
Ingress and NetworkPolicy at the architectural level (the security-
control framing of NetworkPolicy is deliberately deferred to ROOT 09).
Lesson + knowledge check + real Labs (ingress-task, network-policy).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:06:19 -05:00
ced4568andClaude Sonnet 5 86ea035e9b feat(root): author workloads curriculum
root-06-m02-deployments-scaling-updates — a new, full gold-standard-
pattern module: why Deployments/ReplicaSets exist, how a rolling update
actually progresses, and what rollback restores. Lesson, demonstration
(a healthy rollout vs. one stalled on ImagePullBackOff), knowledge
check, guided exercise (planning a safe rollout strategy), real Labs
(rolling-update-task, deployment-rollback), and a reflection.

root-06-m03-jobs-cronjobs-daemonsets — a new PARTIAL module: Jobs,
CronJobs, and DaemonSets as distinct workload shapes from a
Deployment's always-on model. Lesson + knowledge check + real Labs
(job-task, cronjob-task).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:06:00 -05:00
ced4568andClaude Sonnet 5 25409d3bd7 feat(root): author Kubernetes foundations curriculum
root-05-m00-kubernetes-mental-model — a new, full gold-standard-pattern
front-door module for ROOT 05: what Kubernetes solves, the desired-state
model, the control-plane/worker split, and Pods/Namespaces/labels
through kubectl. Lesson, demonstration (watching a Pod get replaced
after manual deletion), knowledge check, guided exercise (planning a
Namespace layout), a real Lab (namespaces-basics), and a reflection —
sequenced ahead of the existing bare kubectl-fluency lab bank.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:05:40 -05:00
ced4568andClaude Sonnet 5 046a0fba18 fix(capstone): enforce unlock rule server-side
POST /api/assessments/:definitionId/start only ever checked that the
definition existed and was active — it never checked unlock_rule. GET
/api/capstone correctly computed and displayed lock state, and the
student UI never exposed a start action while locked, but a direct POST
to this route bypassed the check entirely (documented as a known gap in
docs/PHASE6A_ROOT_COURSE_FOUNDATION.md §10). Student UI state is not the
enforcement boundary.

Fixed by binding to the exact same computeCurriculumState() gate
/api/capstone already relies on, rather than re-parsing
definition.unlock_rule independently — one computation of 'is this
unlocked,' not two that could drift. Only a definition the curriculum
path actually names as ITS capstone is gated this way; an ordinary exam
is completely unaffected. Returns a clean 403 { error: 'locked',
blocking_stage_ids } — the same shape /api/capstone already exposes.

Found while fixing this: e2e-phase5b.js's own capstone-completion test
had been passing only because of this exact gap — it never included the
real-ops stage in its completion loop before attempting to start the
capstone, and the missing unlock check let it succeed anyway. Fixed
alongside the platform code (REAL_OPS added to ALL_NON_CAPSTONE), not
routed around.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:05:18 -05:00
ced4568andClaude Sonnet 5 bee57d89fd feat(root): wire ROOT 05-10/12/Capstone curriculum expansion
023_phase6b_curriculum.sql — additive on top of 022_course_content.sql:
13 new teaching modules (ROOT 08/09 move from PLANNED to PARTIAL; ROOT
05-07/10 gain a new front-door module sequenced ahead of the existing
bare-lab bank; ROOT 12 gains an incident-response framing lesson +
reflection; Capstone gains a readiness module). No existing row is
deleted; a small number of existing rows have their sequence/
prerequisites updated so a new teaching module supersedes the bare-lab
bank as the front door — never their scenario_id/activity_type/
completion_behavior, so no student's derived completion changes meaning.

is_required (new module_activities column, defaults true — every Phase
6A activity unaffected) lets a module distinguish CORE teaching from
GUIDED/INDEPENDENT-practice duplicates and ADVANCED/optional extras.
course-progress.js's totals() now considers a module complete once
every REQUIRED activity is done, not literally every activity — an
incomplete optional one no longer blocks completion, though it still
counts toward the displayed percentage.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:04:55 -05:00
ced4568andClaude Sonnet 5 82f17a05e8 docs(curriculum): map Phase 6B ROOT coverage
Course -> module -> objective -> lesson/demo/knowledge-check/guided-
exercise -> scenario(s) -> Real Ops connection -> status, for ROOT 05-10,
12, and Capstone, grounded in the Phase 6A inventory. Every scenario
placement has an intentional reason, not a coverage-percentage target —
includes an explicit 'duplicate concepts identified' section for the
near-duplicate scenario pairs (pod-security-context vs.
cks-pod-security-context, rbac-clusterrole vs. cks-rbac-clusterrole,
network-policy's architectural vs. security-control framing) and which
scenarios are deliberately left out of structured curriculum entirely.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-26 09:04:30 -05:00
ced4568andClaude Sonnet 5 fd38d06055 docs(phase6a): document RootNode course foundation
COURSE_AUTHORING_STANDARD.md — the standard every future lesson/module
must follow (target learner, prerequisites, measurable objectives,
concept-before-lab, instructor notes, terminology, versioning, review
requirements), grounded in the gold-standard module as the reference
implementation.

PHASE6A_ROOT_COURSE_FOUNDATION.md — the phase summary: youth-ladder
context, the Google Workspace status already recorded in Phase 5C's own
docs, architecture/build summary, honest content states, test results,
explicit non-goals, and known limitations/Phase 6B recommendations.

Updated ENGINEERING_SOP.md (new Phase 6A section + a curriculum-content
cross-reference), REMOTE_AND_SELF_PACED_ARCHITECTURE.md (a 7th
closed gap: self-paced enrollment now resolves to a real course tree,
not just a scenario list), LAB_ARCHITECTURE.md (Phase 6A reuses the
disposable Lab unmodified, verified by a real rehearsal), and
platform/README.md (course model in the api/ summary, new e2e suite
descriptions for 5A/5B/5C/6A which were missing entirely, and the
real-Lab rehearsal script).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:30:11 -05:00
ced4568andClaude Sonnet 5 8ffc923a84 test(phase6a): add course-delivery acceptance coverage + real-Lab rehearsal
e2e-phase6a.js (21 checks): hierarchy/ordering, honest READY/PARTIAL/
PLANNED states, all three completion behaviors (manual marker/
scenario-derived/assessment-derived), prerequisite locking (including
the zero-content-is-vacuously-satisfied rule), resume, persistence
across logout/login, cross-student isolation, RBAC for self-paced
no-cohort students, draft-content hiding, and content-version
stability.

test-phase6a-course-rehearsal.js + run-phase6a-course-rehearsal.sh: a
real-k3s dress rehearsal of the gold-standard module end to end — a
student reads the real lesson content, launches a real disposable Lab,
the Lab's own POST /api/scenarios/:id/setup (called through the
Gateway's existing /lab/:token/* proxy exactly as the browser UI does)
stands up the broken api-gateway deployment, a real kubectl patch fixes
the readiness probe, and the module completes and unlocks the next one.
No mocked KubeKosh anywhere in this suite.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:30:00 -05:00
ced4568andClaude Sonnet 5 db927b8081 feat(instructor): add curriculum teaching view
CurriculumView.jsx — a new 'Curriculum' tab in the admin portal:
program structure (courses -> modules -> activities), mapped scenarios/
assessments, estimated teaching time, and instructor notes rendered per
activity. A student_id lookup shows one student's real per-activity
progress alongside the same structure, for spotting where someone is
stuck. Read-only, no WYSIWYG editor — course/module authoring stays
Git/migration-based this phase per docs/COURSE_CONTENT_MODEL.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:50 -05:00
ced4568andClaude Sonnet 5 8c09ef8f3e feat(student): add structured course learning experience
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]>
2026-08-25 23:29:41 -05:00
ced4568andClaude Sonnet 5 28c925069b feat(root): author the health-probes gold-standard module
content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/:
lesson (probe concepts) -> demonstration (reading kubectl describe pod
Events for a probe failure) -> knowledge check -> guided exercise
(write a readiness probe on paper) -> Launch Lab intro
(rootnode-probe-port-mismatch) -> reflection (root cause + Real Ops
prevention takeaway). 5 of 6 activities carry instructor notes (demo
plan, misconceptions, timing, discussion questions, extensions).

This is the reference implementation docs/COURSE_AUTHORING_STANDARD.md
documents for all future RootNode lesson authoring.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:32 -05:00
ced4568andClaude Sonnet 5 e2298e95de feat(root): wire course/module/activity routes into the Platform API
course-routes.js — student-facing: the full course tree through an
enrollment (GET /api/student/programs/:id/courses) and the lesson
completion marker (POST /api/student/activities/:id/complete, refused
for anything but a manual_marker activity — a lab/assessment can never
be faked complete through this route).

admin-course-routes.js — instructor-facing, read-only (no WYSIWYG CMS
this phase): program structure with instructor notes
(GET /api/admin/programs/:id/courses) and per-student progress
(GET /api/admin/students/:id/courses), scoped via
requireStudentRelationshipAccess — the same self-paced-no-cohort rule
Phase 5C's notification routes already established, not
requireStudentCohortAccess (which would 403 a plain instructor out of
every self-paced student).

Dockerfile now also COPYs content/ alongside the existing scenarios/ —
same 'read-only, never executed' reasoning as that line.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:24 -05:00
ced4568andClaude Sonnet 5 fe702754cd feat(content): add course/module/activity content model
New tables (courses, course_modules, module_activities,
activity_completions) layered over the existing scenario/assessment/
achievement systems — a lab activity references an existing scenario_id,
an assessment/capstone activity references an existing
assessment_definition_id, neither duplicated. Long-form lesson prose
lives in git-tracked Markdown (content/root/), split into shared vs.
instructor-only sections by a '## Instructor Notes' heading convention
(content.js) rather than a second file or a CMS.

course-progress.js derives completion from EXISTING sources only:
auto_scenario reads the progress table, auto_assessment reads
achievements by assessment_definition_id, manual_marker is the one
genuinely new piece of state (for lesson-only activities with nothing
else to derive completion from). Prerequisite locking treats a
zero-content (PLANNED) prerequisite as vacuously satisfied, so an
honestly-unbuilt course never permanently locks everything after it.

Found by testing: computing a level's prerequisite lock and stripping
its working totals in the same map iteration made the result depend on
iteration order. Fixed with two explicit passes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:13 -05:00
ced4568andClaude Sonnet 5 f152b9a489 docs(curriculum): define the ROOT_ program architecture
13 courses (ROOT 01-12 + Capstone) under the existing single
root-kubernetes-foundations program — root_ is one continuous ladder a
student enrolls in once, not thirteen separate purchases. Honest
READY/PARTIAL/PLANNED states grounded in the actual inventory: no course
reaches READY yet, 5 are fully PLANNED (no Linux/Git/Docker/DevOps
scenario content exists anywhere in this repo), 7 are PARTIAL (real
labs, no lessons yet). Documents the prerequisite graph and why it
deviates from a strictly linear 01->12 chain where the existing content
doesn't actually support one.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:29:03 -05:00
ced4568andClaude Sonnet 5 e4cbcf6c40 docs(curriculum): inventory existing RootNode learning assets
Full inventory of scenarios/data/ (98 scenarios), scenarios/bundles/,
rootnode-path.json's stage structure, assessment_definitions, and the
programs/achievements model — gathered by inspection before any Phase 6A
design work, nothing modified to produce this.

Only 3 scenarios are RootNode-authored; the other 95 (including all 44
wired into rootnode-path.json) are generic upstream Kubernetes/CKS
content. 54 scenarios exist with no rootnode-path.json stage at all —
a real, working content bank for future ROOT 07/08/09/10 modules,
inventoried and grouped by category rather than left undiscovered.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-25 23:28:55 -05:00
ced4568 4c0a950afb fix: retry Google's transient domain-wide-delegation propagation-delay error
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
Live Google Workspace delivery validation surfaced a real, self-resolving
Google-side condition: freshly-authorized domain-wide delegation can take
up to about a minute to propagate across Google's auth infrastructure,
producing intermittent unauthorized_client on the OAuth token exchange
even with correct configuration. gws-provider.js now classifies
unauthorized_client as a temporary (retryable) error instead of permanent,
so the existing bounded-backoff worker absorbs it automatically.

Also documents the alias-vs-delegated-user env passthrough in
docker-compose.dev.yml, and records both real findings (this one and the
GOOGLE_DELEGATED_USER alias distinction) plus the full live-delivery
validation outcome in the runbook and Phase 5C doc.

Validated live: isolated gws-email-test.js send (DKIM/SPF passing,
delivered) and a full real lifecycle test (create student, enrollment,
real activation email, activate, login, see program, real password-reset
email, reset, login with new password) via [email protected].
2026-08-25 21:36:12 -05:00
ced4568 7baf7074e5 fix(email): support a delegated-mailbox alias distinct from the delegated Workspace account
Real-world case found during setup: the sending address ([email protected]) is an
alias of an existing Workspace user, not its own paid seat (a deliberate cost-saving choice).
Domain-wide delegation authorizes a real Workspace ACCOUNT, not an arbitrary alias string, so
using the alias directly as the delegated 'sub' claim would have been wrong. Added
GOOGLE_DELEGATED_USER (the account that actually owns the alias and was granted delegation) as
an optional override, falling back to EMAIL_FROM_ADDRESS unchanged when the sending address is
itself a primary account. EMAIL_FROM_ADDRESS remains the visible From: header in both cases.
Documented in docs/RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md with a concrete example.
2026-08-25 17:38:59 -05:00
ced4568 777efd168d docs(phase5c): enrollment communications documentation + Google Workspace runbook
- PHASE5C_ENROLLMENT_COMMUNICATIONS.md: the full design doc.
- RUNBOOK_GOOGLE_WORKSPACE_EMAIL.md: every REQUIRES GOOGLE WORKSPACE ADMIN ACTION step
  (mailbox, service account, domain-wide delegation scoped to gmail.send only, SPF/DKIM/DMARC),
  environment variables, testing procedure, production secret handling, rotation, revocation,
  troubleshooting, quotas.
- RUNBOOK_STUDENT_ACTIVATION.md: day-to-day operation of the pipeline from /admin.
- WEBSITE_ENROLLMENT_INTEGRATION.md: explicit website-vs-RootNode responsibility boundary,
  the new contact_email field, and closes the 'RootNode doesn't send activation email yet'
  known limitation this doc previously documented.
- RUNBOOK_SELF_PACED_ENROLLMENT.md, RUNBOOK_STUDENT_ACCOUNT_LIFECYCLE.md: point at the new
  automatic-email behavior and the Resend activation control.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md, ENGINEERING_SOP.md: close the enrollment-
  communication gap; new ENGINEERING_SOP §16.
- PRODUCTION_VALIDATION_CHECKLIST.md, README.md, platform/README.md: cross-references and a
  Phase 5C production-validation section.
2026-08-25 14:45:18 -05:00
ced4568 b38c46aaec test(phase5c): permanent regression suite + enrollment/activation dress rehearsal
- platform/test/e2e-phase5c.js (29 checks): activation-vs-confirmation routing,
  duplicate/concurrent webhook idempotency, temporary/permanent/exhausted retry
  classification, resend/regenerate token invalidation, activation/reset single-use and
  expiration, HTML-injection regression, production token-secrecy gating via a real container
  recreate with COOKIE_SECURE=1, minor/contact-email resolution, RBAC scoping, metrics/audit
  hygiene, and a real Platform API restart.
- infra/scripts/drills/test-phase5c-enrollment-activation.js (10 checks): one continuous
  story — website enrollment through a real rendered/extracted activation email, a real Lab
  launch and scenario completion, a second enrollment's confirmation email, a full
  password-reset round trip, and the minor/no-email contact-email path.
- infra/scripts/gws-email-test.js: the opt-in (RUN_GWS_EMAIL_TEST=true) live Google Workspace
  delivery test — does nothing without the explicit flag, sends exactly one message to one
  operator-configured recipient when enabled.
2026-08-25 14:44:25 -05:00
ced4568 be92be81cd feat(observability): notification delivery metrics, no PII/high-cardinality labels
- rootnode_notifications_total{notification_type,status} (computed fresh from the DB, like
  the existing enrollment_breakdown gauge — never drifts from what's actually stored),
  rootnode_notification_queue_depth, rootnode_notification_oldest_queued_seconds,
  rootnode_notification_delivery_attempts_total{provider,result} (a real in-process counter —
  per-attempt outcomes aren't reconstructable from the DB after the fact),
  rootnode_student_activations_{success,invalid}_total. Every label is a bounded enum —
  never student_id/email/token/notification_id/external_event_id.
2026-08-25 14:43:48 -05:00
ced4568 f25535027c feat(admin): notification operations — resend activation, delivery status, RBAC-scoped views
- notification-routes.js: global list (owner/admin only), per-student list (scoped via
  requireStudentRelationshipAccess), resend-activation (mechanically the same as
  regenerate — Phase 5A's token model already guarantees only the latest is ever valid, so one
  action covers both), a dev-only preview route (unconditionally refuses when
  COOKIE_SECURE=1, mirroring student-auth-routes.js's existing dev_reset_token gate).
- NotificationsView.jsx: owner/admin global notification list, paginated, matching
  AuditView.jsx's exact convention.
- StudentsView.jsx: student detail gained a Resend activation control and a Notifications
  history card; Credentials card now explains the automatic-email behavior.
- AdminPortal.jsx: new Notifications nav entry (owner/admin only).
2026-08-25 14:43:10 -05:00
ced4568 3c7f0ca59c feat(activation): wire notifications into enrollment/auth, real password-reset delivery
- integration-routes.js: accepts student.contact_email, calls notifyForEnrollment on a
  genuinely new enrollment only (never on an idempotent replay — inherits Phase 5B's proven
  enrollment-level idempotency, no separate notification dedup needed).
- admin-routes.js: create-student/convert/reset-password now enqueue the corresponding
  notification using the SAME token already minted synchronously (never mints twice, which
  would silently invalidate the token just handed back in the response). Fixed a real bug:
  request-password-reset would mint a reset token for a never-activated student, which makes
  no sense and would confuse them — now falls through to the generic anti-enumeration response.
- admin-programs-routes.js: manual enrollment triggers the same notification decision as the
  website path.
- student-auth-routes.js: /activate records a metric; /change-password and /reset-password
  both queue a password_changed security notice; /request-password-reset now actually delivers
  email (production still never returns the token — unchanged anti-enumeration/isProduction
  gating, just now with a real send behind it).
- rbac.js: requireStudentRelationshipAccess/checkStudentRelationshipAccess — the notification
  visibility rule (cohort-shared or an enrollment they created), reusable beyond just
  enrollments.
- server.js: wires notification-routes.js and starts the notification worker in main().
- docker-compose.dev.yml: PUBLIC_APP_ORIGIN, EMAIL_FROM_NAME, and a parametrized COOKIE_SECURE
  (was hard-commented-out; needed so e2e-phase5c.js can actually prove production gating).
2026-08-25 14:42:34 -05:00
ced4568 9d382c95c5 feat(email): provider-neutral email service — Google Workspace (Gmail API) + dev provider
- email/provider.js: the seam. EMAIL_PROVIDER unset always defaults to the dev provider,
  regardless of any other setting — misconfiguration means email silently doesn't send for
  real, never that a dev/test run accidentally reaches a real student.
- email/gws-provider.js: Gmail API users.messages.send via a service account with Workspace
  domain-wide delegation, impersonating one dedicated mailbox. No human password, no SMTP
  basic auth, no googleapis/google-auth-library dependency — a hand-rolled RS256 JWT-bearer
  OAuth exchange + one REST call (~120 lines), matching this codebase's established
  hand-roll-before-adding-a-dependency convention.
- email/dev-provider.js: never makes a network call, returns the rendered message back to the
  caller for dev/test inspection. Supports magic +forcetempfail/+forcepermfail recipient
  markers so the permanent test suite can deterministically exercise retry/failure
  classification without needing a real flaky provider.
- email/templates.js: activation/enrollment-confirmation/password-reset/password-changed,
  HTML + plain-text, no external images/tracking/JS. Every interpolated value is HTML-escaped
  in the HTML body (a real injection finding — display_name can come straight from a website
  integration event).
- notification-worker.js: a setInterval poller (same pattern as sessions.js's sweeper), claims
  via notifications.js, renders via templates.js, builds activation/reset URLs from
  PUBLIC_APP_ORIGIN, stores a dev-only preview (never in production) so tests can extract the
  real URL without a mailbox.
2026-08-25 14:42:01 -05:00
ced4568 5183a6af09 feat(notifications): durable notification lifecycle + minor-safe recipient resolution
- migrations/021: notification_events (queued/sending/sent/failed/cancelled, bounded
  exponential backoff via attempt_count/next_attempt_at, a classified last_error_code, never a
  raw stack trace or provider credential).
- notifications.js: enqueue (records a no-recipient case as already-failed rather than
  silently dropping it), claimNext (FOR UPDATE SKIP LOCKED — safe if two worker processes ever
  run), markSent/markFailedOrRetry (bounded retry, terminal-state token scrubbing),
  notifyForEnrollment (the shared activation-vs-confirmation decision every enrollment-creation
  path calls), maskEmail/toAdminView (the only shapes any admin API ever returns).
- enrollment.js: enrollmentContactEmail() — reads back the minor/no-email transitional
  contact-email convention (stored in the existing enrollments.metadata JSONB, never written to
  students.email) that createFromIntegrationEvent/createManualEnrollment now accept.
2026-08-25 14:41:29 -05:00
ced4568 1c08a30c7f docs(phase5b): self-paced enrollment documentation + website integration contract
- PHASE5B_SELF_PACED_ENROLLMENT.md: the full design doc.
- RUNBOOK_SELF_PACED_ENROLLMENT.md: operational runbook for /admin -> Programs.
- WEBSITE_ENROLLMENT_INTEGRATION.md: the complete contract for the separate website team —
  endpoint, auth, request/response schema, idempotency/retry rules, student matching, minor/
  no-email behavior, error responses, what not to send, local testing, production-secret
  handling. Written to be usable without reading the Platform API source.
- REMOTE_AND_SELF_PACED_ARCHITECTURE.md: closes the self-registration/curriculum-assignment/
  delivery-mode/enrollment-model gaps this doc previously flagged as Phase 5+ scope.
- ENGINEERING_SOP.md: new §15. LAB_ARCHITECTURE.md, PRODUCTION_VALIDATION_CHECKLIST.md,
  README.md, platform/README.md: cross-references and a Phase 5B production-validation section.
2026-08-25 12:10:13 -05:00
ced4568 23f881884d test(phase5b): permanent regression suite + self-paced dress rehearsal
- platform/test/e2e-phase5b.js (32 checks): program seed, entitlement gating
  (pending/future/expired/cancelled), cross-student isolation, real Lab launch/destroy/
  reaper-reconciliation/recreate persistence, server-derived idempotent completion via a full
  real capstone completion, the integration boundary (auth, idempotency, real concurrency,
  unknown/inactive program rejection, minor/no-email path, no card data), RBAC scoping, a
  regression test for the process-crash bug this suite found, Phase 5A smoke, and a real
  Platform API restart.
- infra/scripts/drills/test-phase5b-selfpaced-rehearsal.js (15 checks): one continuous story —
  activation, a simulated website enrollment event, real Lab work, Lab destroy, a new session,
  Continue Learning, a fresh Lab, full real capstone completion, Completed transition, and a
  second uninvolved student proven unable to reach any of it.
2026-08-25 12:09:34 -05:00
ced4568 f89a769540 feat(observability): enrollment/completion metrics, no PII/high-cardinality labels
- metrics.js: rootnode_enrollments_total{status,delivery_mode}, rootnode_selfpaced_active_
  enrollments, rootnode_program_completions_total{program_id}, rootnode_enrollment_events_
  {created,duplicate,rejected}_total. Every label is a bounded enum or an admin-curated slug —
  never student_id/email/enrollment_id/external_reference.
2026-08-25 12:09:17 -05:00
ced4568 4bc202e5f3 feat(integration): provider-neutral website enrollment boundary
- integration-routes.js: POST /api/integrations/enrollments — separate from student/instructor
  session auth entirely (no cookie read), pre-shared-secret header auth matching the existing
  Gateway<->Provisioner trust-boundary pattern. Idempotent via a DB unique constraint on the
  caller's own event_id (not app-level check-then-insert) — proven safe under real concurrent
  duplicate requests. Student resolution is create-or-match by email OR login_identifier
  (minor/no-email support), reusing Phase 5A's exact credential-issuing path. A small,
  provider-neutral status vocabulary (paid/active/confirmed/pending/awaiting_payment) — never
  a raw payment-provider object model.
- docker-compose.dev.yml: INTEGRATION_SHARED_SECRET, optional in dev (fails closed if unset).
2026-08-25 12:09:02 -05:00
ced4568 1b6da0a966 feat(selfpaced): student-facing My Programs dashboard
- 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.
2026-08-25 12:08:44 -05:00
ced4568 17160da89e feat(admin): Programs area, enrollment management, student enrolled-programs view
- admin-programs-routes.js: program CRUD (admin/owner authoring, all roles can view — matches
  assessment_definitions' convention), scoped enrollment listing, manual enroll, activate/
  cancel (via rbac.js's requireEnrollmentAccess).
- ProgramsView.jsx: cohort-picker-free program list/create, per-program detail with
  enrollment table and a manual-enroll form.
- StudentsView.jsx: student detail gained an 'Enrolled programs' card.
- AdminPortal.jsx: new Programs nav entry.
2026-08-25 12:08:29 -05:00
ced4568 611e1ac4ba feat(enrollment): enrollment lifecycle, entitlement gating, self-paced student API
- 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.
2026-08-25 12:08:12 -05:00
ced4568 895e16fea4 feat(programs): first-class program model — init_/compile_/root_ ladder, seeded root-kubernetes-foundations
- migrations/019: programs table (slug PK, matching assessment_definitions/cohorts
  convention), family (init/compile/root) and delivery_mode (self_paced/cohort/hybrid) as real
  CHECK-constrained enums, versioned like assessment_definitions so an edit to
  curriculum_path/delivery_mode never silently changes what an existing enrollment means. Seeds
  exactly one program wrapping the existing RootNode/Kubernetes curriculum and capstone — no
  new curriculum content invented.
- programs.js: list/get/create/update, with automatic version bump only when a
  snapshot-relevant field actually changes.
2026-08-25 12:07:34 -05:00
ced4568 896c230142 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.
2026-08-25 01:20:13 -05:00
ced4568 bfa16d9162 test(phase5a): permanent regression suite + dress rehearsal drill
- platform/test/e2e-phase5a.js (21 checks): the two-student RBAC matrix (student can't reach
  any /api/admin/* route, assigned/unassigned/viewer instructor access) and the full password
  security checklist (correct/wrong/unknown-identifier/deactivated/immediate-deactivation/
  rate-limit/rate-limit-survives-a-real-restart/change-password/session-revocation/
  forgot-password anti-enumeration/admin-issued reset/CSRF), plus a classroom lifecycle smoke
  pass. Replaces the throwaway smoke5a/smoke5b scripts used during development.
- infra/scripts/drills/test-phase5a-rehearsal.js (15 checks): 5 pilot students with real
  seeded pre-existing 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 zero data was orphaned.
- Both run against a live docker-compose.dev.yml stack over real HTTP, not mocks.
2026-08-25 01:20:02 -05:00
ced4568 7557fdc753 feat(admin): student credential management, CSV import UI, remote classroom control center
- StudentsView: credential status in the roster table, a create-student form
  (email/login-ID/cohort), convert/reset-password/revoke-sessions controls per student, a
  bulk CSV import form. Every one-time activation/reset link is surfaced exactly once, never
  re-fetchable.
- ClassroomView: 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). Polls the dashboard endpoint
  rather than a second push channel.
- AdminPortal: new Classroom nav entry. CohortsView's existing quick-add-student call updated
  for createStudent's new object-argument signature.
2026-08-25 01:19:54 -05:00
ced4568 5c7e49c399 feat(student): live student experience — real login, classroom, profile
- 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.
2026-08-25 01:19:44 -05:00
ced4568 ee8ccb2ab6 feat(classroom): remote classroom backend — lifecycle, presence, help queue, dashboard
- classroom.js: create/start/end/cancel (DB-level partial unique index guarantees at most one
  live classroom per cohort; app layer catches 23505 and returns a clean ALREADY_LIVE), join/
  leave/heartbeat, derivePresence (pure, derived from last_seen_at at read time — no stored
  status to go stale), help-request create/acknowledge/resolve, announcements, current-activity
  (guidance only, never redirects a browser or marks work complete), append-only event log.
- admin-classroom-routes.js: instructor-facing classroom API, RBAC via rbac.js's
  requireClassroomAccess/requireClassroomWriteAccess/requireHelpRequestWriteAccess (resolves
  through to the classroom's cohort, same owner/admin/instructor/viewer rules as everywhere
  else cohort-scoped). The dashboard route (GET /api/admin/classroom-sessions/:id) combines
  real Provisioner Lab state and real academic records with classroom.js's own data — no
  second Lab-state or academic-state system.
- classroom-routes.js: student-facing API (live classroom, join/leave/heartbeat, help-request,
  announcements), requireEnrolledClassroom middleware.
- prepare-labs.js: shared prepareLabsForCohort, extracted so the existing cohort-level and new
  classroom-level Prepare Labs routes share one implementation; maps Phase 4.1 admission
  control's CAPACITY_EXCEEDED into a capacity_waiting per-student result.
2026-08-25 01:19:33 -05:00
ced4568 d34ddae458 feat(student): admin student-account management + CSV import, session hardening
- admin-routes.js: create-with-credentials, /convert, /reset-password, /revoke-sessions,
  /sessions, /import (transactional CSV import — whole file validated, one Postgres
  transaction, never accepts a password/hash column from input). Fixed GET
  /api/admin/students to select an explicit safe column list instead of SELECT * — the new
  password_hash/credential_token_hash columns must never be exposed through this route.
- server.js: /api/login now also blocks on credential_token_hash (not just password_hash) —
  closes a real window where a student mid-conversion stayed reachable via the old
  bare-student_id path. studentSession middleware now re-verifies status='active' on every
  request, not just at session creation, matching instructorSession's existing behavior.
- sessions.js: revokeAllStudentSessions, listStudentSessions.
- rbac.js: extracted checkCohortAccess/checkCohortWriteAccess as shared core functions so the
  new classroom/help-request RBAC (next commit) reuses the exact same authorization rule
  instead of a parallel copy.
- student-csv.js + test: hand-rolled parser (no new dependency), unit tested.
- prometheus.yml.j2: activated the platform-api scrape job.
2026-08-25 01:19:19 -05:00
ced4568 2f09b1a498 feat(student-auth): real student credentials — email/login-ID + password, token-based activation and reset
- migrations/017: email, login_identifier, password_hash, last_login_at, credential_token_*
  on students. Fully additive, both nullable/independent, no destructive change.
- students-auth.js: shared credential/token lifecycle (issue/consume/find), reusing
  instructors' exact scrypt password.js verbatim.
- student-auth-routes.js: /api/student/login, /activate, /change-password,
  /request-password-reset (anti-enumeration, dev-only token echo), /reset-password, /profile.
- metrics.js: hand-rolled /metrics Prometheus exporter, login success/failure counters, no
  student/instructor ID ever used as a label value.
- migrations/018: classroom_sessions/participants/events/announcements/help_requests, with a
  partial unique index enforcing at most one live classroom per cohort at the DB level.
2026-08-25 01:18:59 -05:00
ced4568 5bc0743b08 docs(phase4.1): pre-production hardening documentation
Release to Docker Hub / Build & Push Docker Image (push) Canceled after 0s
Release to Docker Hub / Create GitHub Release (push) Canceled after 0s
Master overview (PHASE4_1_PREPRODUCTION_HARDENING.md) covering all 27
substantive items, plus REMOTE_AND_SELF_PACED_ARCHITECTURE.md (reviews
what already works for remote/self-paced delivery - student-driven
provisioning, cohort-less students, idle-reaper-destroy-then-continue -
and the one real gap: student authentication, explicitly Phase 5 scope,
not built here).

Updated PRODUCTION_NETWORK_SECURITY.md (the Gateway-to-Lab gap is now
RESOLVED, with the full design writeup and a new control-plane-vs-data-plane
section), PRODUCTION_DEPLOYMENT.md (four-cert TLS bundle),
PRODUCTION_VALIDATION_CHECKLIST.md (Router mTLS check, admission control
checks), PHASE4_PRODUCTION_INFRASTRUCTURE.md and ENGINEERING_SOP.md and
LAB_ARCHITECTURE.md (stale 'known gap' pointers updated to point at the
resolution), BACKUP_DR.md (destructive drill), README.md and
platform/README.md (Phase 4.1 pointers, lab-router component, updated
known-limitations).

Also fixes a real pre-existing bug found while auditing what gets
committed: infra/.gitignore's patterns were written repo-root-relative
(e.g. "infra/terraform/...") but the file lives in infra/, so git resolved
them relative to THAT directory and none of them ever matched anything -
a real .terraform/ provider cache showed up as untracked when it should
have been ignored. Fixed by dropping the redundant leading "infra/" from
every pattern.

Status: PHASE 4.1 COMPLETE / PRE-PRODUCTION HARDENING COMPLETE / READY FOR
HARDWARE DEPLOYMENT.
2026-08-24 20:16:20 -05:00
ced4568 f3c356951c test(preprod): two-host routing simulation, drill suite, CI security scanning
- infra/scripts/simulate-two-host/: real two-Docker-network simulation
  (Gateway genuinely isolated from any Lab bridge network) proving the
  Lab Host Router fix - 13/13 real assertions (HTTP+WebSocket through the
  Router, Alice/Bob isolation, garbage/destroyed tokens, mTLS-only Router
  access).
- infra/scripts/drills/: Lab Host loss/rebuild (9/9), deployment rollback,
  CI-fails-closed (6 required failure modes), fake cohort dress rehearsal
  (14/14, one continuous narrative through the real app).
- infra/ci/gitea-workflows/build-and-test.yml: new security-scan job
  (npm audit, gitleaks, hadolint, shellcheck, trivy - all free/open-source),
  severity policy in docs/SECURITY_SCANNING.md (CRITICAL/HIGH blocks,
  MEDIUM/LOW reported only). Gates build-images alongside regression.

Found and fixed two real pre-existing bugs via the CI-fails-closed drill:
staging-smoke-test.sh and production-smoke-test.sh both had a classic
set -e gotcha (curl ... && echo "OK" as one statement - a failing command
that isn't last in an && chain doesn't trigger -e) that let an
unreachable/broken endpoint fall through silently instead of failing the
smoke test.

platform/test/e2e-phase3a.js updated for the new paginated
/api/admin/audit-events response shape (Phase 4.1 pagination work).
2026-08-24 20:16:00 -05:00
ced4568 f5110d3c1b test(dr): destructive Postgres restore drill
infra/backup/test-destructive-restore-drill.sh - realistic academy data
(owner, admin instructor, cohort, 3 students, progress, attempts, an
assessment definition, achievements) seeded through the REAL Platform API,
encrypted-backed-up, the source Postgres container genuinely destroyed
(docker rm, not just left running alongside a second one), a fresh
Postgres created from nothing, restored, and the real Platform API booted
again against the restored database and used to log in as the restored
admin instructor over HTTP. Never touches the developer's real database.
Confirmed passing.
2026-08-24 20:15:45 -05:00
ced4568 a8e543234c feat(platform): pagination, admission control, cohort roles, version endpoint, structured assessment editor
- Real server-side pagination (platform/api/src/pagination.js) on the audit
  log (was a flat LIMIT 200), student attempt history (was LIMIT 100), and
  assessment session history (was fully unbounded - the one genuine growth
  risk found). Frontend updated (AuditView, AssessmentsView, StudentsView).
- Admission control: resource-policy.yaml's new (disabled-by-default)
  admission_control.max_concurrent_labs, checked before touching Docker at
  all, returns a clean 503 CAPACITY_EXCEEDED instead of failing mid-provision.
  Relayed as a clean student-facing message, never a raw 500. New
  GET /capacity endpoint feeds the instructor dashboard.
- cohort_instructors.role_in_cohort (existed since Phase 2B, never read) is
  now enforced: teacher vs viewer, rbac.js's
  requireCohortWriteAccess/requireStudentCohortWriteAccess.
- GET /api/version - app version, deployed Git SHA, latest migration as a
  schema-version stand-in.
- Structured assessment authoring UI (AssessmentsView.jsx's PoolSpecEditor)
  replaces the raw-JSON pool_spec textarea, backed by a new
  GET /api/admin/scenario-catalog endpoint. Owner-only raw-JSON fallback
  remains for anything the structured editor doesn't cover.
- NEEDS_ATTENTION_FAIL_THRESHOLD/STALE_DAYS moved to env-var configuration.

Full regression suite (e2e-phase2a/2b/3a/3b, 67/67) re-run clean after
these changes; frontend production build confirmed clean.
2026-08-24 20:15:39 -05:00
ced4568 addf474b70 feat(security): Postgres-backed login rate limiting
Replaces the in-memory sliding-window Map (reset on every restart, not
shareable across instances) with a login_attempts table (migration 016).
No Redis added - login is not a hot path, and this is a single indexed
range-scan query. Restart behavior confirmed for real:
platform/test/e2e-phase3a.js's rate-limiting test passes after a genuine
Platform API restart in the same run.
2026-08-24 20:15:03 -05:00
ced4568 6dda13b837 feat(learning): abandoned attempt handling
scenario_attempts.status = 'abandoned' (a valid value since Phase 3B,
never previously set by anything) now gets set on: environment
destroy/reset, logout, session expiry (piggybacked on the existing hourly
session sweeper - no new background job), and switching to a different
scenario while one is still open. Never an academic failure marker -
result stays NULL, history is preserved. Call-site wiring in
server.js/admin-routes.js lands in the next commit alongside other
platform hardening in those same files.
2026-08-24 20:14:52 -05:00
ced4568 eae8e48d43 feat(routing): Lab Host Router closes the Gateway-to-Lab cross-VM gap
Adds platform/lab-router/ — a narrow service that runs on the Lab Host,
authenticates the Gateway via mTLS, and resolves an opaque routing token to
a local container purely from Docker label state (no Postgres access, no
callback to the Provisioner). Replaces the Gateway's env.internal_ip
assumption (only valid when Gateway and Lab share a Docker daemon) with a
data-driven direct/router decision per lab_host
(lab_hosts.routing_mode/router_endpoint, migration 004), resolved in one
place (platform/gateway/src/lab-endpoint.js's labTarget()) that every
proxy/fetch call site in server.js now goes through.

Extends the existing Docker TLS CA (infra/scripts/docker-tls/generate-docker-tls.sh)
with a second mTLS leaf pair (router-server/gateway-client) for this new
data-plane connection, deployed by the docker-tls Ansible role into a
separate directory from the control-plane pair. Firewall
(rn-lab-host-01.rules) replaces the old speculative wide port-range
allowance with one narrow rule: platform_ip -> the Router's port only.

dev-local stays routing_mode: direct — zero behavior change for local dev,
confirmed by the full existing regression suite passing unmodified.
2026-08-24 20:14:39 -05:00
ced4568 bf1e1b5c76 docs(phase4): production infrastructure documentation + runbooks
Master overview (PHASE4_PRODUCTION_INFRASTRUCTURE.md) plus five detail docs
(PRODUCTION_DEPLOYMENT, PRODUCTION_NETWORK_SECURITY, BACKUP_DR,
OBSERVABILITY, CAPACITY_PLANNING), a master PRODUCTION_VALIDATION_CHECKLIST,
and 12 runbooks (first deployment, normal deploy, rollback, Postgres
restore, VM rebuild for both hosts, Docker TLS rotation, TLS renewal, owner
recovery, stuck Lab cleanup, failed migration, backup failure, disk
failure, Lab Host resource pressure).

PRODUCTION_NETWORK_SECURITY.md prominently documents the one genuine
architectural gap this phase surfaced: the Gateway's existing
env.internal_ip mechanism for reaching a Lab container assumes Gateway and
Lab share a Docker daemon, which breaks once Labs run on a genuinely
separate rn-lab-host-01 VM. The network-policy half is built (a firewall
allowance for Gateway->Lab-Host on a published port range); the
application half (Provisioner/Gateway address handling) is an explicit,
reviewed, NOT-YET-BUILT follow-up flagged there rather than silently
decided.

Updated LAB_ARCHITECTURE.md, ENGINEERING_SOP.md, README.md, and
platform/README.md with Phase 4 pointers, consistent with how every prior
phase updated these.

Status: PHASE 4 BUILD COMPLETE / PRODUCTION DEPLOYMENT PENDING HARDWARE.
2026-08-18 17:13:51 -05:00
ced4568 1d3afc736b feat(observability): add /healthz to Platform API and Gateway
Unauthenticated liveness/readiness probes for uptime monitoring and Ansible
deploy smoke tests (Phase 4). API's checks Postgres reachability via
SELECT 1; Gateway's answers locally without proxying anywhere, so it can
distinguish 'Gateway itself is down' from 'something behind it is down'.
No student/instructor data exposed.

Verified live against the real dev stack: both return 200 with the
expected JSON body. Full regression suite re-run after this change:
e2e-phase2a (10/10), e2e-phase2b (12/12), e2e-phase3a (21/21), e2e-phase3b
(24/24) - all passing, no regressions. Frontend build and the Lab image
(both the multi-service dev compose and the standalone single-container
Dockerfile) also confirmed still building/booting correctly.
2026-08-18 17:13:42 -05:00
ced4568 51a09241ad feat(loadtest): load-test tooling + capacity model
boot-labs.js drives the real admin Prepare-Labs endpoint (reusing the
existing session-helper.js from Phase 3A) and records boot time/failure
rate; sample-resources.sh samples real docker stats for running Labs.
capacity-model.js turns measured per-Lab CPU/memory into a recommended/
warning/hard-admission-control concurrency threshold - no concurrency
number is hardcoded as a marketing figure, every input is explicitly
placeholder data until measured against the real host.

Plus deploy/rollback/smoke-test scripts used by the CI/CD and deployment
runbooks. All syntax-checked; validate-compose.sh and capacity-model.js
actually run against real inputs.
2026-08-18 17:13:32 -05:00
ced4568 67e172c800 feat(cicd): Gitea Actions pipeline, deterministic image tagging
Automatic test/build/tag/staging-deploy on push to main; production
promotion stays a manual, human-run step (deploy-production.sh) - never
automated. SHA-tagged images always, semantic tags only on deliberate
release, never :latest alone in staging/production. No paid CI provider or
registry required (uses Gitea's own). Real regression job installs all
services, applies real migrations against a service Postgres, runs the
infra validation scripts.

Validated as real YAML (python3 yaml parse).
2026-08-18 17:13:25 -05:00
ced4568 b1b22b9285 feat(backup): encrypted Postgres backup/restore + off-host adapter
pg_dump -> gzip -> gpg AES-256 symmetric encryption, plaintext deleted
immediately, optional off-host rsync-over-SSH copy (Hetzner Storage Box or
any SSH target), retention pruning, Prometheus textfile-collector metrics
for backup success/age/size.

Genuinely tested end-to-end, not just written: real production migrations
(17 total, correct cross-service order) applied to a disposable Postgres,
sample data inserted, backed up, restored into a second disposable
Postgres, verified table-for-table and byte-for-byte. Exit 0.
2026-08-18 17:13:19 -05:00