Die README nannte `node werkzeuge/konto.js`, im Abbild lag aber nur das Sicherungswerkzeug, und tsx gibt es dort nicht. Ein frisch aufgesetzter Server stand ohne jedes Konto da. Das Kontoskript wird jetzt beim Bau mit esbuild zu einer Datei gebuendelt; die nativen Module bleiben aussen vor, sie bringt die Standalone-Ausgabe mit. Aufgefallen ist das nicht, weil der Abnahmelauf gegen `next start` prueft. Neu ist deshalb `npm run abnahme:container`: Abbild bauen, mit leerem Volume starten, erstes Konto anlegen, anmelden, Passwort neu setzen, sichern. Ausserdem trennt saubererName Windows-Pfade jetzt auch unter Linux ab (path.win32.basename). Der Test dazu war nur unter Windows gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
# Lern-Bar
|
||
|
||
Selbst gehostete Anwendung für die betriebliche Ausbildung zum/zur
|
||
**Fachinformatiker/-in für Systemintegration**. Sie ersetzt den Stapel PDFs:
|
||
Wochenplan, Handouts, Merkkarten, Praxisaufgaben, Checklisten, Lernzielkontrollen
|
||
und – der wichtigste Teil – den gesetzlich vorgeschriebenen Ausbildungsnachweis
|
||
(„Berichtsheft").
|
||
|
||
Gebaut für **einen Ausbilder und ein bis drei Auszubildende**. Kein
|
||
Lernplattform-Baukasten, keine Mandanten, keine Cloud.
|
||
|
||
> **Stand:** Alle zehn Bausteine sind fertig – Fassung 1.0.0.
|
||
>
|
||
> - [**Anleitung mit Bildschirmfotos**](docs/ANLEITUNG.md) – wie man damit arbeitet
|
||
> - [Abnahmeprotokoll](docs/ABNAHME.md) – die Abnahmekriterien, einzeln durchgespielt
|
||
> - [Änderungsprotokoll](CHANGELOG.md) · [Fahrplan](ROADMAP.md) ·
|
||
> [Entscheidungen](ENTSCHEIDUNGEN.md)
|
||
|
||
---
|
||
|
||
## In fünf Minuten zum laufenden System
|
||
|
||
### Voraussetzungen
|
||
|
||
- **Betrieb:** Docker mit Compose-Erweiterung (Docker 24 oder neuer)
|
||
- **Entwicklung:** Node.js 22 oder neuer, Git
|
||
- Ein Reverse Proxy, der TLS beendet (nginx, Caddy, Traefik) – die Anwendung
|
||
selbst spricht nur HTTP und lauscht nur auf der Schleife
|
||
|
||
### Betrieb mit Docker
|
||
|
||
```bash
|
||
git clone <repository-url> lernbar
|
||
cd lernbar
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Die Anwendung lauscht danach auf `127.0.0.1:3000`. Erstes Ausbilderkonto
|
||
anlegen:
|
||
|
||
```bash
|
||
docker compose exec lernbar node werkzeuge/konto.js \
|
||
--name "Vorname Nachname" --email ausbilder@betrieb.de --rolle ausbilder
|
||
```
|
||
|
||
Ohne `--passwort` wird eines erzeugt und ausgegeben. Gibt es die Adresse
|
||
schon, setzt derselbe Aufruf das Passwort neu – das ist zugleich der Weg, wenn
|
||
das einzige Ausbilderkonto sein Passwort vergessen hat.
|
||
|
||
Danach im Browser anmelden, unter **Verwaltung** das Azubikonto anlegen und
|
||
das Ausbildungsverhältnis mit Beginndatum einrichten.
|
||
|
||
### Entwicklung
|
||
|
||
```bash
|
||
npm install
|
||
npm approve-scripts better-sqlite3 esbuild unrs-resolver # nur beim ersten Mal
|
||
npm run konto:anlegen -- --name "Vorname Nachname" --email du@betrieb.de --rolle ausbilder
|
||
npm run dev
|
||
```
|
||
|
||
Ohne `npm approve-scripts` bleiben `better-sqlite3` und `esbuild` unübersetzt –
|
||
npm blockiert seit Version 11 die Installationsskripte fremder Pakete.
|
||
|
||
Die Anwendung läuft dann auf <http://localhost:3000>. Datenbank und Anhänge
|
||
landen unter `daten/` (nicht im Repository).
|
||
|
||
---
|
||
|
||
## Beispiel für einen Reverse Proxy
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name lernbar.betrieb.local;
|
||
|
||
ssl_certificate /etc/ssl/lernbar.crt;
|
||
ssl_certificate_key /etc/ssl/lernbar.key;
|
||
|
||
client_max_body_size 30m; # Fotos vom Aufbau als Anhang (Grenze der Anwendung: 25 MB)
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:3000;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Inhalte pflegen
|
||
|
||
Die fachlichen Inhalte sind **Dateien im Repository**, keine
|
||
Datenbankeinträge – Bearbeiten in VS Code, Versionieren mit Git, Sichern mit
|
||
`git clone`.
|
||
|
||
```
|
||
content/
|
||
wochen/ 01-arbeitsalltag.yaml …
|
||
handouts/ N3-subnetting-rechnen.md …
|
||
aufgaben/ 2.2-notebooks-vergleichen.md …
|
||
checklisten/ 02-windows-installieren.yaml …
|
||
lernzielkontrollen/ woche-03.yaml …
|
||
glossar.yaml
|
||
feedbackgespraech.yaml Leitfaden und Kriterien für das Gespräch
|
||
rahmenplan.yaml Berufsbildpositionen und Lernfelder
|
||
```
|
||
|
||
Ein Handout ist Markdown mit YAML-Kopf:
|
||
|
||
```markdown
|
||
---
|
||
id: N3
|
||
titel: Subnetting rechnen – der Weg in vier Schritten
|
||
block: netzwerk
|
||
woche: 3
|
||
worum: Eine Adresse und eine Maske reichen, um Netz, Broadcast und Hostbereich zu bestimmen.
|
||
rahmenplan: [B2, A8]
|
||
lernfeld: LF3
|
||
merksatz: Blockgröße finden, Vielfache aufschreiben, eigene Adresse einsortieren.
|
||
karten:
|
||
- frage: Zu welchem Netz gehört 192.168.10.200 bei der Maske 255.255.255.224?
|
||
antwort: Blockgröße 32, Netze bei .192 und .224 – die Adresse gehört zu 192.168.10.192/27.
|
||
---
|
||
|
||
## Das Wesentliche
|
||
…
|
||
```
|
||
|
||
Beim Start werden alle Inhalte eingelesen und geprüft: Jede Kennung muss es
|
||
geben, jedes Handout gehört zu einer Woche, jede Rahmenplanposition steht in
|
||
`rahmenplan.yaml`. Passt etwas nicht, startet die Anwendung nicht und nennt
|
||
Datei und Stelle. Prüfen ohne Start:
|
||
|
||
```bash
|
||
npm run inhalte:pruefen
|
||
```
|
||
|
||
---
|
||
|
||
## Datensicherung
|
||
|
||
Gesichert werden **Datenbank und Anhänge**. Die Inhalte stecken im Repository
|
||
und brauchen keine eigene Sicherung.
|
||
|
||
### Sicherung ziehen
|
||
|
||
```bash
|
||
# im laufenden Container
|
||
docker compose exec lernbar /app/werkzeuge/sicherung.sh /daten/sicherungen
|
||
|
||
# aus dem Volume herausholen
|
||
docker compose cp lernbar:/daten/sicherungen ./sicherungen
|
||
```
|
||
|
||
Auf dem Entwicklungsrechner:
|
||
|
||
```bash
|
||
LERNBAR_DATEN=./daten npm run backup -- ./sicherungen
|
||
```
|
||
|
||
Das Archiv heißt `lernbar-JJJJMMTT-HHMMSS.tar.gz` und enthält
|
||
`lernbar.sqlite`, `anhaenge/` und eine kurze Beschreibung. Die Kopie der
|
||
Datenbank entsteht über die Sicherungsschnittstelle von SQLite, ist also auch
|
||
im laufenden Betrieb in sich stimmig.
|
||
|
||
### Zurückspielen
|
||
|
||
```bash
|
||
docker compose stop lernbar
|
||
docker compose cp ./sicherungen/lernbar-20260910-0300.tar.gz lernbar:/tmp/
|
||
docker compose start lernbar
|
||
docker compose exec lernbar /app/werkzeuge/wiederherstellen.sh /tmp/lernbar-20260910-0300.tar.gz
|
||
docker compose restart lernbar
|
||
```
|
||
|
||
Der bisherige Stand wird nicht gelöscht, sondern nach
|
||
`/daten/verdraengt-<zeitstempel>/` verschoben.
|
||
|
||
### Nächtlich per Cronjob
|
||
|
||
`/etc/cron.d/lernbar-sicherung`:
|
||
|
||
```cron
|
||
# Jede Nacht um 03:00 sichern, Archive älter als 30 Tage wegräumen.
|
||
SHELL=/bin/sh
|
||
PATH=/usr/local/bin:/usr/bin:/bin
|
||
|
||
0 3 * * * root cd /opt/lernbar && docker compose exec -T lernbar /app/werkzeuge/sicherung.sh /daten/sicherungen >> /var/log/lernbar-sicherung.log 2>&1
|
||
30 3 * * * root cd /opt/lernbar && docker compose exec -T lernbar find /daten/sicherungen -name 'lernbar-*.tar.gz' -mtime +30 -delete
|
||
```
|
||
|
||
Liegt das Volume auf einem Pfad, den die Sicherung des Servers ohnehin
|
||
mitnimmt, ist damit alles erledigt. Sonst zusätzlich mit `docker compose cp`
|
||
herauskopieren und wegschaffen.
|
||
|
||
---
|
||
|
||
## Aktualisieren
|
||
|
||
Auf dem Linux-Server:
|
||
|
||
```bash
|
||
cd /opt/lernbar
|
||
./update.sh
|
||
```
|
||
|
||
Das Skript sichert zuerst, holt den Quelltext, baut das Abbild neu, schaltet um
|
||
und wartet auf das Lebenszeichen. Meldet sich die neue Fassung nicht gesund,
|
||
schaltet es selbsttätig auf das vorherige Abbild zurück und zeigt die letzten
|
||
Protokollzeilen.
|
||
|
||
| Angabe | Wirkung |
|
||
|---|---|
|
||
| *(ohne)* | sichern, holen, bauen, umschalten |
|
||
| `--ohne-git` | nur neu bauen und neu starten |
|
||
| `--ohne-sicherung` | Sicherung überspringen (nicht empfohlen) |
|
||
| `--zweig main` | einen bestimmten Git-Zweig holen |
|
||
| `--hilfe` | Kurzhilfe |
|
||
|
||
---
|
||
|
||
## Umgebungsvariablen
|
||
|
||
| Name | Vorgabe im Container | Bedeutung |
|
||
|---|---|---|
|
||
| `PORT` | `3000` | Lauschport |
|
||
| `TZ` | `Europe/Berlin` | Zeitzone |
|
||
| `LERNBAR_DATEN` | `/daten` | Wurzel für Datenbank und Anhänge |
|
||
| `LERNBAR_DB` | `<daten>/lernbar.sqlite` | abweichender Pfad der Datenbank |
|
||
| `LERNBAR_INHALTE` | `/app/content` | Wurzel der Inhaltsdateien |
|
||
| `LERNBAR_MIGRATIONEN` | `/app/migrationen` | Verzeichnis der SQL-Migrationen |
|
||
|
||
---
|
||
|
||
## Prüfen und Testen
|
||
|
||
```bash
|
||
npm run typecheck # TypeScript
|
||
npm run lint # ESLint
|
||
npm run test # Vitest – Logik, die wehtut (315 Tests)
|
||
npm run abnahme # der Abnahmelauf, siehe unten
|
||
npm run abnahme:container # derselbe Weg gegen das Docker-Abbild
|
||
```
|
||
|
||
### Der Abnahmelauf
|
||
|
||
`npm run abnahme` ist die Prüfung, auf die es ankommt. Er legt ein
|
||
**Wegwerfverzeichnis** an, startet die Anwendung mit einer **frischen, leeren
|
||
Datenbank** und spielt den ganzen Weg durch:
|
||
|
||
1. Migrationen laufen an, das erste Ausbilderkonto entsteht auf der Kommandozeile
|
||
2. Die Inhaltsprüfung wird mit einem absichtlich eingebauten Fehler auf die
|
||
Probe gestellt – sie muss Datei, Zeile und Wert nennen
|
||
3. Der Ausbilder legt ein Azubikonto an; das Passwort darf danach nirgends mehr
|
||
auftauchen, in der Datenbank steht ein Argon2id-Hash
|
||
4. Der Azubi liest ein Handout, lernt eine Karte, meldet eine Aufgabe zur Abnahme
|
||
5. Der Ausbilder nimmt sie ab; sie schlägt in der Rahmenplanabdeckung durch
|
||
6. Das Wochenblatt wird angelegt und als PDF ausgegeben
|
||
7. **Gesichert, absichtlich beschädigt, zurückgespielt.** Eine Sicherung, die
|
||
nie zurückgespielt wurde, ist keine Sicherung
|
||
|
||
Das Protokoll landet in [`docs/ABNAHME.md`](docs/ABNAHME.md).
|
||
|
||
Voraussetzung: `npm run build` ist gelaufen, und Port 3399 ist frei – sonst
|
||
bricht der Lauf mit einer Meldung ab, statt gegen einen fremden Datenbestand zu
|
||
prüfen.
|
||
|
||
### Der Containerlauf
|
||
|
||
`npm run abnahme` prüft über `next start`, betrieben wird aber der Container.
|
||
`npm run abnahme:container` baut deshalb das Abbild, startet es mit leerem
|
||
Volume und geht den Weg aus dieser README: Lebenszeichen, erstes Konto über
|
||
`werkzeuge/konto.js`, Anmeldung, Passwort neu setzen, Sicherung im Container.
|
||
Danach werden Container, Volume und Abbild wieder entfernt.
|
||
|
||
Voraussetzung: Docker läuft, Port 3398 ist frei (`LERNBAR_CONTAINER_PORT`).
|
||
|
||
### Einzelne Zusagen
|
||
|
||
```bash
|
||
node scripts/pruefungen/musterloesung-dichtheit.mjs # Musterlösung dicht?
|
||
node scripts/pruefungen/baustein-9.mjs # Abdeckung, Feedback
|
||
node scripts/pruefungen/baustein-10.mjs # PWA, Offline, Druck
|
||
```
|
||
|
||
Diese drei brauchen eine **laufende** Anwendung unter `http://localhost:3000`
|
||
mit den Beispieldaten.
|
||
|
||
---
|
||
|
||
## Aufbau
|
||
|
||
```
|
||
src/
|
||
app/ Seiten und Serveraktionen (App Router)
|
||
komponenten/ wiederverwendete Bausteine der Oberfläche
|
||
lib/
|
||
auth/ Passwörter, Sitzungen, Rechteregeln
|
||
db/ Verbindung, Schema, Migrationen
|
||
repos/ der gesamte Datenzugriff – nur hier wird SQL gesprochen
|
||
zeit/ Europe/Berlin, ISO-Kalenderwochen
|
||
content/ fachliche Inhalte als Markdown und YAML
|
||
scripts/ Werkzeuge für die Kommandozeile
|
||
im-container/ Sicherung und Wiederherstellung (Shell, läuft überall)
|
||
```
|
||
|
||
Weiterführend: [Fahrplan](ROADMAP.md) ·
|
||
[Änderungsprotokoll](CHANGELOG.md) · [Arbeitstagebuch](DEVLOG.md) ·
|
||
[Entscheidungen](ENTSCHEIDUNGEN.md)
|