Main app of the aivopod platform.
  • TypeScript 97%
  • CSS 2.3%
  • Shell 0.4%
  • JavaScript 0.2%
  • HTML 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
oliver.maklott 42b510fdd2
All checks were successful
e2e / e2e (push) Successful in 14m57s
check / check (pull_request) Successful in 6m41s
e2e / e2e (pull_request) Successful in 15m22s
publish / release (push) Successful in 1m53s
check / check (push) Successful in 7m2s
Wait for the preview skin's stylesheet: an effect sets it after the page renders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:23:08 +00:00
.forgejo/workflows check runs on push only for main, on PR branches for pull_request only: half the runs, against the browser-test flake under load (docs/ci.md) 2026-10-10 15:39:06 +00:00
.githooks B03: move CI to Forgejo workflows and add the pre-push hook 2026-09-26 21:00:58 +00:00
apps Wait for the preview skin's stylesheet: an effect sets it after the page renders 2026-10-12 20:23:08 +00:00
deploy Release 0.20.0: challenges load on slow and shared lines, the SDK from the platform, a lighter app with a service worker (D249–D257) 2026-10-12 17:21:37 +00:00
docker Loading ahead: while the course list is read, the runner and the next bundles come into the cache (Rückmeldung T, D252); 0.20.0-rc.4 2026-10-12 16:17:11 +00:00
docs Release 0.20.0: challenges load on slow and shared lines, the SDK from the platform, a lighter app with a service worker (D249–D257) 2026-10-12 17:21:37 +00:00
e2e A service worker for the app: the page from the device after 4 s or without a network, hashed files from the cache, a reload offered for a new release, an emergency switch (Rückmeldung T, D257); 0.20.0-rc.8 2026-10-12 16:57:54 +00:00
packages A service worker for the app: the page from the device after 4 s or without a network, hashed files from the cache, a reload offered for a new release, an emergency switch (Rückmeldung T, D257); 0.20.0-rc.8 2026-10-12 16:57:54 +00:00
scripts Browser tests once more when only a test file could not be loaded (the Vite import flake, four times on 2026-10-10); lint ignores the downloaded preview frontends 2026-10-10 17:52:51 +00:00
.dockerignore B25: packs only in their own repositories 2026-09-27 16:39:46 +00:00
.editorconfig A01: scaffold pnpm monorepo 2026-09-25 21:13:54 +00:00
.gitignore E2E: pack preview with the frontends the CLI embeds (its platform version's archive), since its preview API speaks that version's protocol (D189) 2026-10-10 10:39:05 +00:00
.nvmrc B02: switch to Node 26 2026-09-26 20:50:30 +00:00
.prettierignore B26: merge Plan 2 into the SPEC, operations handbook with apcmd 2026-09-27 17:59:05 +00:00
.prettierrc.json A01: scaffold pnpm monorepo 2026-09-25 21:13:54 +00:00
CLAUDE.md G0.1: one English vocabulary for the platform, translated at the border – fromLegacy/toLegacy/withBothNames with a shape per object, so only names the platform defines change; the manifests of the 12 released packs translate without a German key and back without loss (D188); SPEC section 5, rule in CLAUDE.md 2026-10-10 08:48:48 +00:00
eslint.config.js A lighter app: teacher pages, the challenge with its editor, games and previews load when opened; a page that does not arrive offers another try; the web without the CLI's texts; a budget for the start page (Rückmeldung T, D256); 0.20.0-rc.7 2026-10-12 16:45:25 +00:00
package.json Release 0.20.0: challenges load on slow and shared lines, the SDK from the platform, a lighter app with a service worker (D249–D257) 2026-10-12 17:21:37 +00:00
pnpm-lock.yaml G14c: autocompletion from the name table – the code editor offers the student API's names in the code language, nothing without a student API (D220); 0.17.0-rc.4 2026-10-11 01:14:39 +00:00
pnpm-workspace.yaml A14: add pack CLI and asset pipeline 2026-09-26 09:03:52 +00:00
README.md List the students of a course for the CLI 2026-09-28 21:28:20 +00:00
tsconfig.base.json A01: scaffold pnpm monorepo 2026-09-25 21:13:54 +00:00
tsconfig.json B25: packs only in their own repositories 2026-09-27 16:39:46 +00:00
vitest.config.ts B26: merge Plan 2 into the SPEC, operations handbook with apcmd 2026-09-27 17:59:05 +00:00

Aivopod

Aivopod is a learning platform for programming lessons at school. Students work through small, fine-grained exercises ("challenges") in JavaScript: reading code, predicting output, marking parts of code, sorting, running, changing and writing code. Each challenge has three levels (normal, erweitert, experte). Teachers get a minimal view of how the class is doing and CSV exports for their own analysis in Excel.

What sets it apart:

  • No accounts for students. They join with a printed card (code and QR code). Only a pseudonym is visible to other students.
  • Student code never runs on the server. It runs in a sandboxed iframe on its own origin, inside a Web Worker with a time limit, without network access or cookies.
  • Nothing is lost. Code is saved continuously through an offline outbox in IndexedDB. It reaches the server after a lost connection and comes back on another device.
  • Content lives in challenge packs: separate repositories written with a small TypeScript SDK (defineChallenge, step kinds, declarative checkers), tested like code and published with the CLI.
  • Teachers sign in via OIDC with the school's identity provider. Administration (courses, students, cards, assignments) is done with the aivopod CLI.
  • Everything is translatable (i18next with ICU messages). German is the first language.

The binding specification is docs/SPEC.md (German), decisions are in docs/decisions.md, and the remaining differences from the plan are in docs/deviations.md. Operations are described in docs/betrieb.md (German).

Infrastructure

Aivopod runs as a Docker Compose stack behind Caddy on one server, with three origins:

Origin Serves Notes
app.<domain> Web app (React SPA) and the API under /api Strict CSP; session cookies HttpOnly; Secure; SameSite
run.<domain> Runner frame (sandboxed iframe) connect-src 'none', no cookies; runs student code in Blob workers
assets.<domain> Pack bundles and images, addressed by SHA-256 Content-Security-Policy: sandbox, immutable caching
Compose service Role
caddy TLS (Let's Encrypt or a local CA), the three hostnames, security headers, static files of the web app and runner frame
api Fastify API (Node 26, TypeScript)
worker pg-boss worker verify: decides submissions with the configured runner (RUNNER=client in v0)
postgres PostgreSQL 16; every table with student data carries school_id and is protected by row-level security
db-init One-off: creates roles and database, runs migrations and sets up the job-queue schema
backup Nightly pg_dump plus the asset volume, encrypted with age, optionally copied off-site with rsync

Stack: TypeScript (strict), pnpm workspace, React + Vite + React Router, CodeMirror 6, @lezer/javascript, Fastify, valibot (shared schemas), PostgreSQL 16 with Kysely, pg-boss, i18next (ICU), openid-client, marked + DOMPurify, lucide, sharp, Vitest and Playwright.

Repository layout

apps/
  api/            Fastify API, migrations, operator commands (dist/ops.js), worker (dist/worker.js)
  web/            React SPA: student view, teacher view (/teacher), join page
  runner-frame/   Sandboxed runner: loads pack bundles hash-checked, runs code and checkers
  cli/            The `aivopod` command-line tool for teachers and pack authors
packages/
  shared/         valibot schemas for the API and the postMessage protocol, route registry
  sdk/            Pack authoring SDK (defineChallenge, step kinds) and test helpers
  checkers/       Checker blocks, named error patterns, feedback IDs
  harness/        Runs code with limits (Node and browser) and applies the checkers
  analysis/       lezer-based code analysis (nodes, ranges, node-level diff)
  i18n/           i18next setup, catalogs (locales/de), formatting, CSV writer
deploy/           Production compose.yml, .env.example, caddy/Caddyfile, MIGRATION_NOTES.md (the deploy bundle)
docker/           Dockerfile, compose.dev.yml, compose.e2e.yml (test IdP), backup scripts
scripts/release/  Image, bundle and upload scripts of the publish workflow (docs/ci.md)
e2e/              Playwright tests against the running stack, incl. the lesson scenario
docs/             SPEC, decisions, deviations, operations manual
e2e/packs/       Pack repositories checked out for the E2E tests (git-ignored)

Requirements

  • Node.js 26 (.nvmrc) and pnpm 12 (npm install -g corepack && corepack enable; Node 26 no longer ships corepack)
  • Docker with Compose v2 (for the stack, the database tests and the end-to-end tests)
  • For browser tests: Chromium from Playwright (pnpm exec playwright install --with-deps chromium)

Build, test and run

Workspace commands

Command What it does
pnpm install Install all dependencies
pnpm build Build every app and package
pnpm test Unit and integration tests (Vitest); the API tests start PostgreSQL with Testcontainers
pnpm test:browser Browser tests in Chromium (harness workers, web components)
pnpm lint ESLint
pnpm format:check Prettier check
pnpm typecheck Type check, including tests
pnpm check All three: format, lint, types
pnpm format Format everything with Prettier
pnpm test:stack Header tests (CSP, cookies, CORS) against the running stack
pnpm test:e2e Playwright tests against the running stack
pnpm --filter @aivopod/api db:codegen Regenerate the Kysely types from the migrated database

If Chromium's system libraries are missing locally, scripts/playwright-server.sh starts Chromium in the official Playwright image. Point PLAYWRIGHT_WS_ENDPOINT at it.

Run locally

The production compose file with images built from the checkout (docker/compose.dev.yml); it also mounts the built SPA and runner frame, so pnpm build shows up without a new image:

cp deploy/.env.example deploy/.env     # *.localhost, TLS=internal, HTTPS on port 8443
docker compose -f deploy/compose.yml -f docker/compose.dev.yml up -d --build --wait

The web app is then at https://app.localhost:8443 (Caddy's local CA; accept the certificate or trust /data/caddy/pki/authorities/local/root.crt from the caddy container). Stop with down, and add -v to also delete all data.

For teacher login without a real identity provider, use the E2E override with the test IdP instead (development and tests only):

docker compose -f deploy/compose.yml -f docker/compose.e2e.yml up -d --build --wait

End-to-end tests

docker compose -f deploy/compose.yml -f docker/compose.e2e.yml down -v    # the lesson needs a fresh stack
docker compose -f deploy/compose.yml -f docker/compose.e2e.yml up -d --build --wait
pnpm build && pnpm test:stack && pnpm test:e2e

The lesson scenario (e2e/tests/lesson.spec.ts) plays a whole lesson:

  1. A teacher signs in through OIDC, sets up a course with the CLI, prints cards and assigns the example pack.
  2. Three students join with the QR codes from the card PDF. One completes a level, one loses the connection while writing, and one clears the browser storage and continues on a "new device".
  3. The teacher checks the overview and exports a CSV.

It needs the Grundlagen pack repository in e2e/packs/grundlagen (or E2E_PACK_DIR) with its dependencies installed (npm ci), and the compiled CLI (AIVOPOD_CLI, default aivopod-cli/dist/aivopod).

CI runs the same in .forgejo/workflows/check.yml and e2e.yml, see docs/ci.md. pnpm install activates a pre-push hook (.githooks/pre-push) that runs the format check and ESLint.

Deploy

The server builds nothing. It runs the deploy bundle of a release (aivopod-deploy-<version>.zip from the generic package registry: compose.yml, .env.example, caddy/Caddyfile, release-trust.json, VERSION, MIGRATION_NOTES.md) with images pulled by digest, managed with apcmd (SPEC section 11). Short version (details in docs/betrieb.md, German):

  1. Point DNS for the four host names (app, run, assets and the preview domain, which must be its own registrable domain) to the server, with ports 80 and 443 open.
  2. Install apcmd: curl -fsSL https://git.belowtoxic.cloud/api/packages/aivopod/generic/apcmd/latest/install.sh | sh
  3. sudo apcmd install --app-host … --preview-host … --tls <acme e-mail> --oidc-issuer … --oidc-client-id … --oidc-client-secret … loads and verifies the bundle, writes .env with generated secrets, pulls the images, starts the stack and checks its health. Copy the generated backup key (backup-identity.key) and a copy of .env off the server.
  4. sudo apcmd school create --name … --sub <oidc sub> --teacher-name … creates the school and its first teacher, who then signs in at https://<app host>/teacher.

Updating: sudo apcmd update takes a backup, verifies the new bundle, shows MIGRATION_NOTES.md, adds new variables to .env, pulls the images and restarts (db-init applies new migrations; they only go forward). If the new version does not come up healthy, it returns to the previous one; apcmd rollback [--restore-db --identity <key>] does so by hand.

Management commands

Operator commands (in the API image)

dist/ops.js in the API image holds every command that touches the database, so it always matches the installed version. apcmd calls it; by hand, in the installation directory:

docker compose run --rm ops node dist/ops.js <command> [--json] [--locale de]
Command What it does
bootstrap Create the roles migrator and app and the database (idempotent; db-init does this on start)
migrate Apply pending migrations and set up the job-queue schema (db-init does this on start)
create-school --name <name> [--school-locale de] [--issuer <iss> --sub <sub> --teacher-name <name>] Create a school (idempotent by name); optionally bind a known login as owner
list-schools Schools with their teachers (IDs) and open claim links
add-teacher --school <id|name> [--teacher-name <name>] [--valid-hours 72] One-time claim link for a new teacher of the school (D075)
link-teacher --teacher <id> [--school <id|name>] [--valid-hours 72] One-time link that moves a teacher to another login
health Database, migrations, job queue, asset volume (with unpublished uploads) and API
restore-prepare --confirm <database> Drop the database and create it empty again, before restoring a backup
cleanup [--hours 24] Remove uploads that were never published
version Version of the image

Output is a message from the ops catalog (German by default); with --json, one object {ok, id, params, message, data}. Exit code 0 on success, 1 on failure, 2 on wrong usage.

Backup and restore

Command What it does
apcmd backup now Take a backup now (database and assets, encrypted with age)
apcmd backup list List the backups in backups/ of the installation directory
apcmd backup restore <file>.dump.age --identity <key> Restore database and assets, migrate, start, check health

The restore procedure, also for an empty system, is in docs/betrieb.md, section 6.

Teacher CLI: aivopod

A single executable without Node or Deno, from its own repository aivopod/aivopod-cli:

curl -fsSL https://git.belowtoxic.cloud/api/packages/aivopod/generic/aivopod-cli/latest/install.sh | sh   # Linux, macOS
irm https://git.belowtoxic.cloud/api/packages/aivopod/generic/aivopod-cli/latest/install.ps1 | iex        # Windows

aivopod login --url https://app.<domain> signs in with a token from https://app.<domain>/cli-token, or with --device through the IdP's device flow. Messages come from the platform in the teacher's language; aivopod help lists all commands: administration (school, year, course, teacher, students import, students list, student add, student delete, codes issue, cards pdf, assign, reorder, unlock, export), profile for several platforms, self-update, and the pack tools below. Every command takes --json.

Pack authors

Packs live in their own repositories (aivopod pack init <name> creates one). In a pack directory, or with --dir <path>:

Command What it does
aivopod pack build Bundle the pack and check assets, icons, image references, technik and step IDs
aivopod pack test Run the pack's tests (expectPass / expectFail from @aivopod/sdk/test), without vitest
aivopod pack preview [--port] Play the pack in the browser with the platform's real step components; nothing is stored
aivopod pack publish Build, test and publish (refuses failing tests, an existing version, an SDK newer than the platform)

The author handbook (German) is in the CLI repository: docs/handbuch.md.

In the teacher view (/teacher), teachers see the course overview, each student's history with code diffs, figures per assignment, and CSV downloads.