Files
rootnode-academy/platform/api/migrations/022_course_content.sql
T
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

267 lines
30 KiB
SQL

-- Phase 6A §3 — the course/module/activity content model. This is a STRUCTURAL layer on top
-- of everything Phase 1-5 already built (scenarios/*.json, assessment_definitions, achievements,
-- progress/scenario_attempts) — it never duplicates that content, only references it:
--
-- program (existing, 019_programs.sql)
-- -> course (new)
-- -> course_module (new)
-- -> module_activity (new) -- type lab: scenario_id references scenarios/data/*.json
-- -- type assessment/capstone: assessment_definition_id
-- -- references assessment_definitions (013_assessments.sql)
-- -- everything else: content_path references a Markdown
-- -- file under content/root/ (git-tracked prose, not a
-- -- Postgres blob — see docs/COURSE_CONTENT_MODEL.md)
--
-- Long-form lesson prose deliberately does NOT live in Postgres — only the structural metadata
-- (ordering, type, references, status) does. See docs/COURSE_CONTENT_MODEL.md for the full
-- rationale (git-editable content vs. a CMS) and the Markdown convention (a `## Instructor
-- Notes` heading splits shared content from instructor-only content in the same file).
--
-- No CMS, no WYSIWYG editor tables, no content versioning system beyond the same plain
-- incrementing `version` integer every other definitional table here already uses
-- (assessment_definitions, programs) — bumped by hand when a semantic edit is made, exactly the
-- same convention, not a new one.
CREATE TABLE IF NOT EXISTS courses (
course_id TEXT PRIMARY KEY, -- slug, e.g. 'root-05-kubernetes-foundations'
program_id TEXT NOT NULL REFERENCES programs(program_id),
title TEXT NOT NULL,
description TEXT,
sequence INTEGER NOT NULL, -- position within the program's course list
objectives TEXT[] NOT NULL DEFAULT '{}',
prerequisites TEXT[] NOT NULL DEFAULT '{}', -- course_ids; see docs/COURSE_CONTENT_MODEL.md
-- §"prerequisite/locking rule" for how an
-- empty-content prerequisite is treated
estimated_duration_hours INTEGER,
status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('ready', 'partial', 'planned')),
visibility TEXT NOT NULL DEFAULT 'published' CHECK (visibility IN ('published', 'draft')),
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_courses_program ON courses(program_id, sequence);
CREATE TABLE IF NOT EXISTS course_modules (
module_id TEXT PRIMARY KEY, -- slug, e.g. 'root-05-m01-pods-and-kubectl-basics'
course_id TEXT NOT NULL REFERENCES courses(course_id),
title TEXT NOT NULL,
description TEXT,
sequence INTEGER NOT NULL, -- position within the course's module list
objectives TEXT[] NOT NULL DEFAULT '{}',
prerequisites TEXT[] NOT NULL DEFAULT '{}', -- module_ids
estimated_duration_minutes INTEGER,
status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('ready', 'partial', 'planned')),
visibility TEXT NOT NULL DEFAULT 'published' CHECK (visibility IN ('published', 'draft')),
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_modules_course ON course_modules(course_id, sequence);
CREATE TABLE IF NOT EXISTS module_activities (
activity_id TEXT PRIMARY KEY, -- slug, e.g. 'root-10-m01-...-a05-rootnode-probe-port-mismatch'
module_id TEXT NOT NULL REFERENCES course_modules(module_id),
sequence INTEGER NOT NULL, -- position within the module's activity list
activity_type TEXT NOT NULL CHECK (activity_type IN
('lesson', 'demonstration', 'knowledge_check', 'guided_exercise',
'lab', 'reflection', 'assessment', 'capstone')),
title TEXT NOT NULL,
content_path TEXT, -- Markdown file under content/root/, relative to repo root; NULL for a pure lab/assessment reference with no extra prose
scenario_id TEXT, -- 'lab' activities only — references scenarios/data/<id>.json (not a DB FK; that content isn't a DB table)
assessment_definition_id TEXT REFERENCES assessment_definitions(assessment_definition_id), -- 'assessment'/'capstone' activities only
completion_behavior TEXT NOT NULL DEFAULT 'manual_marker' CHECK (completion_behavior IN
('auto_scenario', 'auto_assessment', 'manual_marker')),
estimated_duration_minutes INTEGER,
status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('ready', 'partial', 'planned')),
visibility TEXT NOT NULL DEFAULT 'published' CHECK (visibility IN ('published', 'draft')),
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT module_activity_lab_has_scenario CHECK (activity_type != 'lab' OR scenario_id IS NOT NULL),
CONSTRAINT module_activity_assessment_has_definition CHECK (activity_type NOT IN ('assessment', 'capstone') OR assessment_definition_id IS NOT NULL)
);
CREATE INDEX IF NOT EXISTS idx_activities_module ON module_activities(module_id, sequence);
CREATE INDEX IF NOT EXISTS idx_activities_scenario ON module_activities(scenario_id);
-- Explicit completion marker — ONLY for activities whose completion_behavior is
-- 'manual_marker' (lesson/demonstration/knowledge_check/guided_exercise/reflection). A 'lab'
-- activity's completion is derived live from the existing progress table
-- (scenario_id -> progress[scenario_id].status = 'completed', exactly what curriculum-rules.js
-- already checks) and an 'assessment'/'capstone' activity's completion is derived live from the
-- existing achievements table — this table is never a second source of truth for either. See
-- docs/COURSE_CONTENT_MODEL.md §"completion rules".
CREATE TABLE IF NOT EXISTS activity_completions (
student_id TEXT NOT NULL REFERENCES students(student_id),
activity_id TEXT NOT NULL REFERENCES module_activities(activity_id),
completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (student_id, activity_id)
);
CREATE INDEX IF NOT EXISTS idx_activity_completions_student ON activity_completions(student_id);
-- ═══════════════════════════════════════════════════════════════════════════════════════════
-- Seed: the ROOT_ course architecture (docs/ROOT_PROGRAM_ARCHITECTURE.md), all under the
-- existing 'root-kubernetes-foundations' program — root_ is one continuous ladder a student
-- enrolls in once, not twelve separate commercial products, so no programs-table change.
--
-- Every module/activity below references EXISTING scenario/assessment content only — nothing
-- here duplicates or forks scenarios/*.json or assessment_definitions. See
-- docs/ROOT_CURRICULUM_INVENTORY.md for the full inventory this mapping is grounded in.
-- ═══════════════════════════════════════════════════════════════════════════════════════════
INSERT INTO courses (course_id, program_id, title, description, sequence, objectives, prerequisites, estimated_duration_hours, status) VALUES
('root-01-computing-linux-foundations', 'root-kubernetes-foundations', 'Computing & Linux Foundations',
'How a computer actually works, the Linux filesystem and shell, and the command-line fluency everything else in root_ builds on. No lab content exists in this repository yet.',
1, '{}', '{}', NULL, 'planned'),
('root-02-linux-administration-networking', 'root-kubernetes-foundations', 'Linux Administration & Networking',
'Users/permissions, services and systemd, package management, and TCP/IP networking fundamentals. No lab content exists in this repository yet.',
2, '{}', '{}', NULL, 'planned'),
('root-03-git-collaborative-development', 'root-kubernetes-foundations', 'Git & Collaborative Development',
'Version control, branching, code review, and working in a shared repository. No lab content exists in this repository yet.',
3, '{}', '{}', NULL, 'planned'),
('root-04-containers-and-docker', 'root-kubernetes-foundations', 'Containers & Docker',
'What a container actually is, building images, and running/networking containers with Docker, as the on-ramp to Kubernetes. No lab content exists in this repository yet.',
4, '{}', '{}', NULL, 'planned'),
('root-05-kubernetes-foundations', 'root-kubernetes-foundations', 'Kubernetes Foundations',
'Pods, kubectl fluency, labels/selectors, and the core object model — the existing "Foundation" curriculum stage.',
5, '{"Explain what a Pod is and its lifecycle states","Use kubectl confidently to inspect and manipulate cluster objects","Apply labels and selectors to organize resources"}', '{}', 8, 'partial'),
('root-06-kubernetes-application-workloads', 'root-kubernetes-foundations', 'Kubernetes Application Workloads',
'Deployments, scaling, rolling updates, Jobs/CronJobs, and DaemonSets — the existing "Workloads" curriculum stage.',
6, '{"Deploy and scale a stateless application","Perform a rolling update and understand deployment strategies","Distinguish Jobs, CronJobs, and DaemonSets"}', '{root-05-kubernetes-foundations}', 4, 'partial'),
('root-07-kubernetes-networking-config', 'root-kubernetes-foundations', 'Kubernetes Networking & Configuration',
'Services, exposing applications, ConfigMaps, Secrets, and environment configuration — the existing "Networking" and "Configuration" curriculum stages.',
7, '{"Expose a workload with the right Service type","Externalize configuration with ConfigMaps and Secrets"}', '{root-06-kubernetes-application-workloads}', 4, 'partial'),
('root-08-kubernetes-storage-scheduling', 'root-kubernetes-foundations', 'Kubernetes Storage & Scheduling',
'PersistentVolumes/Claims, StorageClasses, taints/tolerations, priority, and disruption budgets. Real scenario content exists in this repository (see docs/ROOT_CURRICULUM_INVENTORY.md) but is not yet wired into a module — Phase 6B.',
8, '{}', '{root-07-kubernetes-networking-config}', NULL, 'planned'),
('root-09-kubernetes-security', 'root-kubernetes-foundations', 'Kubernetes Security',
'RBAC, Pod Security Admission, network policy, supply-chain and runtime hardening — a substantial existing CKS-style scenario bank (22 scenarios) not yet wired into a module — Phase 6B.',
9, '{}', '{root-06-kubernetes-application-workloads}', NULL, 'planned'),
('root-10-kubernetes-troubleshooting-observability', 'root-kubernetes-foundations', 'Kubernetes Troubleshooting & Observability',
'Reading logs, exec/cp into containers, node scheduling, and diagnosing real failures — health probes are this course''s gold-standard module.',
10, '{"Diagnose a Pod stuck in a bad state using kubectl and logs","Explain liveness, readiness, and startup probes and how misconfiguring them causes real outages"}', '{root-06-kubernetes-application-workloads}', 5, 'partial'),
('root-11-devops-platform-engineering', 'root-kubernetes-foundations', 'DevOps & Platform Engineering Foundations',
'CI/CD, infrastructure as code, and GitOps principles. No lab content exists in this repository yet.',
11, '{}', '{root-10-kubernetes-troubleshooting-observability}', NULL, 'planned'),
('root-12-real-ops', 'root-kubernetes-foundations', 'RootNode Real Ops',
'RootNode-authored incident scenarios — operate and recover systems the way a real on-call engineer would, not textbook exercises. The existing "RootNode: Real Ops" curriculum stage.',
12, '{"Recover a cluster from a resource-exhaustion incident","Diagnose and fix a broken deployment without being told what''s wrong","Apply resource defaults that prevent a class of outage from recurring"}', '{root-10-kubernetes-troubleshooting-observability}', 3, 'partial'),
('root-capstone', 'root-kubernetes-foundations', 'ROOT Capstone — Operate and Recover a Production-Like Environment',
'The existing RootNode Capstone assessment, combining skills from every stage into one operational evaluation.',
13, '{"Operate a Kubernetes environment under exam conditions without step-by-step guidance"}', '{root-12-real-ops}', 2, 'partial')
ON CONFLICT (course_id) DO NOTHING;
INSERT INTO course_modules (module_id, course_id, title, description, sequence, objectives, prerequisites, estimated_duration_minutes, status) VALUES
('root-05-m01-pods-and-kubectl-basics', 'root-05-kubernetes-foundations', 'Pods & kubectl Basics',
'The 20 scenarios in the existing "Foundation" curriculum stage, as disposable-Lab activities. No teaching lesson content authored yet — Phase 6B.',
1, '{}', '{}', NULL, 'partial'),
('root-06-m01-deployments-and-workloads', 'root-06-kubernetes-application-workloads', 'Deployments & Workloads',
'The 8 scenarios in the existing "Workloads" curriculum stage, as disposable-Lab activities. No teaching lesson content authored yet — Phase 6B.',
1, '{}', '{}', NULL, 'partial'),
('root-07-m01-services-and-networking', 'root-07-kubernetes-networking-config', 'Services & Networking',
'The 2 scenarios in the existing "Networking" curriculum stage, as disposable-Lab activities. No teaching lesson content authored yet — Phase 6B.',
1, '{}', '{}', NULL, 'partial'),
('root-07-m02-configuration-and-secrets', 'root-07-kubernetes-networking-config', 'Configuration & Secrets',
'The 5 scenarios in the existing "Configuration" curriculum stage, as disposable-Lab activities. No teaching lesson content authored yet — Phase 6B.',
2, '{}', '{}', NULL, 'partial'),
('root-10-m01-health-probes-and-troubleshooting', 'root-10-kubernetes-troubleshooting-observability', 'Kubernetes Health Probes & Troubleshooting',
'The Phase 6A gold-standard module: liveness/readiness/startup probe concepts taught BEFORE the student is asked to fix a real probe misconfiguration in rootnode-probe-port-mismatch. See docs/COURSE_AUTHORING_STANDARD.md — this module is the template for all future RootNode lesson authoring.',
1, '{"Explain why Kubernetes health checks exist and what problem each probe type solves","Distinguish liveness, readiness, and startup probes and choose the right one","Diagnose a probe misconfiguration from Pod status and events, not guesswork"}',
'{root-05-m01-pods-and-kubectl-basics}', 75, 'ready'),
('root-10-m02-general-troubleshooting', 'root-10-kubernetes-troubleshooting-observability', 'General Troubleshooting',
'The 5 scenarios in the existing "Troubleshooting" curriculum stage, as disposable-Lab activities. No teaching lesson content authored yet — Phase 6B.',
2, '{}', '{root-10-m01-health-probes-and-troubleshooting}', NULL, 'partial'),
('root-12-m01-real-ops-incidents', 'root-12-real-ops', 'Real Ops Incidents',
'The 4 scenarios in the existing "RootNode: Real Ops" curriculum stage, as disposable-Lab activities. rootnode-probe-port-mismatch also appears here — deliberate reuse: the gold module teaches the concept in depth, this module exercises it again alongside three other unrelated incident types, the same way the RootNode Capstone pool also reuses it. No teaching lesson content authored yet — Phase 6B.',
1, '{}', '{root-10-m01-health-probes-and-troubleshooting}', NULL, 'partial'),
('root-capstone-m01-operate-and-recover', 'root-capstone', 'Operate and Recover',
'The existing RootNode Capstone assessment (90 minutes, mixed required/random incident pool). No capstone orientation/briefing lesson authored yet — Phase 6B.',
1, '{}', '{root-12-m01-real-ops-incidents}', 90, 'partial')
ON CONFLICT (module_id) DO NOTHING;
-- ── Gold-standard module activities (root-10-m01-health-probes-and-troubleshooting) ──────────
-- Content lives in content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/
-- — see docs/COURSE_CONTENT_MODEL.md for the Markdown convention these files follow.
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-m01-a01-lesson-health-check-concepts', 'root-10-m01-health-probes-and-troubleshooting', 1, 'lesson',
'Why Health Checks Exist & Probe Types',
'content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/01-lesson-health-check-concepts.md',
NULL, NULL, 'manual_marker', 15, 'ready'),
('root-10-m01-a02-demonstration-probes-in-action', 'root-10-m01-health-probes-and-troubleshooting', 2, 'demonstration',
'Demonstration: Probes in Action',
'content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/02-demonstration-probes-in-action.md',
NULL, NULL, 'manual_marker', 10, 'ready'),
('root-10-m01-a03-knowledge-check', 'root-10-m01-health-probes-and-troubleshooting', 3, 'knowledge_check',
'Knowledge Check: Health Probes',
'content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/03-knowledge-check.md',
NULL, NULL, 'manual_marker', 5, 'ready'),
('root-10-m01-a04-guided-exercise-readiness-probe', 'root-10-m01-health-probes-and-troubleshooting', 4, 'guided_exercise',
'Guided Exercise: Configure a Readiness Probe',
'content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/04-guided-exercise-readiness-probe.md',
NULL, NULL, 'manual_marker', 15, 'ready'),
('root-10-m01-a05-lab-probe-port-mismatch', 'root-10-m01-health-probes-and-troubleshooting', 5, 'lab',
'Launch Lab: rootnode-probe-port-mismatch',
'content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/05-lab-intro.md',
'rootnode-probe-port-mismatch', NULL, 'auto_scenario', 20, 'ready'),
('root-10-m01-a06-reflection-real-ops-takeaway', 'root-10-m01-health-probes-and-troubleshooting', 6, 'reflection',
'Root-Cause Reflection & Real Ops Takeaway',
'content/root/root-10-kubernetes-troubleshooting-observability/m01-health-probes/06-reflection-real-ops-takeaway.md',
NULL, NULL, 'manual_marker', 10, 'ready')
ON CONFLICT (activity_id) DO NOTHING;
-- ── Mechanically-mapped lab activities for the other partial modules ────────────────────────
-- Generated 1:1 from scenarios/rootnode-path.json's existing stage scenario_ids (see
-- docs/ROOT_CURRICULUM_INVENTORY.md) — no new lab content invented, titles taken from each
-- scenario's own JSON. Estimated duration is a rough heuristic from the scenario's declared
-- difficulty (15/20/30 min); refine once real per-activity completion-time data exists.
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, visibility, version) VALUES
('root-05-m01-pods-and-kubectl-basics-a01-pod-basics-mcq', 'root-05-m01-pods-and-kubectl-basics', 1, 'lab', 'Lab: Pod Lifecycle States', NULL, 'pod-basics-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a02-kubectl-essentials-mcq', 'root-05-m01-pods-and-kubectl-basics', 2, 'lab', 'Lab: Essential kubectl Commands', NULL, 'kubectl-essentials-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a03-labels-selectors-mcq', 'root-05-m01-pods-and-kubectl-basics', 3, 'lab', 'Lab: Labels and Selectors', NULL, 'labels-selectors-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a04-init-containers-mcq', 'root-05-m01-pods-and-kubectl-basics', 4, 'lab', 'Lab: Init Containers Behaviour', NULL, 'init-containers-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a05-namespaces-basics', 'root-05-m01-pods-and-kubectl-basics', 5, 'lab', 'Lab: Working with Namespaces', NULL, 'namespaces-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a06-multi-container-pod-basics', 'root-05-m01-pods-and-kubectl-basics', 6, 'lab', 'Lab: Create a Multi-Container Pod', NULL, 'multi-container-pod-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a07-pod-labeling-basics', 'root-05-m01-pods-and-kubectl-basics', 7, 'lab', 'Lab: Labelling Resources', NULL, 'pod-labeling-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a08-jsonpath-basics', 'root-05-m01-pods-and-kubectl-basics', 8, 'lab', 'Lab: JSONPath Data Extraction', NULL, 'jsonpath-basics', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a09-dry-run-manifest-basics', 'root-05-m01-pods-and-kubectl-basics', 9, 'lab', 'Lab: Generating YAML Manifests', NULL, 'dry-run-manifest-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a10-delete-by-label-basics', 'root-05-m01-pods-and-kubectl-basics', 10, 'lab', 'Lab: Bulk Deletion by Label', NULL, 'delete-by-label-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a11-annotate-resource-basics', 'root-05-m01-pods-and-kubectl-basics', 11, 'lab', 'Lab: Annotating Kubernetes Resources', NULL, 'annotate-resource-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a12-init-container-task-basics', 'root-05-m01-pods-and-kubectl-basics', 12, 'lab', 'Lab: Pod with Init Container', NULL, 'init-container-task-basics', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a13-restart-policy-mcq', 'root-05-m01-pods-and-kubectl-basics', 13, 'lab', 'Lab: Pod Restart Policies', NULL, 'restart-policy-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a14-kubectl-output-format-mcq', 'root-05-m01-pods-and-kubectl-basics', 14, 'lab', 'Lab: kubectl Output Formats', NULL, 'kubectl-output-format-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a15-kubectl-explain-basics', 'root-05-m01-pods-and-kubectl-basics', 15, 'lab', 'Lab: Exploring the API with kubectl explain', NULL, 'kubectl-explain-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a16-kubectl-patch-basics', 'root-05-m01-pods-and-kubectl-basics', 16, 'lab', 'Lab: Patching Resources with kubectl patch', NULL, 'kubectl-patch-basics', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a17-field-selector-basics', 'root-05-m01-pods-and-kubectl-basics', 17, 'lab', 'Lab: Filtering Resources with Field Selectors', NULL, 'field-selector-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a18-sort-by-basics', 'root-05-m01-pods-and-kubectl-basics', 18, 'lab', 'Lab: Sorting kubectl Output', NULL, 'sort-by-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a19-kubectl-diff-basics', 'root-05-m01-pods-and-kubectl-basics', 19, 'lab', 'Lab: Previewing Changes with kubectl diff', NULL, 'kubectl-diff-basics', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-05-m01-pods-and-kubectl-basics-a20-static-pods-mcq', 'root-05-m01-pods-and-kubectl-basics', 20, 'lab', 'Lab: Static Pods in Kubernetes', NULL, 'static-pods-mcq', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a01-deploy-nginx', 'root-06-m01-deployments-and-workloads', 1, 'lab', 'Lab: Deploy and Expose Nginx', NULL, 'deploy-nginx', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a02-scale-deployment', 'root-06-m01-deployments-and-workloads', 2, 'lab', 'Lab: Scaling a Deployment', NULL, 'scale-deployment', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a03-rolling-update-task', 'root-06-m01-deployments-and-workloads', 3, 'lab', 'Lab: Perform a Rolling Update', NULL, 'rolling-update-task', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a04-liveness-probe-task', 'root-06-m01-deployments-and-workloads', 4, 'lab', 'Lab: Configure a Liveness Probe', NULL, 'liveness-probe-task', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a05-jobs-cronjobs-mcq', 'root-06-m01-deployments-and-workloads', 5, 'lab', 'Lab: Jobs vs CronJobs', NULL, 'jobs-cronjobs-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a06-daemonset-mcq', 'root-06-m01-deployments-and-workloads', 6, 'lab', 'Lab: Understanding DaemonSets', NULL, 'daemonset-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a07-edit-deployment-basics', 'root-06-m01-deployments-and-workloads', 7, 'lab', 'Lab: Updating Deployments', NULL, 'edit-deployment-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-06-m01-deployments-and-workloads-a08-deployment-strategy-mcq', 'root-06-m01-deployments-and-workloads', 8, 'lab', 'Lab: Deployment Update Strategies', NULL, 'deployment-strategy-mcq', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-07-m01-services-and-networking-a01-services-mcq', 'root-07-m01-services-and-networking', 1, 'lab', 'Lab: Kubernetes Service Types', NULL, 'services-mcq', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-07-m01-services-and-networking-a02-expose-service-basics', 'root-07-m01-services-and-networking', 2, 'lab', 'Lab: Exposing an Application', NULL, 'expose-service-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-07-m02-configuration-and-secrets-a01-configmap-volume', 'root-07-m02-configuration-and-secrets', 1, 'lab', 'Lab: Mount ConfigMap as Volume', NULL, 'configmap-volume', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-07-m02-configuration-and-secrets-a02-resource-limits-task', 'root-07-m02-configuration-and-secrets', 2, 'lab', 'Lab: Resource Requests and Limits', NULL, 'resource-limits-task', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-07-m02-configuration-and-secrets-a03-env-vars-configmap', 'root-07-m02-configuration-and-secrets', 3, 'lab', 'Lab: Environment Variables from ConfigMap', NULL, 'env-vars-configmap', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-07-m02-configuration-and-secrets-a04-secrets-basics', 'root-07-m02-configuration-and-secrets', 4, 'lab', 'Lab: Creating and Using Secrets', NULL, 'secrets-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-07-m02-configuration-and-secrets-a05-pod-env-vars-inline', 'root-07-m02-configuration-and-secrets', 5, 'lab', 'Lab: Pod with Inline Environment Variables', NULL, 'pod-env-vars-inline', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-10-m02-general-troubleshooting-a01-extract-logs-basics', 'root-10-m02-general-troubleshooting', 1, 'lab', 'Lab: Extract Container Logs', NULL, 'extract-logs-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-10-m02-general-troubleshooting-a02-kubectl-cp-basics', 'root-10-m02-general-troubleshooting', 2, 'lab', 'Lab: Copy Files to Containers', NULL, 'kubectl-cp-basics', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-10-m02-general-troubleshooting-a03-exec-command-basics', 'root-10-m02-general-troubleshooting', 3, 'lab', 'Lab: Execute Commands in Pods', NULL, 'exec-command-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-10-m02-general-troubleshooting-a04-node-cordon-basics', 'root-10-m02-general-troubleshooting', 4, 'lab', 'Lab: Cordoning and Uncordoning a Node', NULL, 'node-cordon-basics', NULL, 'auto_scenario', 15, 'ready', 'published', 1),
('root-10-m02-general-troubleshooting-a05-pod-node-assignment-mcq', 'root-10-m02-general-troubleshooting', 5, 'lab', 'Lab: Assigning Pods to Nodes', NULL, 'pod-node-assignment-mcq', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-12-m01-real-ops-incidents-a01-rootnode-runaway-monitor', 'root-12-m01-real-ops-incidents', 1, 'lab', 'Lab: Recover the Monitoring Stack', NULL, 'rootnode-runaway-monitor', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-12-m01-real-ops-incidents-a02-broken-deployment', 'root-12-m01-real-ops-incidents', 2, 'lab', 'Lab: Debug a Failing Deployment', NULL, 'broken-deployment', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-12-m01-real-ops-incidents-a03-rootnode-probe-port-mismatch', 'root-12-m01-real-ops-incidents', 3, 'lab', 'Lab: Stuck Not-Ready: The Probe Port Mismatch', NULL, 'rootnode-probe-port-mismatch', NULL, 'auto_scenario', 20, 'ready', 'published', 1),
('root-12-m01-real-ops-incidents-a04-rootnode-enforce-limits', 'root-12-m01-real-ops-incidents', 4, 'lab', 'Lab: Stop the Next Outage: Enforce Resource Defaults', NULL, 'rootnode-enforce-limits', NULL, 'auto_scenario', 30, 'ready', 'published', 1)
ON CONFLICT (activity_id) DO NOTHING;
-- ── Capstone activity (references the existing assessment engine, no duplicate definition) ──
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-m01-a01-capstone', 'root-capstone-m01-operate-and-recover', 1, 'capstone', 'RootNode Capstone', NULL, NULL, 'rootnode-capstone', 'auto_assessment', 90, 'ready')
ON CONFLICT (activity_id) DO NOTHING;