Auf dem Dashboard standen dauerhaft drei rote Kaesten mit Warndreieck und
Knopf "Frist jetzt bearbeiten": Aufwandspauschalen mit Verfall 2027, 2028
und 2029.
Alle anderen Bloecke des Dashboards filtern nach Dringlichkeit. Dieser
fragte nur, OB etwas eine Ausschlussfrist ist, und nie, WANN sie faellig
wird - eine Frist im Mai 2029 landete damit in derselben Aufmachung wie
eine, die morgen ablaeuft.
Die Daten sind richtig: Pflichtfristen werden bewusst drei Jahre im Voraus
angelegt. Sie gehoeren nur nicht in die Warnung, sondern in die Liste.
Neu ist ein Horizont von 90 Tagen. Er ist grosszuegig gewaehlt, weil die
kuerzeste hier vorkommende Ausschlussfrist sechs Monate laesst. Abgelaufene
warnen weiterhin immer - eine versaeumte Aufwandspauschale bleibt eine
schlechte Nachricht, auch wenn sie lange zurueckliegt.
Der Grund ist nicht Bequemlichkeit: Eine Warnung, die monatelang dasteht
und nichts von einem will, stumpft ab. Man uebersieht die vierte, die
etwas will - und bei § 1878 BGB ist das die Jahrespauschale, die ersatzlos
erlischt.
Ein bestehender Test behauptete "unabhaengig vom Faelligkeitstopf" und
belegte das mit einer Frist 134 Tage in der Zukunft. Die Aussage stimmt
weiterhin, das Beispiel nicht mehr - es liegt jetzt innerhalb des
Horizonts, aber ausserhalb aller Faelligkeitstoepfe.
Gegen drei Mutationen geprueft: Filter entfernt, Abgelaufenes
ausgeschlossen, Horizont um einen Tag verschoben - jede wird rot.
Refs #40
Zwei Kommentare versprachen Schutz, den es nicht gab. Das ist schlimmer
als eine fehlende Zusage, weil man sich darauf verlaesst:
- In middleware.ts stand "Ohne Authentifizierungs-Cookie gar nicht erst
weiterreichen". Die Middleware prueft keine Anmeldung, sie setzt
Kopfzeilen; die Liste steuert allein den Cache-Control-Kopf. Wer den
Satz glaubt, verzichtet an einer neuen Seite auf erfordereBenutzer().
Die Liste heisst jetzt OHNE_CACHE_SPERRE und sagt selbst, was sie ist.
- In datei.ts hiess es, die Pfadpruefung fange "Symlink-Tricks" ab.
resolve() rechnet mit Zeichenketten und folgt keinem Link - dafuer
braeuchte es realpath(). Nachgeruestet wird es nicht: Wer dort einen
Symlink anlegen kann, hat schon Schreibrechte im Datenverzeichnis. Der
Satz musste trotzdem weg.
Dazu docs/SICHERHEIT.md: Bedrohungsmodell, der Durchgang entlang der
OWASP Top 10 - und ein eigener Abschnitt darueber, was ausdruecklich NICHT
geschuetzt ist.
Aus dem OWASP-Durchgang kamen keine weiteren Funde. Zwei Stellen sahen
danach aus und waren es nicht: Prisma.join() in suche.ts fuegt nur bereits
parametrisierte Fragmente zusammen, und der Suchausschnitt mit den
<mark>-Markern aus ts_headline wird zerlegt und als React-Kinder
gerendert, nicht als HTML - obwohl die Quelle der OCR-Text hochgeladener
Dokumente ist.
Refs #38
Im S3-Treiber stand nur ServerSideEncryption: 'AES256'. Das ist
Verschluesselung mit dem Schluessel des Anbieters - er entschluesselt beim
Ausliefern selbst. Gegen eine verlorene Platte im Rechenzentrum hilft das,
gegen den Anbieter und gegen gestohlene Zugangsdaten zum Eimer nicht. In
diesem Eimer liegen Bescheide, Arztbriefe und Kontoauszuege.
Der Dateitreiber hat es von Anfang an richtig gemacht. Der Umschlag zieht
deshalb nach src/lib/speicher/umschlag.ts um und wird von beiden benutzt,
statt ihn ein zweites Mal zu schreiben.
Neu ist die Bindung an den Ablageort: Der Schluessel des Objekts wandert
in die Zusatzdaten. Wer Schreibrechte auf den Eimer hat, kann die Bytes
von Dokument A damit nicht unter dem Namen von Dokument B ablegen - beim
Lesen schlaegt die Pruefung fehl, statt den falschen Bescheid mit
gueltiger Pruefsumme auszuliefern.
Damit fallen die presigned URLs weg. Sie liessen den Browser direkt beim
Anbieter laden; was dort liegt, ist jetzt Geheimtext. Die Auslieferung
geht ueber dieselbe eigene Route wie beim Dateitreiber - die prueft
zusaetzlich die Berechtigung und schreibt einen Audit-Eintrag. Ein
weitergeleiteter Link allein oeffnet also nichts mehr.
Bestehende Dateien bleiben durchgehend lesbar: Der Treiber erkennt am
Kopf, ob ein Umschlag da ist, und reicht alles andere unveraendert durch.
Auch die alten Umschlaege ohne Ortsbindung gehen weiter auf - sonst waere
die Verbesserung ein Datenverlust. Nachgezogen wird mit
npm run speicher:verschluesseln # nur schauen
npm run speicher:verschluesseln -- --anwenden
Geprueft: 12 Tests gegen drei Mutationen, ein Trockenlauf ueber die 120
Dateien der Entwicklungsablage, und eine Probe mit einer Klartextdatei,
die nach dem Lauf verschluesselt auf der Platte liegt und sich trotzdem
korrekt zurueeklesen laesst.
Refs #38
Zwei Wege, auf denen ein hochgeladenes Dokument den Server umbringen
konnte. Beide haben gemeinsam, dass die Groesse aus der Datei kam.
1. Dekompressionsbombe. Die PDF-Stroeme wurden mit inflateSync ohne
maxOutputLength ausgepackt. Ein Flate-Strom sagt nicht, wie gross er
ausgepackt wird - ein paar Kilobyte Fuellung blaehen sich auf Gigabyte
auf. Jetzt ein Deckel je Strom (8 MB) und einer ueber die Summe
(64 MB); zlib bricht selbst ab, der Strom wird verworfen, der Text ist
eben unvollstaendig.
2. Riesenseiten. "pdftoppm -r 300" rendert mit fester Aufloesung, aber
die Seitengroesse steht im PDF. Bei 200 x 200 Zoll ergeben 300 dpi ein
Bild von 60000 x 60000 Pixeln, und zwar je Seite. Jetzt "-scale-to"
statt fester dpi: Die laengste Kante bekommt eine feste Pixelzahl,
unabhaengig davon, was im Dokument als Papierformat steht. Fuer A4
entspricht das etwa den bisherigen 300 dpi. Dazu ein Deckel auf die
Gesamtgroesse der gerenderten Dateien.
Der erste Bombentest war wertlos: Die Nutzlast enthielt keine
PDF-Textoperatoren, also war das Ergebnis mit und ohne Grenze leer. Mit
einer Marke in der Nutzlast und einer Gegenprobe knapp unter der Grenze
schlagen beide Mutationen an.
Refs #38
Die Aktion ist die einzige Stelle der Anwendung, an der jemand ohne
Anmeldung schreiben darf. Zwei Dinge fehlten.
Erstens stand die Seite zwischen dem ersten Start und dem Anlegen des
Zugangs fuer jeden offen: Wer sie in diesem Fenster fand, wurde
Administrator einer Anwendung mit Gesundheits- und Finanzdaten. Das
Fenster ist kurz, aber Zufall ist keine Zugriffskontrolle. Jetzt muss
ERSTEINRICHTUNG_GEHEIMNIS aus der .env mit eingegeben werden; verglichen
wird ueber Hashes in gleichbleibender Zeit, und ein falscher Wert
antwortet mit derselben Mindestdauer wie das Anmeldeformular.
Fehlt der Wert, ist die Einrichtung gesperrt statt offen. Das ist
Absicht: Eine Installation, bei der die Zeile vergessen wurde, soll eine
Meldung zeigen und nicht einrichtbar sein.
Zweitens waren Pruefung und Anlegen zwei Schritte. Beide laufen jetzt in
einer Transaktion, und die Sperrzeile mit fester Id entscheidet - eine
zweite gleichzeitige Anfrage scheitert am Primaerschluessel, bevor ein
zweiter Administrator entsteht.
Die Seite liest die Sperrzeile mit: Lag der erste Zugang im Papierkorb,
stand Schritt 1 sonst wieder offen. Der Seed setzt sie ebenfalls.
Refs #38
Vier Stellen, an denen eine Zusage nicht gehalten wurde.
1. Ein TOTP-Code galt mehrfach. Der Kommentar in totp.ts versprach seit
dem Anfang, der Aufrufer speichere das getroffene Zeitfenster - getan
hat es niemand. Damit liess sich ein abgelesener Code innerhalb von 90
Sekunden noch einmal einloesen. Jetzt geht jede Pruefung durch
zweitenFaktorEinloesen(), und die Bedingung steht im WHERE von
updateMany: Zwei gleichzeitige Anmeldungen mit demselben Code treffen
nicht beide eine Zeile.
2. Der Anschluss kam aus dem ERSTEN Eintrag von "x-forwarded-for". Das
ist der Teil, den der Aufrufer selbst schreibt. Wer bei jeder Anfrage
eine andere Zahl hineinschrieb, sah fuer die Anmeldesperre wie tausend
Anschluesse aus - die Zaehlung war nicht nur wirkungslos, sondern
irrefuehrend. Jetzt von hinten gelesen, und nur wenn VERTRAUE_PROXY
gesetzt ist. Ohne Proxy ist der Kopf fremdbestimmt; dann lieber keine
Zaehlung nach Anschluss als eine, die eine Kopfzeile aushebelt. Beim
ersten Mal steht eine Zeile im Log, damit das Fehlen auffaellt.
3. trustHost stand fest auf true. Ist AUTH_URL gesetzt - der
dokumentierte Betrieb setzt es, Passkeys brauchen es -, gibt es keinen
Grund, die Adresse aus einer Kopfzeile von aussen abzuleiten.
AUTH_TRUST_HOST bleibt wirksam, damit die dokumentierte Einstellung
nicht stillschweigend uebergangen wird.
4. Passkeys liessen sich ohne erneute Bestaetigung anlegen und entfernen.
Ein Passkey ist ein vollwertiger Zugang, der einen Passwortwechsel
ueberlebt - an einem unbeaufsichtigten Rechner waren das zwei Klicks.
Jetzt dasselbe Vorzimmer wie beim Vollexport. Dabei musste
alsFehler() lernen, die Weiterleitung von Next durchzulassen: Sonst
verschwand die Zwischenfrage in "Der Passkey konnte nicht geprueft
werden".
Refs #38
Zwei Felder fuer zwei Luecken, in einer Migration:
benutzer.totpLetzterSchritt haelt das zuletzt akzeptierte TOTP-Fenster
fest. Ohne das gilt ein Code bis zu 90 Sekunden lang beliebig oft.
Die Tabelle ersteinrichtung hat genau eine Zeile mit fester Id. Das ist
kein Protokoll, sondern eine Sperre: Bisher entschied "gibt es schon
einen Benutzer?" und danach wurde angelegt - zwei Anfragen gleichzeitig
kamen beide durch. Jetzt entscheidet der Primaerschluessel.
Enthalten ist eine Datenmigration: Wo schon ein Zugang besteht, wird die
Sperrzeile gesetzt. Ohne sie stuende die Einrichtungsseite auf jedem
bestehenden System nach dem Update wieder offen.
Die DROP-INDEX-Zeilen zu den Trigramm-Indizes hat das Skript aus #36
wieder entfernt; "npm run db:pruefen" bestaetigt, dass beide stehen.
Refs #38
Die Sperre hielt sich selbst am Leben. Jeder Aufruf wurde festgehalten,
auch der, der wegen der laufenden Sperre gar nicht geprueft wurde; die
Frist lief zugleich ab dem juengsten Fehlversuch. Wer also alle zehn
Minuten ein falsches Passwort an eine fremde Adresse schickte, sperrte
deren Inhaber dauerhaft aus - ohne das Passwort zu kennen. Jeder dieser
Aufrufe hinterliess ausserdem eine Zeile, die erst der Wartungslauf nach
30 Tagen wieder los wurde.
Jetzt zwei getrennte Dinge: Ein geprueft fehlgeschlagener Versuch zaehlt
und wird einzeln festgehalten. Ein waehrend der Sperre abgewiesener
Aufruf zaehlt nicht und wird hoechstens einmal je Sperre vermerkt.
Die Auswertung geht die Versuche von alt nach neu durch und zieht die
Runden nach: Ist das Mass voll, beginnt an diesem Versuch eine Sperre
und der Zaehler faengt von vorn an. Damit laeuft die Frist auch dann ab,
wenn die Fehlversuche noch im Zeitfenster stehen - vorher blieb der
Zugang bis zum Fensterende dicht.
Die Tests sind gegen vier Mutationen geprueft; jede wird rot.
Refs #38
"Der zweite Faktor ist Pflicht" stand im Kopf von auth.ts, durchgesetzt
wurde es aber nur im App-Layout. Ein Layout schuetzt, was durch das
Layout gerendert wird - Server Actions und Route Handler laufen daran
vorbei. Wer eine Passwort-Sitzung hatte, die TOTP-Einrichtung aber nie
abgeschlossen hat, kam an die Daten.
Die Pruefung steckt jetzt in erfordereBenutzer() selbst und damit ueber
erfordereZugang() automatisch in jeder Server Action. Massgeblich ist
der Datenbankstand, nicht das Feld im Sitzungstoken - das laesst sich
ueber session.update() vom Client beeinflussen.
Fuer Route Handler gibt es benutzerFuerRoute(): 401 bzw. 403 statt einer
Umleitung. Eine Anmeldeseite als Inhalt einer Word-Datei waere die
falsche Antwort.
Dabei aufgefallen: Der Dokumentenabruf unter /api/dokumente/datei fragte
nur nach der Anmeldung. Auch geschlossen.
Ausnahmen sind aufgezaehlt und werden von tests/mfa-grenze.test.ts
festgehalten - der Test liest den Quelltext und schlaegt an, sobald eine
weitere dazukommt.
Refs #38
npm audit meldete 32 Schwachstellen, davon 5 kritische. Der Vorschlag
des Werkzeugs war ein Sprung auf Next.js 16 - noetig war er nicht: next,
next-auth und @auth/core waren nur durchgereicht markiert, ein eigenes
Advisory hatten allein nodemailer, postcss und sharp. Die drei sind
Blattpakete und lassen sich gezielt ueberschreiben.
Nodemailer 9 kollidiert mit einer optionalen Peer-Angabe von next-auth
(hoechstens 8). Das ist folgenlos, weil der E-Mail-Provider von next-auth
hier nicht benutzt wird, sondern Nodemailer direkt.
Danach: 0 Schwachstellen.
Refs #38
Abgeschnittene Leitungen unter pipefail beendeten update.sh stumm und
haetten die Selbstpruefung der Sicherung falsch negativ gemacht. Behoben,
mit ERR-Falle und Test gegen das Muster.
Beim Sprung von 1.5.0 auf 1.10.1 endete scripts/update.sh mitten in der
Ausgabe. Kein Fehler, keine Zeile, einfach zurueck zur
Eingabeaufforderung.
Ursache war eine abgeschnittene Leitung:
git log --oneline "HEAD..$ZIEL" | head -15 | sed ...
head schliesst die Leitung nach 15 Zeilen, git log schreibt weiter und
bekommt SIGPIPE (141). Mit "set -o pipefail" gilt die ganze Kette als
gescheitert, "set -e" beendet das Skript - stumm.
Zeitabhaengig, deshalb monatelang unauffaellig: Solange git log fertig
wurde, bevor head zumachte, ging es gut. Die Schwelle liegt exakt bei
15 Commits, und v1.5.0..v1.10.0 waren die ersten 16.
Behoben, indem der Erzeuger selbst begrenzt (git log -15) statt die
Leitung zu kappen. Dieselbe Behandlung fuer die beiden Tag-Listen.
Dieselbe Falle steckte in der Selbstpruefung von sicherung.sh:
openssl enc -d ... | head -c 5 | grep -q "PGDMP"
Beide Leser schliessen frueh. Bei kleiner Datenbank steht alles im
Puffer und es faellt nicht auf; mit wachsender Datenbank kippt es - und
dann meldet das Skript eine INTAKTE Sicherung als kaputt und verweigert
das Update. Jetzt liest dd eine feste Menge und endet regulaer.
Damit so etwas nicht wieder stumm passiert:
- Eine ERR-Falle nennt Zeilennummer und Exitcode und erklaert bei 141,
dass es SIGPIPE ist und ein Fehler im Skript, nicht beim Benutzer.
- Ein Test verbietet "| head" in Skripten mit pipefail und nennt die
Zeile. Gegenprobe gemacht: Er schlaegt an.
- Bei mehr als 15 Commits steht "... und N weitere" darunter, statt sie
wortlos wegzulassen.
Nachgestellt: der Ablauf von 1.5.0 auf 1.10.1 laeuft durch.
Ausschliesslich Zeilenenden. Nachgewiesen mit
"git diff --cached --ignore-cr-at-eol": Der Diff ist leer, es hat sich
kein Zeichen Inhalt geaendert.
Deshalb ein eigener Commit: 7448 geaenderte Zeilen in einem Commit mit
echten Aenderungen zu verstecken, macht die Historie an dieser Stelle
unlesbar. So laesst er sich beim Durchsehen ueberspringen.
Refs #26
Ohne .gitattributes liegen Dateien gemischt vor: 29 von 500 mit CRLF,
der Rest mit LF. Jedes zeilenweise arbeitende Werkzeug schreibt eine
CRLF-Datei dann mit LF zurueck, und der Diff zeigt statt drei
geaenderten Zeilen die ganze Datei - bei Issue #7 waren es 578 statt 8,
kurz vor dem Push bemerkt.
Dieser Commit legt nur die Regel fest. Die Normalisierung der 29 Dateien
kommt getrennt, damit die Historie lesbar bleibt.
Refs #26
Behebt Issue #28: Kommentar an der Dokumentenliste, Muster und drei
tragfaehige Wege in e2e/hilfen.ts, Begruendung warum die Verdopplung
bleibt.
Closes#28
Die Dokumentenliste rendert jeden Eintrag zweimal: als Karte fuers Handy,
als Tabellenzeile ab Tablet. Eine Fassung ist per Breakpoint versteckt,
steht aber im DOM. Beim Testen trifft getByText(...).first() deshalb die
unsichtbare Kopie, und die Pruefung scheitert mit "element is not
visible" an einer Stelle, die augenscheinlich in Ordnung ist. Das hat
mehrfach Zeit gekostet, weil man den Grund nicht sieht.
Nachgemessen an /dokumente mit acht Dokumenten: ein Titel liefert 16
Treffer, davon 8 sichtbar - und first() ist unsichtbar.
- Kommentar an der Komponente, mit dem Verweis auf die Hilfsdatei.
- In e2e/hilfen.ts das Muster beschrieben und drei Wege genannt, die
tragen: innerText fuer "steht das da", visible=true zum Klicken, die
Zusammenfassungszeile fuer Anzahlen. Alle drei nachgeprueft, bevor sie
aufgeschrieben wurden - eine Anleitung mit kaputtem Rezept waere
schlimmer als keine.
Zur Frage aus dem Issue, ob sich die Verdopplung vermeiden laesst: nein,
nicht ohne Verlust. Eine Tabelle, die zu Karten umbricht, verlangt
display:block auf Zeilen und Zellen - damit ist es keine Tabelle mehr,
Vorlesesoftware verliert den Zusammenhang von Spaltenkopf und Wert, und
genau der macht eine Liste mit acht Spalten erst benutzbar. Die
Begruendung steht jetzt am Code, statt die Frage offen zu lassen.
Betroffen ist uebrigens nur diese eine Stelle, nicht mehrere:
kalender-monat.tsx und das Stammdatenformular nutzen Breakpoints, ohne
Inhalt doppelt zu rendern.
Refs #28
Behebt Issue #23: Durchgang durch alle Server Actions, eine Luecke
geschlossen (buchung.vorgangId), die Pruefung in lib/auth/verweise.ts
zusammengefasst.
Closes#23
Wenn ein Formular einen Fremdschluessel mitschickt, kommt der aus einer
Auswahlliste im Browser. Der Server darf sich darauf nicht verlassen -
eine veraenderte Anfrage kann jede beliebige Id enthalten.
Alle 22 Dateien unter src/server/actions/ durchgesehen. Das Ergebnis ist
besser als befuerchtet: Bis auf eine Stelle war jeder Verweis geprueft.
buchung.vorgangId ging ungeprueft durch. Konto, Beleg und Vertrag
wurden geprueft, der Vorgang nicht.
Die Pruefung liegt jetzt in lib/auth/verweise.ts statt verstreut in den
Aktionen. Die acht erlaubten Ziele sind eine ausgeschriebene Aufzaehlung
und kein dynamischer Tabellenzugriff: So sieht der Typpruefer jede
Abfrage, und welche Tabellen erreichbar sind, ist eine Entscheidung und
keine Nebenwirkung. Mehrere Verweise laufen nebeneinander - bei vier
Verweisen eine Wartezeit statt vier.
Umgestellt: die vier Einzelpruefungen der Buchung, die drei des
Vermoegenspostens, die eigene Hilfsfunktion fuer den Ansprechpartner.
Nicht angefasst wurden die Stellen, die ihre eigene, passendere Form
haben - etwa das Kontaktprotokoll, das beide Verweise parallel laedt und
feldgenaue Meldungen zurueckgibt.
Zum Test: Der erste Entwurf rief die Server Action per fetch nach. Der
waere auch ohne jede Pruefung gruen gewesen, weil Next einen Aufruf ohne
gueltige Action-Id ohnehin ablehnt - er haette nichts bewiesen. Der
jetzige schiebt dem echten Auswahlfeld einen Eintrag mit fremder Id
unter und schickt das Formular normal ab. Gegenprobe mit ausgebauter
Pruefung: faellt um, wie er soll.
Refs #23
Behebt Issue #22: 107 Buchungen ueber zwoelf Monate im Demofall, mit
gerechnetem Kontostand. Dazu behoben: gGmbH fehlte in der Rechtsformliste
beim Zusammenfassen wiederkehrender Zahlungen.
Closes#22
Der Demofall brachte Konten und Kontostaende mit, aber keine einzige
Buchung. Kassenbuch, Belegpflicht, Dauerzahlungen und die Erkennung
wiederkehrender Zahlungen liefen darin also leer - wer die Anwendung zum
ersten Mal ansah, musste erst eine CSV importieren.
Neu sind 107 Buchungen ueber zwoelf Monate, erzeugt aus einem Muster
statt einzeln abgetippt: Rente, Betriebsrente und Pflegekasse als
Einnahmen, Heimentgelt, Barbetrag, Sterbegeldbeitrag, Telefon und
Kontofuehrung monatlich, dazu Rundfunkbeitrag und Grundsteuer
vierteljaehrlich sowie eine Handvoll Einzelposten.
Was der Fall damit zeigt:
- Das Heimentgelt steht in drei Schreibweisen auf dem Auszug und wird
trotzdem als eine monatliche Zahlung gefuehrt.
- Genau eine Buchung hat weder Quittung noch Begruendung. Eine, nicht
zwanzig: Die Liste soll etwas zu zeigen haben und nicht rot leuchten.
- Die Sterbegeld-Lastschriften bleiben unzugeordnet, damit sich der
Handgriff "zwoelf Zahlungen als belegt zuordnen" ausprobieren laesst.
Der Beitrag steigt im Mai von 18,40 auf 19,80 - damit auch die
Abweichungspruefung sichtbar wird.
- Der Kontostand zum 31.07. wird aus Anfangsstand und Buchungen
gerechnet, nicht behauptet. Ein Beispielfall, der sich selbst
widerspricht, waere schlimmer als einer ohne Buchungen.
Zwei Funde beim Bauen:
Der Unique-Index auf (kontoId, importReferenz) hat einen echten Fehler in
den Daten gefangen: Zwei Renten am Monatsersten ergaben dieselbe
Referenz. Genau dafuer ist er da.
"gGmbH" fehlte in der Rechtsformliste. Dadurch galten "Seniorenzentrum
St. Katharina" und "... gGmbH" als zwei Empfaenger - und die Gruppe
zerfiel ausgerechnet in dem Fachgebiet, in dem fast jeder Traeger eine
gGmbH ist. Ergaenzt, mit Test.
Geprueft: ein Einheitstest fuer die Rechtsform, vier End-to-End-Tests am
Beispielfall - Umfang und Zeitraum, Kontostand gegen Buchungssumme, die
erkannten Gruppen und das Kassenbuch.
Refs #22
Behebt Issue #21: Strasse, PLZ und Ort unter "Mein Konto", von dort in
den Absenderblock jedes Schreibens und als Ort in die Datumszeile. Was
fehlt, bleibt weg.
Closes#21
Jeder erzeugte Brief ging bisher ohne Ruecksendeadresse hinaus: Im
Absenderblock stand nur der Name, in der Datumszeile fehlte der Ort. Bei
einer Versicherung oder Behoerde wirkt das unserioes und macht es der
Gegenstelle unnoetig schwer zu antworten.
Neu unter "Mein Konto" ein Feld fuer Strasse, PLZ und Ort. Ab da stehen
sie im Absenderblock jedes Schreibens, und die Datumszeile lautet
"Bayreuth, den 23.08.2026".
Die Anschrift haengt am Benutzer, nicht an der betreuten Person: Sie
gehoert zu mir und gilt fuer alle Akten.
Nicht die Logik fehlte, sondern die Datenquelle - absenderBlock() und
datumsZeile() konnten die Felder schon, briefkopfBauen() hat sie mit
einem Kommentar auf null gesetzt, weil es sie im Datenmodell nicht gab.
Aufbau:
- Drei Felder am Benutzer, Migration ueber den Wrapper aus #24. Das war
sein erster echter Einsatz: erzeugte Datei ohne Handarbeit brauchbar,
DROP-INDEX-Zeilen bereits heraus.
- Ein Absender-Typ statt eines blossen Namens, durchgereicht bis in den
Serienlauf - der hat bisher {name} gebaut und haette die Anschrift
sonst still verloren.
- Zwei Platzhalter fuer eigene Vorlagen: betreuer_anschrift,
betreuer_ort.
Was fehlt, bleibt weg: Jede der drei Angaben ist einzeln freiwillig. Nur
ein Ort ergibt eine Datumszeile mit Ort und einen Absenderblock, der
weiterhin nur aus dem Namen besteht. Eine halb ausgefuellte Adresse
sieht nach Fehler aus, ein blosser Name nach Absicht.
Geprueft: sieben Einheitstests fuer den Absenderblock inklusive jeder
Teilbefuellung und der Gegenprobe, dass nie die Heimadresse der
betreuten Person im Absender landet; vier End-to-End-Tests vom leeren
Zustand ueber das Speichern bis zum fertigen Brief.
Refs #21
Behebt Issue #27: Uebersicht der sieben Datenbankbefehle in der README,
db:migrate als einziger interaktiver benannt, Ersteinrichtung auf
npm run setup vereinheitlicht.
Closes#27
"npm run db:migrate" ist "prisma migrate dev" und damit interaktiv: Es
fragt nach einem Namen und bietet an, die Datenbank zurueckzusetzen. In
einem Skript, in der CI oder aus einem Agenten heraus bleibt es haengen.
Aus dem Namen ging das nicht hervor, also lief man einmal hinein.
- Neuer Abschnitt in der README mit allen sieben Datenbankbefehlen,
wofuer sie da sind und welcher Rueckfragen stellt. Es ist genau einer.
- Der uebliche Weg fuer eine Schemaaenderung steht als Dreizeiler
daneben - ueber den Wrapper aus #24, nicht ueber migrate dev.
- Die Ersteinrichtung in der README rief "npx prisma migrate dev --name
fundament" auf, obwohl es dafuer "npm run setup" gibt. Das war der
Ausgangspunkt der Verwirrung; jetzt steht dort der eine Befehl.
- Der Hinweis unter "Betriebswissen", die SQL-Skripte seien von Hand
anzustossen, galt nur fuer die Ersteinrichtung. Beim Aktualisieren
erledigt update.sh das mit - jetzt steht das dabei.
"db:migrate" bleibt bestehen. Der interaktive Ablauf hat seinen Platz,
etwa um eine verkorkste Entwicklungsdatenbank zurueckzusetzen; er ist
nur nicht mehr der Standardweg.
Refs #27
Fuenf Migrationen verwiesen fuer die Herkunft der Trigramm-Indizes auf
prisma/sql/suche.sql. Diese Datei gibt es nicht; sie heisst
volltextsuche.sql. Der Hinweis steht genau dort, wo ihn jemand liest,
der die naechste Migration erzeugt - ein falscher Pfad schickt ihn ins
Leere.
Dazu ein Verweis auf das Werkzeug aus #24: "npm run db:migration" nimmt
die Zeilen inzwischen selbst heraus, die Kommentare rieten aber
weiterhin nur zum Nachsehen.
Geprueft, bevor die Dateien angefasst wurden: Prisma speichert zwar eine
Pruefsumme je Migration, "migrate deploy" verifiziert sie fuer bereits
angewendete Migrationen aber nicht. Nachgestellt mit genau der Lage, die
auch auf dem Server entsteht - Pruefsumme in der Datenbank weicht ab,
"migrate status" und "migrate deploy" bleiben zufrieden.
Refs #25
Behebt Issue #24: npm run db:migration erzeugt Migrationen ohne die
bekannten DROP-INDEX-Zeilen und prueft nach dem Anwenden, dass die
Indizes stehen.
Closes#24
"prisma migrate diff" kennt die Trigramm-Indizes aus
prisma/sql/volltextsuche.sql nicht und schreibt deshalb in jede
Migration ein DROP INDEX dafuer. Fuenfmal mussten die Zeilen von Hand
raus. Auf dem Server waere es folgenlos geblieben - update.sh legt die
Indizes hinterher neu an -, in der Entwicklung dagegen waere die Suche
still langsam geworden.
npm run db:migration -- <name> [--anwenden]
npm run db:pruefen
Aufbau:
- Die Liste der geschuetzten Indizes steht an einer Stelle. Wer in
prisma/sql/ einen hinzufuegt, traegt ihn dort ein.
- dropZeilenEntfernen() ist bewusst eng: Nur eine DROP-Anweisung, die
einen geschuetzten Index nennt, faellt weg. Ein DROP INDEX auf einen
Prisma-eigenen Index bleibt stehen - der soll ja weg. Ein Kommentar,
der den Namen bloss erwaehnt, ebenfalls.
- Der Kopf der erzeugten Datei sagt, was entfernt wurde und warum.
- Nach --anwenden wird gegen die Datenbank geprueft, ob die Indizes
wirklich noch stehen. Das ist der Unterschied zwischen "sollte
gutgegangen sein" und "nachgesehen".
Beim Probelauf gefunden und behoben: Stimmen Schema und Datenbank
ueberein, besteht der ganze Diff aus den beiden bekannten DROP-Zeilen.
Das Skript legte dafuer eine leere Migration an - und eine leere
Migration in der Historie ist schlimmer als keine, weil sie als
angewendet gilt und niemand mehr weiss, wofuer sie gut war. Jetzt bricht
es vorher ab, ohne einen Ordner zu hinterlassen.
Die Typen liegen als .d.mts daneben: Das Skript bleibt .mjs und laeuft
ohne Bauschritt, die Tests laufen trotzdem nicht gegen "any".
Geprueft: zwoelf Einheitstests, dazu von Hand der Lauf mit echter
Schemaaenderung und der Lauf ohne.
Refs #24
Behebt Issue #9: Kontakt als Ansprechpartner am Vorgang, namentliche
Anrede in der Sachstandsanfrage, Name ueber der Behoerde, Anschrift im
Schreiben vorbelegt. Ohne Ansprechpartner bleibt es bei der allgemeinen
Anrede; geraten wird nie.
Nebenbei behoben: Der Schreiben-Editor verwarf die Empfaengeranschrift
aus dem Entwurf.
Closes#9
Ein namentlich adressiertes Schreiben landet bei der Person, die den
Vorgang kennt, statt in der Poststelle. Am Vorgang laesst sich jetzt ein
Kontakt aus dem Verzeichnis als Ansprechpartner benennen; die
Sachstandsanfrage redet ihn an, und sein Name steht ueber der Behoerde.
Die Sachstandsanfrage konnte das schon - sie bekam den Namen nur nie
uebergeben. entwurf.ts lud einen der verknuepften Kontakte und verwarf
ihn wieder. Wer angeredet wird, ist jetzt eine Entscheidung und kein
Zufall.
Aufbau:
- Migration: ansprechpartnerId am Vorgang (SetNull - ein Vorgang darf
nicht daran haengen, dass es den Kontakt noch gibt) und anrede am
Kontakt.
- Die Anrede als reine Funktion in kontakte/anrede.ts. Geraten wird nie:
ohne hinterlegte Anrede wird mit vollem Namen angeredet. Ein leerer
Name mit gesetzter Anrede faellt auf die allgemeine Anrede zurueck -
"Sehr geehrte Frau ," darf nie herausgehen.
- Der Wechsel des Ansprechpartners steht mit NAMEN im Protokoll. Zwei
cuids nebeneinander waeren protokolliert, aber nicht nachvollziehbar,
und nachvollziehbar war die Anforderung.
- Der Ansprechpartner wird gegen die Akte geprueft, bevor er gespeichert
wird - sein Name steht anschliessend im Briefkopf.
Nebenbei behoben: Der Schreiben-Editor verwarf die empfaengerAnschrift
aus dem Entwurf, sein State startete immer leer. Jetzt kommt die
Anschrift des Ansprechpartners im Formular an.
Geprueft: neun Einheitstests fuer die Anrede, fuenf End-to-End-Tests von
der Auswahl bis zum Protokolleintrag.
Refs #9
Behebt Issue #8: letzter Wert mit Stichtag und Quelle am Vertrag,
Kennzeichnung ab einem Jahr, "noch nicht erfasst" statt leerer Zelle.
Ohne Migration - der Wert bleibt der Wertstand am Vermoegensposten, aus
dem auch das Vermoegensverzeichnis rechnet.
Nebenbei behoben: Zuordnung ueber die Verknuepfung statt ueber die Art
des Postens, und die fehlende Zugehoerigkeitspruefung bei den Verweisen
auf Vertrag und Immobilie.
Closes#8
Am Telefon mit der Gesellschaft braucht man die Zahl dort, wo man sie
gefragt hat. Auf der Vertragsseite steht jetzt oben der zuletzt
mitgeteilte Wert mit Stichtag und Quelle, darunter die Historie; in der
Uebersicht traegt ein Wert, der aelter als ein Jahr ist, ein Abzeichen.
Ohne Migration, und das ist keine Einsparung, sondern der Punkt: Die
Werthistorie gibt es schon als Wertstand am Vermoegensposten, und der
kann auf einen Vertrag zeigen. Ein Feld am Vertrag waere genau die
zweite Wahrheit, die das Issue ausschliesst - dieselbe Zahl steht im
Vermoegensverzeichnis, weil es dieselbe Zahl ist.
Aufbau:
- Die Ein-Jahres-Grenze als eine getestete Regel in
finanzen/wertalter.ts. Sie stand bisher als "> 365" mitten in der
Vermoegensseite; jetzt gilt sie am Vertrag genauso.
- wertHistorie() fuehrt die Wertstaende aller mit dem Vertrag
verknuepften Posten zu einer Reihe zusammen.
- Wo ein Rueckkaufswert zu erwarten waere, aber keiner erfasst ist,
steht "noch nicht erfasst" statt einer leeren Zelle.
Zwei Fehler dabei behoben:
- Vertrag und Vermoegensposten wurden ueber die ART des Postens
zusammengefuehrt. Ein von Hand als "Sonstiges" angelegter Posten wurde
nicht gefunden, und beim ersten Erfassen am Vertrag entstand still ein
zweiter - Bericht und Vertrag haetten verschiedene Zahlen gezeigt.
Massgeblich ist jetzt die Verknuepfung.
- Beim Speichern eines Vermoegenspostens wurde geprueft, ob das
verknuepfte Konto zur betreuten Person gehoert; bei Vertrag und
Immobilie fehlte die Pruefung, beim Vertragsverweis einer Buchung
ebenso. Bei mehreren betreuten Personen haette ein manipuliertes
Formular ueber Aktengrenzen hinweg verknuepfen koennen.
Geprueft: zehn Einheitstests, vier End-to-End-Tests - darunter der neu
eingetragene Wert, der unveraendert im Vermoegensverzeichnis
wiederauftaucht, und der Posten mit abweichender Art.
Refs #8
Behebt Issue #7: eigene Schreibvorlagen unter Einstellungen, Platzhalter
mit sichtbaren Luecken, zweistufige Bestellungsabsatz-Warnung, vier
mitgelieferte Vorlagen als Kopiervorlage, Auswahl beim freien Schreiben.
Closes#7
Wer zum dritten Mal denselben Brief tippt, tippt ihn beim vierten Mal
falsch. Neu unter Einstellungen -> Schreibvorlagen: eigene Texte
hinterlegen, beim freien Schreiben auswaehlen, Platzhalter fuellen sich
aus den Daten, die ohnehin im System stehen.
Aufbau:
- Die Ersetzung als reine Funktionen in schreiben/platzhalter.ts. Drei
Faelle, keiner davon still: Wert einsetzen, sichtbare Luecke
[Aktenzeichen des Gerichts], unbekannter Platzhalter markiert. Ein
Tippfehler in der Vorlage darf das Schreiben nicht verhindern - aber
er muss ins Auge springen.
- Die Bestellungsabsatz-Warnung ist zweistufig: Der erste Speicherversuch
ohne ihn kommt mit einer Rueckfrage zurueck, erst die Bestaetigung
speichert. Ohne den Nachweis der Vertretungsmacht kommt keine Auskunft,
und das faellt erst nach vier Wochen auf. Ein ausformulierter Absatz
zaehlt so viel wie der Platzhalter, ein halber Nachweis ("Beschluss der
Eigentuemerversammlung") nicht.
- Vier mitgelieferte Vorlagen bleiben Code und damit unveraenderlich;
"Als Kopie uebernehmen" legt eine eigene Fassung an. Fuer Rueckkaufswert
und Kuendigung fuehrt die Oberflaeche weiter zuerst zu den
fallbezogenen Erzeugern - die rechnen Kuendigungstermine aus, was eine
Vorlage nicht kann.
- Vorlagen haengen an der betreuten Person, nicht am Benutzer: Sie
beziehen sich auf ihren Fall, liegen im Papierkorb wie alles andere und
sind im Vollexport ihrer Akte enthalten.
Der Editor haelt Betreff und Text als eigenen State. Beim Wechsel der
Vorlage ueberlebte der alte Entwurf die Navigation, die Vorlage kam nie
im Textfeld an - ein key auf dem Editor setzt ihn neu auf. Gefunden hat
das erst der End-to-End-Test.
Geprueft: dreizehn Einheitstests fuer die Ersetzung, sechs
End-to-End-Tests fuer Warnung, Kopie, Aufloesung und Papierkorb.
Refs #7
Vor Vermoegensverzeichnis und Jahresbericht braucht es von jeder Lebens-
und Rentenversicherung den Rueckkaufswert zum Stichtag - jedes Mal
dieselben fuenf bis zehn Briefe. Neu unter Vertraege: Stichtag waehlen,
Liste pruefen, ein Klick. Heraus kommt ein ZIP mit einer Word-Datei je
Gesellschaft plus Begleitblatt; je Vertrag entstehen nebenbei die
PDF-Fassung in der Akte, die Wiedervorlage nach 28 Tagen und der
Anfragevermerk.
Aufbau:
- Auswahllogik als reine Funktionen in vertraege/serienabfrage.ts:
Vorauswahl, Hindernisse mit Begruendung statt stummem Ausblenden,
28-Tage-Sperre im Gleichschritt mit der Wiedervorlage.
- Der Lauf in vertraege/serienlauf.ts: je Vertrag das volle Paket aus
Issue #4, aber in einem Durchgang. Kein Alles-oder-nichts - ein
gescheiterter Vertrag steht mit Grund im Begleitblatt, die uebrigen
laufen durch. Eine Transaktion ueber alle waere die falsche Zusage.
- Dublettenschutz tagesgenau: Der Doppelklick erzeugt nichts Doppeltes,
die bewusste zweite Anfrage nach einer Woche bleibt moeglich.
- Die Anzahl steht vor dem Klick auf dem Knopf.
Gekuendigte Vertraege bleiben anfragbar: Bis zum Wirksamwerden besteht
der Vertrag, und genau dann will der Sozialhilfetraeger den Wert sehen.
Geprueft: zwoelf Einheitstests, drei End-to-End-Tests inklusive der
Nebenwirkungen in der Datenbank und des Dublettenschutzes. 602
Unit-Tests, 87 e2e, null Typfehler.
Refs #6
Im Schreiben-Editor gibt es jetzt den Umschalter Brief/E-Mail. Die
E-Mail-Fassung wird aus dem - womoeglich bearbeiteten - Brieftext
abgeleitet: ohne Absenderblock, Anschrift und Datumszeile, aber mit dem
Bestellungsabsatz. Betreff und Text einzeln kopierbar.
Die offene Entscheidung aus dem Issue (mailto oder .eml) ist nach
Rueckfrage mit BEIDES beantwortet:
- mailto fuer kurze Texte. Der Knopf verschwindet, sobald der kodierte
Link 1800 Zeichen uebersteigt - mailto uebergibt den Text in der
Adresszeile, deren Laenge nirgends zugesichert ist, und ein halbierter
Brief faellt erst beim Empfaenger auf. Die Grenze wird NACH der
URL-Kodierung gerechnet; 900 Umlaute sind roh harmlos und kodiert
weit drueber.
- .eml immer: RFC 5322 mit X-Unsent: 1, quoted-printable fuer den Rumpf,
RFC-2047-Kodierung fuer Umlaute im Betreff. Bewusst ohne From-Kopf -
ein erfundener Absender wuerde je nach Programm uebernommen.
Beide Knoepfe erscheinen nur mit E-Mail-Adresse (aus dem Kontakt
uebernommen oder von Hand). Ohne Adresse bleibt das Kopieren.
Die Ablage kennt den Unterschied: Eine E-Mail heisst in der Akte
"E-Mail: ...", und abgelegt wird die E-Mail-Fassung samt
Empfaengeradresse im Kopf der PDF - in der Akte soll stehen, was
tatsaechlich hinausging.
Sicherung in emailFassung(): Findet sich keine Anrede (frei umgebauter
Text), wird NICHTS entfernt. Lieber ein Briefkopf zu viel in der Mail
als stillschweigend verlorener Inhalt.
Geprueft: 20 Einheitstests, vier End-to-End-Tests auf beiden
Geraeteprofilen. 590 Unit-Tests, 81 e2e, null Typfehler.
Refs #5
Behebt Issue #4: Ein Schreiben geht jetzt den ganzen Weg - bearbeiten,
Word-Datei, Ablage als Ausgangsdokument, Wiedervorlage. Auch an Stellen,
zu denen es noch keinen Datensatz gibt.
Closes#4
Die Anschreiben gab es bisher nur als Text zum Kopieren. Danach war
Handarbeit: in Word nachbauen, ausdrucken, abschicken - und noch einmal
von Hand als Dokument in die Akte legen. Der letzte Schritt wurde am
haeufigsten vergessen, und dann fehlt der Nachweis im Bericht.
Neu unter /schreiben/neu: bearbeiten, als Word-Datei herunterladen, in
die Akte legen, Wiedervorlage setzen - in einem Zug. Eine Seite fuer alle
Arten und alle drei Empfaengerwege, statt die Bearbeitung dreimal an
Vertrag, Vorgang und Freitext nachzubauen.
Der dritte Weg ist der eigentliche Zugewinn: Schreiben an eine Stelle,
zu der es noch keinen Datensatz gibt. Am Anfang einer Betreuung ist das
der Normalfall - man fragt ja an, um zu erfahren, ob dort ueberhaupt
etwas besteht. Aus der freien Anschrift laesst sich mit einem Klick ein
Kontakt anlegen.
Zwei Entscheidungen, die erklaert gehoeren:
Die Akte bekommt ein PDF, heruntergeladen wird .docx. Der
Objektspeicher nimmt nur PDF und Bilder an, geprueft an den Magic Bytes.
Eine .docx ist ein ZIP-Archiv; sie durchzulassen hiesse jedes ZIP
durchzulassen, und diese Pruefung ist eine bewusste Zusage der
Anwendung. Sachlich ist PDF fuer den Nachweis ohnehin richtiger: Es
aendert sich nicht mehr. Die Oberflaeche sagt dazu, dass eine spaeter in
Word geaenderte Fassung von der Akte abweicht.
Der Berichtssatz in berichte/docx.ts bleibt unangetastet. Er setzt einen
Bericht mit Kopfzeilen und Unterschriftenblock; ein Brief braucht
Absender, Anschriftenfeld, Datumszeile und Grussformel. Beides in eine
Funktion zu zwingen haette beide Ausgaben schlechter gemacht. Der
Umsetzungshinweis im Issue ("muss nur angebunden werden") war insoweit
falsch - von mir selbst geschrieben.
Die vorhandenen Erzeuger (Rueckkaufswert, Kuendigung, Sachstandsanfrage)
bleiben unveraendert; ihr Wortlaut ist getestet und erprobt.
Geprueft: zehn Einheitstests, darunter der Gleichschritt der
Wiedervorlagefristen ueber zwei Module hinweg. Fuenf End-to-End-Tests
auf beiden Geraeteprofilen: Entwurf, Vertragsbezug, Word-Download,
Ablage samt Wiedervorlage, Kontakt aus freier Anschrift.
Refs #4
Behebt Issue #11: Bei einem Dauerschuldverhaeltnis ist der Vertrag der
Beleg - fuer zwoelf gleiche Lastschriften gibt es keine zwoelf
Quittungen, sondern eine Police.
Closes#11
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
Am Anfang einer Betreuung weiss niemand, welche Vertraege es gibt. Die
Antwort steht aber im Kontoauszug: Wer regelmaessig abbucht, hat einen
Vertrag. Neu unter Vermoegen & Kasse wird aus hunderten Buchungszeilen
eine Arbeitsliste.
Aufbau:
- Die Erkennung liegt als reine Funktionen in
src/lib/finanzen/wiederkehrend.ts, ohne Datenbank. Nur so laesst sich
jede Einordnung einzeln nachrechnen.
- Der Datenzugriff steht getrennt in wiederkehrend-laden.ts.
- Ein kleines Modell haelt fest, was bewusst KEIN Vertrag ist. Es haengt
am normalisierten Empfaengernamen, nicht an Buchungen - nur so
ueberlebt die Entscheidung den naechsten Import.
Zwei Haltungen liegen der Erkennung zugrunde:
Im Zweifel getrennt lassen. Zusammengefasst wird nur das Unstrittige:
Gross- und Kleinschreibung, Satzzeichen, Umlaute, Rechtsform am Ende.
Keine Aehnlichkeitssuche - "Stadtwerke Bamberg" und "Stadtwerke
Bayreuth" trennen drei Zeichen und zwei Rechnungssteller. Zwei Zeilen zu
viel sind harmlos; eine falsche Gruppe verbirgt einen Vertrag.
Nichts behaupten, was nicht belegt ist. Statt "monatlich" steht dort
"6 Zahlungen, etwa monatlich". Schwankt der Betrag, wird die Spanne
genannt. Ohne erkennbaren Abstand wird nicht hochgerechnet, sondern
gezeigt, was geflossen ist.
Dazu eine zweite Liste fuer einzelne groessere Zahlungen: Die
Gruppierung braucht zwei Zahlungen, ein Jahresbeitrag erscheint bei
einem importierten Jahr aber nur einmal - und ausgerechnet
Versicherungen zahlt man oft jaehrlich.
Geprueft: 37 Einheitstests, die Haelfte davon in der Gegenrichtung, plus
vier End-to-End-Tests auf beiden Geraeteprofilen. Der Browsertest hat
zwei Fehler gefunden, die Typpruefung und Einheitstests durchgelassen
haben - darunter ein verlorener Backslash, der die Vorbelegung des
Beitrags stillschweigend verschluckte.
Nebenbei: .claude/ ins .gitignore. Dort legen parallel arbeitende
Agenten ihre Worktrees ab; ein "git add -A" haette einen kompletten
zweiten Checkout eingecheckt.
Refs #3
Das Thema stand als einzelne Zeile unter "Spaeter, ohne Datum" — also
unter "waere schoen, und ehrlich gesagt vielleicht nie". Nach der
Priorisierungsregel der Datei selbst faellt es unter Kriterium 2: Es
kostet jeden Monat Zeit, vor Vermoegensverzeichnis und Jahresbericht
besonders viel.
Neuer Abschnitt 1.2a mit begruendeten Unterpunkten zu #3 (Vertraege
aufspueren), #4 (Schreiben erzeugen, ablegen, nachhalten), #5 (E-Mail),
#6 (Serienabfrage der Rueckkaufswerte), #7 (eigene Vorlagen) und #11
(Dauerzahlungen gelten als belegt). Die Zeile unter "Spaeter, ohne
Datum" entfaellt, damit dasselbe Thema nicht zweimal steht.
"Rueckkaufswert am Vertrag" (1.1, #8) und "Ansprechpartner am Vorgang"
(1.2, #9) bleiben, wo sie stehen, sind aber als zugehoerig kenntlich
gemacht.
Refs #10