Zwei Fehler, beide erst im Betrieb sichtbar. 1. Schritt 3 der Ersteinrichtung meldete "Bitte pruefe die markierten Felder" - und markierte keines. Man kam dort nicht weiter. Das kurze Formular schickt neun Felder, das vollstaendige 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. Damit das nicht wiederkommt, unterscheidet die Aktion jetzt zwischen Eingabefehler und Formular-Schema-Bruch: Beanstandet die Pruefung ein Feld, das nicht abgeschickt wurde, sagt sie das ausdruecklich und schreibt es ins Serverlog, statt es dem Nutzer als Eingabefehler unterzuschieben, den er nirgends finden kann. 2. Eine betreute Person liess sich nicht endgueltig loeschen. Postgres setzt beim Loeschen den Verweis im Audit-Log auf NULL, und genau dieses UPDATE hat der Unveraenderlichkeits-Trigger abgewiesen. Damit scheiterte auch das Leeren des Papierkorbs nach 30 Tagen. Der Trigger erlaubt jetzt genau eine Ausnahme: das Nullen der beiden Fremdschluessel. Jede andere Aenderung bleibt verboten, auch das Setzen eines Fremdschluessels auf einen anderen Wert. Der Nachweis bleibt vollstaendig - es verschwindet nur der Verweis auf einen Datensatz, den es nicht mehr gibt. Geprueft: sieben neue Tests gegen die tatsaechlichen Formularinhalte, der Ablauf einmal komplett im Browser bis zum Dashboard, und der Trigger gegen eine laufende Datenbank - Loeschen gelingt, Faelschen scheitert. Etappe: 8 Phase: Haertung
16 KiB
Änderungen
Alle nennenswerten Änderungen an dieser Anwendung, in umgekehrter Reihenfolge. Aufbau nach Keep a Changelog, Versionsnummern nach Semantic Versioning.
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.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 nichtoptional— ein Feld, das gar nicht abgeschickt wurde, istundefined, nichtnull. 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
betreuteIdunduserId. 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.
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.exampleband die Anwendung stillschweigend an127.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:APP_BIND=10.0.0.5 # Adresse, über die der Proxy diesen Server erreicht docker compose up -d
Geändert
docs/INSTALLATION.mdnenntAPP_BINDjetzt schon in Schritt 3, wo die.envausgefü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 zweicurl-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.ymlreichtENC_KEY_V2immer an den Behälter durch, mit leerem Wert, wenn in der.envkeiner steht. Die Anwendung erkannte Schlüssel aber am Variablennamen statt am Inhalt, fand also ein leeresENC_KEY_V2und 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_AKTIVumstellen.
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 eingestellteAUTH_URL.docker compose exec app node scripts/anmeldung-pruefen.mjs deine@adresse.deEs ä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: gesetztesENC_KEY_V1, leeresENC_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 mitCannot find module. Betroffen waren alle Skripte, die im laufenden Behälter gedacht sind — auchwartungsschluessel.mjsundschluessel-erzeugen.mjs.Nach dem Update funktioniert der Befehl wie dokumentiert. Wer nicht gleich aktualisieren will, kommt einmalig auch so an das Skript:
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/rendererbraucht 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.