Commit Graph
17 Commits
Author SHA1 Message Date
Florian Wilde 848aaf1b9f Absenderanschrift in jedem Schreiben
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
2026-08-23 20:29:17 +02:00
Florian Wilde 9e68d10787 Ansprechpartner am Vorgang hinterlegen
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
2026-08-23 16:31:10 +02:00
Florian Wilde 09a4528c4b Rueckkaufswert am Vertrag sichtbar machen
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
2026-08-23 15:06:25 +02:00
Florian Wilde f8d7752c27 Eigene Vorlagen fuer Anschreiben
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
2026-08-23 13:41:51 +02:00
Florian Wilde b59d11c9b4 Schreiben als E-Mail uebernehmen
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
2026-08-23 11:02:00 +02:00
Florian Wilde c21d03abf6 Schreiben erzeugen, ausgeben und ablegen
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
2026-08-23 10:14:59 +02:00
Florian Wilde fa994350d6 Dauerzahlungen zu einem Vertrag gelten als belegt
Bei einer monatlichen Praemie gibt es keinen Beleg - es gibt einen
Vertrag, und der wird einmal abgeschlossen, nicht monatlich quittiert.
Bisher blieb nur ein Freitextfeld pro Buchung: zwoelfmal im Jahr
derselbe Satz, mal Anzahl der Vertraege.

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

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

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

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

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

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

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

Refs #11
2026-08-23 09:00:46 +02:00
Florian Wilde aa0cc66292 Wiederkehrende Zahlungen aufspueren
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
2026-08-23 02:42:26 +02:00
Florian Wilde c38c988c3c feat: Anmelden mit Passkey
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.
2026-08-22 18:44:56 +02:00
Florian Wilde 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: Haertung
2026-08-22 14:36:18 +02:00
Florian Wilde 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
2026-08-22 07:31:55 +02:00
Florian Wilde 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
2026-08-22 06:23:04 +02:00
Florian Wilde 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
2026-08-22 06:22:44 +02:00
Florian Wilde 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
2026-08-22 06:21:45 +02:00
Florian Wilde 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
2026-08-22 02:35:08 +02:00
Florian Wilde 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
2026-08-22 02:16:22 +02:00
Florian Wilde 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
2026-08-22 01:54:56 +02:00