- Node.js 22 or higher
- npm (included with Node.js)
- Git
-
Clone the repository:
git clone https://github.com/devops-ia/self-learning-platform.git cd self-learning-platform -
Install dependencies:
npm install
-
Create and seed the database:
npm run db:seed
-
Import exercise definitions:
npm run exercises:import
-
Start the development server:
npm run dev
The application will be available at http://localhost:3000
npm run dev- Start development server on localhost:3000npm run build- Production buildnpm run lint- Run ESLintnpm run db:seed- Create or reset SQLite database tablesnpm run exercises:import- Import YAML exercises into database
For convenience, the project includes a Makefile with common tasks:
make install- Install npm dependenciesmake dev- Start development servermake build- Full production build (seed + import + build)make lint- Run ESLintmake seed- Create or reset database tablesmake import- Import YAML exercises into databasemake test- Run tests
See make help for the complete list of available commands.
- TypeScript: All code must use TypeScript in strict mode
- Styling: Use Tailwind CSS 4 for all styles. Do not use custom CSS unless absolutely necessary
- Database: Use Drizzle ORM for all database operations
- Validation: Use Zod schemas for input validation
- Internationalization: Use the
useT()hook for all UI strings. Add translations tosrc/lib/i18n/locales/ - Code formatting: No emojis in code, comments, or commit messages
- File organization: Follow the existing directory structure. Prefer editing existing files over creating new ones
-
Create a new branch from
main:git checkout -b feat/your-feature-name
-
Make your changes following the coding standards
-
Verify your changes:
npm run lint npm run build
-
Commit your changes using conventional commits:
feat:for new featuresfix:for bug fixesdocs:for documentation changesrefactor:for code refactoringtest:for test additions or changeschore:for maintenance tasks
Example:
git add . git commit -m "feat: add Ansible module support"
-
Push your branch and open a pull request:
git push origin feat/your-feature-name
-
CI will run automatically to verify linting and build. Address any issues before requesting review.
Exercises are stored in the database and loaded at runtime. There are two ways to add them: YAML import (recommended) or admin panel.
-
Copy the template:
cp exercises/_template.yaml exercises/<module>/NN-slug.yaml
-
Edit the YAML file (see YAML Format below)
-
Import into the database:
npm run exercises:import
-
Test:
npm run dev
That's it. The import script reads the YAML, validates it, and inserts/updates the exercise in the database. The app loads it at runtime.
- Log in as admin (default:
admin@devopslab.local/admin1234) - Go to
/admin/exercisesand click "Create exercise" - Fill in the fields (ID, module, title, briefing, initial code, validations, terminal commands)
- Save — the exercise is immediately available
-
Add the module to
exercises/_modules.yaml:modules: ansible: title: "Ansible" description: es: "Aprende a escribir playbooks de Ansible corrigiendo errores comunes." en: "Learn to write Ansible playbooks by fixing common errors." icon: "Cog" # lucide-react icon name prefix: "ans" # ID prefix for exercises language: "yaml" # default language for the module
-
Create exercises in
exercises/ansible/:mkdir exercises/ansible cp exercises/_template.yaml exercises/ansible/01-broken-playbook.yaml
-
Import and test:
npm run exercises:import npm run dev # Navigate to /modules/ansible
- Go to
/admin/modulesand click "Create module" - Fill in the module details (ID, title, prefix, icon, language, descriptions)
- Create exercises under the new module at
/admin/exercises
Available icons: Terminal, Box, Cog, Settings, Server, Cloud, Database, Shield.
Each exercise is a YAML file in exercises/<module>/NN-slug.yaml.
id: tf-13-no-tags
title: "Recursos sin tags"
briefing: "Estos recursos no tienen tags. Anade los tags necesarios."
prerequisites: [] # optional, empty = no prerequisites
# language: "hcl" # optional, defaults to module's language
initialCode: |
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
}
hints:
- "Primera pista general."
- "Segunda pista mas especifica."
- "Tercera pista: casi la solucion."
successMessage: |
Correcto!
Lo que aprendiste:
- Punto 1
- Punto 2
validations:
- type: syntax
errorMessage: "Descripcion corta."
check:
contains: "tags"
failMessage: |
Error: no tags found
Explicacion detallada.
terminalCommands:
"terraform plan":
- when:
not_contains: "tags"
output: "Error: missing tags"
exitCode: 1
- output: "Plan: 1 to add"
exitCode: 0Checks define conditions on the student's code. Used in both validations[].check and terminalCommands[].when.
| Check | Description | Example |
|---|---|---|
contains: "str" |
Code includes string | contains: "required_providers" |
not_contains: "str" |
Code does NOT include string | not_contains: "zones" |
match: "regex" |
Code matches regex | match: "region\\s*=" |
not_match: "regex" |
Code does NOT match regex | not_match: "zones\\s*=" |
yaml_valid: true |
YAML parses without errors | yaml_valid: true |
yaml_has: "path" |
Nested field exists | yaml_has: "spec.containers" |
yaml_not_has: "path" |
Nested field does NOT exist | yaml_not_has: "spec.container" |
yaml_is_array: "path" |
Field is an array | yaml_is_array: "spec.containers" |
yaml_equals: {path, value} |
Field equals value | yaml_equals: {path: "kind", value: "Pod"} |
yaml_items_have: {path, fields} |
Array items have fields | yaml_items_have: {path: "spec.containers", fields: ["name","image"]} |
Paths support array indices: spec.containers.0.resources.limits.cpu
# AND — all must be true
check:
all:
- contains: "Name"
- contains: "Environment"
# OR — at least one must be true
check:
any:
- contains: "Always"
- contains: "IfNotPresent"
# NOT — negate a check
check:
not: { yaml_valid: true }For validations that can't be expressed declaratively:
validations:
- type: semantic
errorMessage: "Custom check."
check:
custom: |
const match = code.match(/resource\s+"aws_instance"/);
if (!match) {
return { passed: false, errorMessage: "No instance found" };
}
return { passed: true };
failMessage: "" # not used when custom provides errorMessageFor Kubernetes exercises, yaml (js-yaml) and _get (nested field helper) are available:
check:
custom: |
const parsed = yaml.load(code) as Record<string, unknown>;
const containers = _get(parsed, "spec.containers");
if (!Array.isArray(containers)) {
return { passed: false, errorMessage: "containers must be an array" };
}
return { passed: true };Order validations from basic to advanced:
- syntax — Parse-level checks (YAML valid, required blocks present)
- semantic — Schema-level checks (correct field names, types)
- intention — Logic checks (dependencies, cross-references)
Ordered list of condition-response pairs. Evaluated top to bottom; first match wins. Last entry (without when) is the default.
terminalCommands:
"kubectl apply -f pod.yaml":
- when: { not: { yaml_valid: true } }
output: "error: invalid YAML"
exitCode: 1
- when: { yaml_not_has: "spec.containers" }
output: "Error: missing containers"
exitCode: 1
- output: "pod/my-pod created" # default
exitCode: 0The platform supports multiple languages. The default language is Spanish (Spain), and English (US) is included. You can add more languages.
- UI strings: defined in
src/lib/i18n/locales/{es,en}.ts - Exercise content: default in YAML (Spanish), optional
i18n:block for translations - Module descriptions: per-language in
exercises/_modules.yaml - Language selection: client-side via
<select>in the navbar, saved in localStorage
-
Copy an existing locale file:
cp src/lib/i18n/locales/en.ts src/lib/i18n/locales/fr.ts
-
Translate all strings in the new file
-
Register the locale in
src/lib/i18n/context.tsx:import { fr } from "./locales/fr"; const locales: Record<string, Translations> = { es, en, fr }; export const availableLanguages = [ { code: "es", label: "Español" }, { code: "en", label: "English" }, { code: "fr", label: "Français" }, ];
-
Add the locale to the server-side files too:
src/lib/terminal/simulator.ts: addimport { fr } from "../i18n/locales/fr";and add tolocalessrc/lib/validators/engine.ts: same pattern
Add an i18n: block to any exercise YAML:
title: "Playbook sin hosts"
briefing: "Este playbook no tiene hosts..."
i18n:
en:
title: "Playbook without hosts"
briefing: "This playbook is missing the hosts field..."
hints:
- "An Ansible play needs the hosts: field..."
successMessage: |
Correct! The playbook now has hosts defined.
...Only include the fields you want to translate. Missing fields fall back to the default (Spanish).
In exercises/_modules.yaml, use a map for description:
modules:
terraform:
title: "Terraform"
description:
es: "Aprende a configurar infraestructura como código..."
en: "Learn to configure infrastructure as code..."The default language is Spanish (Spain, tu form):
- Use tu form: "corrige" not "corregi", "revisa" not "revisa"
- Use "anade" instead of "agrega"
- Keep briefings to 1-2 sentences
- Error messages should mimic real CLI output, then explain in Spanish
- Success messages should list 3-5 specific things the student learned
NN-slug.yaml
NN= two-digit sequential number (01, 02, 03...)slug= lowercase, hyphen-separated description
<prefix>-<NN>-<slug>
prefix= module prefix from_modules.yaml(tf, k8s, ans, etc.)NN= matches the file numberslug= matches the file slug
- YAML file created in
exercises/<module>/ - ID matches
<prefix>-NN-slugformat -
npm run exercises:importsucceeds -
npm run buildpasses - Exercise appears in module overview
- Terminal commands produce realistic output
- Validation errors explain why, not just what
- Hints are progressive (vague -> specific)
- Default text in Spanish (Spain, tu form)
- English translation added in
i18n.enblock (optional but appreciated)
- Module added to
exercises/_modules.yamlwith per-language descriptions - At least one exercise YAML exists
-
npm run exercises:importsucceeds - Module page renders at
/modules/<slug>
- Locale file created in
src/lib/i18n/locales/ - Registered in
src/lib/i18n/context.tsx - Added to
src/lib/terminal/simulator.tsandsrc/lib/validators/engine.ts - All UI strings translated
- Module descriptions added in
exercises/_modules.yaml