From bee57d89fd98d311b73ef0aff156a1c5187712ef Mon Sep 17 00:00:00 2001 From: ced4568 Date: Wed, 26 Aug 2026 09:04:55 -0500 Subject: [PATCH] feat(root): wire ROOT 05-10/12/Capstone curriculum expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../api/migrations/023_phase6b_curriculum.sql | 332 ++++++++++++++++++ platform/api/src/course-progress.js | 13 +- 2 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 platform/api/migrations/023_phase6b_curriculum.sql diff --git a/platform/api/migrations/023_phase6b_curriculum.sql b/platform/api/migrations/023_phase6b_curriculum.sql new file mode 100644 index 0000000..16ec3c3 --- /dev/null +++ b/platform/api/migrations/023_phase6b_curriculum.sql @@ -0,0 +1,332 @@ +-- Phase 6B — curriculum authoring & coverage expansion. Additive on top of Phase 6A's +-- 022_course_content.sql: new teaching modules for ROOT 05/06/07/08/09/10/12/Capstone, built +-- from real, previously-unwired scenario content (see docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md +-- for the evidence and reasoning behind every placement). No existing row is deleted; a small +-- number of existing rows have their `sequence`/`prerequisites` updated so a new teaching +-- module sits in front of the bare-lab bank it supersedes as the "front door" — never their +-- scenario_id/activity_type/completion_behavior, so no student's derived completion state +-- changes meaning. See docs/COURSE_CONTENT_MODEL.md §"content revision" for what does and +-- doesn't require this kind of change. + +-- ── is_required: Phase 6B §18/§27's optional-vs-required distinction. Defaults true, so every +-- Phase 6A activity is unaffected (required, as it always implicitly was) — this is additive, +-- not a redefinition. An optional activity (independent-practice duplicates, advanced/CKS-exam- +-- style extras — see docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md's ADVANCED/INDEPENDENT +-- classifications) still counts toward a module's percent_complete display but NOT toward +-- whether the module is considered complete — see course-progress.js's totals(). +ALTER TABLE module_activities ADD COLUMN IF NOT EXISTS is_required BOOLEAN NOT NULL DEFAULT true; + +-- ── Course status: ROOT 08/09 move from PLANNED to PARTIAL — real modules exist now ───────── +UPDATE courses SET status = 'partial', updated_at = now() WHERE course_id IN ('root-08-kubernetes-storage-scheduling', 'root-09-kubernetes-security'); + +-- ── Resequencing: new teaching modules become the front door; existing bare-lab banks move +-- behind them. Sequence/prerequisites only — activities inside the existing modules are +-- untouched. ────────────────────────────────────────────────────────────────────────────── +UPDATE course_modules SET sequence = 2, prerequisites = '{root-05-m00-kubernetes-mental-model}', version = version + 1, updated_at = now() + WHERE module_id = 'root-05-m01-pods-and-kubectl-basics'; +UPDATE course_modules SET sequence = 3, prerequisites = '{root-06-m02-deployments-scaling-updates}', version = version + 1, updated_at = now() + WHERE module_id = 'root-06-m01-deployments-and-workloads'; +UPDATE course_modules SET sequence = 3, prerequisites = '{root-07-m03-services-and-service-discovery}', version = version + 1, updated_at = now() + WHERE module_id = 'root-07-m01-services-and-networking'; +-- root-07-m02 (configuration bank) is intentionally left UNGATED (no new configuration-teaching +-- module was authored this phase — an honest PARTIAL gap, see +-- docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md) but still needs to move behind the new +-- teaching modules in display order, to avoid colliding with m04's sequence=2. +UPDATE course_modules SET sequence = 4, version = version + 1, updated_at = now() + WHERE module_id = 'root-07-m02-configuration-and-secrets'; +UPDATE course_modules SET sequence = 3, prerequisites = '{root-10-m03-troubleshooting-framework}', version = version + 1, updated_at = now() + WHERE module_id = 'root-10-m02-general-troubleshooting'; + +INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES +('root-05-m00-kubernetes-mental-model', 'root-05-kubernetes-foundations', 'The Kubernetes Mental Model & Core Objects', + 'What Kubernetes actually solves, the control-plane/worker/desired-state model, and Pods/Namespaces/labels through kubectl — the real front door to ROOT 05, taught before the bare kubectl-fluency lab bank.', + 1, '{"Explain what problem Kubernetes solves and why running containers by hand does not scale","Describe the control-plane/worker split and what \"desired state\" means in practice","Navigate Pods, Namespaces, and labels with kubectl confidently"}', + '{}', 45, 'ready'), + +('root-06-m02-deployments-scaling-updates', 'root-06-kubernetes-application-workloads', 'Deployments, Scaling & Updates', + 'Why Deployments/ReplicaSets exist, how a rolling update actually progresses, and how to reason about rollback — taught before the bare Deployments lab bank.', + 1, '{"Explain why a Deployment manages Pods instead of creating them directly","Perform and reason about a rolling update in progress","Explain what a rollback actually restores and when to use one"}', + '{root-05-m00-kubernetes-mental-model}', 55, 'ready'), +('root-06-m03-jobs-cronjobs-daemonsets', 'root-06-kubernetes-application-workloads', 'Jobs, CronJobs & DaemonSets', + 'Distinguishing one-shot Jobs, scheduled CronJobs, and per-node DaemonSets from long-running Deployments.', + 2, '{"Choose the right workload primitive for a one-shot task, a scheduled task, and a per-node agent","Explain why a Job differs from a Deployment in restart/completion semantics"}', + '{root-06-m02-deployments-scaling-updates}', 30, 'partial'), + +('root-07-m03-services-and-service-discovery', 'root-07-kubernetes-networking-config', 'Services & Service Discovery', + 'The Service/Endpoints/DNS request path and choosing the right Service type — taught before the bare Services lab bank.', + 1, '{"Trace how a request actually reaches a Pod through a Service","Choose ClusterIP vs NodePort correctly for a given requirement","Explain why a Service with zero endpoints looks like network failure from outside"}', + '{root-06-m02-deployments-scaling-updates}', 45, 'ready'), +('root-07-m04-ingress-networkpolicy-foundations', 'root-07-kubernetes-networking-config', 'Ingress & NetworkPolicy Foundations', + 'How Ingress routes external traffic, and what a NetworkPolicy architecturally does (the security posture comes later, in ROOT 09).', + 2, '{"Explain how Ingress differs from a Service and why it exists","Describe what a NetworkPolicy actually controls at the architectural level"}', + '{root-07-m03-services-and-service-discovery}', 30, 'partial'), + +('root-08-m01-storage-fundamentals', 'root-08-kubernetes-storage-scheduling', 'Storage Fundamentals', + 'Ephemeral vs. persistent storage, and how PersistentVolumes/PersistentVolumeClaims/StorageClasses fit together.', + 1, '{"Explain the difference between ephemeral and persistent storage in Kubernetes","Describe how a PVC binds to a PV, directly or via dynamic provisioning"}', + '{root-07-m03-services-and-service-discovery}', 35, 'partial'), +('root-08-m02-scheduling-resource-governance', 'root-08-kubernetes-storage-scheduling', 'Scheduling & Resource Governance', + 'How the scheduler places Pods, and how quotas/limits/priority/disruption budgets shape that placement.', + 2, '{"Explain how taints/tolerations and affinity influence scheduling decisions","Explain why a PodDisruptionBudget matters during a voluntary disruption","Describe what a ResourceQuota/LimitRange enforces at the namespace level"}', + '{root-08-m01-storage-fundamentals}', 40, 'partial'), + +('root-09-m01-security-model-and-securitycontext', 'root-09-kubernetes-security', 'Security Model & SecurityContext', + 'Defense in depth, least privilege, and hardening a Pod with SecurityContext (non-root, capabilities, read-only filesystem).', + 1, '{"Explain defense in depth and least privilege as applied to a running Pod","Configure a Pod/container SecurityContext to run non-root with a read-only filesystem and no unnecessary Linux capabilities"}', + '{root-06-m02-deployments-scaling-updates}', 50, 'ready'), +('root-09-m02-serviceaccounts-and-rbac', 'root-09-kubernetes-security', 'ServiceAccounts & RBAC', + 'Identity for workloads and least-privilege permissions via Roles/ClusterRoles.', + 2, '{"Explain why a Pod needs its own identity separate from the human operating it","Write a least-privilege Role/RoleBinding for a specific, narrow permission"}', + '{root-09-m01-security-model-and-securitycontext}', 40, 'partial'), +('root-09-m03-network-and-supplychain-security', 'root-09-kubernetes-security', 'Network & Supply Chain Security', + 'NetworkPolicy as a security control (not just an architecture concept), image/registry trust, and TLS ingress.', + 3, '{"Apply a default-deny NetworkPolicy and open only the access a workload actually needs","Explain why an image-pull secret and a trusted registry matter for supply-chain security"}', + '{root-09-m02-serviceaccounts-and-rbac,root-07-m04-ingress-networkpolicy-foundations}', 40, 'partial'), +('root-09-m04-cluster-hardening-and-runtime', 'root-09-kubernetes-security', 'Cluster Hardening & Runtime Security', + 'Pod Security Admission and a survey of cluster-hardening/runtime-sandboxing topics — deliberately advanced/optional, not daily-driver core.', + 4, '{"Explain what Pod Security Admission enforces at the namespace level and how it differs from the old PodSecurityPolicy"}', + '{root-09-m03-network-and-supplychain-security}', 25, 'partial'), + +('root-10-m03-troubleshooting-framework', 'root-10-kubernetes-troubleshooting-observability', 'The RootNode Troubleshooting Framework', + 'A repeatable framework — Observe, Gather Evidence, Form Hypothesis, Test, Repair, Validate, Prevent Recurrence — applied to a CrashLoopBackOff and a broken Service.', + 2, '{"Apply a repeatable troubleshooting framework instead of guessing under pressure","Diagnose a CrashLoopBackOff from evidence, not assumption","Diagnose a broken Service-to-Pod selector mismatch"}', + '{root-10-m01-health-probes-and-troubleshooting}', 40, 'partial'), + +('root-capstone-m02-capstone-readiness', 'root-capstone', 'Capstone Readiness', + 'What the RootNode Capstone measures, what it requires, how the timed/randomized draw works, and how to manage your time — without publishing the draw or the answers.', + 1, '{"Explain what skills the RootNode Capstone measures and why","Describe how the timed, partially-randomized scenario draw works","Apply the RootNode troubleshooting framework under time pressure"}', + '{}', 20, 'ready') +ON CONFLICT (module_id) DO NOTHING; + +-- The existing capstone assessment module keeps its original sequence position (2) relative to +-- the new readiness module (1) — readiness comes first, matching that it now needs no +-- prerequisite of its own (the course-level root-12-real-ops -> root-capstone chain still +-- gates entry into this course at all; the actual capstone START is gated server-side by +-- curriculum-rules.js's unlock_rule regardless of whether this readiness module was viewed — +-- see docs/ROOT_CAPSTONE_READINESS.md for why those two gates are deliberately independent). +UPDATE course_modules SET sequence = 2 WHERE module_id = 'root-capstone-m01-operate-and-recover'; + +-- ═══════════════════════════════════════════════════════════════════════════════════════════ +-- New module_activities — gold-standard modules (lesson+demo+knowledge_check+guided_exercise+ +-- lab+reflection) for root-05-m00, root-06-m02, root-07-m03, root-09-m01. PARTIAL modules get +-- lesson+knowledge_check(+lab) only, honestly, per docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md. +-- ═══════════════════════════════════════════════════════════════════════════════════════════ + +-- ROOT 05 m00 — 6 activities +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-05-m00-a01-lesson-mental-model', 'root-05-m00-kubernetes-mental-model', 1, 'lesson', 'What Kubernetes Solves & the Desired-State Model', + 'content/root/root-05-kubernetes-foundations/m00-mental-model/01-lesson-mental-model.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-05-m00-a02-demonstration-desired-state', 'root-05-m00-kubernetes-mental-model', 2, 'demonstration', 'Demonstration: Desired State Reconciliation', + 'content/root/root-05-kubernetes-foundations/m00-mental-model/02-demonstration-desired-state.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-05-m00-a03-knowledge-check', 'root-05-m00-kubernetes-mental-model', 3, 'knowledge_check', 'Knowledge Check: The Kubernetes Mental Model', + 'content/root/root-05-kubernetes-foundations/m00-mental-model/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-05-m00-a04-guided-exercise-namespaces', 'root-05-m00-kubernetes-mental-model', 4, 'guided_exercise', 'Guided Exercise: Planning a Namespace Layout', + 'content/root/root-05-kubernetes-foundations/m00-mental-model/04-guided-exercise-namespaces.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-05-m00-a05-lab-namespaces-basics', 'root-05-m00-kubernetes-mental-model', 5, 'lab', 'Launch Lab: Working with Namespaces', + 'content/root/root-05-kubernetes-foundations/m00-mental-model/05-lab-intro.md', 'namespaces-basics', NULL, 'auto_scenario', 15, 'ready'), +('root-05-m00-a06-reflection', 'root-05-m00-kubernetes-mental-model', 6, 'reflection', 'Reflection: Why Desired State Matters in Production', + 'content/root/root-05-kubernetes-foundations/m00-mental-model/06-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 06 m02 — 6 activities +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-06-m02-a01-lesson-deployments', 'root-06-m02-deployments-scaling-updates', 1, 'lesson', 'Why Deployments Exist & How a Rolling Update Progresses', + 'content/root/root-06-kubernetes-application-workloads/m02-deployments-scaling-updates/01-lesson-deployments.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-06-m02-a02-demonstration-rollout', 'root-06-m02-deployments-scaling-updates', 2, 'demonstration', 'Demonstration: Watching a Rollout, Good and Bad', + 'content/root/root-06-kubernetes-application-workloads/m02-deployments-scaling-updates/02-demonstration-rollout.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-06-m02-a03-knowledge-check', 'root-06-m02-deployments-scaling-updates', 3, 'knowledge_check', 'Knowledge Check: Deployments & Rollouts', + 'content/root/root-06-kubernetes-application-workloads/m02-deployments-scaling-updates/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-06-m02-a04-guided-exercise-rollout-strategy', 'root-06-m02-deployments-scaling-updates', 4, 'guided_exercise', 'Guided Exercise: Planning a Safe Rollout Strategy', + 'content/root/root-06-kubernetes-application-workloads/m02-deployments-scaling-updates/04-guided-exercise-rollout-strategy.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-06-m02-a05-lab-rolling-update', 'root-06-m02-deployments-scaling-updates', 5, 'lab', 'Launch Lab: Perform a Rolling Update', + 'content/root/root-06-kubernetes-application-workloads/m02-deployments-scaling-updates/05-lab-intro.md', 'rolling-update-task', NULL, 'auto_scenario', 15, 'ready'), +('root-06-m02-a05b-lab-deployment-rollback', 'root-06-m02-deployments-scaling-updates', 6, 'lab', 'Lab: Rolling Back a Deployment', NULL, 'deployment-rollback', NULL, 'auto_scenario', 15, 'ready'), +('root-06-m02-a06-reflection', 'root-06-m02-deployments-scaling-updates', 7, 'reflection', 'Reflection: Rollback Is a Plan, Not a Panic Button', + 'content/root/root-06-kubernetes-application-workloads/m02-deployments-scaling-updates/06-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 06 m03 — 4 activities (PARTIAL: lesson + knowledge_check + two labs) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-06-m03-a01-lesson-jobs-cronjobs-daemonsets', 'root-06-m03-jobs-cronjobs-daemonsets', 1, 'lesson', 'Jobs, CronJobs, and DaemonSets: The Other Workload Shapes', + 'content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/01-lesson-jobs-cronjobs-daemonsets.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-06-m03-a02-knowledge-check', 'root-06-m03-jobs-cronjobs-daemonsets', 2, 'knowledge_check', 'Knowledge Check: Choosing the Right Workload Primitive', + 'content/root/root-06-kubernetes-application-workloads/m03-jobs-cronjobs-daemonsets/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-06-m03-a03-lab-job-task', 'root-06-m03-jobs-cronjobs-daemonsets', 3, 'lab', 'Lab: Create a One-Shot Job', NULL, 'job-task', NULL, 'auto_scenario', 15, 'ready'), +('root-06-m03-a04-lab-cronjob-task', 'root-06-m03-jobs-cronjobs-daemonsets', 4, 'lab', 'Lab: Create a CronJob', NULL, 'cronjob-task', NULL, 'auto_scenario', 15, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 07 m03 — 6 activities +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-07-m03-a01-lesson-services', 'root-07-m03-services-and-service-discovery', 1, 'lesson', 'The Service/Endpoints/DNS Request Path', + 'content/root/root-07-kubernetes-networking-config/m03-services-and-service-discovery/01-lesson-services.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-07-m03-a02-demonstration-zero-endpoints', 'root-07-m03-services-and-service-discovery', 2, 'demonstration', 'Demonstration: A Service With Zero Endpoints', + 'content/root/root-07-kubernetes-networking-config/m03-services-and-service-discovery/02-demonstration-zero-endpoints.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-07-m03-a03-knowledge-check', 'root-07-m03-services-and-service-discovery', 3, 'knowledge_check', 'Knowledge Check: Services & Service Discovery', + 'content/root/root-07-kubernetes-networking-config/m03-services-and-service-discovery/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-07-m03-a04-guided-exercise-service-type', 'root-07-m03-services-and-service-discovery', 4, 'guided_exercise', 'Guided Exercise: Choosing the Right Service Type', + 'content/root/root-07-kubernetes-networking-config/m03-services-and-service-discovery/04-guided-exercise-service-type.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-07-m03-a05-lab-expose-service', 'root-07-m03-services-and-service-discovery', 5, 'lab', 'Launch Lab: Exposing an Application', + 'content/root/root-07-kubernetes-networking-config/m03-services-and-service-discovery/05-lab-intro.md', 'expose-service-basics', NULL, 'auto_scenario', 15, 'ready'), +('root-07-m03-a06-reflection', 'root-07-m03-services-and-service-discovery', 6, 'reflection', 'Reflection: Service Discovery Is Infrastructure, Not Magic', + 'content/root/root-07-kubernetes-networking-config/m03-services-and-service-discovery/06-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 07 m01 (existing bare bank) — extend with the two newly-wired networking scenarios +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-07-m01-a03-nodeport-task', 'root-07-m01-services-and-networking', 3, 'lab', 'Lab: Expose a Deployment via NodePort', NULL, 'nodeport-task', NULL, 'auto_scenario', 15, 'ready'), +('root-07-m01-a04-dns-resolution-mcq', 'root-07-m01-services-and-networking', 4, 'lab', 'Lab: Kubernetes DNS Resolution', NULL, 'dns-resolution-mcq', NULL, 'auto_scenario', 15, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 07 m04 — 4 activities (PARTIAL) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-07-m04-a01-lesson-ingress-networkpolicy', 'root-07-m04-ingress-networkpolicy-foundations', 1, 'lesson', 'Ingress and NetworkPolicy: What They Architecturally Do', + 'content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/01-lesson-ingress-networkpolicy.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-07-m04-a02-knowledge-check', 'root-07-m04-ingress-networkpolicy-foundations', 2, 'knowledge_check', 'Knowledge Check: Ingress & NetworkPolicy Foundations', + 'content/root/root-07-kubernetes-networking-config/m04-ingress-networkpolicy-foundations/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-07-m04-a03-lab-ingress-task', 'root-07-m04-ingress-networkpolicy-foundations', 3, 'lab', 'Lab: Create an Ingress Resource', NULL, 'ingress-task', NULL, 'auto_scenario', 20, 'ready'), +('root-07-m04-a04-lab-network-policy', 'root-07-m04-ingress-networkpolicy-foundations', 4, 'lab', 'Lab: Isolate Traffic with NetworkPolicy', NULL, 'network-policy', NULL, 'auto_scenario', 30, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 08 m01 — 4 activities (PARTIAL) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-08-m01-a01-lesson-storage-fundamentals', 'root-08-m01-storage-fundamentals', 1, 'lesson', 'Ephemeral vs. Persistent Storage', + 'content/root/root-08-kubernetes-storage-scheduling/m01-storage-fundamentals/01-lesson-storage-fundamentals.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-08-m01-a02-knowledge-check', 'root-08-m01-storage-fundamentals', 2, 'knowledge_check', 'Knowledge Check: Storage Fundamentals', + 'content/root/root-08-kubernetes-storage-scheduling/m01-storage-fundamentals/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-08-m01-a03-lab-emptydir', 'root-08-m01-storage-fundamentals', 3, 'lab', 'Lab: Shared emptyDir Volume Between Containers', NULL, 'emptydir-pod', NULL, 'auto_scenario', 15, 'ready'), +('root-08-m01-a04-lab-pv-pvc', 'root-08-m01-storage-fundamentals', 4, 'lab', 'Lab: Persistent Storage with PV and PVC', NULL, 'pv-pvc-mount', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m01-a05-lab-pvc-dynamic', 'root-08-m01-storage-fundamentals', 5, 'lab', 'Lab: Dynamic PVC Provisioning', NULL, 'pvc-dynamic-task', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m01-a06-lab-storageclass', 'root-08-m01-storage-fundamentals', 6, 'lab', 'Lab: StorageClass Reclaim Policies', NULL, 'storageclass-mcq', NULL, 'auto_scenario', 15, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 08 m02 — 8 activities (PARTIAL) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-08-m02-a01-lesson-scheduling', 'root-08-m02-scheduling-resource-governance', 1, 'lesson', 'How the Scheduler Places Pods, and How Governance Shapes It', + 'content/root/root-08-kubernetes-storage-scheduling/m02-scheduling-resource-governance/01-lesson-scheduling.md', NULL, NULL, 'manual_marker', 20, 'ready'), +('root-08-m02-a02-knowledge-check', 'root-08-m02-scheduling-resource-governance', 2, 'knowledge_check', 'Knowledge Check: Scheduling & Resource Governance', + 'content/root/root-08-kubernetes-storage-scheduling/m02-scheduling-resource-governance/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-08-m02-a03-lab-node-label-selector', 'root-08-m02-scheduling-resource-governance', 3, 'lab', 'Lab: Node Labels and nodeSelector', NULL, 'node-label-selector', NULL, 'auto_scenario', 15, 'ready'), +('root-08-m02-a04-lab-node-taint-toleration', 'root-08-m02-scheduling-resource-governance', 4, 'lab', 'Lab: Taints and Tolerations', NULL, 'node-taint-toleration', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m02-a05-lab-pod-affinity', 'root-08-m02-scheduling-resource-governance', 5, 'lab', 'Lab: Pod Affinity vs Anti-Affinity', NULL, 'pod-affinity-mcq', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m02-a06-lab-priority-class', 'root-08-m02-scheduling-resource-governance', 6, 'lab', 'Lab: Pod Priority and PriorityClass', NULL, 'priority-class-task', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m02-a07-lab-pdb', 'root-08-m02-scheduling-resource-governance', 7, 'lab', 'Lab: Configure a PodDisruptionBudget', NULL, 'pod-disruption-budget-task', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m02-a08-lab-resource-quota', 'root-08-m02-scheduling-resource-governance', 8, 'lab', 'Lab: ResourceQuota for a Namespace', NULL, 'resource-quota-ns', NULL, 'auto_scenario', 20, 'ready'), +('root-08-m02-a09-lab-limitrange', 'root-08-m02-scheduling-resource-governance', 9, 'lab', 'Lab: LimitRange for Default Container Limits', NULL, 'limitrange-task', NULL, 'auto_scenario', 20, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 09 m01 — 8 activities (READY: full gold-standard sequence + independent/guided/advanced practice) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m01-a01-lesson-security-model', 'root-09-m01-security-model-and-securitycontext', 1, 'lesson', 'Defense in Depth & the Kubernetes Security Model', + 'content/root/root-09-kubernetes-security/m01-security-model-and-securitycontext/01-lesson-security-model.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-09-m01-a02-demonstration-securitycontext', 'root-09-m01-security-model-and-securitycontext', 2, 'demonstration', 'Demonstration: A Pod Before and After Hardening', + 'content/root/root-09-kubernetes-security/m01-security-model-and-securitycontext/02-demonstration-securitycontext.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-09-m01-a03-knowledge-check', 'root-09-m01-security-model-and-securitycontext', 3, 'knowledge_check', 'Knowledge Check: Security Model & SecurityContext', + 'content/root/root-09-kubernetes-security/m01-security-model-and-securitycontext/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-09-m01-a04-guided-exercise-hardening-plan', 'root-09-m01-security-model-and-securitycontext', 4, 'guided_exercise', 'Guided Exercise: Planning a Pod Hardening Checklist', + 'content/root/root-09-kubernetes-security/m01-security-model-and-securitycontext/04-guided-exercise-hardening-plan.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-09-m01-a05-lab-non-root', 'root-09-m01-security-model-and-securitycontext', 5, 'lab', 'Lab: Enforce Non-Root Container', NULL, 'cks-non-root-enforce', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m01-a06-lab-drop-capabilities', 'root-09-m01-security-model-and-securitycontext', 6, 'lab', 'Lab: Drop Linux Capabilities', NULL, 'cks-drop-capabilities', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m01-a07-lab-readonly-filesystem', 'root-09-m01-security-model-and-securitycontext', 7, 'lab', 'Lab: Read-Only Root Filesystem', NULL, 'cks-readonly-filesystem', NULL, 'auto_scenario', 15, 'ready'), +('root-09-m01-a08-lab-pod-security-context', 'root-09-m01-security-model-and-securitycontext', 8, 'lab', 'Lab: Pod Security Context (CKS)', NULL, 'cks-pod-security-context', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m01-a09-reflection', 'root-09-m01-security-model-and-securitycontext', 9, 'reflection', 'Reflection: Least Privilege Is a Default, Not an Afterthought', + 'content/root/root-09-kubernetes-security/m01-security-model-and-securitycontext/09-reflection.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-09-m01-a10-lab-seccomp-guided', 'root-09-m01-security-model-and-securitycontext', 10, 'lab', 'Lab: Seccomp Profiles (guided practice)', NULL, 'cks-seccomp-profile', NULL, 'auto_scenario', 30, 'ready'), +('root-09-m01-a11-lab-apparmor-advanced', 'root-09-m01-security-model-and-securitycontext', 11, 'lab', 'Lab: AppArmor Profiles (advanced/optional)', NULL, 'cks-apparmor-profile', NULL, 'auto_scenario', 30, 'ready'), +('root-09-m01-a12-lab-pod-security-context-independent', 'root-09-m01-security-model-and-securitycontext', 12, 'lab', 'Lab: Pod Security Context (independent practice)', NULL, 'pod-security-context', NULL, 'auto_scenario', 20, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 09 m02 — 6 activities (PARTIAL: lesson + KC + core labs + one independent-practice dup) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m02-a01-lesson-serviceaccounts-rbac', 'root-09-m02-serviceaccounts-and-rbac', 1, 'lesson', 'Identity for Workloads: ServiceAccounts & RBAC', + 'content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/01-lesson-serviceaccounts-rbac.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-09-m02-a02-knowledge-check', 'root-09-m02-serviceaccounts-and-rbac', 2, 'knowledge_check', 'Knowledge Check: ServiceAccounts & RBAC', + 'content/root/root-09-kubernetes-security/m02-serviceaccounts-and-rbac/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-09-m02-a03-lab-serviceaccount-pod', 'root-09-m02-serviceaccounts-and-rbac', 3, 'lab', 'Lab: ServiceAccount for Pod API Access', NULL, 'serviceaccount-pod', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m02-a04-lab-automount-token', 'root-09-m02-serviceaccounts-and-rbac', 4, 'lab', 'Lab: Disable ServiceAccount Token', NULL, 'cks-automount-token', NULL, 'auto_scenario', 15, 'ready'), +('root-09-m02-a05-lab-rbac-role', 'root-09-m02-serviceaccounts-and-rbac', 5, 'lab', 'Lab: RBAC — Create Role and RoleBinding', NULL, 'rbac-role', NULL, 'auto_scenario', 30, 'ready'), +('root-09-m02-a06-lab-rbac-clusterrole', 'root-09-m02-serviceaccounts-and-rbac', 6, 'lab', 'Lab: Cluster-Level RBAC (CKS)', NULL, 'cks-rbac-clusterrole', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m02-a07-lab-rbac-least-privilege', 'root-09-m02-serviceaccounts-and-rbac', 7, 'lab', 'Lab: RBAC Least Privilege', NULL, 'cks-rbac-least-privilege', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m02-a08-lab-rbac-clusterrole-independent', 'root-09-m02-serviceaccounts-and-rbac', 8, 'lab', 'Lab: ClusterRole and ClusterRoleBinding (independent practice)', NULL, 'rbac-clusterrole', NULL, 'auto_scenario', 20, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 09 m03 — 6 activities (PARTIAL) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m03-a01-lesson-network-supplychain', 'root-09-m03-network-and-supplychain-security', 1, 'lesson', 'NetworkPolicy as a Security Control & Supply Chain Trust', + 'content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/01-lesson-network-supplychain.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-09-m03-a02-knowledge-check', 'root-09-m03-network-and-supplychain-security', 2, 'knowledge_check', 'Knowledge Check: Network & Supply Chain Security', + 'content/root/root-09-kubernetes-security/m03-network-and-supplychain-security/02-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-09-m03-a03-lab-default-deny', 'root-09-m03-network-and-supplychain-security', 3, 'lab', 'Lab: Default Deny Network Policy', NULL, 'cks-network-policy', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m03-a04-lab-egress-namespace', 'root-09-m03-network-and-supplychain-security', 4, 'lab', 'Lab: Restrict Egress to Namespace', NULL, 'cks-egress-namespace', NULL, 'auto_scenario', 30, 'ready'), +('root-09-m03-a05-lab-egress-networkpolicy-guided', 'root-09-m03-network-and-supplychain-security', 5, 'lab', 'Lab: Egress NetworkPolicy (guided practice)', NULL, 'networkpolicy-egress', NULL, 'auto_scenario', 30, 'ready'), +('root-09-m03-a06-lab-tls-ingress', 'root-09-m03-network-and-supplychain-security', 6, 'lab', 'Lab: Secure Ingress with TLS', NULL, 'cks-tls-ingress', NULL, 'auto_scenario', 30, 'ready'), +('root-09-m03-a07-lab-image-pull-secret', 'root-09-m03-network-and-supplychain-security', 7, 'lab', 'Lab: Private Registry Image Pull Secret', NULL, 'cks-image-pull-secret', NULL, 'auto_scenario', 15, 'ready'), +('root-09-m03-a08-lab-network-policy-metadata-advanced', 'root-09-m03-network-and-supplychain-security', 8, 'lab', 'Lab: Block Cloud Metadata (advanced/optional)', NULL, 'cks-network-policy-metadata', NULL, 'auto_scenario', 30, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 09 m04 — 4 activities (PARTIAL: survey-level, deliberately lighter) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-09-m04-a01-lesson-cluster-hardening', 'root-09-m04-cluster-hardening-and-runtime', 1, 'lesson', 'Pod Security Admission & a Survey of Runtime Hardening', + 'content/root/root-09-kubernetes-security/m04-cluster-hardening-and-runtime/01-lesson-cluster-hardening.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-09-m04-a02-lab-psa-namespace', 'root-09-m04-cluster-hardening-and-runtime', 2, 'lab', 'Lab: Pod Security Admission', NULL, 'cks-psa-namespace', NULL, 'auto_scenario', 15, 'ready'), +('root-09-m04-a03-lab-immutable-secret', 'root-09-m04-cluster-hardening-and-runtime', 3, 'lab', 'Lab: Immutable Secrets', NULL, 'cks-immutable-secret', NULL, 'auto_scenario', 15, 'ready'), +('root-09-m04-a04-lab-runtime-class-advanced', 'root-09-m04-cluster-hardening-and-runtime', 4, 'lab', 'Lab: RuntimeClass for Sandboxed Workloads (advanced/optional)', NULL, 'cks-runtime-class', NULL, 'auto_scenario', 20, 'ready'), +('root-09-m04-a05-lab-audit-policy-advanced', 'root-09-m04-cluster-hardening-and-runtime', 5, 'lab', 'Lab: Write a Kubernetes Audit Policy (advanced/optional)', NULL, 'cks-audit-policy', NULL, 'auto_scenario', 30, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 10 m03 — 5 activities (PARTIAL: lesson + demo + KC + two labs) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-10-m03-a01-lesson-framework', 'root-10-m03-troubleshooting-framework', 1, 'lesson', 'The RootNode Troubleshooting Framework', + 'content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/01-lesson-framework.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-10-m03-a02-demonstration-crashloop', 'root-10-m03-troubleshooting-framework', 2, 'demonstration', 'Demonstration: Applying the Framework to a CrashLoopBackOff', + 'content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/02-demonstration-crashloop.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-10-m03-a03-knowledge-check', 'root-10-m03-troubleshooting-framework', 3, 'knowledge_check', 'Knowledge Check: The Troubleshooting Framework', + 'content/root/root-10-kubernetes-troubleshooting-observability/m03-troubleshooting-framework/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-10-m03-a04-lab-crashloop-fix', 'root-10-m03-troubleshooting-framework', 4, 'lab', 'Lab: Fix a CrashLoopBackOff Pod', NULL, 'crashloop-fix', NULL, 'auto_scenario', 20, 'ready'), +('root-10-m03-a05-lab-endpoint-fix', 'root-10-m03-troubleshooting-framework', 5, 'lab', 'Lab: Fix a Broken Service Selector', NULL, 'endpoint-fix-task', NULL, 'auto_scenario', 20, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 10 m02 (existing bare bank) — extend with the newly-wired logging/monitoring MCQ +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-10-m02-a06-container-logging-mcq', 'root-10-m02-general-troubleshooting', 6, 'lab', 'Lab: Container Logging and Monitoring', NULL, 'container-logging-mcq', NULL, 'auto_scenario', 15, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ROOT 12 m01 — a new framework lesson at the FRONT (existing 4 lab activities renumbered +-- +1 to make room; their scenario_id/activity_type/completion_behavior are untouched, so no +-- student's derived completion changes meaning) plus a closing reflection. +UPDATE module_activities SET sequence = sequence + 1, version = version + 1, updated_at = now() + WHERE module_id = 'root-12-m01-real-ops-incidents'; + +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-12-m01-a00-lesson-incident-response', 'root-12-m01-real-ops-incidents', 1, 'lesson', 'The RootNode Incident-Response Framework', + 'content/root/root-12-real-ops/m01-real-ops-incidents/00-lesson-incident-response.md', NULL, NULL, 'manual_marker', 15, 'ready'), +('root-12-m01-a99-reflection-post-incident', 'root-12-m01-real-ops-incidents', 6, 'reflection', 'Post-Incident Reflection: What Real Ops Actually Feels Like', + 'content/root/root-12-real-ops/m01-real-ops-incidents/99-reflection-post-incident.md', NULL, NULL, 'manual_marker', 10, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- Capstone Readiness — 4 activities (READY: informational/orientation, not a lesson-heavy module) +INSERT INTO module_activities (activity_id, module_id, sequence, activity_type, title, content_path, scenario_id, assessment_definition_id, completion_behavior, estimated_duration_minutes, status) VALUES +('root-capstone-m02-a01-lesson-what-it-measures', 'root-capstone-m02-capstone-readiness', 1, 'lesson', 'What the Capstone Measures & How the Draw Works', + 'content/root/root-capstone/m02-capstone-readiness/01-lesson-what-it-measures.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-capstone-m02-a02-lesson-time-management', 'root-capstone-m02-capstone-readiness', 2, 'lesson', 'Time Management & Troubleshooting Strategy Under a Clock', + 'content/root/root-capstone/m02-capstone-readiness/02-lesson-time-management.md', NULL, NULL, 'manual_marker', 10, 'ready'), +('root-capstone-m02-a03-knowledge-check', 'root-capstone-m02-capstone-readiness', 3, 'knowledge_check', 'Readiness Check: Are You Actually Ready?', + 'content/root/root-capstone/m02-capstone-readiness/03-knowledge-check.md', NULL, NULL, 'manual_marker', 5, 'ready'), +('root-capstone-m02-a04-reflection-readiness', 'root-capstone-m02-capstone-readiness', 4, 'reflection', 'Readiness Reflection: What Completion Means', + 'content/root/root-capstone/m02-capstone-readiness/04-reflection-readiness.md', NULL, NULL, 'manual_marker', 5, 'ready') +ON CONFLICT (activity_id) DO NOTHING; + +-- ── Optional activities: guided/independent-practice duplicates and advanced/CKS-exam-style +-- extras (docs/ROOT_CURRICULUM_COVERAGE_MATRIX.md's GUIDED/INDEPENDENT/ADVANCED +-- classifications) count toward a module's displayed progress but not toward whether the +-- module itself is considered complete — see course-progress.js's totals(). Everything not +-- listed here stays required (the column's default), including every Phase 6A activity. +UPDATE module_activities SET is_required = false, version = version + 1, updated_at = now() WHERE activity_id IN ( + 'root-09-m01-a10-lab-seccomp-guided', + 'root-09-m01-a11-lab-apparmor-advanced', + 'root-09-m01-a12-lab-pod-security-context-independent', + 'root-09-m02-a08-lab-rbac-clusterrole-independent', + 'root-09-m03-a05-lab-egress-networkpolicy-guided', + 'root-09-m03-a08-lab-network-policy-metadata-advanced', + 'root-09-m04-a04-lab-runtime-class-advanced', + 'root-09-m04-a05-lab-audit-policy-advanced' +); diff --git a/platform/api/src/course-progress.js b/platform/api/src/course-progress.js index 16f9c14..16b3de1 100644 --- a/platform/api/src/course-progress.js +++ b/platform/api/src/course-progress.js @@ -55,16 +55,27 @@ function shapeActivity(activity, ctx, forInstructor) { scenario_id: activity.scenario_id, assessment_definition_id: activity.assessment_definition_id, completion_behavior: activity.completion_behavior, + is_required: activity.is_required, complete: isActivityComplete(activity, ctx), body: content?.shared || null, ...(forInstructor ? { instructor_notes: content?.instructorNotes || null } : {}), }; } +// Phase 6B §18/§27 — an optional activity (independent-practice duplicates, advanced/CKS-exam- +// style extras) counts toward the displayed total/percent but NOT toward whether the module +// itself is "complete." Every Phase 6A activity has is_required=true (the column's default, +// unaffected by this), so this is purely additive — nothing that was complete before becomes +// incomplete now. function totals(items) { const total = items.length; const completed = items.filter((i) => i.complete).length; - return { total, completed, pct: total ? Math.round((completed / total) * 100) : 0, complete: total > 0 && completed === total }; + const required = items.filter((i) => i.is_required !== false); + const requiredComplete = required.filter((i) => i.complete).length; + return { + total, completed, pct: total ? Math.round((completed / total) * 100) : 0, + complete: required.length > 0 && requiredComplete === required.length, + }; } // A prerequisite with zero content (a PLANNED module/course with nothing built yet) can never