Files
betreuungs-cockpit/CHANGELOG.md
T
Florian Wilde fa994350d6 Dauerzahlungen zu einem Vertrag gelten als belegt
Bei einer monatlichen Praemie gibt es keinen Beleg - es gibt einen
Vertrag, und der wird einmal abgeschlossen, nicht monatlich quittiert.
Bisher blieb nur ein Freitextfeld pro Buchung: zwoelfmal im Jahr
derselbe Satz, mal Anzahl der Vertraege.

Buchung.vertragId mit onDelete: SetNull. Der Belegzustand kennt jetzt
drei Faelle statt zwei, getrennt ausgewiesen - ein Vertrag belegt, eine
Begruendung erklaert nur; zusammengeworfen verliert die Belegquote ihre
Aussage gegenueber dem Rechtspfleger.

Dazu: automatische Zuordnung beim Import bei eindeutiger Gegenstelle,
Sammelzuordnung ueber die Gruppen aus Issue #3, und die Rechnungslegung
nennt den Vertrag samt Nummer statt "ohne Beleg".

Die Sicherung dagegen: Weicht der Betrag um mehr als ein Fuenftel vom
hinterlegten Beitrag ab, faellt die Anerkennung weg und die Buchung wird
als "Betrag pruefen" markiert. Sonst waere die Zuordnung eine Hintertuer.

Zur Toleranz - ein Entwurf und seine Korrektur: Zuerst verglich ich den
Betrag gegen alle ueblichen Zahlweisen, damit es auch passt, wenn die
Bank anders abbucht als der Vertrag es fuehrt. Der eigene Test hat das
widerlegt: Bei 42,50 monatlich liegen 420 innerhalb von 20 Prozent des
Jahresbetrags von 510 - ausgerechnet der Fall aus der Aufgabenstellung
waere durchgerutscht. Jetzt wird stur gegen den hinterlegten Beitrag
verglichen. Bucht die Bank anders ab, stimmen die Vertragsdaten nicht,
und das soll auffallen.

Behoben: Die Buchungsliste hatte eine eigene, fast gleiche Datenabfrage
neben der aus berichte/gemeinsam.ts. Nur eine von beiden wurde
nachgezogen, in der Liste stand weiter "Beleg fehlt". Beide benutzen
jetzt BUCHUNG_EINBEZUG und zuBuchungEingabe - das kann nicht halb
vergessen werden. Gefunden hat es der Browsertest.

Nebenbei: Die Roadmap trug noch "Aktuelle Fassung 1.1.2" - Nachwirkung
der parallelen Bearbeitung von #3 und #10. Nachgezogen wie zugesagt.

Geprueft: 23 neue Einheitstests, die Haelfte davon darauf gerichtet, dass
die Anerkennung auch wirklich wegfaellt. Fuenf End-to-End-Tests auf
beiden Geraeteprofilen. 560 Unit-Tests, 63 e2e, null Typfehler.

Refs #11
2026-08-23 09:00:46 +02:00

715 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Änderungen
Alle nennenswerten Änderungen an dieser Anwendung, in umgekehrter
Reihenfolge. Aufbau nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
Versionsnummern nach [Semantic Versioning](https://semver.org/lang/de/).
**Was hier steht:** was sich für dich beim Arbeiten ändert — nicht, welche
Datei angefasst wurde. Wer den technischen Verlauf braucht, findet ihn im
Git-Log; jeder Commit trägt seine Etappe.
**Was die Zahlen bedeuten:**
| Stelle | Wann sie steigt | Was du tun musst |
|---|---|---|
| **Major** (1.x.x → 2.0.0) | Etwas verhält sich anders als vorher, oder eine Migration lässt sich nicht zurücknehmen | Changelog lesen, **vorher sichern**, Zeit einplanen |
| **Minor** (1.0.x → 1.1.0) | Neue Funktionen, alles Bisherige bleibt wie es war | Changelog überfliegen, normal aktualisieren |
| **Patch** (1.0.0 → 1.0.1) | Fehler behoben, sonst nichts | Einfach aktualisieren |
Eine Datenbank-Migration lässt sich **nicht** zurückrollen. Vor jedem
Update ein Backup anstoßen — siehe README, Abschnitt *Sicherung und
Wiederherstellung*.
---
## [Unveröffentlicht]
_Noch nichts._
## [1.3.0] – 2026-08-23
### Hinzugefügt
- **Dauerzahlungen zu einem Vertrag gelten als belegt**
([#11](https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/issues/11)).
Bei einer monatlichen Versicherungsprämie gibt es keinen Beleg — es gibt
einen Vertrag, und der wird einmal abgeschlossen, nicht monatlich
quittiert. Bisher blieb nur ein Freitextfeld pro Buchung: zwölfmal im
Jahr derselbe Satz, mal Anzahl der Verträge.
Eine Buchung lässt sich jetzt einem **Vertrag** zuordnen, und diese
Zuordnung zählt als Nachweis. Bei einem Dauerschuldverhältnis ist der
Vertrag der Beleg: Ein Rechtspfleger will für zwölf gleiche Lastschriften
keine zwölf Quittungen sehen, sondern die Police.
**Die Belegquote weist drei Fälle getrennt aus** — mit Beleg, durch
Vertrag belegt, begründet ohne Beleg. Zusammengeworfen verlöre die Zahl
ihre Aussage.
- **Automatisch beim Import**, wenn die Gegenstelle zu genau *einem*
Vertrag passt. Bei Mehrdeutigkeit geschieht nichts: Hausrat und
Haftpflicht bei derselben Gesellschaft lassen sich nicht auseinanderhalten,
und eine falsche Zuordnung wäre schlimmer als gar keine — sie behauptet
einen Beleg, und niemand sieht noch einmal hin. Automatisch gesetzte
Zuordnungen sind in der Liste als solche gekennzeichnet.
- **Sammelzuordnung** unter *Wiederkehrende Zahlungen*: Liegt zu einer
Gruppe ein Vertrag vor, werden alle ihre Zahlungen mit einem Klick
zugeordnet. Bei drei Jahren Laufzeit sind das 36 Buchungen.
- **Die Rechnungslegung nennt den Vertrag.** Statt „ohne Beleg – …" steht
dort „Dauerzahlung zu Vertrag «Bezeichnung», Nr. …". Die Lücke wird nicht
versteckt, sondern erklärt: Wer nachprüfen will, weiß sofort, welches
Papier er aus dem Ordner holt.
### Die Sicherung dagegen
**Weicht der Betrag um mehr als ein Fünftel vom hinterlegten Beitrag ab,
gilt die Zahlung nicht als belegt**, sondern wird als *Betrag prüfen*
markiert. Sonst wäre die Zuordnung eine Hintertür, durch die auch eine
verzehnfachte Prämie unbemerkt durchginge.
Ein Fünftel ist bewusst großzügig: Beitragsanpassungen und
Versicherungsteuer bewegen sich in dieser Größenordnung, und dabei soll
die Anwendung nicht meckern. Wer die Grenze enger zieht, bekommt täglich
Warnungen und sieht die eine wichtige nicht mehr.
Steht am Vertrag **kein** Beitrag, findet keine Prüfung statt. Die
Zuordnung gilt trotzdem — es wird nur nicht so getan, als wäre geprüft
worden.
Und: **Verschwindet der Vertrag, fällt die Buchung zurück in „Beleg
fehlt".** Ein Nachweis, der auf etwas zeigt, das es nicht mehr gibt, wäre
schlimmer als gar keiner.
### Zu beachten
- **Eine Datenbank-Migration ist nötig**
(`20260823062000_buchung_vertragszahlung`). Sie fügt zwei Spalten hinzu
und ändert nichts an vorhandenen Daten.
- Bestehende Buchungen sind nicht betroffen: Ohne Zuordnung bleibt alles,
wie es war.
### Geprüft
- 23 neue Einheitstests. Die Hälfte prüft, dass die Anerkennung auch
wirklich **wegfällt** — der gefährliche Fehler wäre hier ein zu mildes
Urteil, nicht ein zu strenges.
- Fünf End-to-End-Tests: Sammelzuordnung, Anzeige in der Buchungsliste,
die abweichende Zahlung, die Kachel auf „Heute", und der Rückfall nach
dem Löschen des Vertrags.
Der Browsertest hat zwei Fehler gefunden, die alle anderen Prüfungen
durchgelassen haben — siehe unten.
### Behoben
- **Die Buchungsliste hatte eine eigene, fast gleiche Datenabfrage** neben
der gemeinsamen aus `berichte/gemeinsam.ts`. Als die Dauerzahlungen
dazukamen, wurde nur eine von beiden nachgezogen: In der Liste stand
weiter „Beleg fehlt", obwohl der Vertrag hinterlegt war. Beide benutzen
jetzt dieselbe Abbildung — die kann nicht halb vergessen werden.
## [1.2.0] – 2026-08-23
### Hinzugefügt
- **Wiederkehrende Zahlungen aufspüren**
([#3](https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/issues/3)).
Am Anfang einer Betreuung weiß niemand, welche Verträge es gibt — es liegt
kein Ordner bereit, und die betreute Person kann es oft selbst nicht mehr
sagen. Die Antwort steht aber schon im Kontoauszug: **Wer regelmäßig
abbucht, hat einen Vertrag.**
Neu unter *Vermögen & Kasse → Wiederkehrende Zahlungen*: Die importierten
Kontobewegungen werden nach Empfänger gruppiert und nach Jahresbetrag
sortiert. Je Zeile stehen Rhythmus, Betrag, Zeitraum und ob dazu schon ein
Vertrag in der Akte liegt. Aus der Zeile heraus geht es weiter — *Vertrag
anlegen* mit vorbelegter Gesellschaft und vorbelegtem Beitrag, oder *kein
Vertrag* mit Begründung.
Die Seite wertet nur aus. Sie legt von sich aus nichts an; jede Zeile
endet in einer Entscheidung, die ein Mensch trifft.
**Was sie nennt, begründet sie.** Statt „monatlich" zu behaupten, steht
daneben „6 Zahlungen, etwa monatlich" — man soll widersprechen können.
Schwankt der Betrag, wird die Spanne genannt statt eines Mittelwerts, der
Genauigkeit vortäuscht. Und ohne erkennbaren Abstand wird nicht aufs Jahr
hochgerechnet, sondern gezeigt, was tatsächlich geflossen ist.
**Zusammengefasst wird nur, was sicher dasselbe ist:** Groß- und
Kleinschreibung, Satzzeichen, Umlaute und die Rechtsform am Ende.
„Stadtwerke Bamberg" und „Stadtwerke Bayreuth" unterscheiden sich in drei
Zeichen und bleiben zwei Zeilen. Zwei Zeilen zu viel sind harmlos — eine
falsch zusammengefasste Gruppe verbirgt dagegen einen Vertrag, und der
fehlt dann im Vermögensverzeichnis.
- **Einzelne größere Zahlungen** stehen in einem eigenen Abschnitt darunter.
Grund: Die Gruppierung braucht mindestens zwei Zahlungen, eine jährlich
abgebuchte Prämie erscheint bei einem importierten Jahr aber nur einmal —
und ausgerechnet Versicherungen werden oft jährlich gezahlt. Ohne diese
zweite Liste wäre die Suche genau dort blind, wo sie am meisten bringen
soll.
- **Hinweis auf „Heute"**, sobald es unbearbeitete Kandidaten gibt. Er steht
in der Kachel *Unverbuchte Belege*, nicht in einer eigenen: Es ist
dieselbe Frage — wozu gehört diese Zahlung? —, nur eine Ebene höher.
### Zu beachten
- **Eine Datenbank-Migration ist nötig**
(`20260823000500_zahlungsgruppe_geklaert`). Sie läuft beim Start des
Behälters automatisch mit und legt nur eine neue Tabelle an; bestehende
Daten bleiben unberührt.
- Ausgewertet werden **nur Ausgaben der betreuten Person**. Umbuchungen
zwischen eigenen Konten und Auslagen des Betreuers bleiben außen vor —
wer monatlich aufs Sparbuch legt, hat deswegen keinen Vertrag.
### Geprüft
- 37 Einheitstests (`tests/wiederkehrend.test.ts`), die Hälfte davon in der
Gegenrichtung: dass ähnliche, aber verschiedene Namen **nicht**
zusammenfallen.
- Vier End-to-End-Tests gegen einen echten Browser: Erkennung, getrennte
Liste der Einzelzahlungen, Übernahme ins Vertragsformular, Abhaken und
Zurückholen samt Neuladen.
Zwei Fehler hat erst der Browsertest gefunden. Einer davon hätte die
Vorbelegung des Beitrags stillschweigend verschluckt — Typprüfung und
Einheitstests waren grün.
## [1.1.2] – 2026-08-23
### Behoben
- **„Mein Konto" war am Schreibtisch praktisch nicht zu finden**
([#1](https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/issues/1)).
Damit kam man dort auch nicht an den Passwortwechsel, den zweiten Faktor
und die frisch eingebaute Passkey-Verwaltung.
Ursache: Die Seitenleiste hatte keine feste Höhe und wuchs mit der Seite
mit. Ihr Fuß — und damit das Benutzermenü — landete deshalb am **Ende der
Seite** statt am unteren Bildschirmrand. Auf „Heute" waren das 1632 Pixel
bei 900 Pixel Fensterhöhe: Man musste bis ganz nach unten scrollen, um
an sein eigenes Konto zu kommen.
Der Fehler sah nach nichts aus. Kein Element fehlte, nichts war kaputt,
die Seite sah richtig aus — und genau deshalb sucht man an der falschen
Stelle.
Die Leiste bleibt jetzt stehen (`sticky`, Fensterhöhe). Kopf,
Betreutenauswahl und Fuß sind immer sichtbar; die Navigation dazwischen
scrollt bei Bedarf in sich selbst. Am Handy war es nie ein Problem —
dort sitzt das Menü in der ohnehin festen Kopfzeile.
### Geprüft
- Fünf End-to-End-Tests (`e2e/benutzermenue.spec.ts`) fragen nicht, ob das
Menü **im DOM** ist, sondern ob es **im sichtbaren Bereich** steht: auf
fünf verschiedenen Seiten, an drei Scrollpositionen, auf einem flachen
Laptopfenster (1280 × 620) und am Handy.
Die Scrollposition in der Mitte ist Absicht. Nur oben und unten zu messen
hätte nicht gereicht: Am Seitenende wird eine mitwachsende Leiste
zufällig doch sichtbar — der Test wäre grün gewesen und der Fehler
trotzdem da. Beide Sichtbarkeitstests sind gegen den kaputten Zustand
gegengeprüft und schlagen dort an.
## [1.1.1] – 2026-08-23
### Behoben
- **`./scripts/update.sh` endete in „Permission denied".** Die
Shell-Skripte lagen ohne Ausführungsrecht im Repository (Modus 644 statt
755). Die Anleitung sagt `./scripts/update.sh` — auf Linux ging genau
das nicht.
Ursache ist der Entwicklungsrechner: Unter Windows steht
`core.filemode=false`, dort merkt git von einem fehlenden
Ausführungsrecht nichts. Aufgefallen ist es erst auf dem Server.
**Wer von 1.1.0 oder älter kommt**, braucht einmalig den Umweg — das
alte Skript ist ja noch das nicht ausführbare:
```bash
bash scripts/update.sh
```
Danach stimmt das Recht, und der Punktschrägstrich funktioniert.
- **`update.sh` setzt das Recht nach dem Umschalten selbst.** Wer Dateien
von Hand kopiert hat — etwa um an das Skript zu kommen, bevor es in der
laufenden Fassung existierte —, verliert es dabei. Ein `chmod +x` nach
dem Umschalten fängt das ab.
- **Das Versionsskript datierte in UTC.** Eine Veröffentlichung nach
22 Uhr deutscher Sommerzeit bekam im Changelog das Datum des Vortages —
diese Fassung hier war der erste Fall. Es rechnet jetzt in
`Europe/Berlin`, wie alles andere in dieser Anwendung auch.
### Geprüft
- Vier neue Prüfungen (`tests/skripte.test.ts`) sehen sich nicht das
Programm an, sondern die **Verpackung**: Liegt jedes `.sh` ausführbar im
Index? Hat es eine Shebang-Zeile? Kommt `scripts/` ins Docker-Abbild?
Sind die Sicherungsskripte in der Compose-Datei eingehängt?
Das ist der zweite Fehler dieser Art — vorher fehlte `scripts/` ganz im
Abbild (1.0.1). Beide Male war der Code richtig und kein Test schlug an,
weil keiner hinsah, wie die Dateien beim Server ankommen. Jetzt tut es
einer. Die Gegenprobe ist gemacht: Mit zurückgesetztem Recht schlägt er
fehl und nennt die Datei.
## [1.1.0] – 2026-08-22
### Hinzugefügt
- **Anmelden mit Passkey.** Auf der Anmeldeseite steht jetzt unter dem
Formular ein zweiter Weg: *Mit Passkey anmelden*. Ein Fingerabdruck, ein
Blick in die Kamera oder die Geräte-PIN — keine E-Mail, kein Passwort,
kein abgetippter Code.
**Der Passkey ersetzt den zweiten Faktor, er kommt nicht zusätzlich.**
Das ist kein Nachlassen: Ein Passkey verlangt den Besitz des Geräts
*und* dessen Entsperrung, das sind dieselben zwei Faktoren wie vorher.
Die Anwendung fordert die Nutzerverifikation ausdrücklich an und weist
eine Antwort ohne sie zurück. Ein Gerät, das auf blosses Antippen hin
signiert, kommt nicht herein.
In einem Punkt ist er dem bisherigen Weg sogar überlegen: Ein Passkey
ist an die Adresse gebunden, unter der er eingerichtet wurde. Auf einer
nachgebauten Anmeldeseite funktioniert er nicht — und Phishing ist der
übliche Weg, an einen TOTP-Code zu kommen.
**Die Authenticator-App muss trotzdem eingerichtet sein**, bevor sich
ein Passkey anlegen lässt. Ein Passkey hängt an einem Gerät. Ginge das
verloren und wäre er der einzige Weg hinein, käme niemand mehr an die
Akte — und darin stehen Unterlagen, die eine betreute Person braucht.
Der Passkey ist die bequeme Tür, nicht die einzige.
Verwaltet werden sie unter **Mein Konto → Passkeys**: einrichten,
benennen, entfernen. Die Liste zeigt, wann ein Gerät zuletzt benutzt
wurde und ob der Schlüssel gesichert ist (iCloud, Google, Bitwarden)
oder nur auf diesem einen Gerät liegt. Einrichten und Entfernen stehen
im Protokoll, jede Anmeldung nennt den verwendeten Passkey namentlich.
- **Bestätigen mit Passkey.** Export, endgültiges Löschen und die
Benutzerverwaltung verlangen eine frische Anmeldung. Auch die geht jetzt
mit dem Passkey. Geprüft wird zusätzlich, dass er zum angemeldeten
Zugang gehört — sonst liesse sich mit dem eigenen Finger eine fremde,
offen stehende Sitzung freischalten. Genau der Fall, gegen den diese
Zwischenfrage gebaut ist.
### Zu beachten
- **Passkeys brauchen `https`.** Der Browser gibt das Verfahren nur in
einem sicheren Kontext frei. Läuft die Anwendung ohne TLS, erscheint der
Knopf gar nicht erst, statt beim Klick mit einer kryptischen Meldung
abzubrechen.
- **`AUTH_URL` muss stimmen — jetzt noch mehr als vorher.** Aus ihr wird
die Kennung abgeleitet, an die der Authenticator den Schlüssel bindet.
Steht dort eine andere Adresse als die im Browser, lässt sich zwar ein
Passkey einrichten, aber nie wieder verwenden.
**Ein Wechsel der Domain macht alle Passkeys unbrauchbar.** Das ist
keine Panne, sondern der Phishingschutz. Nach einem Umzug richtet man
sie neu ein; die Anmeldung mit Passwort und Code bleibt währenddessen
offen.
- **Eine Datenbank-Migration ist nötig** (`20260822160000_passkey_anmeldung`).
Sie läuft beim Start des Behälters automatisch mit. Bestehende Zugänge
sind nicht betroffen: Ohne eingerichteten Passkey ändert sich nichts.
### Geprüft
- 21 neue Prüfungen (`tests/passkey.test.ts`), vor allem für die Herleitung
von Kennung und Herkunft aus `AUTH_URL`: Port, Schrägstrich am Ende,
Unterdomain, `http` gegen `https`, `localhost`. Das ist die Stelle, an
der eine Installation still kaputtginge — dieselbe Fehlerklasse wie
seinerzeit bei `APP_BIND` und `ENC_KEY_V2`.
- Drei End-to-End-Tests gegen einen echten Browser (`e2e/passkey.spec.ts`),
mit Chromiums virtuellem Authenticator: einrichten, abmelden, mit Passkey
anmelden, Benutzung vermerkt, entfernen. Dazu die Bestätigung vor dem
Vollexport und der Fall, dass ein Gerät ohne Nutzerverifikation nicht
hereinkommt.
## [1.0.5] – 2026-08-22
### Hinzugefügt
- **`scripts/update.sh`** — aktualisiert die Anwendung mit einem Befehl.
```bash
./scripts/update.sh # auf die neueste Fassung
./scripts/update.sh --pruefen # nur nachsehen, nichts ändern
./scripts/update.sh v1.2.0 # auf eine bestimmte Fassung
```
Es nimmt die Schritte ab, die man von Hand vergisst: Sicherung
anstoßen und bei Misserfolg **abbrechen**, den Changelog zwischen altem
und neuem Stand anzeigen, umschalten, neu bauen, warten bis die
Anwendung antwortet, **die beiden SQL-Skripte ausführen** und zum
Schluss prüfen, ob wirklich die neue Fassung läuft.
Zwei Eigenschaften, die dabei zählen:
**Bis zur Rückfrage wird nichts verändert.** Bricht etwas vorher ab —
Server nicht erreichbar, falsche Versionsangabe — findest du alles so
vor wie zuvor. Kein Halbzustand.
**Eigene Änderungen an versionierten Dateien blockieren nicht.** Im
Betrieb passt man die `docker-compose.yml` an, bis etwas läuft; genau
diese Anpassungen sind später oft schon eingearbeitet. Das Skript legt
sie vor dem Umschalten mit `git stash` beiseite und sagt, wie man sie
zurückholt. Mit `--verwerfen` werden sie stattdessen weggeworfen. Die
`.env` bleibt immer unangetastet — sie ist nicht versioniert.
Schlägt der Bau fehl, nennt das Skript den Weg zurück auf den
vorherigen Stand.
### Behoben
- **Der Port der MinIO-Konsole ließ sich nicht ändern.** Er stand fest auf
`9001`; lief dort schon etwas, startete der ganze Stapel nicht
(`port is already allocated`). Jetzt über `MINIO_KONSOLE_PORT` in der
`.env` einstellbar — dieselbe Möglichkeit, die `APP_PORT` schon hatte.
## [1.0.4] – 2026-08-22
### Behoben
- **Die Ersteinrichtung ließ sich nicht abschließen.** Schritt 3 meldete
„Bitte prüfe die markierten Felder" — markierte aber keines. Man kam an
dieser Stelle nicht weiter.
Ursache: Das kurze Formular der Ersteinrichtung schickt neun Felder, das
vollständige unter *Stammdaten* alle. Beide gehen durch dasselbe Schema,
und die optionalen Felder waren zwar `nullable`, aber nicht `optional` —
ein Feld, das gar nicht abgeschickt wurde, ist `undefined`, nicht
`null`. Beanstandet wurden also zwei Dutzend Felder, die auf dem
Bildschirm nie standen.
- **Eine Meldung ohne zugehöriges Feld kann nicht mehr unsichtbar
bleiben.** Beanstandet die Prüfung ein Feld, das das Formular gar nicht
gesendet hat, sagt die Anwendung das jetzt ausdrücklich und schreibt es
ins Serverlog — statt es als Eingabefehler auszugeben, den niemand
finden kann. Das ist ein Fehler im Programm, und er soll auch so
aussehen.
- **Eine betreute Person ließ sich nicht endgültig löschen.** Beim Löschen
setzt Postgres den Verweis im Audit-Log auf NULL (`ON DELETE SET NULL`),
und genau dieses UPDATE hat der Unveränderlichkeits-Trigger abgewiesen.
Betroffen war damit auch das Leeren des Papierkorbs nach 30 Tagen und
das Entfernen des Demofalls.
Der Trigger erlaubt jetzt genau eine Ausnahme: das Nullen der beiden
Fremdschlüssel `betreuteId` und `userId`. Jede andere Änderung bleibt
verboten, auch das Setzen eines Fremdschlüssels auf einen **anderen**
Wert. Der Nachweis bleibt vollständig — Zeitpunkt, Aktion, Bezeichnung,
die verschlüsselte Änderung und die Momentaufnahmen von Name und
E-Mail sind unberührt. Es verschwindet nur der Verweis auf einen
Datensatz, den es nicht mehr gibt.
**Beim Update nötig:** das SQL-Skript erneut ausführen, sonst bleibt der
alte Trigger stehen.
```bash
docker compose exec app node node_modules/prisma/build/index.js db execute --schema prisma/schema.prisma --file prisma/sql/audit-schutz.sql
```
### Geprüft
- Sieben neue Tests (`tests/stammdaten.test.ts`) prüfen genau die
Formularinhalte der Ersteinrichtung, dazu das vollständige Formular mit
leeren Feldern und die weiterhin verlangten Pflichtangaben.
- Der Ablauf ist zusätzlich im Browser durchgespielt worden: Anmeldung,
zweiter Faktor, Schritt 3, Landung auf dem Dashboard — und die daraus
abgeleiteten Pflichtfristen samt Ausschlussfrist für die
Aufwandspauschale.
- Der geänderte Trigger ist gegen eine laufende Datenbank geprüft:
Löschen gelingt, Fälschen und Löschen von Einträgen scheitert weiterhin.
## [1.0.3] – 2026-08-22
Keine Änderung am Programm — nur an der Konfigurationsvorlage und der
Anleitung. Ein Neubau des Abbilds ist nicht nötig.
### Behoben
- **`.env.example` band die Anwendung stillschweigend an `127.0.0.1`.**
Der Wert stand dort als fertig ausgefüllte Zeile zwischen lauter anderen
fertigen Zeilen. Wer die Datei kopiert hat und seinen Reverse Proxy auf
einem **anderen** Server betreibt, kam nicht an die Anwendung heran —
und der Fehler sah aus, als läge es am Proxy.
Jetzt steht dort eine Entscheidung mit drei benannten Fällen statt eines
Vorgabewerts. Der sichere Standard bleibt `127.0.0.1`; er wird nur nicht
mehr als „schon erledigt" präsentiert.
Für bestehende Installationen genügt eine Zeile in der `.env`:
```bash
APP_BIND=10.0.0.5 # Adresse, über die der Proxy diesen Server erreicht
docker compose up -d
```
### Geändert
- **`docs/INSTALLATION.md`** nennt `APP_BIND` jetzt schon in Schritt 3, wo
die `.env` ausgefüllt wird — nicht erst in Schritt 7, wo der Proxy
drankommt. Dazu ein eigener Abschnitt in der Fehlersuche: „Der Proxy
erreicht die Anwendung nicht", mit den zwei `curl`-Proben, die den
Unterschied zwischen falscher Bindeadresse und Firewall zeigen.
## [1.0.2] – 2026-08-22
### Behoben
- **Die Einrichtung des zweiten Faktors brach mit einem Serverfehler ab**
(„Application error: a server-side exception has occurred"). Im
Serverlog stand `Umgebungsvariable ENC_KEY_V2 fehlt` — obwohl niemand
einen zweiten Schlüssel wollte.
Ursache: `docker-compose.yml` reicht `ENC_KEY_V2` immer an den Behälter
durch, mit **leerem Wert**, wenn in der `.env` keiner steht. Die
Anwendung erkannte Schlüssel aber am Variablennamen statt am Inhalt,
fand also ein leeres `ENC_KEY_V2` und brach ab. Leere
Schlüsselvariablen werden jetzt übersprungen.
Betroffen war jede Stelle, die verschlüsselte Felder schreibt oder
liest — also praktisch alles außer der reinen Anmeldung. Die Anmeldung
ging durch, weil sie ohne Verschlüsselung auskommt; der Fehler trat
deshalb erst auf der nächsten Seite auf.
- **Die Meldung beim Schlüsselwechsel** nennt jetzt, welche Versionen
tatsächlich vorhanden sind, und die richtige Reihenfolge: erst den
neuen Schlüssel eintragen, dann `ENC_KEY_AKTIV` umstellen.
### Hinzugefügt
- **`scripts/anmeldung-pruefen.mjs`** — beantwortet die Frage, warum eine
Anmeldung nicht klappt. Das Formular sagt bewusst nicht, woran es lag,
sonst ließe sich daran ablesen, welche Adressen es gibt. Auf dem Server
darf man es wissen: Jeder Versuch wird mit Grund protokolliert, und das
Skript liest ihn vor — dazu den Zustand des Zugangs, den Stand der
Login-Sperre und die eingestellte `AUTH_URL`.
```bash
docker compose exec app node scripts/anmeldung-pruefen.mjs deine@adresse.de
```
Es ändert nichts, es liest nur.
### Geprüft
- Sieben neue Tests (`tests/umgebung.test.ts`) decken die
Schlüsselauswertung ab, darunter genau die Konstellation aus dem
Fehlerbericht: gesetztes `ENC_KEY_V1`, leeres `ENC_KEY_V2`.
## [1.0.1] – 2026-08-22
### Behoben
- **Betriebsskripte fehlten im Docker-Abbild.** Das Verzeichnis `scripts/`
wurde beim Bauen nie in die Laufzeitstufe übernommen. `docker compose
exec app node scripts/totp-geheimnis.mjs …` endete deshalb mit
`Cannot find module`. Betroffen waren alle Skripte, die im laufenden
Behälter gedacht sind — auch `wartungsschluessel.mjs` und
`schluessel-erzeugen.mjs`.
Nach dem Update funktioniert der Befehl wie dokumentiert. Wer nicht
gleich aktualisieren will, kommt einmalig auch so an das Skript:
```bash
docker compose cp scripts/totp-geheimnis.mjs app:/app/totp-geheimnis.mjs
docker compose exec app node totp-geheimnis.mjs deine@adresse.de
```
### Hinzugefügt
- **`scripts/totp-geheimnis.mjs`** — liest das TOTP-Geheimnis eines Zugangs
aus, um denselben Zugang auf einem zweiten Gerät einzurichten (Handy und
Tablet, oder ein Ersatzgerät). Die Anwendung zeigt das Geheimnis sonst
nur einmal an, beim Einrichten; bisher blieb nur, den zweiten Faktor
zurückzusetzen und alle Wiederherstellungscodes neu zu erzeugen.
Das Skript läuft nur auf dem Server und nur mit Zugriff auf die Datenbank
**und** auf `ENC_KEY_V1`. Wer beides hat, kann ohnehin alles lesen — es
ist also keine Hintertür, sondern erspart eine unnötige Neueinrichtung.
## [1.0.0] – 2026-08-22
Erste vollständige Fassung. Alle acht Etappen umgesetzt, alle
Abnahmekriterien als Test hinterlegt.
### Hinzugefügt
**Der Alltag**
- **Dashboard „Heute"** — beantwortet morgens, was ansteht: Fristen-Ampel
(überfällig / diese Woche / diesen Monat), wer gerade am Zug ist,
offene Nachforderungen des Bezirks, ungeprüfter Posteingang,
unverbuchte Belege, Kennzahlen zu Vermögen und Heimkosten.
- **Fristen** — die gesetzlichen Pflichtfristen entstehen von selbst aus
dem Bestellungsdatum: Vermögensverzeichnis und Anfangsbericht nach drei
Monaten, Jahresbericht und Vermögensübersicht zum Ende jedes
Betreuungsjahres, Aufwandspauschale, Landespflegegeld,
Zuzahlungsbefreiung, Rückkaufswerte. Liste, Monatskalender und ein
ICS-Feed zum Abonnieren im eigenen Kalender.
- **Vorgänge** als Fallakte mit Zeitstrahl über Dokumente, Protokolle,
Fristen und Buchungen. Neun Vorlagen mit vorbefüllter Aufgabenliste,
darunter Heimeinzug, Antrag Hilfe zur Pflege, Wohnungsauflösung,
Hausverkauf, Darlehen nach § 91 SGB XII und Widerspruch gegen einen
Bescheid.
- **Kontakte und Kontaktprotokoll** — Verzeichnis aller Stellen mit
Aktenzeichen, dazu eine Schnellerfassung, die am Handy in unter zwanzig
Sekunden zu bedienen ist.
**Papier**
- **Dokumentenablage** mit Drag & Drop, Sammel-Upload und Handy-Foto.
Texterkennung auf Deutsch, Volltextsuche über alle Schreiben,
Posteingangsbuch mit den Stufen neu → gesichtet → zugeordnet →
beantwortet, Feld für den Ablageort des Papieroriginals.
- Beim Import werden **Absender, Datum, Aktenzeichen, Dokumentart und
eine im Text genannte Frist vorgeschlagen** — mit der Textstelle
daneben, damit man prüfen kann. Übernommen wird nur auf Klick.
- **Belegmappe**: alle Dokumente eines Vorgangs oder Zeitraums als eine
chronologisch sortierte, paginierte PDF mit Deckblatt und
Inhaltsverzeichnis.
**Geld**
- **Kassenbuch** mit Belegzwang, Kategorien, CSV- und CAMT.053-Import.
Harte Trennung zwischen dem Vermögen der betreuten Person und eigenen
Auslagen, mit Warnung bei Buchungen auf ein als privat markiertes Konto.
- **Konten** mit verschlüsselter IBAN und Saldoständen je Stichtag,
**Vermögensposten** mit vollständiger Wert-Historie und Pflichtangabe
zur Wertquelle. Werte werden nie überschrieben.
- **Vermögensverzeichnis (§ 1835 BGB)** und **Vermögensübersicht bzw.
Rechnungslegung (§§ 1859, 1865 BGB)** als druckfertige PDF, in zwei
Ausgabevarianten für befreite und nicht befreite Betreuer.
**Behörde**
- **Sozialhilfe-Modul** für das Verfahren beim Bezirk: Antragsakte,
Nachforderungs-Tracker mit Fortschrittsbalken, vorbefüllte Checkliste
der Standardunterlagen mit Begründung, warum der Träger sie will.
- **Bescheid-Prüfung** mit Berechnung der Rechtsbehelfsfrist ab dem
Zugangsdatum, einschließlich Wochenende, bayerischer Feiertage und dem
Sonderfall fehlender Rechtsbehelfsbelehrung. Der Rechenweg wird
ausgegeben.
- **Heimkosten-Gegenüberstellung** Monat für Monat: Kosten gegen
Pflegekasse, Leistungszuschlag nach § 43c SGB XI, Rente und
Sozialhilfeanteil — die Deckungslücke wird sichtbar.
- **Schonvermögens-Bewertung** je Posten (verwertbar / geschützt /
strittig) mit Begründungsfeld und Textbausteinen zu § 90 SGB XII.
- **Katalog weiterer Leistungen**: Landespflegegeld, Zuzahlungsbefreiung,
Rundfunkbeitragsbefreiung, Schwerbehindertenausweis, Grundsicherung.
**Substanz**
- **Immobilie** mit Grundbuchdaten, Belastungen nach Abteilung II und
III, Wert-Historie, Status im Sozialhilfeverfahren und einer
Pflichten-Checkliste bei Leerstand — zehn Punkte, jeder mit Begründung.
- **Versicherungen und Verträge** mit Kündigungsfristen-Rechner, der den
spätesten Absendetag nennt, Anschreiben zur Rückkaufswert-Anfrage mit
automatischer Wiedervorlage und einem Kündigungs-Assistenten.
- **Heim und Pflege** mit Heimvertrag, Leistungszuschlag-Staffel,
Pflegegrad-Historie, Abwesenheitstagen und einem Gesundheitsverlauf mit
COPD-Bezug: Exazerbationen, Sauerstoff, Stürze, Krankenhaus,
Hilfsmittel, Impfungen nach STIKO-Plan.
**Ausgabe**
- **Jahresbericht (§ 1863 BGB)** als Entwurf zum Überarbeiten, gespeist
aus Kontaktprotokollen, Gesundheitsereignissen und Vorgangsstatus.
Ausgabe als PDF und als DOCX.
- **Unterlagenpaket für den Bezirk**: die im Tracker markierten
Dokumente als eine PDF mit Anschreiben und Inhaltsverzeichnis.
- **Vollexport** als ZIP mit allen Originaldateien, allen Daten als
entschlüsseltes JSON, den erzeugten Berichten, einer LIESMICH und
SHA-256-Prüfsummen.
- **Wissensbasis** mit 37 Hinweiskarten, pflegbar als JSON-Datei ohne
Neustart. Jede mit Rechtsgrundlage, Link und dem stehenden Vermerk,
dass sie keine Rechtsberatung ersetzt.
**Verwaltung**
- **Mitleser-Rolle** mit modulweiser Freigabe und ausdrücklicher Warnung
bei Gesundheit, Finanzen und Sozialhilfe.
- **Papierkorb** mit 30 Tagen Aufbewahrung.
- **Wartungslauf** über `/api/wartung`: Fristerinnerungen per E-Mail,
Papierkorb leeren, alte Anmeldeversuche entfernen.
### Sicherheit
- **Pflicht-Zweitfaktor** (TOTP nach RFC 6238). Lässt sich nicht
abschalten. Acht Wiederherstellungscodes, einzeln Argon2id-gehasht.
- **Anwendungsseitige Feldverschlüsselung** mit AES-256-GCM für
Diagnosen, den ermittelten Willen, IBANs, Versicherungs-, Renten- und
Steuernummern sowie alle Freitext-Notizen. Der Feldname wird
mitauthentifiziert — ein Geheimtext lässt sich nicht von einem Feld in
ein anderes umhängen. Schlüsselrotation ohne Datenverlust vorgesehen.
- **Argon2id** nach OWASP-Empfehlung, mit automatischem Nachziehen bei
späterer Parametererhöhung.
- **Rate-Limiting** am Login nach E-Mail und Anschluss. Die Sperre wird
geprüft, bevor überhaupt gerechnet wird, und die Antwortzeit ist
gestreckt — sie verrät nicht, welche Adressen existieren.
- **Unveränderliches Audit-Log**: ein Datenbank-Trigger weist UPDATE und
DELETE ab, auch bei direktem Zugriff mit `psql`.
- **Strenge Content-Security-Policy** mit Nonce pro Anfrage. Keine
externen Schriften, kein CDN, keine Tracker — nachgewiesen durch einen
Test, der jede Anfrage an einen fremden Host als Fehler wertet.
- **Uploads** werden an den Magic Bytes geprüft, nicht an Endung oder
gemeldetem MIME-Typ. Ausgeliefert wird nur über kurzlebige signierte
URLs, immer als Anhang mit `nosniff`.
- **Erneute Anmeldung** vor Export, endgültigem Löschen und
Rollenänderungen. Ein Passwortwechsel beendet alle offenen Sitzungen.
### Betrieb
- Docker Compose mit Anwendung, PostgreSQL, MinIO und Sicherungsdienst.
**Ohne Reverse Proxy** — der ist auf dem Zielserver vorhanden. Die
Datenbank hat keine Portfreigabe.
- **Tägliche verschlüsselte Sicherung** um 03:15 Uhr mit Selbstprüfung:
lässt sich die Datei nicht wieder entschlüsseln, endet der Lauf mit
Fehler statt mit einem stillen Scheinerfolg.
- Verarbeitungsverzeichnis und Datenschutzhinweise als ausfüllbare
Textbausteine.
- Bauprotokoll aus den Commit-Trailern.
### Bekannte Einschränkungen
- Die Texterkennung ist gegen ein echtes Tesseract **nicht** end-to-end
geprüft. Der Weg „nicht verfügbar, Upload funktioniert trotzdem" ist
getestet.
- Der CAMT.053-Import ist nur gegen selbst gebaute Beispiele geprüft, nicht
gegen einen echten Bankauszug.
- Das Kostenkonto der Immobilie wertet über Buchungskategorien aus und
wäre bei einem **zweiten** Grundstück falsch. Die Oberfläche sagt das
an der Auswertung dazu.
- Eine Aufgabe aus einer Vorgangs-Vorlage lässt sich nicht entfernen —
passt sie im Einzelfall nicht, bleibt sie offen stehen.
- `@react-pdf/renderer` braucht in diesem Abhängigkeitsbaum eine Krücke
(`src/lib/pdf/standardschrift.ts`), weil seine Unterabhängigkeiten für
Standardschriften kein Schriftobjekt mehr liefern.
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.3.0...HEAD
[1.3.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.2.0...v1.3.0
[1.2.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.2...v1.2.0
[1.1.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.1...v1.1.2
[1.1.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.1.0...v1.1.1
[1.1.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.5...v1.1.0
[1.0.5]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.4...v1.0.5
[1.0.4]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.3...v1.0.4
[1.0.3]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.2...v1.0.3
[1.0.2]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.1...v1.0.2
[1.0.1]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/compare/v1.0.0...v1.0.1
[1.0.0]: https://git.wilde-server.de/florian.wilde/betreuungs-cockpit/releases/tag/v1.0.0