Baustein 1: Grundgeruest, Datenbank, Anmeldung, zwei Rollen

Legt das Fundament der Lern-Bar an:

- Next.js 15 (App Router), React 19, TypeScript, ESLint, Vitest
- SQLite ueber Drizzle hinter einer Repository-Schicht
- Eigener Migrationslauf mit Pruefsummen, beim Start angewendet
- Argon2id, serverseitige Sitzungen, HttpOnly-Cookie, SameSite=Lax
- Rollen Ausbilder und Azubi, Rechteregeln als reine Funktionen
- Verwaltung: Azubikonto und Ausbildung anlegen, Passwort, Sperren
- Gestaltung Petrol/Bernstein, helle und dunkle Darstellung
- Dockerfile, docker-compose, Lebenszeichen, update.sh mit Rueckfall
- Sicherung und Wiederherstellung als Shell-Skripte
- 29 Tests fuer Rechte, Zeitrechnung und Migrationslauf

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 00:52:44 +02:00
co-authored by Claude Opus 5
commit e856349cd4
51 changed files with 11883 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# Entscheidungen
Kurze Notizen zu Festlegungen, die im Auftrag offen waren. Jede Notiz nennt
die Lage, die Wahl und den Grund.
---
## E1 · Stack: Next.js 15 mit App Router
Der Auftrag verweist auf eine Stackvorgabe im „Kopf des Prompts", die nicht
mitgeliefert wurde. Auf Rückfrage: **Next.js 15 (App Router), React 19,
TypeScript**. Datenzugriff über Drizzle ORM auf better-sqlite3.
Serveraktionen ersetzen eine eigene REST-Schicht. Das hält die Rechteprüfung
dort, wo die Daten geholt werden, statt sie über eine API zu verteilen.
## E2 · Zeitstempel als ISO-8601 in UTC
Der Auftrag verlangt „alle Zeitstempel mit Zeitzone". Gespeichert wird
`2026-09-10T07:15:00.000Z`, also ein eindeutiger Zeitpunkt in UTC. Angezeigt
wird durchgehend in Europe/Berlin.
Grund: UTC-Strings sortieren sich lexikografisch richtig, was bei SQLite ohne
echten Zeitstempeltyp entscheidend ist. Ein gespeicherter Ortsversatz
(`+02:00`) würde beim Wechsel zwischen Sommer- und Winterzeit falsch sortieren.
Reine Kalenderdaten – Berichtszeitraum, Tagesnotiz, Ausbildungsbeginn – sind
`YYYY-MM-DD` ohne Uhrzeit, weil sie auch keine haben.
## E3 · Eigener Migrationslauf statt drizzle-kit
Migrationen sind handgeschriebene SQL-Dateien unter
`src/lib/db/migrationen/`, die beim Start der Reihe nach genau einmal
angewendet und mit SHA-256-Prüfsumme vermerkt werden.
Grund: volle Kontrolle über Indizes und `CHECK`-Bedingungen (Statuswerte sind
Aufzählungen, keine freien Zeichenketten), und beim Start ist im Protokoll
sichtbar, was passiert ist. Wird eine bereits angewendete Datei nachträglich
verändert, bricht der Start mit klarer Meldung ab.
Der Preis: das Drizzle-Schema muss von Hand zu den SQL-Dateien passen. Dagegen
läuft ein Test, der die migrierte Datenbank gegen das Schema abgleicht.
## E4 · Schlüssel sind UUIDs, keine laufenden Nummern
Textspalten mit `crypto.randomUUID()`. Läuft auf SQLite und PostgreSQL
gleichermaßen, und beim Zurückspielen einer Sicherung gibt es keine Kollisionen
mit zwischenzeitlich vergebenen Nummern.
## E5 · Debian-slim als Grundlage des Abbilds, nicht Alpine
`better-sqlite3` und `@node-rs/argon2` bringen für glibc fertige Binärdateien
mit, für musl nicht. Auf Alpine müsste im Abbild übersetzt werden – längerer
Bau, größere Bauschicht, mehr Fehlerquellen. Der Größenunterschied im
Laufzeitabbild ist bei einem selbst gehosteten Dienst ohne Belang.
## E6 · Passwortprüfung mit gleichbleibender Antwortzeit
Bei unbekannter E-Mail-Adresse wird trotzdem einmal gegen einen festen
Argon2-Wert gerechnet. Sonst verrät die Antwortzeit, welche Adressen es gibt.
Die Fehlermeldung ist in beiden Fällen dieselbe.
## E7 · Anhänge liegen im Dateisystem, nicht in der Datenbank
Unter `<daten>/anhaenge/`, in der Datenbank steht nur der Verweis. Hält die
Datenbankdatei klein und die Sicherung schnell; das Archiv nimmt beides mit.
## E8 · Sicherung als Shell-Skript, nicht als Node-Werkzeug
`scripts/im-container/sicherung.sh` läuft im Laufzeitabbild, das kein
TypeScript kennt, und auf dem Entwicklungsrechner unter Git Bash. Eine zweite
Fassung in TypeScript wäre eine zweite Fehlerquelle. Die konsistente Kopie der
Datenbank entsteht über die Sicherungsschnittstelle von SQLite – ein `cp` wäre
bei aktivem WAL-Modus nicht verlässlich.
## E9 · Kein `main`-Zweig ohne Freigabe
Die verbindliche Arbeitsweise verbietet Commits direkt auf `main`. Die
Erstfassung entsteht deshalb vollständig auf `feat/lernbar-erstfassung`.
`main` bleibt leer, bis eine Freigabe erfolgt.
---
<!-- Weitere Entscheidungen werden unten angefügt und fortlaufend nummeriert. -->