Compare commits

..
6 Commits
Author SHA1 Message Date
florian.wilde 5e5778b1e6 Merge #55: Anleitung zum Jahresabschluss und zu Genossenschaftsanteilen 2026-09-02 20:55:20 +00:00
Florian Wilde cba2e49211 Anleitung: Jahresabschluss und was aussieht wie ein Konto
Zwei Fragen aus der Praxis, die die Anleitung nicht beantwortete.

## Der Jahresabschluss

Sie beschrieb, was in jedem Bericht steht, aber nicht die Reihenfolge und
nicht, was man vorher besorgen muss. Der neue Abschnitt beginnt deshalb
sechs Wochen vor dem Stichtag: Rueckkaufswerte und Bescheinigungen
brauchen Wochen, und wer erst am Stichtag daran denkt, geraet in Verzug.
Danach die Staende mit Quellenangabe, dann die Berichte in der
Reihenfolge, in der sie aufeinander aufbauen.

## Genossenschaftsanteile

Das Gericht fragt einen "Kontoauszug" an, den es nicht gibt: Ein
Geschaeftsguthaben ist kein Zahlungskonto, sondern eine Forderung gegen
die Genossenschaft. Es laufen keine Buchungen darueber, und ausgezahlt
wird erst nach Kuendigung der Mitgliedschaft.

Der Fall fuehrt zu einem stillen Fehler: Wer die Anteile als Konto
anlegt, kann die Bescheinigung nicht am Wert hinterlegen - genau das
Blatt, das das Gericht sehen will. Ein Saldostand traegt eine
Quellenangabe, aber kein Dokument; ein Wertstand am Vermoegensposten
schon. Deshalb steht im Text nicht nur, was zu tun ist, sondern warum -
sonst legt der naechste Leser es wieder als Konto an.

Dazu der Hinweis, dass "der Wert bleibt doch gleich" nicht traegt: Wird
eine Dividende dem Geschaeftsguthaben gutgeschrieben statt ausgezahlt,
waechst es. Genau deshalb fragt man jaehrlich nach, statt den
Vorjahreswert fortzuschreiben.

Eine Uebersicht behandelt die verwandten Faelle mit - Bausparvertrag,
Depot, Verbindlichkeit. Das Depot ist die Ausnahme: Dort laufen Umsaetze,
es gibt einen Auszug, es ist ein Konto. Versicherungen gehoeren an den
Vertrag, nicht ins Vermoegen.

Zehn Pruefungen in tests/anleitung.test.ts, gegen vier Mutationen
geprueft: als Konto geraten, Dividendenhinweis entfernt, Vorlauf
entfernt, Abschnitt entfernt - jede wird rot.

Refs #55
2026-09-02 22:52:41 +02:00
florian.wilde c5db3baac0 Merge: Version 1.12.0 2026-09-01 14:40:22 +00:00
Florian Wilde 274d7666ea chore: Version 1.12.0 2026-09-01 16:39:59 +02:00
florian.wilde 890e00fb5c Merge #46: Wo die Daten liegen, und drei Fallen aus dem Betrieb 2026-09-01 14:37:25 +00:00
Florian Wilde 042e63812b 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
2026-09-01 16:36:48 +02:00
10 changed files with 546 additions and 20 deletions
+8
View File
@@ -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
+82 -1
View File
@@ -26,6 +26,86 @@ Wiederherstellung*.
_Noch nichts._
## [1.12.0] – 2026-09-01
Diese Fassung geht aus einer Fehlersuche im laufenden Betrieb hervor. Ein
Schreiben ließ sich nicht in der Akte ablegen — und hinter derselben
Meldung lagen nacheinander drei verschiedene Ursachen. Beim Nachsehen kam
ein vierter Punkt dazu, der schwerer wiegt als alle anderen zusammen.
### Nach dem Update: einmal die Sicherung prüfen
```bash
docker compose up -d
docker compose exec sicherung /bin/sh /sicherung.sh
```
Erwartet werden drei Zeilen: `Datenbank gesichert`, `Dokumente gesichert`
und `Selbstpruefung bestanden - Datenbank und Dokumente.`
### Behoben
- **Deine Dokumente waren in keiner Sicherung.** Der Sicherungsdienst
machte täglich ein `pg_dump` und sonst nichts. Nach einem Plattenschaden
wäre die Akte vollständig wiederhergestellt worden — mit allen
Einträgen, Verweisen und Prüfsummen, aber ohne eine einzige Datei
dahinter. Die Prüfsumme in der Akte hätte dann ein Dokument belegt, das
es nicht mehr gibt.
Je Lauf entstehen jetzt zwei verschlüsselte Dateien mit demselben
Zeitstempel — der Datenbankstand und ein Archiv der Dokumente. Beide mit
Prüfsumme, beide in der Aufbewahrung, beide in der Selbstprüfung.
Schlägt der Dokumententeil fehl, bricht der Lauf ab und räumt den
angefangenen Datenbankstand weg: Eine Sicherung, die nur die halbe
Wahrheit enthält, aber vollständig aussieht, ist schlimmer als gar
keine.
- **Der mitgelieferte Objektspeicher konnte keine Dokumente aufnehmen.**
Die Anwendung verlangte bei jeder Ablage serverseitige Verschlüsselung,
das mitgelieferte MinIO kann sie ohne Zusatzdienst nicht. Wer der
Installationsanleitung folgte, landete zwangsläufig dort. Sie ist jetzt
abschaltbar und standardmäßig aus — gebraucht wird sie nicht, weil die
Anwendung seit 1.11.0 selbst verschlüsselt.
- **„Das Schreiben konnte nicht abgelegt werden" sagte nichts.** Jetzt
steht dort, was los ist: fehlender Ablagebereich mit Verweis auf den
Installationsschritt, abgelehnte Zugangsdaten, nicht erreichbarer
Speicher. Und in jedem Fall, ob deine Eingaben erhalten bleiben.
- **Zwei Einstellungen erreichten die Anwendung nie.**
`ERSTEINRICHTUNG_GEHEIMNIS` und `VERTRAUE_PROXY` standen zwar in der
Anleitung, wurden aber nicht an den Behälter durchgereicht. Bei
`VERTRAUE_PROXY` fiel das nicht auf: Die Anmeldesperre zählte still eine
Dimension weniger.
- **Die Beispielkonfiguration zeigte auf einen fremden Anbieter.**
`S3_ENDPOINT` war mit einer externen Adresse vorbelegt und hebelte damit
den richtigen Standard aus. Die Zeile ist jetzt auskommentiert.
### Neu
- **Die Anwendung prüft ihre Dateiablage selbst.** Sie schreibt eine
winzige Datei, liest sie zurück und räumt sie weg — und beantwortet
damit auf einmal, ob der Speicher erreichbar ist, der Ablagebereich
existiert und geschrieben werden darf. Das Ergebnis steht unter
`/api/gesundheit` und im Log. Bisher fiel eine kaputte Konfiguration
erst auf, wenn jemand ein Dokument ablegen wollte.
- **Ein neuer Abschnitt in der Anleitung: „Wo deine Daten liegen".**
Die drei Orte, der Pfad auf der Platte, welche Schlüssel unverzichtbar
sind — und wie man prüft, ob zu jedem Akteneintrag auch eine Datei
gehört.
### Geändert
- **Ausschlussfristen warnen nur noch, wenn sie näherrücken.** Auf dem
Dashboard standen dauerhaft drei rote Kästen mit Warndreieck — für
Aufwandspauschalen, die 2027, 2028 und 2029 ablaufen. Eine Warnung, die
monatelang dasteht und nichts von einem will, stumpft ab; man übersieht
die vierte, die etwas will. Ab drei Monaten vor Ablauf meldet sich eine
Frist wieder, Versäumtes immer. In der Fristenliste stehen sie
unverändert weiter.
## [1.11.0] – 2026-08-24
Diese Fassung geht aus einer Sicherheitsprüfung hervor. Sie ändert nichts
@@ -1265,7 +1345,8 @@ Was davon wann angegangen wird, steht in [`docs/ROADMAP.md`](docs/ROADMAP.md).
---
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.11.0...HEAD
[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.12.0...HEAD
[1.12.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.11.0...v1.12.0
[1.11.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.2...v1.11.0
[1.10.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.1...v1.10.2
[1.10.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.10.0...v1.10.1
+130
View File
@@ -332,6 +332,136 @@ Anwendung jederzeit verlassen. Verlangt eine erneute Anmeldung.
---
## Der Jahresabschluss, Schritt für Schritt
Einmal im Jahr will das Gericht zwei Dinge: einen **Bericht über die
Person** und eine **Rechnung über das Vermögen**. Beides bezieht sich auf
dein Betreuungsjahr, nicht auf das Kalenderjahr.
Die Reihenfolge unten ist keine Formalie. Der erste Schritt liegt sechs
Wochen vor dem Stichtag, weil manche Auskünfte so lange brauchen — wer erst
am Stichtag daran denkt, wartet und gerät in Verzug.
### Sechs Wochen vorher: anfordern, was du nicht selbst hast
Manche Werte kannst du nicht nachsehen, sondern nur erfragen:
- **Rückkaufswerte** aller Lebens-, Renten- und Sterbegeldversicherungen
- **Bescheinigungen** über Geschäftsguthaben, Bausparguthaben, Depotwerte
- **Jahresabrechnungen**, die noch ausstehen
Die Anwendung legt dir dafür rechtzeitig eine Frist an. Schreib die Briefe
gleich, wenn sie fällig wird — unter **Vorgänge** oder direkt am Vertrag.
Jedes Schreiben bekommt eine **Wiedervorlage**: Kommt in drei Wochen keine
Antwort, fasst du nach. Ohne Wiedervorlage geht eine unbeantwortete Anfrage
unter, und das merkst du am Stichtag.
### Am Stichtag: Stände eintragen
**Konten** — *Vermögen & Kasse → Konten → Konto öffnen → „Neuen Kontostand
festhalten"*. Für jedes Konto einzeln: Stichtag, Betrag, **Quelle**.
Die Quelle ist kein Beiwerk. Sie steht später neben der Zahl im Bericht,
und ein Betrag ohne Herkunft ist gegenüber dem Gericht wenig wert. Schreib
hinein, woher du die Zahl hast: „Kontoauszug 11/2026", „Online-Banking,
abgerufen am 07.11.".
Vergiss die kleinen nicht: **Verwahrgeld im Heim** und die **Barkasse**
sind Konten wie jedes andere.
**Vermögen** — *Vermögen & Kasse → Vermögen*. Immobilie, Fahrzeug,
Wertgegenstände, Forderungen, Schulden. Jeder Posten bekommt einen
**Wertstand** zum Stichtag, ebenfalls mit Quelle — und hier kannst du das
**Nachweisdokument gleich anhängen**.
> **Du musst nicht auf den Tag genau treffen.** Die Anwendung nimmt je
> Konto den letzten Stand am oder vor dem Stichtag. Weicht er ab, schreibt
> sie es in den Bericht: „Der Stand von ‚Sparbuch' stammt vom 31.10.2026,
> nicht vom Stichtag." Fehlt ein Stand ganz, steht das Konto ohne Betrag
> drin — mit Hinweis, nicht mit einer erfundenen Null.
### Dann die Berichte
1. **Vermögensübersicht oder Rechnungslegung** — *Vermögen & Kasse →
Berichte*, Betreuungsjahr wählen.
2. **Belegmappe** — *Berichte → Belegmappe*, derselbe Zeitraum. Das ist die
Anlage.
3. **Jahresbericht** — *Berichte → Jahresbericht*. Entwurf überarbeiten,
dann ausgeben.
Vorher lohnt ein Blick ins **Kassenbuch**: Buchungen ohne Beleg und ohne
Begründung fallen dem Gericht auf. Entweder den Beleg nachreichen oder in
einem Satz begründen, warum es keinen gibt.
---
## Was aussieht wie ein Konto, aber keines ist
**Genossenschaftsanteile** sind der Leitfall. Im Alltag redet man von einem
Konto, das Gericht fragt einen „Kontoauszug" an — nur gibt es keinen. Am
Automaten schon gar nicht.
Der Grund: Ein Geschäftsguthaben bei einer Genossenschaft ist **kein
Zahlungskonto**, sondern eine **Forderung gegen die Genossenschaft**. Es
laufen keine Buchungen darüber, es gibt keine Umsätze, und ausgezahlt wird
erst nach Kündigung der Mitgliedschaft — mit Frist.
### Deshalb: als Vermögensposten anlegen, nicht als Konto
*Vermögen & Kasse → Vermögen → Posten anlegen*, Art **Forderung**.
Das ist kein Formalismus, sondern hat zwei handfeste Gründe:
1. **Nur am Vermögensposten kannst du den Nachweis anhängen.** Ein
Kontostand trägt eine Quellenangabe, aber kein Dokument. Die
Bescheinigung der Genossenschaft — genau das Blatt, das das Gericht
sehen will — hätte am Konto keinen Platz.
2. **Als Konto stünde es in der Kontenliste** und würde dort einen Auszug
und Buchungen erwarten, die es nie geben wird.
Trag dann einen **Wertstand** zum Stichtag ein, mit der Bescheinigung als
Nachweisdokument.
### Den „Kontoauszug" bekommst du schriftlich
Die Genossenschaft stellt dir auf Anfrage eine **Bescheinigung über das
Geschäftsguthaben** zum Stichtag aus. Viele verschicken ohnehin einmal
jährlich eine Abrechnung nach der Generalversammlung — dann genügt es,
diese abzuwarten und einzuscannen.
Fehlt sie, schreib hin. Unter **Vorlagen** liegt das *Auskunftsersuchen*
als Kopiervorlage; darin änderst du die Bitte auf „Bescheinigung über das
Geschäftsguthaben zum <Stichtag>". Und setz eine Wiedervorlage.
> **„Der Wert bleibt doch immer gleich."** Meistens ja — aber nicht
> zwangsläufig. Wird eine **Dividende dem Geschäftsguthaben
> gutgeschrieben** statt ausgezahlt, wächst es. Ebenso, wenn weitere
> Anteile gezeichnet wurden. Genau deshalb fragt man einmal im Jahr nach,
> statt den Vorjahreswert fortzuschreiben.
### Dieselbe Bauform, andere Namen
Nach demselben Muster behandelst du alles, was „Konto" heißt und keinen
Auszug kennt:
| Was | Als was anlegen | Nachweis |
|---|---|---|
| Genossenschaftsanteile | Vermögensposten, *Forderung* | Bescheinigung der Genossenschaft |
| Bausparvertrag | Vermögensposten, *Forderung* | Jahreskontoauszug der Bausparkasse |
| Sterbegeld-, Lebensversicherung | **Vertrag**, Rückkaufswert am Vertrag | Rückkaufswertmitteilung |
| Wertpapierdepot | Konto, Art *Depot* | Depotauszug zum Stichtag |
| Darlehen, das die Person schuldet | Vermögensposten, *Verbindlichkeit* | Saldenbestätigung der Bank |
Das Depot ist die Ausnahme: Dort laufen Umsätze, es gibt einen Auszug, und
es verhält sich wie ein Konto.
Bei Versicherungen nimmst du **nicht** den Vermögensbereich, sondern den
Vertrag selbst — dort gehört der Rückkaufswert hin, samt Historie über die
Jahre.
---
## Anmelden mit dem Finger: Passkeys
Drei Felder auszufüllen, nur um kurz eine Frist nachzusehen, ist lästig —
+128 -13
View File
@@ -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)
@@ -322,7 +323,7 @@ betreuungs-cockpit-sicherung-1 Up
```bash
# Antwortet die Anwendung? (Port anpassen, falls du APP_PORT gesetzt hast)
curl -s http://127.0.0.1:3000/api/gesundheit
# {"status":"bereit","version":"1.11.1","commit":"...",
# {"status":"bereit","version":"1.12.0","commit":"...",
# "speicher":{"status":"bereit","treiber":"s3"}}
```
@@ -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/<id>/dokumente/<uuid>.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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "betreuungs-cockpit",
"version": "1.11.0",
"version": "1.12.0",
"private": true,
"description": "Arbeits- und Dokumentationswerkzeug fuer die ehrenamtliche rechtliche Betreuung",
"engines": {
+2 -2
View File
@@ -100,7 +100,7 @@ echo "$(date -Iseconds) Datenbank wiederhergestellt."
# --- Dokumente ---------------------------------------------------------------
# Zu "cockpit_2026-09-01_0315.dump.enc" gehoert
# "dokumente_2026-09-01_0315.tar.gz.enc" - gleicher Zeitstempel, anderer
# Name. Fehlt sie, stammt die Sicherung aus einer Fassung vor v1.11.1.
# Name. Fehlt sie, stammt die Sicherung aus einer Fassung vor v1.12.0.
ZEITSTEMPEL="$(basename "$DATEI" | sed -n 's/^cockpit_\(.*\)\.dump\.enc$/\1/p')"
DOKUMENTE_DATEI="$(dirname "$DATEI")/dokumente_${ZEITSTEMPEL}.tar.gz.enc"
AUSPACKZIEL="$(dirname "$DATEI")/dokumente-wiederhergestellt_${ZEITSTEMPEL}"
@@ -109,7 +109,7 @@ if [ ! -f "$DOKUMENTE_DATEI" ]; then
echo ""
echo "WARNUNG: Zu dieser Sicherung gibt es kein Dokumentenarchiv."
echo " Erwartet wurde: $DOKUMENTE_DATEI"
echo " Sicherungen aus Fassungen vor v1.11.1 enthalten nur die"
echo " Sicherungen aus Fassungen vor v1.12.0 enthalten nur die"
echo " Datenbank. Die Akte ist dann vollstaendig, die Dateien"
echo " dahinter fehlen."
else
+1 -1
View File
@@ -6,7 +6,7 @@ import { speicherfehlerErkennen } from '@/lib/speicher/fehler'
/**
* Ist die Dateiablage benutzbar?
*
* Bis v1.11.1 fiel eine kaputte Speicherkonfiguration erst auf, wenn
* Bis v1.12.0 fiel eine kaputte Speicherkonfiguration erst auf, wenn
* jemand ein Dokument ablegen wollte. Auf dem Livesystem waren das
* mehrere Wochen nach der Installation - und dann mitten in der Arbeit,
* mit einer Meldung, die nichts sagte.
+1 -1
View File
@@ -17,7 +17,7 @@
*/
/** Muss zur "version" in der package.json passen. */
export const VERSION = '1.11.0'
export const VERSION = '1.12.0'
/**
* Kurzer Git-Stand, beim Bauen hineingereicht:
+192
View File
@@ -0,0 +1,192 @@
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.12.0 so drin und stimmt seit der Sicherung nicht mehr.
expect(anleitung).not.toMatch(/Was das Backup nicht enthält:.*Dateien im Objektspeicher/s)
})
})
/**
* Die Bedienungsanleitung muss die Fragen beantworten, die tatsächlich
* gestellt werden.
*
* Zwei davon kamen aus der Praxis:
*
* 1. „Wie mache ich den Jahresabschluss?" — die Anleitung beschrieb, was
* in jedem Bericht steht, aber nicht die Reihenfolge und nicht, was man
* vorher besorgen muss. Rückkaufswerte brauchen Wochen.
* 2. „Das Gericht will einen Kontoauszug für die Genossenschaftsanteile —
* den kann ich nicht am Automaten holen." Richtig: Es ist kein
* Zahlungskonto, sondern eine Forderung, und der Nachweis ist eine
* Bescheinigung, die man anfordert.
*
* Der zweite Fall ist der wichtigere, weil er zu einem stillen Fehler
* führt: Wer die Anteile als Konto anlegt, kann die Bescheinigung nicht am
* Wert hinterlegen — genau das Dokument, das das Gericht sehen will.
*/
describe('Bedienungsanleitung', () => {
const bedienung = readFileSync('docs/ANLEITUNG.md', 'utf8')
it('erklärt den Jahresabschluss mit Reihenfolge', () => {
expect(bedienung).toMatch(/## Der Jahresabschluss, Schritt für Schritt/)
})
it('sagt, was vor dem Stichtag zu besorgen ist', () => {
// Wer erst am Stichtag daran denkt, wartet Wochen auf die Auskunft.
const abschnitt = bedienung.slice(bedienung.indexOf('## Der Jahresabschluss'))
expect(abschnitt).toMatch(/Sechs Wochen vorher/)
expect(abschnitt).toMatch(/Rückkaufswerte/)
expect(abschnitt).toMatch(/Wiedervorlage/)
})
it('besteht auf der Quellenangabe beim Kontostand', () => {
// Ein Betrag ohne Herkunft ist gegenueber dem Gericht wenig wert.
const abschnitt = bedienung.slice(bedienung.indexOf('## Der Jahresabschluss'))
expect(abschnitt).toMatch(/Quelle/)
expect(abschnitt).toMatch(/Kontoauszug 11\/2026|Online-Banking/)
})
describe('Was aussieht wie ein Konto', () => {
const abschnitt = bedienung.slice(bedienung.indexOf('## Was aussieht wie ein Konto'))
it('gibt es überhaupt', () => {
expect(abschnitt.length).toBeGreaterThan(500)
})
it('führt Genossenschaftsanteile als Forderung, nicht als Konto', () => {
expect(abschnitt).toMatch(/Genossenschaftsanteile/)
expect(abschnitt).toMatch(/Art \*\*Forderung\*\*/)
})
it('nennt den Grund, statt ihn vorauszusetzen', () => {
// Ohne Begruendung legt der naechste Leser es wieder als Konto an.
expect(abschnitt).toMatch(/Nachweis anhängen|Nachweis anhaengen/)
expect(abschnitt).toMatch(/kein Dokument/)
})
it('warnt davor, den Vorjahreswert einfach fortzuschreiben', () => {
// "Der Wert bleibt doch gleich" stimmt meistens - aber eine
// gutgeschriebene Dividende erhoeht das Geschaeftsguthaben.
expect(abschnitt).toMatch(/Dividende/)
})
it('sagt, wie man den Nachweis bekommt', () => {
expect(abschnitt).toMatch(/Bescheinigung über das\s+Geschäftsguthaben/)
expect(abschnitt).toMatch(/Auskunftsersuchen/)
})
it('behandelt die verwandten Fälle mit', () => {
for (const fall of ['Bausparvertrag', 'Depot', 'Verbindlichkeit']) {
expect(abschnitt, `${fall} fehlt in der Übersicht`).toContain(fall)
}
})
it('schickt Versicherungen zum Vertrag, nicht ins Vermögen', () => {
// Der Rueckkaufswert gehoert an den Vertrag, samt Historie.
expect(abschnitt).toMatch(/nicht\*\* den Vermögensbereich|Vertrag selbst/)
})
})
})
+1 -1
View File
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
/**
* Die Bereitschaftsprüfung der Dateiablage.
*
* Bis v1.11.1 fiel eine kaputte Speicherkonfiguration erst auf, wenn
* Bis v1.12.0 fiel eine kaputte Speicherkonfiguration erst auf, wenn
* jemand ein Dokument ablegen wollte. Auf dem Livesystem waren das
* mehrere Wochen nach der Installation — und dann mitten in der Arbeit,
* mit einer Meldung, die nichts sagte.