v1.0.5
36
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9603917a86 |
fix: Changelog aus der Zielfassung, blockierende Dateien beiseite
Zwei Fehler beim ersten Probelauf des Update-Skripts. Der Changelog blieb leer. Das Skript las CHANGELOG.md aus der Arbeitskopie - also aus dem LAUFENDEN Stand, der die neuen Abschnitte naturgemaess noch nicht kennt. Jetzt kommt er ueber git show aus der Zielfassung. Das Umschalten scheiterte an einer unversionierten Datei. Genau das passiert beim Sprung auf die Fassung, die dieses Skript eingefuehrt hat: Man holt es sich von Hand, und in der Zielfassung gehoert es dann dazu - git weigert sich, eine unversionierte Datei zu ueberschreiben. Solche Dateien werden jetzt nicht weggeworfen, sondern nach .vor-update-<zeit> beiseite geschoben, danach laeuft das Umschalten durch. Etappe: 8 Phase: Haertungv1.0.5 |
||
|
|
42eb242767 |
feat: Update-Skript, das die vergessenen Schritte mitnimmt
Bisher war Aktualisieren eine Folge von Befehlen aus der Anleitung, und zwei davon vergisst man zuverlaessig: die Sicherung vorher und die beiden SQL-Skripte hinterher. Genau die hat es hier schon gekostet. ./scripts/update.sh macht das in einem Durchgang - mit zwei Eigenschaften, auf die es ankommt: Bis zur Rueckfrage wird nichts veraendert. Der erste Entwurf hat die eigenen Aenderungen beiseite gelegt und danach am Serverzugriff abgebrochen - Halbzustand, aus dem man erst wieder herausfinden muss. Jetzt laufen Pruefung, Abgleich und Changelog-Anzeige vollstaendig durch, bevor irgendetwas angefasst wird. Eigene Aenderungen an versionierten Dateien blockieren nicht. Im Betrieb passt man die docker-compose.yml an, bis etwas laeuft; genau diese Anpassungen sind spaeter oft schon eingearbeitet. Das Skript legt sie mit git stash beiseite - rueckholbar, nicht weggeworfen - und sagt, wie man sie zurueckbekommt. Wer sie wirklich loswerden will, nimmt --verwerfen. Die .env bleibt immer unangetastet. Schlaegt die Sicherung fehl, wird nicht aktualisiert. Schlaegt der Bau fehl, nennt das Skript den Weg zurueck. Nebenbei behoben: Der Port der MinIO-Konsole stand fest auf 9001. Lief dort schon etwas, startete der ganze Stapel nicht. Jetzt ueber MINIO_KONSOLE_PORT einstellbar - dieselbe Moeglichkeit, die APP_PORT schon hatte. Aufgefallen beim Aufsetzen einer Testumgebung. Etappe: 8 Phase: Haertung |
||
|
|
7780739a1a |
fix: Ersteinrichtung liess sich nicht abschliessen, Betreute nicht loeschen
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: Haertungv1.0.4 |
||
|
|
9365645f72 |
fix: Bindeadresse war in der Vorlage stillschweigend vorbelegt
.env.example setzte APP_BIND=127.0.0.1 als fertigen Wert, mitten zwischen lauter anderen fertigen Werten. Wer die Datei kopiert und seinen Reverse Proxy auf einem anderen Server betreibt, kommt damit nicht an die Anwendung heran - und der Fehler sieht aus, als laege es am Proxy. Der sichere Standard bleibt 127.0.0.1. Er wird nur nicht mehr als "schon erledigt" praesentiert, sondern als Entscheidung mit drei benannten Faellen: Proxy daneben, Proxy woanders mit privatem Netz, Proxy woanders ohne. Beim dritten Fall steht die Firewallregel dabei. Die Installationsanleitung nennt APP_BIND jetzt schon in Schritt 3, wo die .env ausgefuellt wird - nicht erst in Schritt 7, wo der Proxy drankommt. Wer der Anleitung von oben nach unten folgt, stolpert sonst erst vier Schritte spaeter darueber. Dazu ein eigener Abschnitt in der Fehlersuche mit den zwei curl-Proben, die den Unterschied zwischen falscher Bindeadresse und Firewall zeigen - und dem Hinweis, dass dafuer kein Neubau noetig ist. Am Programm aendert sich nichts. Etappe: 8 Phase: Haertungv1.0.3 |
||
|
|
232543e2aa |
fix: Leere Schluesselvariable legte die Verschluesselung lahm
docker-compose.yml reicht ENC_KEY_V2 immer an den Behaelter durch - mit leerem Wert, wenn in der .env keiner steht. src/lib/env.ts erkannte Schluessel aber am Variablennamen statt am Inhalt, fand also ein leeres ENC_KEY_V2 und brach ab mit "Umgebungsvariable ENC_KEY_V2 fehlt". Die Meldung war doppelt irrefuehrend: Sie nannte einen Schluessel, den niemand wollte, und sie erschien an einer Stelle, die damit nichts zu tun hat - beim Einrichten des zweiten Faktors, als Serverfehler ohne Text. Betroffen war jede Stelle, die verschluesselte Felder schreibt oder liest, also praktisch alles ausser der Anmeldung selbst. Die kam durch, weil sie ohne Verschluesselung auskommt - der Fehler trat deshalb erst auf der naechsten Seite auf, und der Zusammenhang war nicht zu sehen. Leere Schluesselvariablen werden jetzt uebersprungen. Sieben Tests decken die Auswertung ab, darunter genau diese Konstellation. Die Meldung beim Schluesselwechsel nennt jetzt ausserdem, welche Versionen vorhanden sind, und die richtige Reihenfolge: erst den neuen Schluessel eintragen, dann ENC_KEY_AKTIV umstellen. Dazu scripts/anmeldung-pruefen.mjs. Das Anmeldeformular sagt bewusst nicht, woran eine Anmeldung scheiterte - sonst liesse 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, zusammen mit dem Zustand des Zugangs, dem Stand der Sperre und der AUTH_URL. Etappe: 8 Phase: Haertungv1.0.2 |
||
|
|
5ee3275112 |
fix: Betriebsskripte fehlten im Docker-Abbild
Das Verzeichnis scripts/ wurde beim Bauen nie in die Laufzeitstufe
uebernommen. Der in der Anleitung dokumentierte Befehl
docker compose exec app node scripts/totp-geheimnis.mjs ...
endete deshalb mit "Cannot find module". Betroffen war nicht nur dieses
eine Skript, sondern alle, die im laufenden Behaelter gedacht sind - auch
wartungsschluessel.mjs und schluessel-erzeugen.mjs.
Kopiert wird jetzt das ganze Verzeichnis, nicht einzelne Dateien. Sonst
faellt beim naechsten Betriebsskript wieder auf, dass es fehlt - und zwar
erst dann, wenn man es braucht. Die Entwicklungsskripte darin laufen dort
mangels Werkzeugen nicht, kosten aber nur ein paar Kilobyte.
Geprueft im fertigen Abbild und gegen eine echte Datenbank: Das Skript
liegt unter /app/scripts, laeuft als Benutzer cockpit und gibt dasselbe
Geheimnis aus wie auf dem Entwicklungsrechner.
Die Anleitung nennt jetzt zusaetzlich den Weg fuer alle, die noch eine
aeltere Fassung laufen haben und nicht sofort aktualisieren wollen:
docker compose cp legt die Datei einmalig in den Behaelter.
Etappe: 8
Phase: Haertung
v1.0.1
|
||
|
|
9df6419a68 |
feat: TOTP-Geheimnis auslesen, um ein zweites Geraet einzurichten
Die Anwendung zeigt das Geheimnis genau einmal an, beim Einrichten - das ist richtig so. Wer den Zugang aber auf ein zweites Geraet legen will, Handy und Tablet oder ein Ersatzgeraet im Schrank, braucht es noch einmal. Bisher blieb dafuer nur, den zweiten Faktor zurueckzusetzen und alle Wiederherstellungscodes neu zu erzeugen. Das Skript laeuft nur auf dem Server und nur mit Zugriff auf die Datenbank UND auf ENC_KEY_V1. Wer beides hat, kann ohnehin alles lesen - genau deshalb liegt der Schluessel nicht in der Datenbank. Es ist also keine Hintertuer, sondern erspart eine unnoetige Neueinrichtung. Es gibt den otpauth-URI mit aus, aber mit dem ausdruecklichen Hinweis, ihn in einen QR-Erzeuger auf demselben Rechner zu geben und nicht in einen Dienst im Netz - sonst haette man das Geheimnis gerade dorthin gegeben, wovor der zweite Faktor schuetzen soll. Geprueft gegen den Entwicklungszugang: der ausgelesene Wert stimmt mit SEED_DEV_TOTP_SECRET ueberein. Etappe: 8 Phase: Haertung |
||
|
|
7633603a2a |
docs: Nginx Proxy Manager als erste Proxy-Variante
Die Werte, die in der Oberflaeche einzutragen sind, Feld fuer Feld - Details, SSL und Advanced. Steht vorn, weil es der Fall ist, der hier tatsaechlich laeuft. Drei Punkte, die man in der Oberflaeche nicht sieht und die sonst Zeit kosten: - Scheme ist http, nicht https. Die Anwendung beendet kein TLS. - HSTS bleibt in NPM aus - die Anwendung setzt den Kopf bereits selbst, sonst geht er zweimal raus. - Die vier proxy_set_header gehoeren NICHT ins Advanced-Feld. NPM setzt sie schon, und zwar im location-Block; ein zweiter Satz laege auf server-Ebene und wuerde ueberschrieben. Wer sie trotzdem eintraegt, sucht spaeter an der falschen Stelle. Ins Advanced-Feld gehoeren nur die Werte, die die Oberflaeche nicht abbildet: Uploadgroesse, Zeitgrenzen und das Abschalten der Pufferung fuer den Vollexport. Etappe: 8 Phase: Haertung |
||
|
|
c35fc16133 |
docs: Installationsanleitung fuer vorhandenes Docker und externen Proxy
Die Anleitung setzt jetzt voraus, was ohnehin da ist: ein Server mit laufendem Docker und ein Reverse Proxy, der schon steht - auch auf einem anderen Rechner. Docker-Installation und Betriebssystemhaertung sind raus, dafuer ist der Proxy-Abschnitt der ausfuehrlichste geworden. Damit ein externer Proxy die Anwendung ueberhaupt erreicht, war eine Aenderung noetig: Die Bindeadresse stand fest auf 127.0.0.1. Jetzt gibt es APP_BIND, Standard bleibt die Loopback-Adresse. Der Weg dorthin ist nach Vorzug sortiert - privates Netz, dann WireGuard, dann als Notloesung oeffentliche Adresse mit Firewallregel. Zwischen Proxy und Anwendung laeuft unverschluesseltes HTTP; das gehoert nicht ins offene Netz. Deshalb steht in der Kontrolle eine Gegenprobe, die fehlschlagen MUSS: Antwortet der Anwendungsport von aussen, ist die Firewallregel falsch. Fertige Konfigurationen fuer Caddy, nginx und Traefik, jeweils mit den Werten, die sonst uebersehen werden: - Zeitgrenze 10 Minuten - sonst brechen Belegmappe, Rechnungslegung und Vollexport mitten im Erzeugen ab. Die Standardwerte von nginx liegen bei 60 Sekunden. - Antwort nicht puffern - der Vollexport kommt als Strom. Puffert der Proxy, wartet der Browser, bis alles im Speicher des Proxys liegt, und laeuft dabei in die Zeitgrenze. - Und der Hinweis, dass der Proxy KEINE eigene CSP setzen darf: Sie enthaelt eine Nonce, die pro Anfrage neu ist. Eine feste CSP blockiert die eigenen Skripte - die Seite laedt dann, laesst sich aber nicht bedienen. Der A-Record zeigt auf den Proxy-Server, nicht auf den Anwendungsserver. Stand vorher falsch da. Etappe: 8 Phase: Haertung |
||
|
|
89bc5fe5a8 |
docs: Installationsanleitung Schritt fuer Schritt
Die README hatte Installationsabschnitte, aber keine durchgehende Anleitung fuer jemanden, der vor einem frischen Server sitzt. Genau da faengt diese hier an. Dreizehn Schritte vom leeren Server bis zur laufenden Anwendung, jeder mit einer Kontrolle am Ende - man geht nicht weiter, ohne geprueft zu haben, ob der letzte Schritt geklappt hat. Am Ende eine Abnahmeliste zum Abhaken, bevor echte Daten hineinkommen. Aufgenommen sind die Fallstricke, die tatsaechlich Zeit kosten: - AV-Vertrag klaeren, BEVOR der Server bestellt wird - Zeitzone setzen, sonst rechnet der Server in UTC und verschiebt Fristen - AUTH_URL muss exakt der aufgerufenen Adresse entsprechen - sonst landet die Anmeldung stumm wieder auf dem Formular, ohne Fehlermeldung. Das ist der Fehler, den man am laengsten sucht. - Der Proxy muss X-Forwarded-For weiterreichen, sonst zaehlt die Login-Sperre alle Zugriffe als denselben Anschluss. - Die beiden SQL-Skripte laufen nicht mit den Migrationen mit. Zwei Kontrollen sind keine Formalie, sondern Nachweise: Der Schutz des Audit-Logs wird geprueft, indem ein UPDATE ausdruecklich fehlschlagen MUSS. Und die Sicherung wird nicht nur angestossen, sondern einmal zurueckgespielt - solange noch nichts drinsteht, was weh tut. Der letzte Schritt bringt ENC_KEY_V1 in Sicherheit, an zwei Orte, von denen keiner der Server ist. Ein gestohlenes Backup ist ohne ihn wertlos - und ein verlorener Schluessel macht auch das eigene Backup wertlos. Das ist dieselbe Eigenschaft von zwei Seiten. Etappe: 8 Phase: Haertung |
||
|
|
0e7da42f27 |
docs: Bedienungsanleitung und Roadmap
Bis jetzt gab es Code und Betriebsdokumentation, aber nichts, was einem beim Arbeiten hilft. Die Anleitung ist kein Handbuch zum Durchlesen, sondern die zwoelf Handgriffe, die im Alltag vorkommen - vom Behoerdenschreiben aufnehmen bis zum Vertrag kuendigen. Dazu der Jahresrhythmus, die Berichte, was am Handy anders ist, und ein Abschnitt "wenn etwas schiefgeht" mit den Faellen, die wirklich passieren: Handy verloren, Code wird nicht angenommen, Frist stimmt nicht, Bericht zeigt eine Differenz. Ein eigener Abschnitt sagt, was die Anwendung NICHT tut. Wer sich auf etwas verlaesst, das sie nicht leistet, faellt sonst genau dann darauf herein, wenn es darauf ankommt. Sie ist unter /anleitung auch in der Anwendung lesbar - dieselbe Datei, kein zweiter Text, der auseinanderlaeuft. Wer im Heim steht und nicht weiterweiss, hat sie auf dem Telefon dabei, und der Druckknopf legt sie zu den Betreuungsunterlagen. Dafuer ein schlichter Markdown-Baustein statt einer Bibliothek: die Anwendung zeigt genau zwei solche Dokumente, beide schreibe ich selbst. Rohes HTML wird nicht durchgereicht. Die Datenschutzseite benutzt ihn jetzt ebenfalls, statt eine zweite Umsetzung zu pflegen. Beide Dateien werden ueber outputFileTracingIncludes in den Standalone-Build gezogen - der Pfad entsteht erst zur Laufzeit, die Dateiverfolgung von Next.js kann ihn nicht erkennen. Ohne den Eintrag waeren beide Seiten im Behaelter leer. Die Roadmap sortiert die 44 Schema-Wuensche der Etappen nach einer einzigen Frage zuerst: Kann daraus ein falscher Wert in einem Bericht ans Gericht werden? Was das kann, steht in 1.1. Dazu ein Abschnitt "bewusst nicht geplant" mit Begruendung - damit klar ist, worauf niemand wartet. Etappe: 8 Phase: Haertung |
||
|
|
b2ac13e128 |
feat: Versionierung mit Changelog, Version 1.0.0
Bis jetzt stand nirgends, welcher Stand laeuft. Beim Aktualisieren auf einem Server ist das die erste Frage. CHANGELOG.md nach Keep a Changelog, Versionsnummern nach SemVer. Der Changelog sagt, was sich beim Arbeiten aendert - nicht, welche Datei angefasst wurde. Dazu eine Tabelle, was Major, Minor und Patch fuer den Betreiber praktisch bedeuten, und der Hinweis, dass sich eine Datenbank-Migration nicht zurueckrollen laesst. Die Version steht bewusst an zwei Stellen: package.json und src/lib/version.ts. Ein Import der package.json waere im Standalone-Build eine andere Datei als die des Projekts - die Zahl haenge dann an der Laufzeit. Ein Test haelt beide zusammen, und "npm run version" setzt sie gemeinsam samt Changelog-Abschnitt. Committet und getaggt wird nicht automatisch. Ein Changelog-Eintrag, den niemand gegengelesen hat, ist keinen Deut besser als keiner. Sichtbar wird die Version unten in der Seitenleiste und unter /api/gesundheit - dort mit Git-Stand, den der Docker-Build hineinreicht. Auf der Anmeldeseite steht sie nicht: wer nicht angemeldet ist, muss nicht wissen, welche Fassung hier laeuft. Etappe: 8 Phase: Haertungv1.0.0 |
||
|
|
4b953e7887 |
fix: public-Verzeichnis und Anwendungssymbol ergaenzt
Das Docker-Abbild liess sich nicht bauen: der Dockerfile kopiert /app/public, das Verzeichnis gab es aber nie. Der Fehler waere erst beim ersten Deployment aufgefallen. Dasselbe fehlende Verzeichnis war die Ursache fuer den Favicon-404, der im Entwicklungsmodus in der Browserkonsole stand. Jetzt liegt dort robots.txt (Disallow fuer alles - diese Anwendung gehoert in keinen Suchindex) und das Symbol kommt aus src/app/icon.svg. Abbild geprueft: 583 MB, Node 22, Zeitzone Europe/Berlin, tesseract 5.5.2 mit deutschen Sprachdaten, pdftoppm aus poppler-utils, Lauf als unprivilegierter Benutzer cockpit, Migrationen und beide SQL-Skripte an Bord. Etappe: 8 Phase: Haertung |
||
|
|
6865bae05e |
docs: Betriebswissen und Stand in die README
Vier Dinge, die beim Bauen Zeit gekostet haben und die nirgends stehen: - Dev-Server und Build schreiben beide nach .next. Laufen sie gleichzeitig, fehlen dem Build einzelne Chunks, und die Meldung (Cannot find module './638.js') zeigt nicht auf die Ursache. - AUTH_URL muss exakt der aufgerufenen Adresse entsprechen. Steht dort localhost und man ruft 127.0.0.1 auf, wird das Sitzungs-Cookie fuer die falsche Herkunft gesetzt - die Anmeldung landet stumm wieder auf dem Formular, ohne Fehlermeldung. - Der Standalone-Server liest keine .env. Im Behaelter kommen alle Variablen aus dem environment-Abschnitt. - Die beiden SQL-Skripte laufen nicht mit den Migrationen mit. Dazu ein kurzer Abschnitt mit dem Stand: 71.051 Zeilen, 24 Modelle, 46 Seiten, 458 Unit-Tests, 34 End-to-End-Tests, null Typfehler. Etappe: 8 Phase: Haertung |
||
|
|
468202249a |
fix: Doppelte Wurzelroute entfernt, Produktionsbuild geprueft
Zwei Dateien loesten denselben Pfad "/" auf: src/app/page.tsx und src/app/(app)/page.tsx. Next.js uebersetzt das noch, erzeugt beim Standalone-Build aber ein unvollstaendiges Client-Manifest - und genau dieses Verzeichnis kopiert das Docker-Abbild. Im Behaelter waere die Anwendung damit erst beim Start umgefallen. Die Weiterleitung liegt jetzt nur noch ausserhalb der Routengruppe. Dazu vier ungenutzte Importe und Konstanten, die den Build blockierten, und eine ESLint-Regel, die mit Unterstrich benannte Parameter durchgehen laesst - _gemeldeterMimeTyp in der Dateiablage wird absichtlich nicht benutzt, weil der vom Browser gemeldete MIME-Typ nichts beweist. Der Name sagt das; ihn zu entfernen wuerde die Schnittstelle verschlechtern. Geprueft am fertigen Standalone-Build, also genau so, wie der Behaelter startet: - strenge CSP greift (nonce plus strict-dynamic, kein unsafe-inline fuer Skripte) und blockiert dabei nichts Eigenes - Anmeldung, alle Fachseiten, Benutzermenue und Schnellerfassung funktionieren - kein Konsolenfehler, keine blockierte Anfrage - keine einzige Anfrage an einen fremden Host Etappe: 8 Phase: Haertung |
||
|
|
1dba4f3413 |
fix: Zeilenumbruch im Hinweistext des Bauprotokolls
Beim Einfuegen war ein echter Zeilenumbruch in ein Zeichenkettenliteral geraten - das Skript liess sich nicht mehr ausfuehren. Etappe: 8 Phase: Haertung |
||
|
|
c5bd8b53d6 |
docs: Bauprotokoll mit Zeiten, Aufwand und gefundenen Fehlern
Aufstellung, wann was entstanden ist und was es gekostet hat - zur Nachvollziehbarkeit und zur Kostenrechnung. Die reine Commit-Rechnung taugt hier nur bedingt: Die Etappen 2 bis 7 sind parallel entstanden, erst drei, dann fuenf Agenten gleichzeitig. Aus der Historie sieht es aus, als haette Etappe 3 dreissig Minuten gedauert - tatsaechlich lief sie knapp eine Stunde und gleichzeitig mit vier anderen. Deshalb steht neben der automatischen Auswertung eine Tabelle mit den tatsaechlichen Laufzeiten und den Ausgabe-Token je Agent; daran bemessen sich die Kosten. Ebenfalls festgehalten: die gut drei Stunden erzwungener Pause am Sitzungslimit, die in der kalendarischen Spanne stecken, aber keine Bauzeit sind. Reine Bauzeit rund zwei Stunden 45 Minuten. Und die sechs Fehler, die waehrend des Baus aufgefallen sind - von der Server/Client-Grenze bis zur Doppelzaehlung bei den Heimkosten. Die stehen dort, weil sie zeigen, wo bei dieser Art Anwendung die Fallstricke liegen. Etappe: 8 Phase: Haertung |
||
|
|
dccc34d669 |
test: Abnahmekriterien als Playwright-Tests, Anmeldungen im Protokoll
Zehn Tests, die genau die Zusagen aus dem Auftrag pruefen - an der laufenden Anwendung, nicht an einzelnen Funktionen: - Aus dem Bestellungsdatum entstehen die gesetzlichen Fristen, mit Rechtsgrundlage, und die Aufwandspauschale ist als Ausschlussfrist gekennzeichnet. - Ein PDF laesst sich hochladen und in zwei Klicks einem Vorgang zuordnen. - Eine Datei, die sich nur als PDF ausgibt, wird abgewiesen. Das ist der Angriff, gegen den die Pruefung an den Magic Bytes gebaut ist: HTML mit PDF-Endung und PDF-MIME-Typ, das spaeter als Seite ausgeliefert wuerde. - Ein genehmigungspflichtiger Vorgang bietet die Stufe "Vollzogen" gar nicht erst an, solange kein Beschluss hinterlegt ist (§ 1858 BGB). - Das Vermoegensverzeichnis kommt als echtes PDF heraus - geprueft wird die Signatur %PDF-, nicht nur der Dateiname. - Der Vollexport verlangt eine frische Anmeldung. - Am Handy: kein waagerechtes Scrollen der Seite, Bedienelemente mindestens 40 Pixel hoch, Schublade per Escape schliessbar. Dazu ein echter Fund aus dem Protokolltest: Anmeldungen und Abmeldungen tragen keine betreuteId - sie haengen am Konto, nicht an der Akte. Die Protokollansicht filterte nur nach betreuteId und liess damit ausgerechnet die Frage weg, wer wann auf die Akte zugegriffen hat. Jetzt werden beide Arten von Eintraegen gezeigt. 34 Tests, je einmal am Schreibtisch und am Handy. Etappe: 8 Phase: Haertung |
||
|
|
1a91f62ced |
feat: Erinnerungen, Wartungslauf und Playwright-Tests der Anmeldung
Etappe 8, erster Teil. Erinnerungsmails sind bewusst eine Zugabe: Ist kein SMTP konfiguriert, wird nichts gesendet und nichts geworfen. Die Fristen stehen ohnehin im Dashboard und im abonnierten Kalender - eine ausgefallene Mail darf nicht dazu fuehren, dass der Wartungslauf abbricht und danach auch das Aufraeumen liegen bleibt. In der Mail steht, DASS etwas faellig ist und wo man nachsieht - nicht, worum es geht. Kein Name der betreuten Person, keine Inhalte. E-Mail ist unterwegs unverschluesselt und landet auf fremden Servern. Je Frist geht hoechstens eine Mail pro Vorwarnstufe raus. Taeglich waere nach einer Woche Rauschen, das man wegklickt - und genau dann verpasst man die eine, auf die es ankam. Der Wartungslauf faengt jeden Schritt einzeln ab: Papierkorb leeren, Anmeldeversuche und altes Audit-Log aufraeumen laufen auch dann, wenn der SMTP-Server nicht antwortet. Angestossen wird er ueber /api/wartung mit einem HMAC aus AUTH_SECRET - ohne gueltigen Schluessel antwortet die Route mit 404 statt 401. Ein echter Fund aus den Tests: Auth.js reicht den Fehlercode aus authorize() nicht an die Server Action zurueck. Bei gesperrtem Zugang sah man deshalb nur die allgemeine Meldung "stimmen nicht" und haette ratlos weiterprobiert, obwohl die Sperre bereits lief. Die Sperre wird jetzt zusaetzlich in der Server Action geprueft und benannt; die Pruefung in authorize() bleibt als eigentliche Sperre bestehen. Sechs Playwright-Tests der Anmeldung, darunter zwei, die Sicherheitszusagen nachweisen: dass zwei Fehlversuche mit unterschiedlicher Ursache dieselbe Meldung liefern (sonst liesse sich ablesen, welche Adressen existieren), und dass die Anmeldeseite keine einzige Anfrage an einen fremden Host stellt. Etappe: 8 Phase: Haertung |
||
|
|
5cc7f5c140 |
feat: Jahresbericht, Vollexport, Wissensbasis und Mitleser-Freigaben
Etappe 7. Die Ausgabeschicht - alles, was diese Anwendung nach aussen abgibt. Der Jahresbericht nach § 1863 BGB ist ein ENTWURF zum Ueberarbeiten, kein fertiger Text. Er zieht sich die fuenf Gliederungspunkte aus Kontaktprotokollen, Gesundheitsereignissen und Vorgangsstatus zusammen; was davor abgegeben wird, entscheidet der Betreuer. Das sagt auch die Oberflaeche. Ausgabe als PDF und als DOCX, damit sich weiterschreiben laesst. Vollexport als ZIP-Strom mit Gegendruck, damit auch grosse Bestaende ohne Speicherausbruch durchgehen: Originaldateien nach Jahren sortiert, alle Tabellen als entschluesseltes JSON, die erzeugten Berichte, ein LIESMICH und SHA-256 je Datei. Damit laesst sich die Anwendung verlassen - kein Datensilo. Der Export ist eine sensible Aktion: nur BETREUER und ADMIN, nur nach frischer Anmeldung, immer mit Audit-Eintrag. Ein Mitleser kommt nicht heran. 37 Hinweiskarten in daten/hinweise.json, Zod-geprueft und ohne Neustart pflegbar. Jede mit Rechtsgrundlage, Link und dem stehenden Vermerk, dass sie keine Rechtsberatung ersetzt. Modulweise Freigaben fuer Mitleser, mit ausdruecklicher Warnung bei Gesundheit, Finanzen und Sozialhilfe. Rollen- und Freigabeaenderungen erfordern eine frische Anmeldung und stehen im Audit-Log. Nebenbei geloest: Webpack loest .tsx vor .ts auf, TypeScript und Vitest umgekehrt. Ein Modul, das beide Endungen traegt, importiert sich unter einem der beiden Werkzeuge selbst. Deshalb heissen die Bausteine jetzt eindeutig (jahresbericht.entwurf.ts, .inhalt.tsx, .ausgabe.ts) und werden ueber eine gemeinsame Fassade weitergereicht. 41 Tests. Etappe: 7 Phase: Ausgabe |
||
|
|
b69a12da8f |
feat: Immobilie, Versicherungen und Heim mit COPD-Verlauf
Etappe 6. Das leerstehende Haus, die Vertraege und der Gesundheitsverlauf - die drei Bereiche, in denen fuer diesen Fall das meiste Geld und das meiste Risiko liegt. Leerstands-Checkliste mit zehn Punkten, jeder mit Begruendung. Der wichtigste: den Gebaeudeversicherer ueber den Leerstand informieren - sonst droht Leistungsfreiheit, und zwar genau dann, wenn es brennt oder eine Leitung platzt. Dazu Verkehrssicherungspflicht, Frostschutz zum Oktober, Kontrollgaenge, Zaehlerstaende, Schluesselverzeichnis. Der Kuendigungsrechner nennt den spaetesten ABSENDETAG, nicht den Fristablauf: eine Kuendigung wirkt mit dem Zugang, nicht mit dem Abschicken. Drei Tage Postlaufpuffer sind einkalkuliert. Der Kuendigungs-Assistent beantwortet die Genehmigungsfrage nach § 1850 BGB nicht - er stellt sie und verlangt den Gang zum Rechtspfleger. Der Versandnachweis ist Pflichtfeld: ohne ihn kommt die Kuendigung nicht durch die Pruefung. Leistungszuschlag nach § 43c SGB XI mit Stand 01.01.2024 (15/30/50/75 Prozent nach Verweildauer, erst ab Pflegegrad 2). Die Oberflaeche zeigt, ab wann die naechste Stufe greift - das ist bares Geld und wird regelmaessig uebersehen. COPD-Schnellerfassung fuer Exazerbation, Sauerstoff, Sturz, Krankenhaus, Hilfsmittel und Impfungen, mit STIKO-Plan fuer COPD. Beschreibungen verschluesselt. Die Jahresauswertung ueber Exazerbationen und Krankenhaustage wandert in den Jahresbericht und begruendet eine Hoeherstufung. 74 Tests. Etappe: 6 Phase: Substanz |
||
|
|
d35e24f830 |
feat: Sozialhilfe-Modul mit Nachforderungs-Tracker und Fristpruefung
Etappe 5. Bildet das laufende Verfahren beim Bezirk Oberfranken ab. Rechtsbehelfsfrist: § 187 Abs. 1, § 188 Abs. 2 und 3 BGB, danach § 64 Abs. 3 SGG - hier verschieben Wochenende und Feiertag das Fristende auf den naechsten Werktag. Das ist genau umgekehrt zu den Betreuerfristen, wo bewusst nicht verschoben wird, und deshalb liegt die Feiertagsliste getrennt. Zwei bayerische Sonderfaelle, die haeufig falsch gemacht werden: Mariae Himmelfahrt verschiebt NICHT (Art. 1 Abs. 1 S. 2 BayFTG - der Feiertag gilt nur in ueberwiegend katholischen Gemeinden, Bayreuth ist keine), und der Buss- und Bettag ist in Bayern seit 1995 kein Feiertag mehr. Beide erzeugen stattdessen einen ausdruecklichen Hinweis, damit man die Frist im Zweifel selbst nachrechnet. Der Rechenweg wird als Schrittliste ausgegeben. Heimkosten: Der Dashboard-Agent hatte einen fachlichen Widerspruch gefunden - der einrichtungseinheitliche Eigenanteil auf der Heimrechnung ist bereits um die Leistung der Pflegekasse gekuerzt. Wird sie zusaetzlich als Deckung erfasst, entsteht eine Doppelzaehlung von ueber tausend Euro im Monat. Das Modul rechnet jetzt ausdruecklich nach der Lesart EIGENANTEIL_NETTO, fuehrt die Pflegekassenleistung nur nachrichtlich mit und warnt, wenn sie doch belegt ist. Die Oberflaeche sagt auf einer eigenen Karte, was hier gegen was gerechnet wird. Die Schonvermoegens-Bewertung liefert Argumentationshilfen zum Uebernehmen, keine Automatik - die Einordnung obliegt dem Traeger und gegebenenfalls dem Sozialgericht. Das steht an jeder dieser Karten. Alle jaehrlich veraenderlichen Betraege stehen an einer Stelle mit Stand-Datum und Prueftermin. Die Bekleidungspauschale bewusst ohne Zahl: sie ist regional und steht im Bescheid. 46 Tests. Etappe: 5 Phase: Behoerde |
||
|
|
e7006c894e |
feat: Konten, Kassenbuch, Vermoegen und die beiden Pflicht-PDFs
Etappe 4. Aus diesen Daten entstehen das Vermoegensverzeichnis nach § 1835 BGB und die Rechnungslegung nach §§ 1859, 1865 BGB - beides geht ans Betreuungsgericht, beides ohne dass eine Zahl abgetippt wird. Betreuungsjahr n laeuft von bestellungAm + (n-1) Jahre bis bestellungAm + n Jahre - 1 Tag. Bei Bestellung am 29. Februar endet Jahr 1 am 27.02. und Jahr 2 beginnt am 28.02.: lueckenlos und ohne Ueberschneidung, per Test abgesichert. Rechenprobe der Rechnungslegung: Der Anfangsbestand ist der juengste Saldostand bis zum VORTAG des Zeitraumbeginns - ein Saldostand gilt als Bestand am Tagesende, sonst zaehlte der erste Tag doppelt. Geht die Probe nicht auf, wird die Differenz im PDF ausgewiesen und jeder fehlende Saldostand namentlich benannt. Nie geglaettet: eine still korrigierte Rechnungslegung ist gegenueber dem Gericht wertlos. Posten ohne Wertstand zum Stichtag erscheinen als Luecke, nicht als 0,00 EUR - und Werte aus der Zukunft werden nie herangezogen. Konten kommen ueber ihren Saldostand ins Verzeichnis; Vermoegensposten der Art KONTO werden dort uebersprungen, sonst stuende das Geld doppelt drin. Der CSV- und CAMT-Import raet KEINE Kategorien, sondern bucht alles auf "Sonstige". Eine falsch geratene Kategorie faellt in der Jahresuebersicht niemandem auf und verfaelscht den Bericht. Fremdwaehrung wird abgelehnt statt umgerechnet. Der XML-Leser weist DOCTYPE ab (XXE). 68 Tests, darunter echte IBAN-Pruefziffern und die Rundung: es darf nirgends ein Cent verschwinden. Etappe: 4 Phase: Geld |
||
|
|
56d4cca8c9 |
feat: Dokumentenablage, OCR, Volltextsuche und Belegmappe
Etappe 3. Dazu die geteilte Infrastruktur, auf der die Berichte der
folgenden Etappen aufsetzen: Speichertreiber und PDF-Grundgeruest.
Dateiablage hinter einer Schnittstelle, zwei Treiber - lokales
Verzeichnis fuer die Entwicklung, S3-kompatibel fuer den Betrieb. Der
Schluessel wird erzeugt, nie aus dem Dateinamen abgeleitet; ein
Pfadausbruch ueber den Namen ist damit ausgeschlossen.
Der Dateityp wird ausschliesslich an den Magic Bytes geprueft, und ein
PDF muss bei Offset 0 stehen - damit fallen auch Polyglot-Dateien durch,
die gleichzeitig gueltiges PDF und gueltiges HTML sind. Ausgeliefert
wird nur ueber kurzlebige signierte URLs, immer als Anhang und mit
nosniff. Die Route prueft Signatur, Anmeldung UND Zugriff auf die
betreuteId aus dem Schluessel; jeder Abruf steht im Audit-Log.
Beim Dateitreiber liegt die Datei zusaetzlich AES-256-GCM-verschluesselt.
Beim S3-Treiber geht das nicht, weil signierte URLs am Server vorbei
ausgeliefert werden - das ist in der Datei dokumentiert, nicht
stillschweigend anders gemacht.
@react-pdf/renderer konnte in diesem Abhaengigkeitsbaum keinen einzigen
Buchstaben setzen: die Unterabhaengigkeiten sind weitergewandert und
liefern fuer Standardschriften kein Schriftobjekt mehr.
src/lib/pdf/standardschrift.ts baut es aus den Metriken, die pdfkit
ohnehin mitbringt - kein neues Paket, kein Netz - und haengt sich nur
ein, wenn die Aufloesung tatsaechlich kaputt ist.
Fristerkennung im OCR-Text ("innerhalb von vier Wochen", "binnen eines
Monats") wird immer nur VORGESCHLAGEN, nie automatisch angelegt. Die
Textstelle steht daneben, damit man nachprueft, bevor man uebernimmt.
Gerechnet wird auf lokaler Mittagszeit: in UTC verschoebe die Sommerzeit
"vier Wochen ab 12.03." um einen Tag.
57 Tests.
Etappe: 3
Phase: Papier
|
||
|
|
1234a57fa6 |
fix: Typisierung von oeffnen(), CSP im Entwicklungsmodus, Nonce-Abweichung
Drei Fehler, die sich beim Durchsehen der Typpruefung und der
Browserkonsole gezeigt haben.
1. oeffnen() lieferte Record<string, string | null>. Zusammen mit
noUncheckedIndexedAccess wurde daraus bei jedem Zugriff
string | null | undefined - und jede aufrufende Seite haette das
wegkaesen muessen, obwohl die Felder immer da sind. Jetzt leitet ein
Mapped Type die Klartextnamen aus VERSCHLUESSELTE_FELDER ab:
oeffnen('Betreute', zeile).diagnosen ist sauber string | null.
Das hat auf einen Schlag 17 Folgefehler in fremden Modulen erledigt.
2. Die CSP blockierte im Entwicklungsmodus die eigenen Skripte.
'strict-dynamic' schaltet die Freigabe nach Herkunft ab, also auch
'self' - und Hot-Reload und Fehleranzeige laden ohne Nonce. Man sah
eine tote Seite statt der Fehlermeldung. Im Produktivbetrieb bleibt
es bei Nonce plus 'strict-dynamic', im Entwicklungsmodus gilt die
lockere Fassung.
3. Das Theme-Skript im Layout meldete bei jedem Seitenaufruf eine
Hydrations-Abweichung: React gibt das nonce-Attribut absichtlich
nicht an den Browser weiter, serverseitig steht es aber im HTML.
suppressHydrationWarning am Skript selbst - das html-Element allein
deckt das nicht ab.
Nebenbei: Algorithm.Argon2id ist ein const enum und laesst sich mit
isolatedModules nicht importieren. Die Angabe entfaellt, @node-rs/argon2
benutzt von sich aus Argon2id.
Die Anmeldeseite laedt jetzt ohne einen einzigen Konsolenfehler.
Etappe: 8
Phase: Haertung
|
||
|
|
3833a9dd8f |
docs: README, Datenschutztexte, Dockerfile und Sicherungsskripte
Damit ist Etappe 1 abgeschlossen und betriebsfertig dokumentiert. README mit beiden Betriebswegen. Docker Compose auf eigenem Server ist der Hauptweg; Vercel und Neon sind vollstaendig beschrieben, aber mit einer klaren Begruendung, warum ich davon abrate: beide Anbieter sind US-Unternehmen und unterliegen auch bei EU-Region potenziell dem CLOUD-Act. Bei Gesundheitsdaten einer nicht entscheidungsfaehigen Person ist das eine Abwaegung, die man bewusst treffen muss. Reverse-Proxy-Beispiele fuer Caddy, nginx und Traefik - der Proxy selbst ist nicht Teil des Compose-Setups, weil er auf dem Zielserver schon steht. Die Datenbank hat keine Portfreigabe. Sicherung: pg_dump schreibt direkt in die Verschluesselung, der Klartext landet nie auf der Platte. Danach eine Selbstpruefung - laesst sich die Datei nicht wieder entschluesseln, endet der Lauf mit Fehler statt mit einem stillen Scheinerfolg. Der jeweils erste Stand eines Monats bleibt dauerhaft stehen. Die README sagt ausdruecklich, was das Backup NICHT enthaelt: den Schluessel ENC_KEY_V1 und die Dateien im Objektspeicher. Dazu eine Anleitung, die Wiederherstellung einmal im Jahr zu ueben - mit dem letzten Schritt, in der Anwendung nachzusehen, ob die Diagnosen im Klartext erscheinen. Erst dann stimmt nicht nur die Datenbank, sondern auch der Schluessel. Verarbeitungsverzeichnis und Datenschutzhinweise als ausfuellbare Textbausteine. Die Hinweise sind in einfacher Sprache geschrieben - sie sollen der betreuten Person vorgelesen werden koennen. Die Seite /datenschutz liest denselben Text aus docs/, damit es nicht zwei Fassungen gibt, die auseinanderlaufen. Dockerfile in drei Stufen mit Tesseract, deutschen Sprachdaten und poppler-utils. Zeitzone Europe/Berlin ist gesetzt - ohne sie rechnet der Server in UTC und verschiebt Fristen um einen Tag. Etappe: 1 Phase: Fundament |
||
|
|
6b4df4c9cb |
feat: Dashboard Heute, Kontakte und Kontaktprotokoll
Das Dashboard beantwortet die Frage, was heute ansteht - nicht, welche Daten es gibt. Jede Kachel endet in einer Handlung: Fristen-Ampel, wartet auf andere (mit Tagen seit dem letzten Kontakt), offene Nachforderungen des Bezirks mit Fortschrittsbalken, ungepruefter Posteingang, unverbuchte Belege, Kennzahlen. Zwoelf Abfragen in einem Promise.all, kein N+1. Cent-Betraege werden beim Verlassen der Serverseite gewandelt. Kacheln zu Modulen, die ein Mitleser nicht freigegeben bekommen hat, entfallen ganz - sie zeigen nicht "0", denn das waere eine Aussage ueber Daten, die er nicht sehen darf. Kontaktprotokoll als Schnellerfassung: Datum vorbelegt, Art als grosse Knopfreihe, Dauer als Schnellwahl, ein Freitextfeld. Alles andere eingeklappt. Ziel sind unter 20 Sekunden am Handy. Das Haekchen "persoenlicher Kontakt zur betreuten Person" ist bewusst NICHT vorbelegt - es wandert in den Jahresbericht nach § 1863 BGB, und ein versehentlich gesetztes Haekchen waere dort eine falsche Angabe gegenueber dem Gericht. Aus demselben Grund gilt der Textbaustein "regelmaessiger persoenlicher Kontakt" erst ab persoenlichem Kontakt in mindestens 75 Prozent der Monate. Sonst schreibt die Auswertung ausdruecklich, dass kein persoenlicher Kontakt dokumentiert ist. Ein geschoenter Bericht hilft niemandem. Protokolldaten werden als 12:00 UTC gespeichert und durchgaengig in UTC gerechnet, sonst kippt die Zeitzone einzelne Besuchstage. 51 Tests. Etappe: 2 Phase: Alltag |
||
|
|
0cdd4edfdc |
feat: Fristen, Pflichtfristen aus dem Bestellungsdatum und ICS-Feed
Die gesetzlichen Fristen entstehen von selbst, sobald das Bestellungsdatum in den Stammdaten steht - Vermoegensverzeichnis und Anfangsbericht nach drei Monaten, Jahresbericht und Vermoegensuebersicht zum Ende jedes Betreuungsjahres, Aufwandspauschale, Landespflegegeld, Zuzahlungsbefreiung, Rueckkaufswerte. Zwei Punkte, an denen es sonst schiefgeht: Das Betreuungsjahr endet am Tag VOR dem Jahrestag (§ 188 Abs. 2 BGB), bei Bestellung am 29. Februar am 28. (§ 188 Abs. 3). Ohne diese Regel verrutscht die ganze Kette um einen Tag. Faellt eine Frist auf Samstag, Sonntag oder Feiertag, wird sie NICHT verschoben. § 193 BGB verschiebt zwar den Ablauf, aber als Betreuer will ich frueher fertig sein, nicht spaeter. Stattdessen steht ein Hinweis in der Beschreibung. Bayerische Feiertage inklusive, ueber die Osterformel berechnet. Pflichtfristen tragen bewusst keine RRULE: sonst entstuende beim Erledigen eine Dublette neben der ohnehin abgeleiteten Folgefrist. Beim erneuten Ableiten werden nur offene Fristen nachgezogen, Erledigtes bleibt unangetastet. Der ICS-Feed kommt ohne Token-Feld im Schema aus - die Signatur ist ein HMAC ueber betreuteId und userId. VALARM je Vorwarnstufe, Zeilenfaltung nach RFC 5545. 51 Tests. Etappe: 2 Phase: Alltag |
||
|
|
1217725076 |
feat: Vorgaenge als Fallakte mit Genehmigungskette und Vorlagen
Jeder Vorgang ist eine kleine Akte: Kopfdaten, Aufgabenliste zum Abhaken, Genehmigungsstufen und ein Zeitstrahl, der Dokumente, Kontaktprotokolle, Fristen, Buchungen und Statuswechsel zusammenfuehrt. Die abnahmerelevante Regel: Ein genehmigungspflichtiger Vorgang laesst sich nicht auf VOLLZOGEN setzen, solange kein Beschlussdokument hinterlegt ist. Geprueft wird in darfStatusWechseln() UND in der Server Action - nicht nur im UI, wo das Auswahlfeld die Stufe ohnehin gar nicht erst anbietet. Hintergrund ist § 1858 BGB: die Genehmigung muss vor dem Rechtsgeschaeft vorliegen, nicht danach. Neun Vorlagen mit durchdachter Aufgabenreihenfolge. Bei Wohnungs- aufloesung und Hausverkauf erzwingen sie die Kette Mitteilung -> Genehmigung -> Rechtsgeschaeft; ein Test prueft das. Kanban ohne Drag-and-Drop: Statuswechsel laufen ueber Auswahlfeld und Knopf. Am Handy ist das verlaesslicher, und am Schreibtisch kostet es einen Klick mehr statt einer verlorenen Karte. Zeitzonen: alsTag() bildet den oertlichen Kalendertag auf Mitternacht UTC ab. Ohne das rutscht in Deutschland jede Faelligkeit einen Tag zurueck. 33 Tests. Etappe: 2 Phase: Alltag |
||
|
|
63352272b8 |
fix: Navigationsstruktur aus der Client-Komponente herausloesen
Das Server-Layout filtert die Navigation danach, welche Module der angemeldete Benutzer sehen darf. Es importierte NAV_STRUKTUR aber aus einer Datei mit "use client" - dort kommt auf dem Server kein Wert an, sondern nur ein Verweis. NAV_STRUKTUR.filter war entsprechend keine Funktion, und jede Fachseite lief in einen Serverfehler. Zweiter Fehler in derselben Zeile: die Eintraege trugen die Icons als React-Komponenten. Die lassen sich nicht ueber die Server/Client-Grenze reichen. Beides behoben, indem die Struktur nach src/lib/navigation-struktur.ts wandert und dort nur noch ein Kuerzel je Icon steht. Die Client- Komponente uebersetzt das Kuerzel. Die Wiederausfuhr aus navigation.tsx wurde bewusst weggelassen, sonst stellt sich dieselbe Falle wieder auf. Dazu scripts/screenshots.mjs: meldet sich mit dem Entwicklungszugang an und fotografiert Seiten am Schreibtisch und am Handy, hell und dunkel. Etappe: 1 Phase: Fundament |
||
|
|
22b3a9b9e9 |
feat: Stammdaten, Ersteinrichtung, Zweitfaktor und Protokollansicht
Damit ist Etappe 1 fachlich vollstaendig. Ersteinrichtung in drei Schritten, bewusst ausserhalb des Anwendungsrahmens: der setzt Benutzer, zweiten Faktor UND eine betreute Person voraus - beim allerersten Start gibt es nichts davon. Die Aktion fuer den ersten Zugang sperrt sich selbst, sobald ein Benutzer existiert. Zweiter Faktor: Das Geheimnis wird zuerst nur abgelegt, ohne totpAktiviertAm. Erst ein echter Code aus der App schaltet ihn scharf. Wer sich beim Abtippen vertut oder die falsche App nimmt, wuerde sich sonst dauerhaft aussperren. Erst danach gibt es die Wiederherstellungscodes - einmal angezeigt, danach nur noch als Hash. Betreuungsjahr (src/lib/stammdaten/betreuungsjahr.ts): laeuft ab dem Tag der Bestellung, nicht ab dem Kalenderjahr. Schrittweise vorwaerts gerechnet statt ueber eine Differenz, damit Schaltjahre und der 29. Februar von selbst stimmen. Daran haengen Jahresbericht, Rechnungslegung und die Ausschlussfrist der Aufwandspauschale. Aendert sich das Bestellungsdatum, werden die Pflichtfristen neu abgeleitet - ueber einen dynamischen Import, damit die Stammdaten auch dann speicherbar bleiben, wenn das Fristenmodul fehlt. Protokollansicht zeigt je Eintrag die geaenderten Felder als alt -> neu. Die Aenderungen liegen verschluesselt, weil dort Diagnosen und Kontostaende stehen koennen. Seed legt bewusst keine echten Personendaten an, nur den ersten Zugang. Mit SEED_DEMO=true kommt ein vollstaendiger Beispielfall dazu - Erika Musterfrau, COPD GOLD III, Pflegegrad 3, Heimeinzug Januar 2026, leerstehendes Haus, zwei Versicherungen, laufender Antrag beim Bezirk mit acht Nachforderungen. Nur ausserhalb der Produktion. Docker Compose ohne Reverse Proxy - der ist auf dem Zielserver vorhanden. Die Datenbank hat keine Portfreigabe und ist nur ueber das interne Netz erreichbar; die Anwendung horcht auf 127.0.0.1. Etappe: 1 Phase: Fundament |
||
|
|
dcd3a8ce71 |
feat: Oberflaechenbausteine, Anwendungsrahmen und Konventionen
Basisbausteine im shadcn/ui-Stil, aber mit drei bewussten Abweichungen: - Bedienelemente sind 44px hoch statt 36px. Ich bediene die App oft am Handy im Heim oder auf dem Amt; kleiner ist dort nicht treffsicher. - Auswahlfelder benutzen das native <select>. Android und iOS oeffnen dafuer ihre eigene Liste - schneller als jeder Nachbau, und mit Screenreader und Tastatur funktioniert es ohne Zutun. - Eingabefelder haben mindestens 16px Schrift, sonst zoomt iOS-Safari beim Hineintippen das Formular heran. <Feld> verdrahtet Beschriftung, Erklaerung und Fehler ueber aria-describedby. Der Erklaerungstext ist kein Beiwerk: die Anwendung benutzt Begriffe aus dem Betreuungsrecht, die niemand nachschlagen soll. <Rechtshinweis> ist der stehende Vermerk, dass hier keine Rechtsberatung stattfindet. Er steht an jeder Stelle mit Paragraphenbezug. Der Anwendungsrahmen fuehrt drei Bedingungen zusammen, ohne die niemand an Fachdaten kommt: angemeldet, zweiter Faktor eingerichtet, mindestens eine betreute Person zugeordnet. Die Navigation zeigt Zaehler fuer ueberfaellige Fristen und neuen Posteingang - die Frage "wo muss ich hin?" wird schon im Menue beantwortet. Das Farbschema wird per Inline-Skript mit CSP-Nonce gesetzt, bevor die Seite gezeichnet wird - sonst blitzt beim Laden das helle Thema auf. docs/KONVENTIONEN.md haelt fest, was in allen weiteren Etappen gilt: Verzeichnisaufbau, Cent-Betraege, Umgang mit verschluesselten Feldern, Zugriffspruefung, Audit-Pflicht, Ampelfarben, Commit-Trailer. Etappe: 1 Phase: Fundament |
||
|
|
bd1c7b801f |
feat: Anmeldung mit Pflicht-2FA, Rollen und Audit-Log
Auth.js mit Zugangsdaten-Provider: E-Mail, Passwort und TOTP-Code werden in einem Formular abgefragt. Ein zweistufiger Ablauf muesste zwischen den Schritten einen Zwischenzustand halten - genau das ist die uebliche Schwachstelle, und am Handy ist ein Formular ohnehin schneller. Anmeldung: - Sperre wird VOR dem Passwortvergleich geprueft, damit ein gesperrter Zugang nicht einmal Argon2-Rechenzeit kostet. - Bei unbekannter Adresse wird gegen einen Dummy-Hash gerechnet und die Antwort auf eine Mindestdauer gestreckt. Sonst verraet die Antwortzeit, welche Adressen existieren. - Wiederherstellungscodes werden nach Gebrauch entfernt. - Veraltete Argon2-Parameter werden beim erfolgreichen Login nachgezogen. Rate-Limiting in der Datenbank statt Redis - ein Server, wenige Benutzer, kein Grund fuer eine weitere Komponente. Gezaehlt wird nach E-Mail und nach Anschluss, damit weder viele Anschluesse noch viele Adressen helfen. Zugriffsschutz (sitzung.ts): - Die Sitzung allein entscheidet nichts. Bei jedem Aufruf wird der Benutzer frisch geladen und die Sitzungsgeneration verglichen - so lassen sich alle Anmeldungen auf einen Schlag beenden. - erfordereZugang() prueft immer gegen die betreuteId, nie nur im UI. - Erneute Bestaetigung vor sensiblen Aktionen ueber ein signiertes, kurzlebiges Cookie statt ueber Serverzustand. Audit-Log: aenderungErmitteln() protokolliert nur die tatsaechlich geaenderten Felder; Passwoerter und TOTP-Geheimnisse werden ersetzt, nie gespeichert. Ein Fehler im Protokoll bricht die Fachaktion nicht ab - ausser die Aktion laeuft in einer Transaktion, dann faellt beides. CSP mit Nonce pro Anfrage in der Middleware: keine Fremdquellen, kein CDN, keine Fonts von aussen. Fachseiten werden nicht zwischengespeichert. Etappe: 1 Phase: Fundament |
||
|
|
c49810d092 |
feat: Feldverschluesselung, Argon2id und TOTP-Zweitfaktor
Die Sicherheitsschicht, auf der alles Weitere aufsetzt. Feldverschluesselung (AES-256-GCM): - Umschlagformat bc1.<version>.<iv>.<tag>.<ct>. Die Schluesselversion erlaubt Rotation, ohne alte Daten unlesbar zu machen. - Der Feldname wird als AAD mitauthentifiziert. Ein Geheimtext laesst sich damit nicht von diagnosen nach notizen umhaengen. - Leere Werte werden zu NULL statt zu einem Umschlag - sonst verriete die Datenbank, dass ueberhaupt etwas eingetragen war. - VERSCHLUESSELTE_FELDER ist der einzige Ort, an dem steht, was verschluesselt gehoert. sealen()/oeffnen() arbeiten darauf. Argon2id nach OWASP-Empfehlung (19 MiB, t=2, p=1), mit hashVeraltet() fuer spaetere Parametererhoehung ohne Migration. Blindindex per HMAC-SHA256 mit eigenem Schluessel: erlaubt die Frage "kenne ich diese IBAN schon?", ohne sie im Klartext zu halten. Derselbe Mechanismus pseudonymisiert IP-Adressen fuers Audit-Log. TOTP mit otpauth, QR-Code serverseitig als Data-URI - kein externer QR-Dienst, kein CDN. Wiederherstellungscodes aus einem Alphabet ohne 0/O und 1/I, einzeln Argon2id-gehasht, Pruefung ohne fruehen Abbruch. 37 Tests decken Rundlauf, Manipulation, Feldbindung, Rotation, Salzung und Normalisierung ab. Etappe: 1 Phase: Fundament |
||
|
|
6c1915ab9c |
feat: Vollstaendiges Datenmodell als Prisma-Schema
Alle Fachentitaeten der Betreuung in einem Schema - bewusst komplett, damit spaetere Etappen keine Migration am Fundament brauchen. Kernentscheidungen: - Geldbetraege durchgaengig als BigInt in Cent. Eine Rechnungslegung nach § 1865 BGB darf keine Rundungsfehler erben. - Felder mit Suffix Verschluesselt tragen einen AES-256-GCM-Umschlag. Wo trotzdem gesucht werden muss (IBAN), steht ein HMAC-Blindindex daneben. - Fristen und Stichtage sind @db.Date, nicht DateTime - sonst verschiebt die Zeitzone eine Frist um einen Tag. - Frist.systemSchluessel macht die aus dem Bestellungsdatum abgeleiteten Pflichtfristen idempotent. - Genehmigung.beschlussDokumentId ist die technische Voraussetzung dafuer, dass ein Vorgang ohne Beschluss nicht auf VOLLZOGEN gesetzt werden kann. - DokumentVerknuepfung mit echten Fremdschluesseln statt loser Typ/ID- Spalte: eine Akte muss referenzsicher sein. - Wertstand und Saldostand sind Historien - Werte werden nie ueberschrieben. Dazu zwei SQL-Skripte, die Prisma nicht abbilden kann: - volltextsuche.sql: tsvector-Trigger (deutsch) mit Gewichtung A/B/C, plus Trigram-Indizes fuer unscharfe Absendersuche. - audit-schutz.sql: Trigger, die UPDATE und DELETE auf audit_log verweigern. Das Log ist Nachweis gegenueber Gericht und Miterben, also nur anhaengend - auch gegen psql. Etappe: 1 Phase: Fundament |
||
|
|
cc72f5efd7 |
chore: Projektgeruest mit Next.js, TypeScript und Tailwind
Grundgeruest der Anwendung: Next.js 15 (App Router), TypeScript im strict-Modus, Tailwind mit shadcn/ui-Farbtokens. Bewusste Festlegungen: - Nur Systemschriften, keine externen Fonts, kein CDN. - Grundschrift eine Stufe groesser, Fokusring immer sichtbar, Touchziele mindestens 44px. - Ampelfarben fuer Fristen als eigene Design-Tokens. - Betraege werden als Cent-Ganzzahl gefuehrt, damit die Rechnungslegung keine Rundungsfehler erbt. - .env.example dokumentiert alle Geheimnisse; npm run schluessel erzeugt sie. Etappe: 1 Phase: Fundament |