From 042e63812b95f927177ea748c89b554128df50cf Mon Sep 17 00:00:00 2001 From: Florian Wilde Date: Tue, 1 Sep 2026 16:36:48 +0200 Subject: [PATCH] Anleitung: wo die Daten liegen, und drei Fallen aus dem Betrieb Aus einer Fehlersuche am 31.08.2026 sind vier Luecken in der Anleitung uebriggeblieben. Keine davon war falsch formuliert - alle vier waren schlicht nicht durchdacht. 1. Es stand nirgends, wo die Daten liegen. Der neue Abschnitt nennt die drei Orte, zeigt den Pfad auf der Platte, sagt ausdruecklich, dass dort keine anklickbaren PDFs liegen, und beschreibt den Abgleich zwischen Akte und Speicher: Zu jedem Eintrag muss eine Datei gehoeren, sonst belegt eine Pruefsumme ein Dokument, das es nicht mehr gibt. 2. Der mc-Alias wurde einmalig gesetzt. Er liegt in /root/.mc IM Behaelter und ist nach jedem "docker compose up -d minio" weg - "mc ls lokal" liefert dann kommentarlos nichts, und man haelt seine Dokumente fuer verschwunden. Genau das ist passiert. Jeder Aufruf setzt den Alias jetzt selbst, aus den Variablen des Behaelters. 3. Der Bucket-Name stand fest im Befehl, die Anwendung liest ihn aus S3_BUCKET. Weichen sie ab, zeigen beide aneinander vorbei. 4. "set -a && source .env" wurde empfohlen, ohne zu sagen, dass die exportierten Werte danach jedes "docker compose up" ueberstimmen. Wer spaeter die .env aendert und neu startet, sieht keine Wirkung und sucht den Fehler ueberall, nur nicht in der eigenen Shell. Der Kniff ist nicht mehr noetig - der neue Befehl kommt ohne ihn aus - und die Falle steht als Erklaerung daneben. Dazu docker-compose.override.yml in der .gitignore und in der Anleitung als vorgesehener Ort fuer Anpassungen, die ein Update ueberleben sollen: update.sh prueft nur versionierte Dateien und laesst sie in Ruhe. Die Aussage zur Sicherung ist mitgezogen - sie stimmt seit #41 nicht mehr, die Dokumente sind jetzt enthalten. Neun Pruefungen in tests/anleitung.test.ts, gegen vier Mutationen geprueft: Name fest verdrahtet, Alias nicht gesetzt, source .env zurueck, Abschnitt entfernt - jede wird rot. Refs #46 --- .gitignore | 8 +++ docs/INSTALLATION.md | 139 ++++++++++++++++++++++++++++++++++++---- tests/anleitung.test.ts | 110 +++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 tests/anleitung.test.ts diff --git a/.gitignore b/.gitignore index d0ce196..235b03b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,14 @@ dist/ next-env.d.ts *.tsbuildinfo +# Eigene Anpassungen der Installation. +# +# Docker Compose liest diese Datei automatisch zusaetzlich zur +# docker-compose.yml. Sie ist der vorgesehene Ort fuer Aenderungen, die ein +# Update ueberleben sollen: update.sh prueft nur versionierte Dateien +# (git status --untracked-files=no) und laesst sie deshalb in Ruhe. +docker-compose.override.yml + # Umgebung - enthaelt Schluessel, gehoert NIE ins Repo .env .env.local diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 11b3b8f..465fe94 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -34,6 +34,7 @@ durchgeht — dann weißt du im Fehlerfall genau, wo es hakt. - [Schritt 9 — Sicherung prüfen](#schritt-9--sicherung-prüfen) - [Schritt 10 — Wartungslauf einrichten](#schritt-10--wartungslauf-einrichten) - [Schritt 11 — Schlüssel in Sicherheit bringen](#schritt-11--schlüssel-in-sicherheit-bringen) +- [Wo deine Daten liegen](#wo-deine-daten-liegen) - [Aktualisieren](#aktualisieren) - [Wenn etwas nicht klappt](#wenn-etwas-nicht-klappt) - [Abnahmeliste](#abnahmeliste) @@ -375,22 +376,37 @@ nichts kaputtmacht. Der Objektspeicher braucht einen Ablagebereich für die Dokumente: ```bash -set -a && source .env && set +a - -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 +docker compose exec minio sh -c ' + mc alias set lokal http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && + mc mb "lokal/$S3_BUCKET" 2>/dev/null || true + mc ls lokal +' ``` -### Kontrolle +Es muss eine Zeile mit dem Namen deines Ablagebereichs erscheinen — +standardmäßig `betreuungs-cockpit/`. -```bash -docker compose exec minio mc ls lokal -# [...] betreuungs-cockpit/ -``` +> **Warum der Befehl so umständlich aussieht.** Er holt die Zugangsdaten +> aus den Variablen, die im MinIO-Behälter ohnehin stehen. Das hat drei +> Gründe, und jeder davon hat schon einmal Zeit gekostet: +> +> 1. **Kein Passwort in der Befehlszeile** — es stünde sonst in der +> Verlaufsdatei deiner Shell. +> 2. **Kein `source .env`** — exportierte Werte haben Vorrang vor der +> `.env`-Datei und überstimmen danach *jedes* `docker compose up`. Wer +> später die `.env` ändert und neu startet, sieht keine Wirkung und +> sucht den Fehler überall, nur nicht in der eigenen Shell. +> 3. **Der Alias überlebt kein Neuerstellen.** `mc alias set` schreibt +> nach `/root/.mc` **im Behälter**. Nach jedem `docker compose up -d +> minio` ist er weg, und `mc ls lokal` liefert dann kommentarlos nichts +> — man hält seine Dokumente für verschwunden. Deshalb setzt jeder +> Aufruf den Alias selbst. +> +> Der Bucket-Name kommt aus `$S3_BUCKET` und ist nicht fest verdrahtet: +> Sonst zeigen Anwendung und Ablagebereich aneinander vorbei, sobald +> jemand den Namen ändert. -> **Serverseitige Verschluesselung?** Brauchst du nicht. Die Anwendung +> **Serverseitige Verschlüsselung?** Brauchst du nicht. Die Anwendung > verschlüsselt jede Datei selbst, bevor sie den Server verlässt — mit > `ENC_KEY_V1`, der den Anbieter nie erreicht. Das mitgelieferte MinIO > läuft ohne KMS und würde jede Ablage ablehnen, sobald `S3_SSE` in der @@ -913,6 +929,105 @@ Denk an die letzten Pflichten: --- +## Wo deine Daten liegen + +Drei Orte, und keiner davon ist ein Ordner, den du einfach öffnest. + +| Was | Wo | In der täglichen Sicherung? | +|---|---|---| +| **Dokumente** (Bescheide, Schreiben, Belege) | Docker-Volume `minio-daten` | ja | +| **Datenbank** (Akte, Fristen, Buchungen) | Docker-Volume `postgres-daten` | ja | +| **Sicherungen** | `./backups` neben der `docker-compose.yml` | — | + +### Den Pfad auf der Platte finden + +```bash +docker volume inspect betreuungs-cockpit_minio-daten --format '{{.Mountpoint}}' +# /var/lib/docker/volumes/betreuungs-cockpit_minio-daten/_data +``` + +Heißt dein Projektverzeichnis anders, heißen auch die Volumes anders — +Docker stellt den Verzeichnisnamen voran: + +```bash +docker volume ls | grep -E "minio-daten|postgres-daten" +``` + +> **Dort hineinzusehen bringt nichts.** MinIO legt jedes Dokument als +> eigenes Verzeichnis mit Metadatei und Datenteilen ab, und der Inhalt ist +> verschlüsselt. Du fändest dort keine anklickbaren PDFs, sondern +> Fragmente. Wer die Dokumente sehen will, nimmt die Anwendung. + +### Nachsehen, was abgelegt ist + +```bash +docker compose exec minio sh -c ' + mc alias set lokal http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && + mc ls --recursive "lokal/$S3_BUCKET" +' +``` + +Je Dokument eine Zeile unter `betreute//dokumente/.pdf`. + +### Prüfen, ob Akte und Speicher übereinstimmen + +Die wichtigste Kontrolle: Zu **jedem** Eintrag in der Akte muss eine Datei +gehören. Fehlt sie, belegt eine Prüfsumme ein Dokument, das es nicht mehr +gibt — gegenüber dem Betreuungsgericht der schlechteste Zustand. + +```bash +docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -At -c "SELECT \"objektSchluessel\" FROM dokument WHERE \"geloeschtAm\" IS NULL ORDER BY 1;"' | sort > /tmp/erwartet.txt + +docker compose exec minio sh -c ' + mc alias set lokal http://localhost:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && + mc ls --recursive "lokal/$S3_BUCKET" +' | awk '{print $NF}' | sort > /tmp/vorhanden.txt + +echo "In der Akte: $(wc -l < /tmp/erwartet.txt) Im Speicher: $(wc -l < /tmp/vorhanden.txt)" +echo "--- Fehlt im Speicher (Handlungsbedarf): ---" +comm -23 /tmp/erwartet.txt /tmp/vorhanden.txt +echo "--- Verwaist im Speicher (harmlos): ---" +comm -13 /tmp/erwartet.txt /tmp/vorhanden.txt +``` + +Beide Zahlen gleich und beide Listen leer — dann passt es. + +Ein Eintrag unter *„Fehlt im Speicher"* ist ein Akteneintrag ohne Datei. +Er entsteht, wenn eine Ablage scheitert. Solche Einträge löschen und das +Dokument neu anlegen. + +### Ohne welche Schlüssel nichts davon lesbar ist + +| Schlüssel | Ohne ihn | +|---|---| +| `ENC_KEY_V1` | Diagnosen, IBANs, Notizen und alle Dokumente bleiben unlesbar — auch aus jeder Sicherung | +| `SICHERUNG_PASSWORT` | keine Sicherung lässt sich öffnen | +| `MINIO_KMS_SECRET_KEY` | nur falls gesetzt: MinIO gibt die Objekte nicht mehr heraus | + +Alle gehören an die zwei Orte aus Schritt 11, und keiner davon ist dieser +Server. + +### Eigene Anpassungen, die ein Update überleben + +Änderungen an der `docker-compose.yml` legt `scripts/update.sh` bei jedem +Update per `git stash` beiseite — sie sind versioniert. Wer etwas dauerhaft +ändern will, legt stattdessen eine **`docker-compose.override.yml`** +daneben: + +```yaml +services: + app: + environment: + EIGENER_WERT: ${EIGENER_WERT:-} +``` + +Docker Compose liest sie automatisch mit. `update.sh` prüft nur +versionierte Dateien (`git status --untracked-files=no`) und lässt sie +deshalb unangetastet. + +--- + + ## Aktualisieren ```bash diff --git a/tests/anleitung.test.ts b/tests/anleitung.test.ts new file mode 100644 index 0000000..4b8665a --- /dev/null +++ b/tests/anleitung.test.ts @@ -0,0 +1,110 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +/** + * Die Installationsanleitung muss ausführbar sein, nicht nur lesbar. + * + * Anlass ist eine Fehlersuche vom 31.08.2026. Drei Stellen der Anleitung + * haben dabei Zeit gekostet — keine davon war falsch formuliert, alle drei + * waren schlicht nicht durchdacht: + * + * 1. `mc alias set` wurde einmalig gesetzt. Der Alias liegt in `/root/.mc` + * **im Behälter** und ist nach jedem `docker compose up -d minio` weg. + * `mc ls lokal` liefert dann kommentarlos nichts — man hält seine + * Dokumente für verschwunden. + * 2. Der Bucket-Name stand fest im Befehl, die Anwendung liest ihn aus + * `S3_BUCKET`. Weichen sie ab, zeigen beide aneinander vorbei. + * 3. `set -a && source .env` exportiert alle Werte in die Shell — und die + * haben danach Vorrang vor der `.env`-Datei. Wer später etwas ändert + * und neu startet, sieht keine Wirkung. + * + * Diese Prüfungen lesen den Text. Sie können die Anleitung nicht + * ausführen, aber sie schlagen an, sobald jemand in eine der Fallen + * zurückfällt. + */ + +const anleitung = readFileSync('docs/INSTALLATION.md', 'utf8') + +/** Alle Zeilen mit einem mc-Aufruf, ohne Fließtext und Zitate. */ +function mcZeilen(): string[] { + return anleitung + .split('\n') + .filter((zeile) => /\bmc (ls|mb|mirror|cp|rm|stat)\b/.test(zeile)) + .filter((zeile) => !zeile.trim().startsWith('>')) +} + +describe('Installationsanleitung', () => { + it('setzt bei jedem mc-Aufruf den Alias selbst', () => { + // Sonst funktioniert der Befehl nur, solange der Behaelter nicht neu + // erstellt wurde - und das sagt einem niemand. + const abschnitte = anleitung.split('```') + const ohneAlias = abschnitte + .filter((block) => /\bmc (ls|mb|mirror|cp|rm|stat)\b/.test(block)) + .filter((block) => !block.includes('mc alias set')) + + expect( + ohneAlias.length, + 'Ein mc-Befehl ohne "mc alias set" im selben Block läuft nur, solange ' + + 'der MinIO-Behälter nicht neu erstellt wurde.', + ).toBe(0) + }) + + it('verdrahtet den Namen des Ablagebereichs nicht fest', () => { + const festVerdrahtet = mcZeilen().filter((zeile) => /lokal\/betreuungs-cockpit/.test(zeile)) + + expect( + festVerdrahtet, + 'Der Name gehört aus $S3_BUCKET - sonst zeigen Anwendung und ' + + 'Ablagebereich aneinander vorbei, sobald jemand ihn ändert.', + ).toEqual([]) + }) + + it('empfiehlt kein "source .env" mehr', () => { + // Exportierte Werte ueberstimmen danach jedes "docker compose up". + const zeilen = anleitung + .split('\n') + .filter((zeile) => !zeile.trim().startsWith('>')) + .filter((zeile) => !zeile.trim().startsWith('#')) + .filter((zeile) => /source \.env/.test(zeile)) + + expect(zeilen).toEqual([]) + }) + + it('erklärt trotzdem, warum es nicht empfohlen wird', () => { + // Weglassen allein hilft nicht: Wer den Kniff von woanders kennt, + // benutzt ihn weiter. + expect(anleitung).toMatch(/Vorrang vor der/) + }) + + it('sagt, wo die Daten liegen', () => { + expect(anleitung).toMatch(/## Wo deine Daten liegen/) + expect(anleitung).toMatch(/docker volume inspect/) + expect(anleitung).toMatch(/minio-daten/) + expect(anleitung).toMatch(/postgres-daten/) + }) + + it('zeigt, wie man Akte und Speicher abgleicht', () => { + // Die wichtigste Kontrolle: Zu jedem Akteneintrag muss eine Datei + // gehoeren, sonst belegt eine Pruefsumme ein Dokument, das es nicht + // mehr gibt. + expect(anleitung).toMatch(/objektSchluessel/) + expect(anleitung).toMatch(/comm -23/) + }) + + it('nennt die Schlüssel, ohne die nichts mehr lesbar ist', () => { + const abschnitt = anleitung.slice(anleitung.indexOf('## Wo deine Daten liegen')) + for (const schluessel of ['ENC_KEY_V1', 'SICHERUNG_PASSWORT', 'MINIO_KMS_SECRET_KEY']) { + expect(abschnitt, `${schluessel} fehlt in der Aufzählung`).toContain(schluessel) + } + }) + + it('weist docker-compose.override.yml als Ort für eigene Anpassungen aus', () => { + expect(anleitung).toMatch(/docker-compose\.override\.yml/) + expect(readFileSync('.gitignore', 'utf8')).toMatch(/docker-compose\.override\.yml/) + }) + + it('behauptet nicht mehr, die Dokumente seien ungesichert', () => { + // Stand bis v1.11.1 so drin und stimmt seit der Sicherung nicht mehr. + expect(anleitung).not.toMatch(/Was das Backup nicht enthält:.*Dateien im Objektspeicher/s) + }) +}) -- 2.54.0