Files
betreuungs-cockpit/README.md
T
Florian Wilde 6865bae05e docs: Betriebswissen und Stand in die README
Vier Dinge, die beim Bauen Zeit gekostet haben und die nirgends stehen:

- Dev-Server und Build schreiben beide nach .next. Laufen sie
  gleichzeitig, fehlen dem Build einzelne Chunks, und die Meldung
  (Cannot find module './638.js') zeigt nicht auf die Ursache.
- AUTH_URL muss exakt der aufgerufenen Adresse entsprechen. Steht dort
  localhost und man ruft 127.0.0.1 auf, wird das Sitzungs-Cookie fuer
  die falsche Herkunft gesetzt - die Anmeldung landet stumm wieder auf
  dem Formular, ohne Fehlermeldung.
- Der Standalone-Server liest keine .env. Im Behaelter kommen alle
  Variablen aus dem environment-Abschnitt.
- Die beiden SQL-Skripte laufen nicht mit den Migrationen mit.

Dazu ein kurzer Abschnitt mit dem Stand: 71.051 Zeilen, 24 Modelle,
46 Seiten, 458 Unit-Tests, 34 End-to-End-Tests, null Typfehler.

Etappe: 8
Phase: Haertung
2026-08-22 07:32:32 +02:00

561 lines
20 KiB
Markdown

# Betreuungs-Cockpit
Arbeits- und Dokumentationswerkzeug für die ehrenamtliche rechtliche Betreuung
eines Angehörigen: Fristen, Vorgänge, Belege, Vermögen, Sozialhilfe, Berichte.
> **Keine Rechtsberatung.** Die Anwendung rechnet Fristen aus, sortiert
> Unterlagen und erzeugt Berichte. Sie kennt den Einzelfall nicht. Jeder
> Hinweis mit Paragraphen-Bezug trägt den sichtbaren Vermerk, dass er die
> Rückfrage bei Betreuungsgericht, Sozialhilfeträger oder Anwalt **nicht**
> ersetzt. Wer sich darauf verlässt, statt beim Rechtspfleger nachzufragen,
> kann Geld und Ansprüche verlieren.
---
## Inhalt
- [Was die Anwendung kann](#was-die-anwendung-kann)
- [Schnellstart zum Ausprobieren](#schnellstart-zum-ausprobieren)
- [Produktivbetrieb mit Docker Compose](#produktivbetrieb-mit-docker-compose)
- [Reverse Proxy](#reverse-proxy)
- [Alternative: Vercel und Neon](#alternative-vercel-und-neon)
- [Sicherheit und Datenschutz](#sicherheit-und-datenschutz)
- [Was rechtlich zu tun ist](#was-rechtlich-zu-tun-ist)
- [Sicherung und Wiederherstellung](#sicherung-und-wiederherstellung)
- [Schlüsselverwaltung](#schlüsselverwaltung)
- [Entwicklung](#entwicklung)
- [Bauprotokoll](#bauprotokoll)
---
## Was die Anwendung kann
| Bereich | Kurz gesagt |
|---|---|
| **Heute** | Eine Seite, die morgens die Frage beantwortet, was ansteht. Fristen-Ampel, wer am Zug ist, offene Nachforderungen des Bezirks, ungeprüfter Posteingang, Kennzahlen. |
| **Fristen** | Die gesetzlichen Pflichtfristen entstehen von selbst aus dem Bestellungsdatum. Liste, Kalender, ICS-Feed zum Abonnieren. |
| **Vorgänge** | Fallakte je Angelegenheit, mit Vorlagen (Heimeinzug, Hausverkauf, Widerspruch …) und der festen Ablaufkette für genehmigungspflichtige Geschäfte. |
| **Dokumente** | Foto oder Scan hochladen, Texterkennung, Volltextsuche über alle Schreiben, Posteingangsbuch, Belegmappe als eine PDF. |
| **Vermögen & Kasse** | Kassenbuch mit Belegzwang, Konten, Vermögensposten mit Wert-Historie, Vermögensverzeichnis und Rechnungslegung als PDF. |
| **Sozialhilfe** | Antragsakte, Nachforderungs-Tracker, Bescheid-Prüfung mit Rechtsbehelfsfrist, Heimkosten-Gegenüberstellung, Schonvermögens-Bewertung. |
| **Immobilie** | Stammdaten, Belastungen, Wert-Historie, Status im Sozialhilfeverfahren, Pflichten-Checkliste bei Leerstand. |
| **Verträge** | Versicherungen und laufende Verträge mit Kündigungsfristen, Rückkaufswert-Anfrage, Kündigungs-Assistent. |
| **Heim & Pflege** | Heimvertrag, Leistungszuschläge, Pflegegrad-Historie, Gesundheitsverlauf mit COPD-Bezug. |
| **Kontakte** | Verzeichnis aller Stellen, Ein-Klick-Kontaktprotokoll, Auswertung für den Jahresbericht. |
| **Berichte** | Vermögensverzeichnis, Jahresbericht, Rechnungslegung, Belegmappe, Unterlagenpaket für den Bezirk, Vollexport. |
---
## Schnellstart zum Ausprobieren
Voraussetzungen: **Node.js ≥ 20.11**, **Docker** und **Git**.
```bash
git clone https://git.wilde-server.de/florian.wilde/betreuungs-cockpit.git
cd betreuungs-cockpit
npm install
```
**1. Datenbank und Objektspeicher starten.** Beide horchen nur auf
`127.0.0.1`, gehen also nicht ins Netz hinaus:
```bash
docker compose -f docker-compose.dev.yml up -d
```
**2. Konfiguration anlegen.** Die Geheimnisse erzeugst du dir:
```bash
cp .env.example .env
npm run schluessel # gibt AUTH_SECRET, ENC_KEY_V1, INDEX_KEY aus
```
Trage die ausgegebenen Werte in die `.env` ein und setze:
```
DATABASE_URL="postgresql://cockpit:entwicklung@localhost:5433/betreuungs_cockpit?schema=public"
```
**3. Datenbank einrichten:**
```bash
npx prisma migrate dev --name fundament
npm run db:sql # Volltextsuche und Audit-Schutz
npm run db:seed
```
**4. Starten:**
```bash
npm run dev
```
Dann [http://localhost:3000](http://localhost:3000) öffnen. Beim ersten Aufruf
führt dich ein Assistent in drei Schritten durch Zugang, zweiten Faktor und
die Stammdaten der betreuten Person.
### Mit Beispieldaten ansehen
Um zu sehen, wie die Anwendung mit Inhalt aussieht:
```bash
echo "SEED_DEMO=true" >> .env
npm run db:seed
```
Das legt den Fall „Erika Musterfrau" an — COPD GOLD III, Pflegegrad 3,
Heimeinzug Januar 2026, leerstehendes Haus in Bindlach, zwei Versicherungen,
laufender Antrag beim Bezirk Oberfranken mit acht Nachforderungen.
**Der Demofall wird in der Produktion nicht angelegt** und lässt sich später
über den Papierkorb vollständig entfernen.
---
## Produktivbetrieb mit Docker Compose
Das Compose-Setup bringt drei Dienste mit: die Anwendung, PostgreSQL und
MinIO als S3-kompatiblen Objektspeicher, dazu einen Sicherungsdienst.
**Einen Reverse Proxy enthält es bewusst nicht** — er ist auf dem Zielserver
bereits vorhanden. Die Anwendung horcht nur auf `127.0.0.1:3000`.
**Die Datenbank hat keine Portfreigabe** und ist ausschließlich über das
interne Docker-Netz erreichbar.
```bash
# 1. Konfiguration
cp .env.example .env
npm run schluessel
# In der .env mindestens setzen:
# POSTGRES_PASSWORD, AUTH_SECRET, AUTH_URL (die echte öffentliche Adresse),
# ENC_KEY_V1, INDEX_KEY,
# S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, SICHERUNG_PASSWORT
# 2. Bauen und starten
docker compose up -d --build
# 3. Datenbank vorbereiten
# (die Migrationen laufen beim Start der Anwendung automatisch;
# die beiden SQL-Skripte müssen einmalig dazu)
docker compose exec app node node_modules/prisma/build/index.js db execute \
--schema prisma/schema.prisma --file prisma/sql/volltextsuche.sql
docker compose exec app node node_modules/prisma/build/index.js db execute \
--schema prisma/schema.prisma --file prisma/sql/audit-schutz.sql
# 4. Bucket im Objektspeicher anlegen
docker compose exec minio mc alias set lokal http://localhost:9000 \
"$S3_ACCESS_KEY_ID" "$S3_SECRET_ACCESS_KEY"
docker compose exec minio mc mb lokal/betreuungs-cockpit
```
Danach im Browser über die eigene Domain auf `/ersteinrichtung` gehen und den
ersten Zugang anlegen. Die Seite sperrt sich selbst, sobald ein Zugang
existiert.
### Aktualisieren
```bash
git pull
docker compose up -d --build
```
Die Migrationen laufen beim Start des Anwendungsbehälters mit. **Vorher ein
Backup anstoßen** (siehe unten) — eine Migration lässt sich nicht zurückrollen.
---
## Reverse Proxy
Die Anwendung erwartet, dass der Proxy TLS beendet und die echte Client-IP
weiterreicht. Sie liest `X-Forwarded-For` bzw. `X-Real-IP` für das
Rate-Limiting am Login und für das Audit-Log (dort nur als Hash).
**Caddy**
```caddyfile
cockpit.example.de {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
# Uploads: Fotos von Behördenschreiben können groß sein.
request_body {
max_size 30MB
}
}
```
**nginx**
```nginx
server {
listen 443 ssl http2;
server_name cockpit.example.de;
ssl_certificate /etc/letsencrypt/live/cockpit.example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cockpit.example.de/privkey.pem;
client_max_body_size 30M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
**Traefik** (Labels am `app`-Dienst)
```yaml
labels:
- traefik.enable=true
- traefik.http.routers.cockpit.rule=Host(`cockpit.example.de`)
- traefik.http.routers.cockpit.tls.certresolver=letsencrypt
- traefik.http.services.cockpit.loadbalancer.server.port=3000
```
In allen Fällen muss `AUTH_URL` in der `.env` auf die öffentliche Adresse
zeigen (`https://cockpit.example.de`), sonst schlägt die Anmeldung fehl.
---
## Alternative: Vercel und Neon
Technisch möglich, hier aber **nicht empfohlen**. Der Grund ist nicht
technischer Natur:
> Vercel Inc. und Neon Inc. sind US-Unternehmen. Auch bei Wahl einer
> EU-Region unterliegen sie potenziell dem US-CLOUD-Act. Für
> Gesundheitsdaten einer nicht selbst entscheidungsfähigen Person — also
> besondere Kategorien nach Art. 9 DSGVO — ist das eine Abwägung, die man
> bewusst treffen und dokumentieren muss. Ein eigener Server in Deutschland
> nimmt einem diese Abwägung ab.
Wenn du es dennoch willst:
1. **Neon**-Projekt in der Region `eu-central-1` (Frankfurt) anlegen,
Verbindungszeichenfolge als `DATABASE_URL` hinterlegen.
2. In **Vercel** unter *Settings → Functions* die Region `fra1` festlegen.
3. Alle Umgebungsvariablen aus `.env.example` als Vercel-Secrets eintragen.
`ENC_KEY_V1` und `INDEX_KEY` gehören zwingend dazu.
4. **Objektspeicher**: nicht Vercel Blob, sondern ein EU-Anbieter, z. B.
Hetzner Object Storage (Falkenstein oder Nürnberg). `SPEICHER_TREIBER=s3`.
5. **OCR und PDF laufen nicht auf Vercel.** Tesseract braucht ein System-
Binary, das es in der Serverless-Umgebung nicht gibt. Setze `OCR_AKTIV=false`
oder betreibe einen kleinen Worker daneben. PDF-Erzeugung mit
`@react-pdf/renderer` funktioniert, kann bei großen Belegmappen aber ins
Zeitlimit laufen.
6. **Migrationen** laufen nicht automatisch: `npx prisma migrate deploy` als
Build-Schritt oder von Hand.
7. Mit **beiden** Anbietern ist ein Auftragsverarbeitungsvertrag nach
Art. 28 DSGVO abzuschließen.
---
## Sicherheit und Datenschutz
Hier liegen Gesundheits- und Finanzdaten einer Person, die nicht selbst
entscheiden kann. Was dafür umgesetzt ist:
**Verschlüsselung**
- Transport über TLS (durch den Reverse Proxy).
- Zusätzlich **anwendungsseitige Feldverschlüsselung** mit AES-256-GCM für
Gesundheitsdaten, Diagnosen, den ermittelten Willen, IBANs,
Versicherungsnummern, Renten- und Steuernummern sowie alle Freitext-Notizen.
Der Schlüssel liegt in der Umgebung, nicht in der Datenbank — ein
Datenbank-Dump allein nützt niemandem etwas.
- Der Feldname wird mitauthentifiziert: ein Geheimtext lässt sich nicht von
„Diagnosen" in „Notizen" umhängen.
- **Blindindex** (HMAC-SHA256 mit eigenem Schlüssel) für die IBAN, damit die
Frage „kenne ich dieses Konto schon?" beantwortbar bleibt, ohne sie im
Klartext zu speichern.
**Zugang**
- Passwörter mit **Argon2id** (19 MiB, t=2, p=1 — OWASP-Empfehlung).
- **Zweiter Faktor ist Pflicht** und lässt sich nicht abschalten. TOTP nach
RFC 6238, kompatibel mit Aegis, 2FAS, Bitwarden, 1Password.
- **Rate-Limiting** am Login, nach E-Mail *und* nach Anschluss. Die Sperre
wird geprüft, bevor Argon2 überhaupt rechnet.
- Die Antwortzeit ist auf eine Mindestdauer gestreckt, damit sie nicht
verrät, welche E-Mail-Adressen existieren.
- **Sitzungsablauf** nach 30 Minuten Leerlauf. Für Export, endgültiges
Löschen und die Benutzerverwaltung ist eine **erneute Anmeldung** nötig.
- Passwortwechsel beendet **alle** offenen Sitzungen, auch auf fremden
Geräten.
**Rollen**
- `betreuer` — voller Zugriff auf die zugeordneten Betreuten.
- `mitleser` — nur lesend, und nur die ausdrücklich je Modul freigegebenen
Bereiche (z. B. Geschwister, die den Kontaktverlauf sehen dürfen, aber
nicht die Finanzen).
- `admin` — Benutzerverwaltung.
Geprüft wird **immer serverseitig gegen die Betreuten-ID**, nie nur im UI
ausgeblendet.
**Audit-Log**
Jede schreibende Aktion und jeder Dokumentzugriff wird protokolliert: wer,
wann, was, alter Wert → neuer Wert. Die Änderungen liegen verschlüsselt.
Das Log ist **unveränderlich**: ein Datenbank-Trigger weist UPDATE und DELETE
ab — auch bei direktem Zugriff mit `psql`. Damit ist es der Nachweis
gegenüber Betreuungsgericht und möglichen Miterben. Nachprüfen lässt sich das
so:
```bash
docker compose exec db psql -U cockpit betreuungs_cockpit \
-c "UPDATE audit_log SET entitaet='x';"
# ERROR: Das Audit-Log ist unveraenderlich. UPDATE auf audit_log ist nicht zulaessig.
```
**Content-Security-Policy**
Streng gesetzt, mit einer Nonce pro Anfrage. Keine externen Schriften, kein
CDN, keine Analytics, keine Tracker. Der QR-Code für den zweiten Faktor wird
serverseitig erzeugt und als Data-URI eingebettet, nicht von einem fremden
Dienst geholt.
**Uploads**
Der Dateityp wird an den **Magic Bytes** geprüft, nicht an der Endung und
nicht am gemeldeten MIME-Typ. Erlaubt sind PDF, JPEG, PNG, WEBP, TIFF, HEIC.
Ausgeliefert wird nur über kurzlebige signierte URLs, immer mit
`Content-Disposition: attachment` und `X-Content-Type-Options: nosniff` —
ein hochgeladenes HTML-Dokument kann also nie als Seite ausgeführt werden.
**Löschen**
Papierkorb mit 30 Tagen, danach echtes Löschen. Endgültiges Löschen erfordert
eine frische Anmeldung.
---
## Was rechtlich zu tun ist
Das erledigt keine Software für dich:
1. **Auftragsverarbeitungsvertrag nach Art. 28 DSGVO** mit dem Hoster und mit
dem Betreiber des Objektspeichers. Bei Hetzner findest du ihn im
Kundenkonto unter *Verträge → AV-Vertrag*, bei Netcup und IONOS ähnlich.
Ohne AV-Vertrag ist der Betrieb formal rechtswidrig.
2. **Verarbeitungsverzeichnis** nach Art. 30 DSGVO. Ein Textbaustein liegt in
[`docs/verarbeitungsverzeichnis.md`](docs/verarbeitungsverzeichnis.md) und
muss nur ergänzt werden.
3. **Datenschutzhinweise** für die betreute Person und für Mitleser:
[`docs/datenschutzhinweise.md`](docs/datenschutzhinweise.md).
4. **Server in Deutschland oder mindestens in der EU.** Keine US-Dienste im
Datenpfad.
5. Bei einer Datenpanne: **Meldepflicht binnen 72 Stunden** (Art. 33 DSGVO)
an die zuständige Aufsichtsbehörde — in Bayern das BayLDA.
---
## Sicherung und Wiederherstellung
### Was automatisch läuft
Der Dienst `sicherung` legt **täglich um 03:15 Uhr** einen Stand nach
`./backups`:
- `pg_dump` im eigenen Format, damit auch einzelne Tabellen zurückholbar sind
- verschlüsselt mit AES-256 und PBKDF2 aus `SICHERUNG_PASSWORT`
- SHA-256-Prüfsumme daneben
- **Selbstprüfung**: direkt nach dem Schreiben wird geprüft, ob sich die
Datei wieder entschlüsseln lässt. Schlägt das fehl, endet der Lauf mit
Fehler statt mit einem stillen Scheinerfolg.
- Aufbewahrung 30 Tage, der jeweils erste Stand eines Monats bleibt dauerhaft
Von Hand anstoßen:
```bash
docker compose exec sicherung /bin/sh /sicherung.sh
```
### Zurückspielen
```bash
docker compose exec sicherung /bin/sh /wiederherstellung.sh \
/backups/cockpit_2026-08-22_0315.dump.enc
```
Das Skript prüft die Prüfsumme, fragt nach und spielt zurück. Danach:
```bash
docker compose exec app npm run db:sql # Trigger und Indizes erneut anlegen
```
### Wiederherstellung üben
**Ein Backup, das nie zurückgespielt wurde, ist kein Backup.** Nimm dir
einmal im Jahr zwanzig Minuten:
```bash
# 1. Testdatenbank neben der echten anlegen
docker compose exec db createdb -U cockpit probe
# 2. Sicherung dorthin zurückspielen
docker compose exec -e PGDATABASE=probe -e SICHERUNG_OHNE_RUECKFRAGE=ja \
sicherung /bin/sh /wiederherstellung.sh /backups/cockpit_<datum>.dump.enc
# 3. Stichprobe: sind die Daten da?
docker compose exec db psql -U cockpit probe \
-c "SELECT vorname, nachname FROM betreute;" \
-c "SELECT count(*) FROM audit_log;"
# 4. Aufräumen
docker compose exec db dropdb -U cockpit probe
```
Wichtig ist der letzte Schritt in der Anwendung selbst: eine betreute Person
öffnen und nachsehen, ob die **Diagnosen im Klartext** erscheinen. Erst dann
weißt du, dass nicht nur die Datenbank, sondern auch der Schlüssel stimmt.
### Was das Backup NICHT enthält
**Den Verschlüsselungsschlüssel.** `ENC_KEY_V1` liegt in der `.env`, nicht in
der Datenbank. Das ist Absicht: ein gestohlenes Backup ist damit wertlos.
Für dich bedeutet es aber: **ohne diesen Schlüssel ist auch dein eigenes
Backup halb blind.** Diagnosen, IBANs, Versicherungsnummern und Notizen
bleiben unlesbar.
Ebenfalls nicht enthalten: die **Dateien im Objektspeicher**. Die sichere
gesondert, zum Beispiel:
```bash
docker compose exec minio mc mirror lokal/betreuungs-cockpit /backups/dokumente
```
Oder du benutzt regelmäßig den **Vollexport** in der Anwendung — der packt
Dateien, Daten und Berichte in ein ZIP.
---
## Schlüsselverwaltung
`ENC_KEY_V1` ist der wichtigste Wert der ganzen Installation.
**Sichere ihn getrennt vom Backup.** Passwortmanager *und* ausgedruckt bei
den Betreuungsunterlagen. Wenn er verloren geht, sind die verschlüsselten
Felder unwiederbringlich weg — es gibt keine Hintertür.
### Schlüssel wechseln
Die Anwendung kann mehrere Schlüsselversionen gleichzeitig lesen:
```bash
# 1. Neuen Schlüssel erzeugen und als ENC_KEY_V2 in die .env eintragen
npm run schluessel
# 2. Umschalten - ab jetzt wird mit V2 verschlüsselt
ENC_KEY_AKTIV=2
# 3. Neu starten
docker compose up -d
```
**`ENC_KEY_V1` bleibt in der `.env` stehen**, solange noch Daten damit
verschlüsselt sind — sonst sind ältere Datensätze nicht mehr lesbar. Erst
wenn alle Felder auf die neue Version umgeschrieben wurden, darf der alte
Schlüssel weg.
---
## Entwicklung
```bash
npm run dev # Entwicklungsserver
npm run typecheck # TypeScript ohne Ausgabe prüfen
npm run lint
npm test # Vitest: Fachlogik
npm run test:e2e # Playwright: kritische Abläufe
npm run db:studio # Prisma Studio
npm run protokoll # Bauprotokoll aus der Git-Historie
```
Konventionen — verbindlich für alle Beiträge:
[`docs/KONVENTIONEN.md`](docs/KONVENTIONEN.md).
Schnittstellen zwischen den Modulen:
[`docs/SCHNITTSTELLEN.md`](docs/SCHNITTSTELLEN.md).
### Bildschirmfotos erzeugen
```bash
node scripts/screenshots.mjs /heute /fristen /vorgaenge
```
Meldet sich mit dem Entwicklungszugang an und fotografiert die Seiten am
Schreibtisch und am Handy, hell und dunkel. Setzt voraus, dass
`SEED_DEV_TOTP_SECRET` in der `.env` steht — das gilt nur außerhalb der
Produktion.
### Betriebswissen, das man einmal gebraucht hat
- **Nicht gleichzeitig `npm run dev` und `npm run build` laufen lassen.**
Beide schreiben nach `.next`; das Ergebnis ist ein Build, dem einzelne
Chunks fehlen, und der Fehler (`Cannot find module './638.js'`) zeigt
nicht auf die Ursache. Im Zweifel `rm -rf .next` und neu bauen.
- **`AUTH_URL` muss exakt der Adresse entsprechen, unter der du die
Anwendung aufrufst** — bis auf Port und Schreibweise. Steht dort
`localhost` und du rufst `127.0.0.1` auf, wird das Sitzungs-Cookie für
die falsche Herkunft gesetzt und die Anmeldung landet stumm wieder auf
dem Formular.
- Der Standalone-Server (`node server.js`, so startet der Container) liest
**keine `.env`**. Alle Variablen kommen von außen — im Compose-Setup
über den Abschnitt `environment:`.
- Beim ersten Start eines Containers laufen die Migrationen automatisch
mit. Die beiden SQL-Skripte (Volltextsuche, Audit-Schutz) sind
**einmalig von Hand** anzustoßen, siehe oben.
### Technische Grundlage
Next.js 15 (App Router) · TypeScript · Tailwind CSS · shadcn/ui-Bausteine ·
PostgreSQL 17 mit Prisma · Auth.js · Zod · Server Actions ·
`@react-pdf/renderer` · Tesseract (deutsch) · Vitest · Playwright
Bewusste Abweichungen von shadcn/ui im Original: Bedienelemente sind 44 px
hoch statt 36 px, Auswahlfelder benutzen das native `<select>`, und
Eingabefelder haben mindestens 16 px Schrift. Alles drei, damit die Anwendung
am Handy zuverlässig zu bedienen ist — im Heim und auf dem Amt.
---
## Bauprotokoll
Jeder Commit trägt die Trailer `Etappe:` und `Phase:`. Daraus lässt sich
nachvollziehen, wie lange welcher Bauabschnitt gedauert hat:
```bash
npm run protokoll # Tabelle auf der Konsole
npm run protokoll:md # Markdown für docs/BAUPROTOKOLL.md
```
Der aktuelle Stand liegt in [`docs/BAUPROTOKOLL.md`](docs/BAUPROTOKOLL.md) —
dort stehen auch die Laufzeiten der parallel gebauten Etappen und die
Fehler, die während des Baus aufgefallen sind.
---
## Stand
| | |
|---|---:|
| Zeilen TypeScript/TSX | 71.051 |
| Prisma-Modelle | 24 |
| Seiten | 46 |
| Unit-Tests (Vitest) | 458 |
| End-to-End-Tests (Playwright) | 34 |
| TypeScript-Fehler | 0 |
Alle acht Etappen sind umgesetzt. Der Produktionsbuild ist gegen den
fertigen Standalone-Server geprüft: strenge CSP aktiv, keine
Konsolenfehler, keine Anfrage an einen fremden Host.