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
Am Schreibtisch war "Mein Konto" praktisch nicht zu finden - und damit
auch nicht Passwortwechsel, zweiter Faktor und Passkey-Verwaltung.
Die Seitenleiste hatte keine feste Hoehe und wuchs mit der Seite mit. Ihr
Fuss mit dem Benutzermenue landete deshalb am Ende der SEITE statt am
unteren Bildschirmrand: auf /heute bei 1632 Pixel in einem 900 Pixel
hohen Fenster. Nachgemessen, nicht geraten - im Browser auf fuenf Seiten.
Der Fehler sah nach nichts aus. Kein Element fehlte, nichts war kaputt,
die Seite sah richtig aus. Man musste nur ganz nach unten scrollen.
Behoben mit lg:sticky, lg:top-0, lg:h-dvh und lg:self-start. Das
self-start gehoert dazu, weil ein Flex-Kind sonst auf die volle
Zeilenhoehe gestreckt wird und die feste Hoehe wieder verliert. Die
Navigation dazwischen hat bereits overflow-y-auto und scrollt bei Bedarf
in sich selbst.
Am Handy war es nie ein Problem - dort sitzt das Menue in der ohnehin
festen Kopfzeile.
Neu: e2e/benutzermenue.spec.ts fragt nicht, ob das Menue im DOM ist,
sondern ob es im sichtbaren Bereich steht - auf fuenf Seiten, an drei
Scrollpositionen, auf einem flachen Laptopfenster und am Handy. Die
Position in der Mitte ist Absicht: Am Seitenende wird eine mitwachsende
Leiste zufaellig doch sichtbar, der Test waere gruen und der Fehler
trotzdem da. Beide Sichtbarkeitstests sind gegen den kaputten Zustand
gegengeprueft und schlagen dort an.
Refs #1
Passkeys (WebAuthn/FIDO2) als zweiter Anmeldeweg - auf der Anmeldeseite
und bei der erneuten Bestaetigung vor sensiblen Aktionen. Verwaltet unter
"Mein Konto".
Der Passkey ERSETZT den zweiten Faktor, statt zu ihm hinzuzukommen. Das
ist tragbar, weil er zwei Faktoren in sich vereint: Besitz des Geraets
und dessen Entsperrung. Damit das keine Annahme bleibt, wird die
Nutzerverifikation angefordert (userVerification: 'required') und
serverseitig nachgeprueft - eine Antwort ohne sie wird abgewiesen.
TOTP muss vorher eingerichtet sein. Ein Passkey haengt an einem Geraet;
waere er der einzige Weg hinein, koennte ein verlorenes Handy die Akte
schliessen.
Aufbau:
- Die Pruefung laeuft vollstaendig im Credentials-Provider "passkey",
nicht in der Server Action davor. Ein "war schon geprueft" ueber die
Grenze zu reichen waere die Stelle, an der sich Anmeldungen faelschen
lassen.
- Challenges liegen serverseitig und werden per DELETE genau einmal
verbraucht - die Datenbank entscheidet, wer sie bekommt.
- Bereichskennung und Herkunft kommen aus AUTH_URL, nicht aus einer
zweiten Konfigurationsquelle.
- Ohne https wird der Knopf gar nicht erst angeboten.
- Rate-Limiting, Kontosperre und Audit-Log gelten unveraendert; die
Anmeldung nennt den verwendeten Passkey namentlich.
Zwei Anmerkungen zur Migration:
- "prisma migrate diff" wollte zusaetzlich die Trigramm-Indizes aus
prisma/sql/suche.sql loeschen, die Prisma nicht kennt. Die Zeilen sind
entfernt und im Migrationskopf vermerkt - ein DROP haette die unscharfe
Suche stillgelegt, ohne dass etwas fehlgeschlagen waere.
- "prisma format" hat das Schema durchgaengig neu ausgerichtet. Ohne
Whitespace betrachtet sind es 88 hinzugefuegte Zeilen und keine
geloeschte.
Geprueft: 21 Einheitstests, drei End-to-End-Tests gegen Chromiums
virtuellen Authenticator (einrichten, anmelden, bestaetigen, entfernen,
Geraet ohne Verifikation abgewiesen), beide Geraeteprofile. 496 Tests,
null Typfehler, Produktionsbau steht.
Nebenbei: Die Zahlen im README waren veraltet (24 statt 33 Modelle, 46
statt 79 Seiten). Sie stehen jetzt mit dem Befehl daneben, mit dem man
sie nachzaehlt.
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
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