Files
betreuungs-cockpit/README.md
T
Florian Wilde 89bc5fe5a8 docs: Installationsanleitung Schritt fuer Schritt
Die README hatte Installationsabschnitte, aber keine durchgehende
Anleitung fuer jemanden, der vor einem frischen Server sitzt. Genau da
faengt diese hier an.

Dreizehn Schritte vom leeren Server bis zur laufenden Anwendung, jeder
mit einer Kontrolle am Ende - man geht nicht weiter, ohne geprueft zu
haben, ob der letzte Schritt geklappt hat. Am Ende eine Abnahmeliste
zum Abhaken, bevor echte Daten hineinkommen.

Aufgenommen sind die Fallstricke, die tatsaechlich Zeit kosten:

- AV-Vertrag klaeren, BEVOR der Server bestellt wird
- Zeitzone setzen, sonst rechnet der Server in UTC und verschiebt Fristen
- AUTH_URL muss exakt der aufgerufenen Adresse entsprechen - sonst landet
  die Anmeldung stumm wieder auf dem Formular, ohne Fehlermeldung. Das
  ist der Fehler, den man am laengsten sucht.
- Der Proxy muss X-Forwarded-For weiterreichen, sonst zaehlt die
  Login-Sperre alle Zugriffe als denselben Anschluss.
- Die beiden SQL-Skripte laufen nicht mit den Migrationen mit.

Zwei Kontrollen sind keine Formalie, sondern Nachweise: Der Schutz des
Audit-Logs wird geprueft, indem ein UPDATE ausdruecklich fehlschlagen
MUSS. Und die Sicherung wird nicht nur angestossen, sondern einmal
zurueckgespielt - solange noch nichts drinsteht, was weh tut.

Der letzte Schritt bringt ENC_KEY_V1 in Sicherheit, an zwei Orte, von
denen keiner der Server ist. Ein gestohlenes Backup ist ohne ihn
wertlos - und ein verlorener Schluessel macht auch das eigene Backup
wertlos. Das ist dieselbe Eigenschaft von zwei Seiten.

Etappe: 8
Phase: Haertung
2026-08-22 10:54:21 +02:00

584 lines
21 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)
- [Die drei Dokumente, die du wirklich brauchst](#die-drei-dokumente-die-du-wirklich-brauchst)
- [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. |
---
## Die drei Dokumente, die du wirklich brauchst
| Wofür | Wo |
|---|---|
| **Einen Server aufsetzen** — von der leeren Maschine bis zur laufenden Anwendung, mit Kontrolle nach jedem Schritt | [`docs/INSTALLATION.md`](docs/INSTALLATION.md) |
| **Täglich damit arbeiten** — die zwölf Handgriffe, der Jahresrhythmus, was tun wenn etwas schiefgeht | [`docs/ANLEITUNG.md`](docs/ANLEITUNG.md), in der Anwendung unter `/anleitung` |
| **Wissen, was sich ändert** — was neu ist, was du vor einem Update beachten musst | [`CHANGELOG.md`](CHANGELOG.md) |
Dazu: [`docs/ROADMAP.md`](docs/ROADMAP.md) für das, was als Nächstes
kommt, und [`docs/BAUPROTOKOLL.md`](docs/BAUPROTOKOLL.md) für die Frage,
wie das hier entstanden ist.
Der Rest dieser Datei ist die Kurzfassung für alle, die schon wissen, was
sie tun.
---
## 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
> **Zum ersten Mal?** Nimm die [Schritt-für-Schritt-Anleitung](docs/INSTALLATION.md).
> Sie fängt beim leeren Server an, hat nach jedem Abschnitt eine Kontrolle
> und endet mit einer Abnahmeliste. Was hier steht, ist die Kurzfassung.
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
npm run version -- patch # Version anheben, Changelog vorbereiten
```
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.