Am Schreibtisch war "Mein Konto" praktisch nicht zu finden - und damit auch nicht Passwortwechsel, zweiter Faktor und Passkey-Verwaltung. Die Seitenleiste hatte keine feste Hoehe und wuchs mit der Seite mit. Ihr Fuss mit dem Benutzermenue landete deshalb am Ende der SEITE statt am unteren Bildschirmrand: auf /heute bei 1632 Pixel in einem 900 Pixel hohen Fenster. Nachgemessen, nicht geraten - im Browser auf fuenf Seiten. Der Fehler sah nach nichts aus. Kein Element fehlte, nichts war kaputt, die Seite sah richtig aus. Man musste nur ganz nach unten scrollen. Behoben mit lg:sticky, lg:top-0, lg:h-dvh und lg:self-start. Das self-start gehoert dazu, weil ein Flex-Kind sonst auf die volle Zeilenhoehe gestreckt wird und die feste Hoehe wieder verliert. Die Navigation dazwischen hat bereits overflow-y-auto und scrollt bei Bedarf in sich selbst. Am Handy war es nie ein Problem - dort sitzt das Menue in der ohnehin festen Kopfzeile. Neu: e2e/benutzermenue.spec.ts fragt nicht, ob das Menue im DOM ist, sondern ob es im sichtbaren Bereich steht - auf fuenf Seiten, an drei Scrollpositionen, auf einem flachen Laptopfenster und am Handy. Die Position in der Mitte ist Absicht: Am Seitenende wird eine mitwachsende Leiste zufaellig doch sichtbar, der Test waere gruen und der Fehler trotzdem da. Beide Sichtbarkeitstests sind gegen den kaputten Zustand gegengeprueft und schlagen dort an. Refs #1
596 lines
23 KiB
Markdown
596 lines
23 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 |
|
|
|---|---|
|
|
| **Die Anwendung aufsetzen** — auf einem Server, der Docker und einen Proxy schon hat, 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
|
|
./scripts/update.sh
|
|
```
|
|
|
|
> **Kommst du von 1.1.0 oder älter?** Dort lag `update.sh` ohne
|
|
> Ausführungsrecht im Repository — `./scripts/update.sh` endet dann in
|
|
> „Permission denied". Einmalig hilft:
|
|
>
|
|
> ```bash
|
|
> bash scripts/update.sh
|
|
> ```
|
|
>
|
|
> Ab 1.1.1 ist das Recht gesetzt und der Punktschrägstrich funktioniert.
|
|
|
|
Das Skript sichert zuerst, zeigt den Changelog zwischen altem und neuem
|
|
Stand, fragt nach, schaltet um, baut neu und führt danach die beiden
|
|
SQL-Skripte aus. Schlägt die Sicherung fehl, wird nicht aktualisiert.
|
|
|
|
`--pruefen` sieht nur nach, ohne etwas zu ändern. Eigene Änderungen an
|
|
versionierten Dateien werden vor dem Umschalten beiseite gelegt
|
|
(`git stash`), die `.env` bleibt unangetastet.
|
|
|
|
Einzelheiten und der Weg von Hand: [`docs/INSTALLATION.md`](docs/INSTALLATION.md).
|
|
|
|
---
|
|
|
|
## Reverse Proxy
|
|
|
|
Die Anwendung **beendet selbst kein TLS** und bringt keinen Proxy mit. Sie
|
|
horcht standardmäßig auf `127.0.0.1:3000`.
|
|
|
|
**Läuft der Proxy auf demselben Server**, ist damit alles gesagt: Er zeigt
|
|
auf `127.0.0.1:3000`, fertig.
|
|
|
|
**Läuft er auf einem anderen Server**, setz in der `.env` die Adresse, über
|
|
die er herankommt — am besten die eines privaten Netzes oder eines
|
|
WireGuard-Tunnels:
|
|
|
|
```bash
|
|
APP_BIND=10.0.0.5
|
|
APP_PORT=3000
|
|
```
|
|
|
|
`0.0.0.0` nur, wenn eine Firewall den Port ausschließlich für die IP des
|
|
Proxy-Servers öffnet. Zwischen Proxy und Anwendung läuft unverschlüsseltes
|
|
HTTP — dieser Weg darf nie durchs offene Netz gehen.
|
|
|
|
### Was der Proxy leisten muss
|
|
|
|
| Anforderung | Was ohne sie passiert |
|
|
|---|---|
|
|
| TLS beenden | Der Browser lehnt Cookies mit `Secure`-Kennzeichen ab, die Anmeldung schlägt stumm fehl |
|
|
| `Host` durchreichen | Auth.js vergleicht ihn mit `AUTH_URL` und weist die Anmeldung ab |
|
|
| `X-Forwarded-Proto: https` | Die Anwendung baut Adressen mit `http` |
|
|
| `X-Forwarded-For` | Die Login-Sperre zählt alle Zugriffe als denselben Anschluss |
|
|
| Uploads ≥ 30 MB | Fotos mehrseitiger Bescheide werden abgeschnitten |
|
|
| Zeitgrenze ≥ 10 Minuten | Belegmappe, Rechnungslegung und Vollexport brechen ab |
|
|
| Antwort nicht puffern | Der Vollexport kommt als Strom |
|
|
|
|
**Keine eigenen Sicherheitskopfzeilen setzen.** CSP, `X-Frame-Options`,
|
|
`nosniff` und HSTS kommen aus der Anwendung. Die CSP enthält eine Nonce,
|
|
die pro Anfrage neu ist — eine feste CSP aus dem Proxy blockiert die
|
|
eigenen Skripte.
|
|
|
|
**Fertige Konfigurationen** für Caddy, nginx und Traefik, mit Zeitgrenzen
|
|
und den Gegenproben, stehen in
|
|
[`docs/INSTALLATION.md`](docs/INSTALLATION.md), Schritt 7.
|
|
|
|
---
|
|
|
|
## 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.
|
|
- **Passkeys** (WebAuthn/FIDO2) als zweiter Anmeldeweg — auch für die
|
|
erneute Bestätigung vor sensiblen Aktionen. Sie *ersetzen* den zweiten
|
|
Faktor, statt zu ihm hinzuzukommen: Nutzerverifikation wird verlangt
|
|
(`userVerification: 'required'`) und serverseitig nachgeprüft, ein
|
|
Schlüssel ohne sie wird abgewiesen. Damit sind es dieselben zwei
|
|
Faktoren — Besitz des Geräts und dessen Entsperrung —, nur an die Domain
|
|
gebunden und deshalb nicht abphishbar. TOTP muss trotzdem eingerichtet
|
|
sein, bevor sich ein Passkey anlegen lässt: Er hängt an einem Gerät und
|
|
darf nicht der einzige Weg hinein sein.
|
|
- **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
|
|
|
|
| | | nachzuzählen mit |
|
|
|---|---:|---|
|
|
| Zeilen TypeScript/TSX | 73.774 | `git ls-files "*.ts" "*.tsx" \| xargs wc -l` |
|
|
| Prisma-Modelle | 33 | `grep -c '^model ' prisma/schema.prisma` |
|
|
| Seiten | 79 | `find src/app -name page.tsx \| wc -l` |
|
|
| Unit-Tests (Vitest) | 500 | `npm test` |
|
|
| End-to-End-Tests (Playwright) | 45 | `npm run test:e2e` — 26 Tests über zwei Geräteprofile, geräteabhängige übersprungen |
|
|
| TypeScript-Fehler | 0 | `npm run typecheck` |
|
|
|
|
Die Befehle stehen dabei, weil die Zahlen sonst altern, ohne dass es
|
|
jemandem auffällt — bei den Modellen und Seiten war genau das passiert.
|
|
|
|
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.
|