# Changelog Alle nennenswerten Änderungen an FLORIAN. Format nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionierung nach [Semantic Versioning](https://semver.org/lang/de/). Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md). ## [1.0.0] — 2026-09-06 **Freigabe für den Echtbetrieb.** Kein neues Modul. Zwölf Phasen sind fertig, und die Wehr kann ab jetzt echte Daten hineingeben, ohne befürchten zu müssen, sie beim nächsten Update zu verlieren. Was 1.0.0 bedeutet: der Funktionsumfang ist vollständig, die Datenbank ist stabil, Migrationen sind aufwärtskompatibel, und ab hier gilt semantische Versionierung — ein Update bricht nichts, ohne dass es in der Hauptnummer steht. Was es nicht bedeutet: dass keine Fehler mehr da sind. ### Hinzugefügt - **`update.sh`** — Aktualisieren auf einem Linux-Server, in einem Befehl. Der Ablauf darf an jeder Stelle abbrechen, ohne eine halb aktualisierte Anlage zu hinterlassen: erst nachsehen, ob es überhaupt etwas Neues gibt, dann **sichern**, dann holen und bauen (schlägt das fehl, läuft die alte Fassung unverändert weiter), dann umschalten, dann nachsehen, ob die Anwendung antwortet. Tut sie es nicht, stehen die letzten Protokollzeilen da und die Befehle, mit denen der vorherige Stand zurückkommt. Vorher prüft es Docker, git, die `.env` und dass keine unverbuchten Änderungen im Verzeichnis liegen. `--pruefen` sieht nur nach, `--ohne-sicherung` gibt es für Testinstallationen. - **`docs/KURZANLEITUNG.md`** — der Einstieg, nach Rollen sortiert und auf ein paar Seiten. Die vollständige Anleitung hat 63 Kapitel; die liest niemand, bevor er das erste Gerät prüfen will. Diese hier ist aufgabenorientiert („ein Gerät prüfen", „einen Kontoauszug einlesen") statt nach Modulen geordnet, und sie sagt bei jedem heiklen Punkt dazu, warum es so ist — warum ein abgeschlossenes Prüfprotokoll nicht änderbar ist, warum gegengebucht und nicht korrigiert wird, warum der Atemschutzhinweis stehen bleibt. ### Geändert - Version auf **1.0.0**. Ab hier gilt semantische Versionierung nach [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md). - Die Roadmap hält den Stand **jeder** Freigabevoraussetzung fest, auch der offenen. Zwei sind organisatorisch und stehen aus: die Abnahme des Löschkonzepts durch die Wehr und die Schulung. An der Software hängt nichts davon ab; sie stehen dort, damit später nachlesbar ist, was zum Zeitpunkt der Freigabe erledigt war und was nicht. ### Behoben - **Die Testläufe flackerten seit Phase 8 — jetzt ist die Ursache weg.** Zwei Datenbanktests schalteten beim Aufräumen ihre Trigger mit `ALTER TABLE ... DISABLE TRIGGER` ab. Das ist eine Schemaänderung: sie gilt für die ganze Datenbank und für alle. Vitest fährt die Testdateien parallel — während die eine Datei abräumte, prüfte die andere gerade, dass genau dieser Trigger greift, und sah keinen. Das erklärt rückwirkend eine ganze Reihe von Fehlschlägen, die als „Flackern" abgetan wurden und keines waren. Statt der Schemaänderung wird jetzt `SET LOCAL session_replication_role = 'replica'` benutzt: dieselbe Wirkung, aber nur in der eigenen Transaktion. Fünf Läufe hintereinander sauber. - **Ein Datenbanktest räumte nur am Ende auf.** Brach ein Lauf ab, scheiterte der nächste an einer Eindeutigkeit, die mit dem Geprüften nichts zu tun hatte. Er räumt jetzt auch am Anfang auf. ### Zum Stand der Freigabevoraussetzungen | Voraussetzung | Stand | |---|---| | Datenübernahme | **entfällt** — die Wehr pflegt von Hand ein | | Sicherungskonzept, einmal wiederhergestellt | **erledigt** (Phase 11) | | Löschkonzept nach DSGVO | **technisch vollständig** (Phase 11), Abnahme steht aus | | Schulung | **steht aus**, Grundlage ist die Kurzanleitung | | Atemschutzhinweis | **bewusst stehen gelassen** — FLORIAN bleibt ergänzend | --- ## [0.12.0] — 2026-09-05 Phase 11: Freigabereife. Die drei Software-Voraussetzungen für 1.0.0 — Personenauskunft nach Art. 15 DSGVO, ein geprüftes Sicherungskonzept und eine Pipeline, die die Tests von allein ausführt. **Das Auskunftsrecht war ein Versprechen ohne Umsetzung.** `person.export` stand seit Phase 0 im Berechtigungskatalog und war dem Kommandanten zugewiesen; implementiert war nichts. Die Löschseite gab es (Anonymisierung im Aufbewahrungslauf), die Auskunftsseite nicht — und ohne Art. 15 ist ein Löschkonzept nicht abnehmbar. **Eine Sicherung, die nie zurückgespielt wurde, ist eine Vermutung.** Genau das hat sich hier bestätigt: der erste Rückspielversuch lief scheinbar sauber durch und hatte dabei sämtliche Unveränderlichkeitsgarantien der Datenbank entfernt. ### Hinzugefügt **Auskunft nach Art. 15 DSGVO** - `lib/dsgvo/auskunft.ts` sammelt alles, was über eine Person vorliegt: Stammdaten, Zugang, Rollen, Qualifikationen, arbeitsmedizinische Vorsorge, Belastungsübungen, Einsatz- und Übungsbeteiligung, Anwesenheiten, Fahrten, Beiträge, Benachrichtigungswege, Ausweise. Dreizehn Abschnitte. - **Über jedem Abschnitt steht, wozu die Daten vorliegen.** Art. 15 Abs. 1 verlangt nicht nur die Daten, sondern auch Zwecke, Empfänger und den Hinweis auf die weiteren Rechte. Eine Liste ohne das sieht aus wie eine Auskunft und ist keine. - **Gesundheitsdaten sind als besondere Kategorie nach Art. 9 gekennzeichnet** — G26 und Belastungsübungen. - **Leere Abschnitte bleiben stehen.** „Dazu liegt nichts vor" ist eine Auskunft; ein weggelassener Abschnitt wirft die Frage auf, ob da doch etwas ist. - **Was die Person getan hat, wird gezählt, nicht mitgeliefert.** Prüfprotokolle und Mängelmeldungen gehören zum Nachweisbestand der Wehr. Vollständig ausgeliefert käme jeder über eine Selbstauskunft an die Geräteakten — die Auskunftspflicht wäre eine Hintertür. - Ein Durchgang liefert alles: die Anzeige, ein PDF für die Akte und JSON nach Art. 20. Bewusst nicht drei Knöpfe, von denen zwei vergessen werden können. - Der Vorgang wird protokolliert. Dass eine Auskunft erteilt wurde, gehört festgehalten — sie legt alles offen, was über einen Menschen gespeichert ist. **Sicherung und Wiederherstellung** - `npm run sicherung` — Datenbank über `pg_dump` im Container, dazu alle Dateien aus MinIO, dazu ein Manifest. Beides aus demselben Moment: eine Datenbank von heute mit Dateien von gestern ergibt Verweise ins Leere. - `npm run wiederherstellen` — mit Probelauf als Vorgabe. Ohne `--wirklich` wird nur angezeigt, was geschähe. - Nach dem Zurückspielen setzt das Skript die Sperren der Anwendungsrolle neu und **prüft danach nach**, ob es gewirkt hat. - `scripts/sicherung-sperren.ts` ist die eine Liste, aus der Wiederherstellen und Prüfen entstehen. Ein Test gleicht sie gegen die migrierte Datenbank ab — läuft sie auseinander, wäre der Schaden lautlos. **Pipeline** - `.gitea/workflows/pruefung.yml` mit zwei Aufträgen: `pruefung` (typecheck, lint, Migrationen, Seed, 599 Unit-Tests) bei jedem Push, `oberflaeche` (E2E) auf `main` und in Pull Requests. - Die Datenbank wird aus `docker/postgres` gebaut statt aus einem fertigen Image: FLORIAN braucht PostGIS **und** pgvector. Ein Dienst-Container mit `postgres:17` ließe die Migrationen durch und scheiterte erst bei der ersten Umkreissuche. - Der Seed läuft mit, weil er selbst ein Test ist. ### Behoben - **Eine Wiederherstellung entfernte alle Unveränderlichkeitsgarantien.** Der Dump lief mit `--no-acl`, die Rechte kamen also nicht zurück; und selbst ohne dieses Flag greift beim Zurückspielen die Vorgabeprivilegierung aus `docker/postgres/10-init-app-role.sh` — jede von `pg_restore` neu angelegte Tabelle bekommt automatisch alle Rechte, und `GRANT` kann nur hinzufügen, nie wegnehmen. Nach einer Wiederherstellung durfte die Anwendungsrolle das Audit-Log ändern, Wiki-Fassungen löschen und Kiosk-Vorgänge umschreiben. Nichts hätte darauf hingewiesen. Gefunden haben es vier Datenbanktests unmittelbar nach der ersten Rückspielprobe — also genau die Probe, wegen der 1.0.0 sie verlangt. - **Ein E2E-Test klickte, bevor die Seite lebendig war.** Unter Last dauert das Hydrieren länger; das Formular verschickte sich dann selbst, und die Fehlerfläche, auf die der Test wartete, entstand nie — obwohl der Server richtig ablehnte. Er wartet jetzt auf einen client-seitig erzeugten Knopf. ### Nachweise - Sicherung und Wiederherstellung einmal vollständig durchgeführt: ein nach der Sicherung angelegter Datensatz war danach weg, alle Zahlen stimmten wieder, und **599 Tests liefen nach dem Zurückspielen grün** — einschließlich derer, die die Sperren prüfen. --- ## [0.11.0] — 2026-09-05 Phase 10: Mandantenfähigkeit. Ein Konto, mehrere Wehren, eine Kreisebene, die zusieht — und eine Verwaltung für den, der die Installation betreibt. **Das Anmeldekonto ist von der Personenakte gelöst.** Bis hierher war die Akte zugleich das Konto: Passwort-Hash und OIDC-Subject hingen an `person`, und `person` gehört genau einer Wehr. Ein Mensch konnte damit nicht zu zwei Wehren gehören, ohne zwei Passwörter zu haben — und die Anmeldung per E-Mail war mehrdeutig, sobald dieselbe Adresse in zwei Wehren stand, weil die Eindeutigkeit nur je Organisation galt. **Das Konto trägt keine Mandantenkennung — und kann keine tragen.** Es ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehört. Geschützt wird es deshalb wie `Berechtigung` aus Phase 0: über Rechte statt über Row Level Security. Was ein Konto in einer Wehr darf, entscheidet nicht es selbst, sondern seine `Mitgliedschaft` — und die trägt `organisationId` wie jede andere Tabelle. **Die Kreisebene sieht und ändert nichts.** Ihre Rechte werden nicht aufgezählt, sondern gefiltert: alles, was `.lesen` heißt, und sonst nichts. Eine Liste von Hand wäre bei der nächsten Phase veraltet, und zwar unbemerkt. ### Hinzugefügt **Datenmodell** - `Benutzer` — das Anmeldekonto. Global eindeutige E-Mail, ein Passwort, ein OIDC-Subject. Ohne `organisationId`, mit Begründung im Schema. - `Mitgliedschaft` — die Zugehörigkeit eines Kontos zu einer Wehr, als `MITGLIED` (mit Personenakte) oder `AUFSICHT` (ohne). - `Person` verliert die Anmeldefelder. Der Passwort-Hash kann damit aus einer Personenabfrage gar nicht mehr herausfallen — die drei Stellen, die ihn bisher vorsichtshalber abgeschnitten haben, sind entfallen. **Was die Datenbank durchsetzt** (Migrationen `20260905090000` und `20260905091500`) - RLS für `mitgliedschaft` wie für jede andere Tabelle. Eine Wehr sieht damit auch nicht, in welchen anderen ein Konto noch geführt wird. - Trigger `mitgliedschaft_akte_gleiche_wehr` — eine Zuordnung, deren Personenakte in einer anderen Wehr liegt, wird abgelehnt. Ein Fremdschlüssel prüft nur, *dass* es die Akte gibt, nicht *wo* sie liegt; ohne den Trigger ließe sich ein Konto über eine fremde Akte in eine Wehr hängen. - Trigger `benutzer_kennung_unveraenderlich` — E-Mail, OIDC-Subject und das Verwaltermerkmal lassen sich nicht aus einer Wehr heraus ändern. - `REVOKE INSERT, DELETE ON benutzer` für die Anwendungsrolle. - CHECKs: `MITGLIED` braucht eine Akte, `AUFSICHT` darf keine haben; ein Konto braucht einen Anmeldeweg; eine E-Mail ohne @ ist keine. **Die Umstellung ist datenerhaltend.** Jede Person mit Anmeldedaten bekommt ein Konto und eine Mitgliedschaft, bevor die alten Spalten fallen. Wer schon mit derselben Adresse in zwei Wehren stand, bekommt **ein** Konto mit **zwei** Mitgliedschaften — genau das, was vorher nicht möglich war. Eine Prüfung am Ende bricht die Migration ab, statt einen halben Zustand zu hinterlassen. **Oberfläche** - **Wehrwähler** in der Seitenleiste. Er erscheint nur bei mehr als einer Wehr — ein Auswahlfeld mit genau einem Eintrag ist eine Frage ohne Antwortmöglichkeit. - Nach der Anmeldung öffnet die **eigene** Wehr, nicht die betreute. - Ein dauerhafter Hinweis, wenn man eine Wehr als Kreisaufsicht sieht — nicht als kurz aufblitzende Meldung. - **`/wehren`** für den Verwalter: Wehren anlegen (samt Systemrollen), Konten führen, Zuordnungen vergeben und beenden. Jede Zuordnung wird in der betroffenen Wehr protokolliert, nicht beim Verwalter — sonst hätte der Betreiber einen Weg, sich still in fremde Daten zu setzen. **Rechte** - Neue Rolle `AUFSICHT` — bewusst **nicht** im Prisma-Enum: sie wird nicht vergeben, sondern ergibt sich aus der Mitgliedschaftsart. - Ausdrücklich ausgenommen, obwohl sie auf `.lesen` enden: `kasse.lesen` (die Kasse gehört dem Verein oder der Kommune, nicht der Kreisaufsicht), `person.export`, `system.auditlog`, `ki.protokoll`. - `istVerwalter` als Merkmal des Kontos, nicht als Recht. **Tests** - 19 neue Unit-Tests, davon 11 gegen eine echte Datenbank: Doppelmitgliedschaft, der Trigger gegen wehrfremde Akten, die CHECKs und die Trennung der Zuordnungen über RLS. - 13 E2E-Tests: kein Wähler bei einer Wehr, Wechsel in die betreute und zurück, die Mitglieder der anderen Wehr, die verschlossene Kasse, und dass der Administrator die Umstellung überlebt hat. **Seed** - Eine zweite, kleine Wehr je Mandant und ein Konto, das in der einen Mitglied und in der anderen Kreisaufsicht ist. Ohne sie ließe sich die Phase nicht ausprobieren — und eine Mandantentrennung, die nie zwei Mandanten hatte, ist keine geprüfte. ### Geändert - Eine Sitzung ohne Personenakte ist gültig. Bis hierher galt genau das als „nicht angemeldet"; für die Kreisaufsicht, die in der betreuten Wehr keine Akte hat, hätte jeder Aufruf auf der Anmeldeseite geendet. - `Sitzung.personId` ist nullbar. Der Typechecker hat damit die fünf Stellen gezeigt, an denen eine Aufsichtssitzung nichts verloren hat — Anmeldemarke, Dienstausweis, Prüfungswizard, Dateiupload, Atemschutz. - Die Mitgliederansicht zeigt statt eines ungenutzten Zeitstempels, ob jemand überhaupt ein Konto hat. Das ist die nützlichere Auskunft: ohne Konto kommt er nicht in FLORIAN hinein. ### Behoben - **Ein Testaufbau war nicht wiederholbar.** `ki-datenbank.test.ts` legte Wehr und Person idempotent an, die Wiki-Seite aber nicht — blieb aus einem abgebrochenen Lauf etwas stehen, scheiterte der ganze Datei-Lauf an einer Eindeutigkeit, die mit dem Geprüften nichts zu tun hat. - **`db:test-reset` ließ die Nachbarwehr stehen** und der Seed hielt daraufhin alles für erledigt — die Mitgliedschaft in der neu angelegten Testwehr entstand nie wieder. Der Reset nimmt sie jetzt mit, und der Seed prüft Nachbarwehr und Kreiskonto getrennt statt mit einem gemeinsamen Ausstieg. - **Der Fehlversuch-Test sperrte das gemeinsame Testkonto.** Fünf Fehlversuche sperren ein Konto für fünf Minuten; bei zwei Projekten und Wiederholungen kam das im vollen Lauf zusammen, und fremde Testdateien fielen mit „Anmeldung fehlgeschlagen“ aus. Er nimmt jetzt eine Adresse, die es nicht gibt — die Meldung muss ohnehin dieselbe sein. --- ## [0.10.0] — 2026-09-04 Phase 9: Kassenverwaltung. Kassenbuch, Mitgliedsbeiträge, Rechnungen an Dritte, Kassenprüfung und die Auswertungen für die Jahreshauptversammlung. **Eine festgeschriebene Buchung ist unveränderbar.** Nicht, weil ein Zweig im Router es so vorsieht, sondern weil die Datenbank es ablehnt. Korrigiert wird über eine Gegenbuchung mit Verweis; beide Buchungen bleiben stehen, und im Kassenbuch ist zu sehen, dass etwas berichtigt wurde. Genau das will ein Kassenprüfer sehen — eine verschwundene Buchung dagegen kann er nicht prüfen. **Der Kassenabschluss schließt seinen Zeitraum.** Danach wird keine Buchung mit einem Datum darin mehr angenommen, auch keine nachträglich hineindatierte. Ohne diese zweite Hälfte stimmte der geprüfte Bestand nach einem Datumswechsel nicht mehr — und niemand hätte es bemerkt. **Gerechnet wird mit `Prisma.Decimal`, nirgends mit Fließkomma.** `0.1 + 0.2` ergibt in JavaScript 0.30000000000000004. In einer Kasse ist das kein Rundungsfehler, sondern eine Differenz in der Prüfung, die niemand erklären kann. ### Hinzugefügt **Datenmodell** - `Kassenkonto` (bar, Bank, Rücklage) mit Anfangsbestand. Der laufende Bestand wird **gerechnet und nie gespeichert**: ein gespeicherter Bestand neben einer nachgetragenen Buchung wäre falsch, ohne falsch auszusehen. - `Buchungskategorie` als zweistufiger Baum, mit Kontonummer für den Export an die Kämmerei. - `Buchung` mit Datum, Richtung, immer positivem Betrag, Verwendungszweck, Belegnummer, Beleg-Datei und dem Verweis auf die Gegenbuchung. - `Beitragssatz` und `Beitrag` (offen, bezahlt, erlassen, gemahnt) mit Mahnstufe und Erlassbegründung. - `Rechnung` und `Rechnungsposition` mit Nummernkreis je Jahr. Die Menge hat drei Nachkommastellen — Einsatzstunden werden viertelstündlich abgerechnet. - `Kassenabschluss` und `Abschlussposten` mit Bestand laut Buch, gezähltem Bestand, Differenz, zwei Prüfern und dem Bericht als PDF. **Was die Datenbank durchsetzt** (Migration `20260904140000`) - RLS für alle neun Tabellen, wie in jeder Phase zuvor. - Trigger `buchung_unveraenderlich` — festgeschriebene Buchungen lassen sich weder ändern noch löschen. Änderbar bleibt genau eines: der Verweis auf die Gegenbuchung. Eine Stornierung entsteht später; sie einzutragen ist keine Änderung des Vorgangs, sondern seine Fortschreibung. - Trigger `buchung_zeitraum_offen` — ein geprüfter Zeitraum nimmt weder eine neue Buchung an noch das Hineindatieren einer vorhandenen. - Trigger `abschluss_unveraenderlich` — ein festgeschriebener Abschluss bleibt, wie er ist. Nachtragen lässt sich nur der Prüfbericht, denn das PDF entsteht im Hintergrund und damit nach dem Festschreiben. - `REVOKE DELETE ON kassenabschluss` für die Anwendung. - CHECKs, die verhindern, was sich hinterher nicht mehr erkennen ließe: ein Betrag von null oder darunter, eine Buchung an eine Person **und** einen Dritten zugleich, ein erlassener Beitrag ohne Begründung, ein Abschluss mit einer Differenz, die nicht zu seinen eigenen Zahlen passt, eine Rechnung, die fällig wird, bevor sie geschrieben wurde. **Kassenbuch** - Buchungsmaske mit **Kategorievorschlag**: zuerst aus den eigenen früheren Buchungen der Wehr, erst danach aus einer kurzen Stichwortliste. Unter der Hälfte gemeinsamer Wörter schweigt der Vorschlag — ein falscher wird übernommen, ohne gelesen zu werden. - Summen über **alle** Treffer, nicht nur über die angezeigte Seite. - Festschreiben als Sammelaktion bis zu einem Stichtag. - Gegenbuchen mit Pflichtbegründung; das Buchungsdatum ist immer heute, damit ein geprüfter Zeitraum zubleibt. - **Aus einem gegengezeichneten Beleg wird eine Buchung.** Die Brücke zu Phase 8: fotografiert und ausgefüllt wird am Terminal oder am Handy, gebucht wird in der Kasse — mit Konto und Kategorie, die keine Maschine entscheiden kann. Das Foto hängt danach an der Buchung, und genau das verlangt eine Kassenprüfung: zu jeder Ausgabe ein Beleg, an der Buchung und nicht in einem Ordner daneben. Ein Entwurf lässt sich nicht buchen. **Kontoauszug einlesen** - CAMT.053 und MT940, beides ohne zusätzliche Abhängigkeit. Erkannt wird am Inhalt, nicht an der Dateiendung. - Es entstehen **Entwürfe, keine fertigen Buchungen**. - Jeder Umsatz trägt eine Kennung — die der Bank, sonst eine aus Datum, Betrag und Zweck gebildete. Ein zweimal eingelesener Auszug wird dadurch als solcher erkannt, statt einen Monat doppelt zu buchen. - Was nicht gelesen werden konnte, steht mit Grund daneben, statt still zu verschwinden. **Beiträge** - Sätze je Abteilung, gültig ab einem Jahr und nicht rückwirkend änderbar. - Beitragslauf mit **Vorschau**: wie viele Beiträge entstehen, welche Summe — und wer keinen passenden Satz hat. Der Lauf ist wiederholbar, ohne dass etwas doppelt entsteht. - Zahlungseingang erzeugt zugleich die Buchung. Ein Häkchen ohne Geld in der Kasse wäre eine Behauptung. - Mahnen als Sammelaktion, Erlassen nur mit Begründung. - Rückstandsliste über alle Jahre — die Ansicht, in der auffällt, wer seit drei Jahren offen steht. **Rechnungen** - Fortlaufende Nummer je Jahr, lückenlos: eine fehlende Nummer muss in der Kassenprüfung erklärt werden. - Der Betrag ist die Summe der Positionen, gerechnet und nicht eingegeben. - Bis zum Versenden ein Entwurf, danach unveränderbar. Korrigiert wird über Storno und eine neue Rechnung — die stornierte bleibt mit Grund stehen. - PDF über Gotenberg, im Hintergrund erzeugt. **Kassenprüfung und Auswertung** - Prüfmaske: links der Bestand laut Buch, rechts der gezählte. In dieser Reihenfolge — wer erst das Buch sieht und dann zählt, zählt, bis es passt. - Ohne Prüfer und Prüfdatum lässt sich nichts festschreiben; eine Differenz ohne Erläuterung ebenso wenig. - Kassenprüfbericht als PDF mit vier Unterschriftsfeldern. - Jahresauswertung nach Kategorie und Monat, mit Vorjahresvergleich. - CSV-Export mit Semikolon und BOM — was Excel in Deutschland ohne Rückfrage richtig öffnet. **Tests** - 67 neue Unit-Tests: Geldrechnen, beide Auszugsformate, Kategorievorschlag, und dreizehn gegen eine echte Datenbank, die die Trigger und CHECKs prüfen. Sie laufen über den Systemzugang: kommt nicht einmal der Eigentümer der Datenbank daran vorbei, kommt niemand vorbei. - 13 E2E-Tests über Desktop und Tablet. **Seed** - Ein vollständiges Kassenjahr: drei Konten, der Standardkontenrahmen, 24 Buchungen über zwei Jahre, die Mitgliedsbeiträge mit Rückständen, zwei Rechnungen und die festgeschriebene Kassenprüfung des Vorjahres. Die Zahlen des Abschlusses sind die Summe der Buchungen und nicht erfunden. ### Geändert - Ein E2E-Test aus Phase 0 sicherte zu, dass die Kasse in der Navigation **kein** Verweis ist — sie war ja noch nicht da. Die Zusicherung ist mit dieser Phase abgelaufen und prüft jetzt das Gegenteil. Ein künftiges Modul zum Ausgrauen gibt es nicht mehr; kommt später eines dazu, gehört sie zurück. ### Behoben - **Die Rechnungsnummer war ein Wettlauf.** Höchste lesen und dann schreiben: schreiben zwei Leute gleichzeitig eine Rechnung, lesen beide dieselbe höchste Nummer, und die zweite läuft in den Eindeutigkeitsfehler. Gefunden hat das der parallele E2E-Lauf. Der seltene Zusammenstoß wird jetzt erkannt und die nächste Nummer genommen — lückenlos bleibt sie dabei. - **Eine Organisation ließ sich nicht mehr löschen**, sobald sie eine festgeschriebene Buchung hatte: der Trigger lehnte auch das Löschen im Aufbewahrungslauf ab. Er kennt jetzt denselben Schalter wie das Audit-Log und das Prüfprotokoll (`app.audit_bereinigung`). Ohne ihn wäre die DSGVO-Löschung einer ausscheidenden Wehr nicht durchführbar gewesen. --- ## [0.9.1] — 2026-09-04 Aufräumarbeiten vor Phase 9. Drei Punkte, die sich über mehrere Phasen angesammelt hatten und in jeder Phase Zeit gekostet haben. ### Hinzugefügt - **ESLint** hat endlich eine Konfiguration (`eslint.config.mjs`, Flat Config). Bis dahin gab es das Skript `npm run lint`, aber nichts, was es hätte prüfen können — ein Werkzeug, das aussieht, als prüfe es etwas, und nichts prüft, ist schlechter als gar keines. Bewusst **ohne** Formatierungsregeln: die macht Prettier. Dazu `npm run lint:fix`. - **Eine eigene Wehr für die E2E-Tests.** `npm run db:test-reset` legt `ff-teststelle` neu an — vollständig, mit denselben Beispieldaten, aber unter eigener Kennung. Ein Riegel im Skript bricht ab, wenn die Testkennung mit der der Beispielwehr übereinstimmt. - `tests/e2e/wehr.ts` — Zugangsdaten und Anmeldung an einer Stelle statt achtmal wortgleich in acht Dateien. Schlägt die Anmeldung fehl, sagt die Meldung, dass die Testwehr fehlt, statt zweihundertmal „Timeout". ### Behoben - **Die E2E-Tests schrieben in die Beispielwehr.** Nach fünf Phasen standen dort 120 Atemschutz-Überwachungen statt einer, 115 Geräte statt 45 und 39 Einsätze statt 8. Zweimal fielen Tests aus, weil ein gesuchter Eintrag von Seite 1 gerutscht war; bei jedem Lauf unterschieden sich alle 72 Bildschirmfotos. Der Bestand ist einmal zurückgesetzt, die Tests laufen jetzt gegen die eigene Wehr. - **`
` in einem `

` auf der Einsatzseite** — die Meldung stand seit Phase 3 in der Konsole und ist der klassische Auslöser für einen Hydration-Fehler. Die Wurzel war das `Badge`: es rendert jetzt ein `` statt eines `

`. Ein Abzeichen ist Fließtext-Inhalt, und die Darstellung ändert sich dadurch nicht. - **Die Fehlerfläche der Karte konnte nie etwas anzeigen** — der Zustand dahinter wurde nirgends gesetzt. Eine gescheiterte Abfrage sah damit aus wie „keine Hydranten vorhanden". Gefunden hat das der neue Linter. - **Die Lagekarte rechnete bei jedem Rendern neu.** Ein `?? []` erzeugte jedes Mal ein neues leeres Feld und machte damit zwei `useMemo` wirkungslos — bei einer Karte mit dreißig Elementen und einem Ereignisstrom im Zwei-Sekunden- Takt kein akademischer Punkt. - Deutsche Anführungszeichen in vier Ansichten schlossen mit einem geraden Zoll-Zeichen statt mit dem typografischen. - Unbenutzte Importe in fünf Dateien. ### Geändert - `npm run lint` ruft jetzt `eslint .` statt des in Next 15.5 abgekündigten `next lint`. - **`tests/e2e` ist nicht mehr vom Typecheck ausgenommen.** Die Ausnahme hat beim Umbau einen doppelt deklarierten Bezeichner durchgelassen, den erst der Testlauf gemeldet hat — `npx tsc --noEmit` hatte geschwiegen. --- ## [0.9.0] — 2026-09-04 Phase 8: Lokale KI. Ein Wiki mit semantischer Suche, Diktat, Berichtsassistent und Belegerfassung — alles gegen selbst betriebene Modelle. **Nichts, was ein Modell erzeugt, gelangt ungeprüft in einen Nachweis.** Jede KI-Ausgabe ist zuerst ein eigener Datensatz und nicht ein Feld im Zieldokument. Sie ist gekennzeichnet, sie steht daneben statt mittendrin, und sie braucht zwei gleich große Knöpfe, bevor etwas mit ihr geschieht. **Es läuft alles im Haus.** Ollama und faster-whisper als Container daneben, kein Anbieter, kein Schlüssel. Es gibt keinen Rückfall auf einen Cloud-Dienst: fehlen die Container, fehlt die Funktion. ### Hinzugefügt **Datenmodell** - `KiVorgang` — jede Befragung eines Modells mit Eingabe, Ausgabe, Modell, Dauer und der Entscheidung darüber. Auch Fehlschläge: ein Modell, das dreimal am Tag in eine Zeitüberschreitung läuft, ist eine Information. - `Wikiseite`, `Wikiabschnitt` (mit `vector(768)`) und `Wikiversion`. - `Diktat` mit Aufnahme in MinIO, `Belegentwurf` mit allen Feldern optional — ein Modell, das den Betrag nicht findet, soll nichts erfinden. **Postgres mit pgvector** - Eigenes Image (`docker/postgres/Dockerfile`), weil FLORIAN PostGIS **und** pgvector braucht und es kein offizielles Image mit beidem gibt. - HNSW-Index statt IVFFlat: IVFFlat muss auf gefülltem Bestand trainiert werden, HNSW ist ab der ersten Zeile brauchbar. Ein Wiki wächst Seite für Seite — es gibt nie den Moment, an dem man den Index einmal aufbaut. **Wiki** - Markdown, selbst gerendert **ohne HTML**: der Text kommt von Anwendern, und jeder Weg über `dangerouslySetInnerHTML` wäre ein Weg für ein `