1389 lines
66 KiB
Markdown
1389 lines
66 KiB
Markdown
# 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).
|
||
|
||
## [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.
|
||
- **`<div>` in einem `<p>` 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 `<span>`
|
||
statt eines `<div>`. 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 `<script>` in
|
||
einer Dienstvorschrift.
|
||
- Zwei Suchen: Wortsuche (immer, mit Textstelle) und Frage ans Modell
|
||
(semantisch, mit Fundstellen). Steht die Antwort nicht in den Unterlagen,
|
||
sagt FLORIAN das — ohne passende Auszüge wird gar nicht erst gefragt.
|
||
- Zerlegt wird an den Überschriften, mit der Überschriftenkette im
|
||
Einbettungstext: ein Abschnitt über „Rückzugsdruck" ohne das Wort
|
||
„Atemschutz" darüber findet sich sonst nicht.
|
||
- Jede Änderung schreibt die vorherige Fassung weg.
|
||
|
||
**Diktat, Bericht, Beleg**
|
||
|
||
- Aufnahme im Browser, Erkennung über faster-whisper im Hintergrund. Die
|
||
Aufnahme bleibt, solange nicht entschieden ist — sie ist das einzige
|
||
Original.
|
||
- Berichtsentwurf aus Stichpunkten. Zeitstempel und Einsatznummer bekommt das
|
||
Modell **nicht**: sie stehen schon als Felder im Bericht.
|
||
- Belegerfassung mit Vision-Modell. Nicht erkannte Felder bleiben leer und
|
||
gelb hinterlegt statt geraten zu werden.
|
||
|
||
**Zwei Protokolle**
|
||
|
||
- `/ki-protokoll` — was die Maschine getan hat, mit Kennzahlen und
|
||
aufklappbarer Eingabe/Ausgabe.
|
||
- `/protokoll` — das Audit-Log. Wird seit Phase 0 geschrieben; die Ansicht
|
||
dazu kam jetzt, weil beides nebeneinander gehört: was hat ein Mensch getan,
|
||
was die Maschine.
|
||
|
||
### Geändert
|
||
|
||
- `lib/integrations/llm.ts` kann jetzt drei Dinge statt einem: Text,
|
||
Einbettungen und Bilder — mit je eigenem Modell, weil ein Rechner ohne
|
||
Grafikkarte das Einbettungsmodell noch schafft und das Vision-Modell nicht.
|
||
- Der Datei-Upload nimmt Tonaufnahmen an (webm, ogg, mp4, wav).
|
||
- Neue Rechte: `wiki.lesen` als Grundrecht (eine Wehr, in der nur die Führung
|
||
nachschlagen darf, hat kein Wiki, sondern einen Aktenschrank),
|
||
`wiki.bearbeiten`, `ki.nutzen`, `ki.protokoll`, `beleg.erfassen`.
|
||
- Der Aufbewahrungslauf zieht nebenbei Wiki-Seiten nach, deren Suchstand
|
||
veraltet ist.
|
||
|
||
### Behoben
|
||
|
||
- Beim Zerlegen einer Wiki-Seite wurde ein kurzer Abschnitt an den vorherigen
|
||
gehängt, auch wenn er unter einer anderen Überschrift stand. Unter einer
|
||
Antwort hätte dann eine Fundstelle gestanden, in der der zitierte Satz gar
|
||
nicht vorkommt. Gefunden hat es der Test, nicht das Ausprobieren.
|
||
|
||
### Sicherheit
|
||
|
||
- Row Level Security für die sechs neuen Tabellen — auch für die Vektorsuche:
|
||
ein perfekt passender Abschnitt einer fremden Wehr taucht nicht auf.
|
||
- Eingabe, Ausgabe, Modell und Art eines KI-Vorgangs sind nach dem Anlegen
|
||
unveränderlich (Trigger). Über einen Vorschlag wird genau einmal
|
||
entschieden. `REVOKE DELETE ON "ki_vorgang"`.
|
||
- `REVOKE UPDATE, DELETE ON "wikiversion"`: eine Dienstvorschrift, deren
|
||
Vorgeschichte sich bereinigen lässt, ist als Nachweis wertlos.
|
||
- Prüfbedingungen: keine Entscheidung ohne Zeitpunkt, kein Fehlschlag ohne
|
||
Grund, kein negativer Betrag auf einer Quittung.
|
||
|
||
### Tests
|
||
|
||
- 73 neue Unit-Tests: Zerlegen einer Seite (Überschriftenketten, Codeblöcke,
|
||
Teilung an Absatzgrenzen), Auswerten einer Modellantwort auf ein Belegfoto
|
||
(deutsche Zahlen, Tausenderpunkt, unmögliche Daten), die Anweisungen an das
|
||
Modell und die Datenbankregeln samt Vektorsuche über Mandanten hinweg.
|
||
- 14 neue E2E-Tests, die **ohne laufendes Modell** prüfen, dass alles andere
|
||
vollständig benutzbar bleibt und ehrlich sagt, was fehlt.
|
||
|
||
---
|
||
|
||
## [0.8.0] — 2026-09-04
|
||
|
||
Phase 7: Benachrichtigungen und Termine. Ab hier weiß die Mannschaft, wann der
|
||
nächste Dienst ist — ohne dass jemand eine Rundmail tippt.
|
||
|
||
**Niemand wird ungefragt benachrichtigt.** Wer nichts einträgt, bekommt nichts.
|
||
Es gibt keine Voreinstellung, die stillschweigend alle anmeldet: ein neu
|
||
eingerichteter Zustellweg hat eine leere Ereignisliste, und erst wer ankreuzt,
|
||
wofür er ihn will, bekommt etwas.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Termin` mit Art, Ort, Beginn und Ende, Leitung, Platzbegrenzung,
|
||
Anmeldeschluss und Serienkennung. Abgesagt wird mit Grund und Zeitpunkt —
|
||
**gelöscht wird nicht**, wer zugesagt hat, soll den Grund sehen.
|
||
- `Terminanmeldung` mit Zu-, Absage oder Warteliste, dem Grund einer Absage und
|
||
der Anwesenheit als eigenem Feld. Zusage und Anwesenheit sind bewusst
|
||
getrennt: wer zugesagt hat und nicht kam, steht genau so in der Liste.
|
||
- `Zustellkanal` — ein Datensatz je Person und Weg, mit der Adresse dieses Wegs
|
||
und der Liste der Ereignisse, für die er benutzt wird. Leer bedeutet: für
|
||
nichts.
|
||
- `Benachrichtigung` — steht **vor** dem Versand in der Tabelle, nicht danach.
|
||
Damit ist nachvollziehbar, wer wann worüber informiert wurde und wer nicht,
|
||
weil sein Kanal nicht erreichbar war.
|
||
- `Benachrichtigungsvorlage` je Ereignis und Kanal, `Kalenderzugang` mit
|
||
SHA-256 des Tokens.
|
||
|
||
**Terminkalender**
|
||
|
||
- Liste mit Filter nach Art und „nur meine", Rückmeldung direkt aus der Zeile.
|
||
- Serientermine werden **ausgeschrieben**, nicht als Regel gespeichert: jeder
|
||
Abend ist ein eigener Datensatz. Sonst wäre jeder ausgefallene Übungsabend
|
||
ein Sonderfall. Vor dem Anlegen steht in der Maske, wie viele Termine daraus
|
||
werden.
|
||
- Kalenderrechnung statt Millisekunden: „monatlich" heißt derselbe Tag im
|
||
nächsten Monat, und der 31. Januar landet im Februar auf dem 28. — nicht im
|
||
März, wie es passiert, wenn man stur Monate addiert.
|
||
- **Warteliste, die von selbst nachrückt.** Wer bei belegten Plätzen zusagt,
|
||
landet auf ihr; sagt jemand ab, rückt der Erste nach und erfährt es.
|
||
- Absage mit Grund, wahlweise für alle folgenden Termine einer Serie.
|
||
Vergangene bleiben unberührt. Benachrichtigt werden die Angemeldeten, nicht
|
||
die ganze Wehr.
|
||
- Anwesenheitsliste über **alle aktiven Mitglieder**, nicht nur die
|
||
Angemeldeten — auf einem Blatt zum Abhaken muss jeder stehen. Zum Abhaken am
|
||
Tablet oder zum Ausdrucken.
|
||
|
||
**Kalenderabonnement**
|
||
|
||
- ICS-Feed nach RFC 5545 unter `/api/kalender/<token>.ics`. Die Termine der
|
||
Wehr stehen damit im Handy neben allem anderen.
|
||
- **Bewusst kein CalDAV**: das ist ein Schreib-Lese-Protokoll mit
|
||
Rechteverwaltung und Konfliktauflösung. Gebraucht wird nur Lesen.
|
||
- Die Adresse steht genau einmal auf dem Bildschirm; gespeichert wird nur ihr
|
||
Hash. Jeder Zugang lässt sich einzeln widerrufen, und wer austritt, verliert
|
||
ihn.
|
||
- Ausgeliefert werden nur Termine — keine Anmeldelisten, keine Personen außer
|
||
der Leitung.
|
||
|
||
**Vier Zustellwege, hinter Schnittstellen**
|
||
|
||
- **SMTP** löst den Platzhalter aus Phase 0 ab: nodemailer mit
|
||
Verbindungspool, damit ein Sammellauf mit vierzig Nachrichten Sekunden statt
|
||
Minuten braucht.
|
||
- **ntfy** (neu, selbst gehostet, Compose-Profil `push`): Push aufs Handy ohne
|
||
Konto, ohne Telefonnummer, ohne Telefonbuchabgleich. Läuft der Server im
|
||
Gerätehaus, verlässt keine Nachricht das Haus.
|
||
- **Web-Push** (neu, VAPID): Push in den Browser ohne App. Schlüsselpaar über
|
||
`npm run vapid`.
|
||
- **Telegram** aus Phase 0 wird jetzt tatsächlich benutzt.
|
||
- Was das Netz der Wehr verlässt (Telegram, Web-Push), bekommt **nur die
|
||
Kurzfassung** — „3 Prüfungen fällig", nicht welche. In der Oberfläche steht
|
||
das neben jedem externen Kanal.
|
||
|
||
**Textvorlagen**
|
||
|
||
- Je Ereignis und Kanal, mit eingebautem Standardtext für alles. Eine Wehr, die
|
||
nichts einstellt, bekommt trotzdem brauchbare Nachrichten.
|
||
- Platzhalter zum Anklicken, Vorschau mit erfundenen Beispielwerten. Ein
|
||
Platzhalter, den es für das Ereignis nicht gibt, wird beim Speichern
|
||
abgewiesen — mit der Liste der erlaubten daneben.
|
||
- Ein unbekannter Platzhalter bleibt im Text **sichtbar stehen**, statt still
|
||
zu verschwinden. Eine Lücke, die niemand bemerkt, ist schlimmer als ein
|
||
seltsam aussehender Text, den jemand korrigiert.
|
||
|
||
**Digitaler Dienstausweis**
|
||
|
||
- QR-Code auf dem Handy, Prüfseite ohne Anmeldung unter `/ausweis/<code>`.
|
||
Sie zeigt Name, Wehr, Funktion, Foto und die gültigen Qualifikationen.
|
||
- Signiert statt gespeichert (HMAC über Person, Wehr und Zeitstempel), **fünf
|
||
Minuten gültig**. Die Prüfseite nennt den Zeitpunkt der Ausstellung — ein
|
||
Ausweis, der „gültig" sagt, ohne zu sagen seit wann, ist wertlos.
|
||
- Der Zweck steht im signierten Teil, damit ein Ausweis nicht als Anmeldemarke
|
||
aus Phase 4 durchgeht und umgekehrt.
|
||
- Ein gültig signierter Code für jemanden, der ausgetreten ist, wird
|
||
abgewiesen. Das Passbild ist über denselben Code erreichbar und damit genauso
|
||
lange gültig.
|
||
|
||
**Was von selbst benachrichtigt**
|
||
|
||
- Prüfung fällig, Mangel gemeldet, Qualifikation läuft ab (90/30/7 Tage
|
||
vorher), neuer Termin, Terminerinnerung (7 und 1 Tag vorher), Einsatzbericht
|
||
freigegeben.
|
||
- **Anlegen und Zustellen sind getrennt.** Wo ein Ereignis entsteht, wird nur
|
||
eine Zeile geschrieben; der Worker schickt sie weg. Sonst hinge das Speichern
|
||
eines Termins am Antwortverhalten von Telegram.
|
||
- Erinnert wird um sieben Uhr morgens, zugestellt jede Minute: eine
|
||
Push-Meldung um drei Uhr weckt Leute, die am nächsten Tag arbeiten müssen —
|
||
eine Absage am Dienstagabend muss dagegen ankommen, solange sie noch etwas
|
||
nützt.
|
||
- Die Sammelmail an den zuständigen Gerätewart bleibt, wie sie war. Wer sie
|
||
ohnehin bekommt, wird von der zusätzlichen Benachrichtigung ausgenommen und
|
||
hat sie damit nicht doppelt.
|
||
|
||
### Geändert
|
||
|
||
- `lib/integrations/mail.ts` verschickt tatsächlich, statt eine Meldung ins Log
|
||
zu schreiben. Ohne `MAIL_ENABLED` bleibt es beim Log.
|
||
- Neue Rechte: `termin.lesen` und `benachrichtigung.eigene` als Grundrechte
|
||
(jeder sieht den Kalender und richtet seine eigenen Wege ein — das ist keine
|
||
Sache des Kommandanten), `termin.bearbeiten`, `termin.anwesenheit` und
|
||
`benachrichtigung.vorlagen` bei Kommandant, Schriftführer und Ausbilder.
|
||
- `DIENSTGRAD_TEXT` liegt jetzt in `lib/stammdaten/texte.ts` statt zweimal fast
|
||
gleich in zwei Seiten.
|
||
- Der Service Worker nimmt Push-Meldungen an und öffnet beim Antippen ein schon
|
||
offenes FLORIAN, statt einen zwölften Tab aufzumachen.
|
||
- Der Aufbewahrungslauf räumt zugestellte Benachrichtigungen nach Frist ab.
|
||
**Fehlgeschlagene bleiben** — sie zeigen einen kaputten Kanal an, und das
|
||
verliert seinen Wert nicht durch Zeitablauf.
|
||
|
||
### Behoben
|
||
|
||
- Die Vorlagenmaske überschrieb den gerade getippten Text, sobald die Liste im
|
||
Hintergrund nachgeladen wurde (nach dem Speichern, beim Zurückkommen ins
|
||
Fenster). Gefunden hat es der E2E-Test, nicht das Ausprobieren.
|
||
|
||
### Sicherheit
|
||
|
||
- Row Level Security und Mandantenfilter für alle sechs neuen Tabellen.
|
||
- `REVOKE DELETE ON "benachrichtigung"` für die Anwendungsrolle: wer wann
|
||
worüber informiert wurde, lässt sich nicht über die Oberfläche wegräumen.
|
||
Die Aufbewahrungsfrist läuft über den Systemzugang.
|
||
- Prüfbedingungen in der Datenbank: kein Ende vor dem Beginn, kein
|
||
Anmeldeschluss nach dem Termin, kein Kontingent von null Plätzen, keine
|
||
negative Versuchszahl.
|
||
- Kalendertoken nur als SHA-256; ein Push-Abo, das der Dienst als tot meldet,
|
||
wird stillgelegt statt gelöscht, damit die Person es in ihren Einstellungen
|
||
sieht und neu erlauben kann.
|
||
- **Kein WhatsApp** — Begründung im README unter „Warum kein WhatsApp":
|
||
Verarbeitung über Meta-Server, Kosten je Konversation, Telefonbuchabgleich.
|
||
|
||
### Tests
|
||
|
||
- 56 neue Unit-Tests: Serienrechnung (Monatsenden, Schaltjahr, Kappung),
|
||
ICS-Erzeugung (Maskierung, Zeilenumbruch nach 75 Oktetten, CRLF, Absage),
|
||
Textbausteine (Kurzfassung für externe Kanäle, unbekannte Platzhalter),
|
||
Dienstausweis (Frist, Fälschung, Verwechslung mit der Anmeldemarke) und die
|
||
Datenbankregeln der neuen Tabellen.
|
||
- 17 neue E2E-Tests für Kalender, Rückmeldung, Warteliste, Absage,
|
||
Anwesenheitsliste, Kalenderabonnement, Vorlagen und Prüfseite.
|
||
|
||
---
|
||
|
||
## [0.7.0] — 2026-09-04
|
||
|
||
Phase 6: Kartenmodule. Ab hier weiß die Wehr auf der Anfahrt, wo der nächste
|
||
Hydrant liegt und was er hergibt.
|
||
|
||
**Keine Kacheln aus dem Netz.** Wo eine Wehr ausrückt, wo ihre Hydranten liegen
|
||
und welche Objekte sie im Blick hat, ist niemandes Geschäft. Der Kartenserver
|
||
läuft selbst (tileserver-gl im Compose-Profil `karte`); ohne ihn zeigt die
|
||
Karte einen leeren Grund und sagt das auch — Punkte, Entfernungen und
|
||
Umkreissuche funktionieren trotzdem.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Hydrant` mit Typ (Unterflur, Überflur, Saugstelle, Zisterne, offenes
|
||
Gewässer), Nennweite, Durchfluss, Zustand, letzter Prüfung und Hinweis für
|
||
das, was man vor Ort wissen muss.
|
||
- `Objekt` mit Kategorie, **Zufahrt, Gefahren und Schlüsseldepot als eigene
|
||
Felder** — im Einsatz sucht niemand in einem Absatz. Dazu `Objektkontakt`
|
||
(mit „jederzeit erreichbar") und `Objektplan` (DIN 14095).
|
||
- `Lagekarte` und `Lageelement` — eine Karte je Einsatz, jedes Element mit
|
||
Geometrie, Zeichen und Urheber.
|
||
- `Wasserfoerderung` und `Foerderpunkt` — die Eingaben einer Förderstrecke.
|
||
Das Ergebnis wird **nicht** gespeichert, sondern bei jeder Anzeige neu
|
||
gerechnet.
|
||
- Standorte haben jetzt Dezimalgrade neben dem PostGIS-Punkt und liegen damit
|
||
auf der Karte.
|
||
|
||
**Karte**
|
||
|
||
- MapLibre GL aus dem eigenen Paketstand, Kacheln aus dem eigenen
|
||
tileserver-gl. Kein Skript, kein Stylesheet, kein Bild von einem fremden
|
||
Server.
|
||
- Hydranten und Objekte als getrennt schaltbare Ebenen, Filter nach Art und
|
||
Zustand, Suche über Straße, Nummer, Objektname und Gefahrenstichwort.
|
||
- **Umkreissuche über PostGIS**: `ST_DWithin` auf der Geografie mit
|
||
GiST-Index — in die Karte tippen, und es steht da, was in 500 Metern
|
||
erreichbar ist, nach Entfernung sortiert.
|
||
|
||
**Import**
|
||
|
||
- GeoJSON und Tabellen der Stadtwerke. Spaltennamen werden erkannt („DN",
|
||
„Lage", „Latitude", „Pruefdatum"), deutsche Kommazahlen und Datumsangaben
|
||
ebenso, vertauschte Achsen werden erkannt und zurückgedreht.
|
||
- Zwei Schritte wie beim Geräteimport aus Phase 1: erst zeigen, was
|
||
herauskäme, dann übernehmen. Übersprungene Zeilen stehen mit Zeilennummer
|
||
und Grund da.
|
||
- Wiederholbar: Hydranten mit einer Nummer des Versorgers werden aktualisiert
|
||
statt doppelt angelegt.
|
||
|
||
**Objekte und Feuerwehrpläne**
|
||
|
||
- Zufahrt, Gefahren, Schlüsseldepot und Löschwasser am Objekt stehen ganz
|
||
oben — in dieser Reihenfolge werden sie auf der Anfahrt gelesen.
|
||
- Ansprechpartner mit Telefonnummern zum Antippen und der Angabe, wer nachts
|
||
ans Telefon geht.
|
||
- Planblätter nach DIN 14095 mit Art und Stand. Ein Plan von 2009 ist eine
|
||
Vermutung, und das steht dran.
|
||
|
||
**Lagekarte**
|
||
|
||
- 22 taktische Zeichen nach FwDV 102, **selbst gezeichnet** als SVG in
|
||
`lib/karte/taktische-zeichen.ts`: Fahrzeuge, Einheiten, Führung, Stellen,
|
||
Gefahren, Maßnahmen.
|
||
- Zeichen setzen, Linien ziehen, Flächen aufziehen, beschriften — ein
|
||
Werkzeug, ein Klick.
|
||
- Abgleich zwischen Geräten über denselben Ereignisstrom wie die
|
||
Atemschutzüberwachung aus Phase 5; der Zustand der Verbindung steht sichtbar
|
||
da.
|
||
- Zurücknehmen statt löschen: was um 14:12 eingezeichnet wurde, gehört zur
|
||
Mitschrift.
|
||
|
||
**Wasserförderung**
|
||
|
||
- Strecke in die Karte tippen, Durchmesser und Fördermenge einstellen — die
|
||
Rechnung läuft mit: Reibungsverlust, Höhenunterschied, Zahl der
|
||
Verstärkerpumpen, Schlauchlängen je Abschnitt.
|
||
- Gerechnet wird im Browser **und** auf dem Server mit demselben Code.
|
||
- Reicht ein Abschnitt selbst mit voller Pumpe nicht, sagt die Ausgabe das,
|
||
statt eine Zahl auszuwerfen, die niemand halten kann.
|
||
- Unter jeder Rechnung steht, dass sie eine Planungshilfe ist — was zählt, ist
|
||
der Druck am Verteiler.
|
||
|
||
**Dateien abrufen**
|
||
|
||
- `/api/dateien/[id]` liefert eine abgelegte Datei aus, mandantengebunden und
|
||
mit Eintrag im Protokoll. Bis Phase 6 gab es nur den Upload und eigene
|
||
Endpunkte je Dokumentart.
|
||
|
||
**Sonstiges**
|
||
|
||
- Der Seed legt 24 Löschwasserstellen an — mit Weiher, Zisterne und
|
||
Saugstelle im Außenbereich, zwei defekten und zwei ungeprüften — sowie drei
|
||
Objekte mit Ansprechpartnern: Schule, Pflegeheim, Aussiedlerhof.
|
||
- 59 neue Tests: 48 Unit-Tests zu Druckverlust, Förderplan und Import,
|
||
11 Playwright-Durchläufe je Browser.
|
||
|
||
### Geändert
|
||
|
||
- Der Koordinaten-Trigger aus Phase 3 ist jetzt allgemein: eine Funktion für
|
||
jede Tabelle mit Breiten- und Längengrad. Sie hängt an Standort, Hydrant und
|
||
Objekt.
|
||
- Der Ereignisstrom-Hook aus Phase 5 nimmt jetzt eine Adresse entgegen und
|
||
wird von Lagekarte und Atemschutzüberwachung gemeinsam benutzt. Zwei Ströme
|
||
mit unterschiedlichem Verhalten wären ein Ort mehr, an dem sich ein Fehler
|
||
verstecken kann.
|
||
|
||
---
|
||
|
||
## [0.6.0] — 2026-09-03
|
||
|
||
Phase 5: Atemschutzüberwachung nach FwDV 7. Das sicherheitsrelevanteste Modul
|
||
des Projekts — und deshalb das mit dem höchsten Testanteil.
|
||
|
||
Über jeder Ansicht steht: **Ergänzendes System — die analoge
|
||
Atemschutzüberwachung bleibt führend.** Der Hinweis lässt sich nur abschalten,
|
||
wenn zusätzlich die Fundstelle einer Freigabe des Kommandanten hinterlegt ist;
|
||
ohne sie bleibt er stehen.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Atemschutzeinsatz` — eine Überwachung mit Überwacher, Abfrageintervall,
|
||
Warndruck und Sicherheitszuschlag. Hängt an einem Einsatz oder einer Übung,
|
||
kann aber auch für sich stehen: im Ernstfall wird zuerst überwacht und
|
||
später zugeordnet.
|
||
- `Atemschutztrupp` mit Status (bereit, im Einsatz, Rückweg, draußen), den
|
||
Zeitstempeln dazu, dem errechneten Rückzugsdruck und dem Notfallzeitpunkt.
|
||
- `Truppmitglied` — zwei oder drei Personen je Trupp, genau eine davon
|
||
Truppführer, optional mit dem verwendeten Pressluftatmer.
|
||
- `Druckmessung` mit Art (Eingang, Einsatzziel, Zwischenmeldung, Ausgang).
|
||
- `AtemschutzEreignis` — jede Zeile des Protokolls mit Zeitpunkt.
|
||
- `G26Untersuchung` und `Belastungsuebung` als eigene Nachweise.
|
||
|
||
**Die Zeit gehört dem Server**
|
||
|
||
- Nirgends wird eine Restzeit heruntergezählt. Jede Dauer entsteht aus zwei
|
||
Zeitstempeln — einem aus der Datenbank und der aktuellen Uhr. Deshalb
|
||
übersteht die Anzeige ein Neuladen, einen zweiten Bildschirm und einen
|
||
Verbindungsabbruch: es gibt nichts, das dabei verloren gehen könnte.
|
||
- Server und Browser rechnen mit **denselben** Funktionen aus
|
||
`lib/atemschutz/berechnung.ts`. Was das Tablet anzeigt, weist das Protokoll
|
||
später genauso aus.
|
||
|
||
**Rückzugsdruck nach dem doppelten Anmarschverbrauch**
|
||
|
||
- Der Trupp meldet beim Erreichen des Einsatzziels einmal seinen Druck. Daraus:
|
||
`2 × Anmarschverbrauch + Ansprechdruck der Warneinrichtung + Zuschlag`.
|
||
- Solange keine Zielmeldung vorliegt, greift die Zwei-Drittel-Regel — sie liegt
|
||
höher und ist damit die vorsichtigere Annahme.
|
||
- Maßgeblich ist immer der ungünstigste Wert im Trupp: der Trupp geht
|
||
gemeinsam zurück.
|
||
- Ampel in drei Stufen mit Klartext daneben, Gelb schon zehn Prozent über der
|
||
Grenze. Bei Notfall zusätzlich ein erzeugter Ton — ausdrücklich eine Zugabe.
|
||
|
||
**Überwachungsschirm**
|
||
|
||
- Trupp anlegen mit Eingangsdruck je Person; daneben steht, ob die Person nach
|
||
FwDV 7 überhaupt einsetzbar ist. **Gesperrt wird nichts** — im Einsatz
|
||
entscheidet der Einheitsführer, und ein Programm, das ihm im entscheidenden
|
||
Moment eine Person verweigert, wird umgangen.
|
||
- Druckabfrage für den ganzen Trupp in einer Zeile, Erinnerung im
|
||
konfigurierbaren Intervall (Vorgabe zehn Minuten).
|
||
- Mayday je Trupp, ausdrücklich aufzuheben statt wegzuklicken.
|
||
- Rückzug anordnen, Statuswechsel, Notizen ins Protokoll.
|
||
- Die Überwachung lässt sich erst beenden, wenn alle Trupps als draußen
|
||
gemeldet sind.
|
||
|
||
**Ereignisstrom über Server-Sent Events**
|
||
|
||
- Ein Route Handler unter `/api/atemschutz/strom` meldet Änderungen; der
|
||
Browser holt daraufhin den Stand. Kein eigener Server nötig, `EventSource`
|
||
verbindet nach einem Abbruch von selbst neu.
|
||
- Der Zustand der Verbindung steht sichtbar auf dem Schirm. Bleibt das
|
||
Lebenszeichen des Servers aus, gilt die Verbindung nach acht Sekunden als
|
||
tot — auch wenn der Browser keinen Fehler meldet.
|
||
- Fällt der Strom ganz aus, bleibt die Ansicht bedienbar: sie fragt zusätzlich
|
||
im festen Takt nach.
|
||
|
||
**Protokoll**
|
||
|
||
- Lückenloses PDF mit Rahmen, Trupps und dem Verlauf aus Ereignissen und
|
||
Druckmessungen in einer gemeinsamen Zeitachse.
|
||
- Entsteht beim Beenden im Hintergrundprozess und bei Bedarf beim Abruf — ein
|
||
Nachweis, den man nicht bekommt, weil ein Dienst einen schlechten Tag hatte,
|
||
wäre keiner.
|
||
- Im Kopf steht, ob die digitale Überwachung freigegeben war. Wer das Blatt
|
||
später liest, soll wissen, was er in der Hand hält.
|
||
|
||
**Tauglichkeit nach FwDV 7**
|
||
|
||
- Drei Nachweise, die gleichzeitig gelten müssen: gültige G26.3,
|
||
Belastungsübung im laufenden Jahr, Einsatzübung im laufenden Jahr. Ein
|
||
Einsatz unter Atemschutz ersetzt die Einsatzübung.
|
||
- Ampel mit **Klartext**, was fehlt — eine rote Ampel ohne Begründung hilft am
|
||
Fahrzeug niemandem.
|
||
- Fristvorschlag für die G26 nach Alter: drei Jahre, ab fünfzig eines.
|
||
- Nachweise pflegen: G26 je Person, Belastungsübung für die ganze Gruppe auf
|
||
einmal.
|
||
- Kachel auf der Startseite: wie viele Träger heute einsetzbar sind und wie
|
||
viele davon knapp davor stehen, es nicht mehr zu sein.
|
||
|
||
**Sonstiges**
|
||
|
||
- Der Seed legt acht G26-Untersuchungen an — darunter eine abgelaufene und
|
||
eine bald ablaufende —, zwölf Übungsnachweise und eine vollständig
|
||
durchgespielte Überwachung mit zwei Trupps.
|
||
- 68 neue Tests: 51 Unit-Tests zu Rückzugsdruck, Ampel, Restzeit, Statusfolge
|
||
und Tauglichkeit, 17 gegen eine echte Datenbank und 8 Playwright-Durchläufe
|
||
je Browser — darunter die beiden, um die es in dieser Phase geht: die Uhr
|
||
nach einem Neuladen und bei abgerissenem Ereignisstrom.
|
||
|
||
### Geändert
|
||
|
||
- Die Datenbank verweigert dem Anwendungszugang `UPDATE` und `DELETE` auf
|
||
`druckmessung` und `atemschutz_ereignis`; zusätzlich sitzt auf beiden ein
|
||
Trigger. Ein Fehler wird durch einen neuen Eintrag richtiggestellt, nicht
|
||
durch Ändern — sonst wäre jede Aufzeichnung nachträglich glättbar.
|
||
- Neue Bedingungen: Druck zwischen 0 und 400 bar, kein Rückzug vor dem
|
||
Einstieg, kein Ende vor dem Beginn, genau ein Truppführer je Trupp.
|
||
- Die Startseite stand noch auf dem Stand von Phase 1 („Als Nächstes kommt das
|
||
Prüfmanagement"). Sie nennt jetzt den tatsächlichen Ausbaustand.
|
||
- Der Navigationseintrag „Protokoll" führte auf eine Seite, die es nicht gibt.
|
||
Die Ansicht des Audit-Logs gehört zum Löschkonzept in Phase 8; bis dahin
|
||
steht der Eintrag ausgegraut da statt ins Leere zu führen.
|
||
|
||
---
|
||
|
||
## [0.5.0] — 2026-09-03
|
||
|
||
Phase 4: das Terminal im Gerätehaus. Ein Wandtablet, das mit Handschuhen
|
||
bedienbar ist und weiterarbeitet, wenn das Netz weg ist.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Fahrt` — Fahrtenbuch je Fahrzeug: Fahrer, Zweck, Ziel, Kilometerstand bei
|
||
Abfahrt und Rückkehr, getankte Menge. Eine Fahrt ohne Rückkehr ist die
|
||
laufende Fahrt.
|
||
- `Anwesenheit` — wer im Gerätehaus ist, mit Anlass (Dienst, Übung, Einsatz,
|
||
Arbeitsdienst) und offener Abmeldung, solange jemand da ist.
|
||
- `Waescheabgabe` — Schutzkleidung in der Reinigung, mit Grund und Stand von
|
||
„abgegeben" bis „zurück".
|
||
- `Inventurgang` und `Inventurposten` — der Gerätewart geht mit dem Tablet durch
|
||
die Halle und scannt; am Ende steht die Liste dessen, was fehlt.
|
||
- `KioskVorgang` — jeder vom Terminal übermittelte Vorgang, **mit der im
|
||
Browser erzeugten Kennung als Primärschlüssel**. Das ist der Kern des
|
||
Offline-Betriebs: kommt derselbe Vorgang nach einem abgebrochenen
|
||
Sendeversuch ein zweites Mal an, wird er wiedererkannt statt neu angelegt.
|
||
- `SyncKonflikt` — was sich beim Nachtragen nicht anwenden ließ. Nichts geht
|
||
verloren, es wartet auf eine Entscheidung.
|
||
|
||
**Terminaloberfläche unter `/kiosk`**
|
||
|
||
- Kachelschirm: sieben Kacheln, jede mindestens 160 Pixel hoch, jede mit Symbol
|
||
und Text, sichtbar nach Rechten. Keine Navigationsleiste.
|
||
- Anmeldung auf drei Wegen: Ausweis auflegen (der RFID-Leser tippt die Kennung
|
||
als Tastatureingabe, erkannt am Anschlagstempo), QR-Code vom Handy oder
|
||
Mitgliedsnummer samt PIN.
|
||
- Mangel melden in drei Schritten — Gerät scannen, Foto und ein Satz dazu,
|
||
Dringlichkeit antippen. Anspruch war „unter dreißig Sekunden".
|
||
- Kommen und Gehen mit zwei Tipps, Fahrtenbuch, Wäscheabgabe und Inventurgang.
|
||
- Abmeldeuhr: nach `KIOSK_IDLE_TIMEOUT` Sekunden Untätigkeit meldet sich das
|
||
Terminal von selbst ab, mit sichtbarer Warnung vorher.
|
||
- Hallenmonitor unter `/kiosk/monitor`: Uhrzeit, laufender Einsatz, wer im Haus
|
||
ist, Fahrzeuge, fällige Prüfungen und die nächsten Termine — Vollbild ohne
|
||
Bedienung.
|
||
|
||
**Offline-Betrieb**
|
||
|
||
- Ein von Hand geschriebener Service Worker (`public/sw.js`): App-Shell und
|
||
Stammdaten im Zwischenspeicher, `network-first` für Seiten mit
|
||
Ausweichseite `/kiosk/offline`, `stale-while-revalidate` für lesende
|
||
tRPC-Aufrufe. Schreibende Anfragen und alles unter `/api/auth` fasst er nicht
|
||
an — eine zwischengespeicherte Anmeldung wäre eine Sicherheitslücke.
|
||
- Warteschlange in einer IndexedDB (Dexie): **jede** schreibende Aktion geht
|
||
zuerst dorthin, auch online. Sonst gäbe es zwei Wege durch den Code, und der
|
||
seltenere wäre der ungetestete.
|
||
- Fotos liegen als Blob in derselben Warteschlange und werden beim Übertragen
|
||
zuerst hochgeladen.
|
||
- Sichtbarer Zustand als Leiste über allem: verbunden, wie viele Änderungen
|
||
warten, kein Netz — in Klartext, nicht nur als Farbe.
|
||
- Erledigt heißt: angenommen **oder** als bereits verarbeitet erkannt **oder**
|
||
in der Konfliktliste gelandet. In allen drei Fällen ist der Vorgang beim
|
||
Server und darf aus der Warteschlange verschwinden.
|
||
|
||
**Anmeldemarke**
|
||
|
||
- `/anmeldemarke` zeigt einen QR-Code fürs Handy: signiert mit `AUTH_SECRET`,
|
||
gültig 60 Sekunden, nicht gespeichert. Ein abfotografierter Code aus der
|
||
Umkleide nützt nach einer Minute nichts mehr.
|
||
|
||
**Terminalübersicht in der Verwaltung**
|
||
|
||
- `/terminal` zeigt Konflikte zuerst — das sind die Vorgänge, die ein Mensch
|
||
anfassen muss —, danach Anwesende, Wäsche und Fahrtenbuch.
|
||
|
||
**Installierbar als App**
|
||
|
||
- Web-App-Manifest, eigene Symbole (`npm run icons`), Startseite `/kiosk`. Kein
|
||
externes CDN, keine fremde Schriftart.
|
||
|
||
**Wetterwarnungen (optional, aus)**
|
||
|
||
- Warnungen des DWD auf dem Hallenmonitor, hinter einer Schnittstelle in
|
||
`lib/integrations/`. Standardmäßig **aus** — es ist die einzige Verbindung,
|
||
die FLORIAN von sich aus nach außen aufbaut. Fällt sie aus, bleibt die Kachel
|
||
leer.
|
||
|
||
**Sonstiges**
|
||
|
||
- Der Seed legt ein Fahrtenbuch mit vier Fahrten (eine läuft noch), vier
|
||
Anwesenheiten (eine Person ist noch im Haus) und drei Wäscheabgaben an.
|
||
- 31 neue Tests: 20 Unit-Tests zu Anmeldemarke und Warteschlange (letztere
|
||
unter jsdom mit `fake-indexeddb`), 11 gegen eine echte Datenbank
|
||
(Kilometerstände, Anwesenheitszeiten, Unveränderbarkeit der Vorgänge,
|
||
Mandantentrennung) und 9 Playwright-Durchläufe je Browser — darunter der
|
||
Offline-Fall mit `context.setOffline(true)` und der doppelt gesendete
|
||
Vorgang, dessen Antwort unterwegs verloren geht.
|
||
|
||
### Geändert
|
||
|
||
- Die Datenbank setzt vier neue Regeln selbst durch: Kilometerstände laufen
|
||
vorwärts, eine Rückkehr liegt nicht vor der Abfahrt, ein Gehen nicht vor dem
|
||
Kommen, eine Wäscheabgabe umfasst mindestens ein Stück. Dazu `REVOKE UPDATE,
|
||
DELETE` auf `kiosk_vorgang` — ein übermittelter Vorgang lässt sich nicht
|
||
nachträglich glätten.
|
||
- Die Beschriftung der DWD-Warnstufen liegt in
|
||
`src/lib/integrations/wetter-texte.ts`, getrennt vom `server-only`-Modul
|
||
daneben — dieselbe Trennung wie bei `qr.ts` und `qr-parser.ts`.
|
||
- Neue Rechte `fahrtenbuch.lesen` und `fahrtenbuch.fuehren`, beide im
|
||
Grundrechtebündel.
|
||
|
||
### Behoben
|
||
|
||
- Die Warteschlange rief sich selbst in eine Renderschleife: `jetztSenden`
|
||
hatte das Mutationsobjekt von React Query in seinen Abhängigkeiten, und das
|
||
ist bei jedem Rendern ein neues. Damit wurde der 20-Sekunden-Zeitgeber
|
||
ständig neu aufgezogen und lief nie ab — ein liegen gebliebener Vorgang wäre
|
||
ohne Zutun nie ein zweites Mal losgeschickt worden. Gefunden vom
|
||
Offline-E2E-Test, der genau darauf wartet.
|
||
- `/kiosk/einrichten` ließ sich nicht vorab erzeugen, weil `useSearchParams()`
|
||
ohne Suspense-Grenze stand. Der Produktionsbuild brach dabei ab.
|
||
- Der Anwesenheits- und der Fahrten-Wizard zeigten erst die eine, nach dem
|
||
Laden des eigenen Stands die andere Fassung — wer zügig tippte, drückte ins
|
||
Leere. Bis der Stand bekannt ist, steht dort jetzt ein Platzhalter.
|
||
- Die Offline-Seite blieb stehen, wenn nur eine einzelne Anfrage abgebrochen
|
||
war. Am Hallenmonitor, den niemand bedient, hieße das: bis jemand vorbeikommt.
|
||
Sie versucht es jetzt alle zehn Sekunden von selbst.
|
||
- Der Hallenmonitor stapelte alle laufenden Einsätze übereinander und schob
|
||
damit Anwesenheit, Fahrzeuge und Prüfungen aus dem Bild. Zwei stehen groß,
|
||
der Rest als Zeile.
|
||
|
||
---
|
||
|
||
## [0.4.0] — 2026-09-03
|
||
|
||
Phase 3: Einsätze und Übungen. Ab hier dokumentiert die Wehr, was sie tut —
|
||
für die Gemeinde, für die Versicherung und für die eigene Mannschaft.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Einsatz` mit den sechs Zeitstempeln (alarmiert, ausgerückt, eingetroffen,
|
||
Einsatzende, eingerückt, einsatzbereit), Ort samt Koordinaten, Kategorie nach
|
||
der bayerischen Feuerwehrstatistik und den Zahlen für die Gemeindemeldung.
|
||
- `Einsatzfahrzeug` mit eigenen Zeiten je Fahrzeug — Nachalarmierung bleibt
|
||
sichtbar —, Kilometerstand und Wasserabgabe.
|
||
- `Einsatzkraft` mit Funktion nach FwDV 3, Fahrzeug, Zeitraum und
|
||
Atemschutz-Kennzeichen.
|
||
- `Materialverbrauch` und `Lagemeldung` mit Foto und Herkunft.
|
||
- `PortalZugang` — ein zeitlich begrenzter Link für eine Fremdwehr, gespeichert
|
||
nur als Hash.
|
||
- `Uebung` und `Uebungsteilnahme` mit Thema, Lernzielen und Anwesenheit.
|
||
|
||
**Schnellerfassung**
|
||
|
||
- Stichwort, Ort, ausgerückte Fahrzeuge — mehr fragt die Erfassung im Alarmfall
|
||
nicht. Alarmzeit ist der Moment des Anlegens, alles Weitere wird nachgetragen.
|
||
- Fortlaufende Einsatznummer `JJJJ-NNNN` je Wehr und Jahr, vergeben unter einer
|
||
Transaktionssperre.
|
||
|
||
**Zeiten und Stärke**
|
||
|
||
- Ein Knopf je Zeitstempel, solange der Einsatz läuft.
|
||
- Daraus gerechnet: Ausrückzeit, Anfahrtszeit, Hilfsfrist und Einsatzdauer.
|
||
- Stärke nach FwDV 3 in der Schreibweise `Führer/Unterführer/Mannschaft/Gesamt`,
|
||
mit Zuordnung zur taktischen Einheit (Trupp, Staffel, Gruppe, Zug).
|
||
- Hinweise statt Sperren: eine verdrehte Zeitreihenfolge oder eine fehlende
|
||
Führungskraft wird angezeigt, verhindert das Speichern aber nicht — ein
|
||
Einsatz, der drei Wochen später nachgetragen wird, ist immer noch besser als
|
||
einer, den niemand erfasst.
|
||
|
||
**Personalvorschlag**
|
||
|
||
- Sortiert nach Einsatzerfahrung der letzten zwölf Monate, mit dem
|
||
Qualifikationsstand daneben: eine abgelaufene G26.3 fällt auf, bevor jemand
|
||
unter Atemschutz geht.
|
||
|
||
**Einsatzbericht und Freigabe**
|
||
|
||
- Bericht als PDF über Gotenberg: Kopf, Zeitleiste mit Kennzahlen, Fahrzeuge,
|
||
Mannschaft mit Stärke, Material, Verlauf und Bericht.
|
||
- Mit der Freigabe wird der Einsatz festgeschrieben — durchgesetzt von einem
|
||
Datenbanktrigger, nicht vom Anwendungscode. Ausgenommen bleiben das
|
||
nachgetragene PDF und die Lagemeldungen.
|
||
- Jahresexport als quer gedrucktes PDF unter `/api/einsaetze/jahresexport`.
|
||
|
||
**Statistik**
|
||
|
||
- Einsätze nach Kategorie und Monat, Verteilung der Hilfsfristen mit dem
|
||
80-Prozent-Wert, Beteiligung je Person (Einsätze, davon unter Atemschutz,
|
||
Übungen) und Auslastung je Fahrzeug.
|
||
|
||
**Einsatzportal für Fremdwehren**
|
||
|
||
- Signierter Link ohne Anmeldung, gültig 24 Stunden, jederzeit widerrufbar,
|
||
jeder Aufruf wird mitgezählt.
|
||
- Zeigt Stichwort, Ort, Zeitstempel und Lagemeldungen — keine Namen von
|
||
Einsatzkräften, keinen Bericht, keinen Weg in den Rest der Anwendung.
|
||
- Nimmt Lagemeldungen mit Foto an, begrenzt auf 30 Meldungen und 10 Fotos je
|
||
Zugang und Stunde.
|
||
- Eigene Zugriffsbegrenzung über Redis (`src/lib/ratelimit.ts`), die bei
|
||
Störung durchlässt statt zu blockieren.
|
||
|
||
**Übungen**
|
||
|
||
- Thema, Art, Lernziele, Ort und Leitung; Anwesenheit als Liste zum Abhaken.
|
||
- Fließt in die Beteiligungsstatistik ein.
|
||
|
||
**Adressen zu Koordinaten**
|
||
|
||
- Anbindung an einen **selbst betriebenen** Nominatim hinter einer Schnittstelle
|
||
in `lib/integrations/`. Ohne Konfiguration bleibt die Suche leer und die
|
||
Koordinaten werden von Hand eingetragen.
|
||
- Koordinaten liegen doppelt: als Dezimalgrad für die Anwendung und als
|
||
PostGIS-Punkt für die Karte ab Phase 6, zusammengehalten von einem Trigger.
|
||
|
||
**Sonstiges**
|
||
|
||
- Der Seed legt acht Einsätze über das Jahr verteilt an — Brände, technische
|
||
Hilfeleistungen, ein Fehlalarm einer Brandmeldeanlage — dazu sechs Übungen
|
||
und die Beteiligung der Mannschaft.
|
||
- 57 neue Tests: 32 Unit-Tests zu Stärke, Zeiten und Nummernkreis, 14 gegen eine
|
||
echte Datenbank (Koordinaten-Trigger, Freigabe, Mandantentrennung) und 11
|
||
Playwright-Durchläufe einschließlich des Portals in einem Browser **ohne**
|
||
Sitzung.
|
||
|
||
### Geändert
|
||
|
||
- Der Nummernkreis `JJJJ-NNNN` liegt jetzt in `src/lib/nummernkreis.ts` und wird
|
||
von Prüfprotokollen, Einsätzen und Übungen gemeinsam verwendet.
|
||
- Die Seitenleiste zeigt Einsätze und Übungen als verfügbar.
|
||
- Das Zeitlimit der E2E-Erwartungen steigt von 15 auf 25 Sekunden: mit fünfzig
|
||
Routen dauert die erste Übersetzung einer Seite im Entwicklungsserver unter
|
||
Last länger als vorher, und daran scheiterten gelegentlich Tests, die einzeln
|
||
grün sind.
|
||
|
||
### Behoben
|
||
|
||
- Die Anwesenheitsliste einer Übung forderte 300 Mitglieder an und lief damit
|
||
in die Höchstzahl von 200 je Seite — die Liste blieb leer. Gefunden über die
|
||
Server-Ausgabe während der E2E-Tests.
|
||
|
||
---
|
||
|
||
## [0.3.0] — 2026-09-03
|
||
|
||
Phase 2: Wartungs- und Prüfmanagement. Das Modul, wegen dem eine Wehr so ein
|
||
Programm einführt — Fristen im Blick behalten und im Streitfall nachweisen
|
||
können, dass geprüft wurde.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Wartungsvorlage` — was an einem Gerätetyp in welchem Takt geprüft wird, mit
|
||
Intervall, Vorwarnzeit, Prüfvorschrift und Erfahrungswert für die Dauer.
|
||
- `Checklistenpunkt` in vier Ausprägungen: Ja/Nein, Messwert (mit zulässigem
|
||
Bereich und Einheit), Text und Foto. Pflichtpunkte sind die, an denen die
|
||
Einsatzbereitschaft hängt.
|
||
- `Wartungstermin` — ein fälliger Termin an einem konkreten Gerät. Eindeutig
|
||
über Gerät, Vorlage und Fälligkeitstag, damit ein zweiter nächtlicher Lauf
|
||
nichts doppelt anlegt.
|
||
- `Pruefung`, `Pruefergebnis` und `Signatur` — die durchgeführte Prüfung mit
|
||
Antworten je Punkt und Unterschrift.
|
||
- `Mangel` mit dem Weg melden → zugewiesen → in Arbeit → behoben.
|
||
- `Rueckruf`, `RueckrufGeraetetyp` und `RueckrufBetroffenheit` — eine
|
||
Herstellerwarnung, die betroffene Bauart und der Stand je Gerät.
|
||
- `Erinnerung` als Nachweis und gleichzeitig als Sperre gegen Doppelversand.
|
||
|
||
**Unveränderbarkeit der Protokolle**
|
||
|
||
- Ein Trigger in der Datenbank weist jede Änderung an einer abgeschlossenen
|
||
Prüfung ab. Zulässig bleiben genau drei Dinge: der Wechsel auf „ersetzt", das
|
||
Nachtragen des PDF, solange keines hinterlegt ist, und `geaendertAm`.
|
||
- Dieselbe Sperre gilt für die Bestandteile: an einem abgeschlossenen Protokoll
|
||
lassen sich weder Einzelergebnisse ändern noch Unterschriften nachtragen.
|
||
- Korrekturen erzeugen eine **neue** Prüfung mit Verweis auf die alte. Beide
|
||
bleiben lesbar.
|
||
- Gelöscht wird ein Protokoll nur im gekennzeichneten Aufbewahrungslauf —
|
||
derselbe Schalter wie beim Audit-Log.
|
||
|
||
**Fristen-Dashboard** (`/pruefungen`)
|
||
|
||
- Vier Töpfe nach Dringlichkeit: überfällig, diese Woche, diesen Monat, nächste
|
||
90 Tage. Jede Zahl ist gleichzeitig ein Filter.
|
||
- Bündelung nach Fahrzeug, Standort, Gerätetyp oder Zuständigem — wer einen
|
||
Prüftag ansetzt, arbeitet ein Fahrzeug ab, keine alphabetische Liste.
|
||
- Sammelzuweisung mehrerer Termine an eine Person.
|
||
- Daneben die Lebensdauerüberwachung (`/pruefungen/lebensdauer`) mit Vorschlag
|
||
zur Aussonderung.
|
||
|
||
**Prüfwizard** (`/pruefungen/neu`)
|
||
|
||
- Ein Prüfpunkt je Bildschirm, große Schaltflächen, Foto direkt aus der Kamera.
|
||
- Zeiterfassung läuft mit und landet im Protokoll.
|
||
- Unterschrift auf einem Zeichenfeld, das Finger, Stift und Maus versteht; das
|
||
PNG landet in MinIO.
|
||
- Jede Antwort geht sofort zum Server — bricht das WLAN weg, ist höchstens die
|
||
letzte verloren, und der Entwurf wird beim nächsten Öffnen fortgesetzt.
|
||
- Mängel werden aus den beanstandeten Punkten vorgeschlagen.
|
||
- Externe Wartung: Fachfirma, Kosten und das Fremdprotokoll als PDF.
|
||
|
||
**Prüfprotokoll als PDF**
|
||
|
||
- Erzeugt der Hintergrundprozess über Gotenberg und legt es in MinIO ab: Kopf
|
||
mit Wehr, Gerät und Prüfer-Kennung, Checkliste mit Soll, Ist und Befund,
|
||
eingebettete Fotos und Unterschriften, Fußzeile mit Prüfvorschrift, Prüfdatum
|
||
und Seitenzahl.
|
||
- Laufende Protokollnummer `JJJJ-NNNN` je Wehr und Jahr.
|
||
|
||
**Nächtlicher Lauf**
|
||
|
||
- `fristen.pruefen` (3:15 Uhr) plant Termine aus den Vorlagen und zieht Frist
|
||
und Status an jedem Gerät nach.
|
||
- `erinnerung.senden` (3:45 Uhr) verschickt Vorwarnungen bei T-30, T-7, T-0 und
|
||
einmal bei Überfälligkeit — als Sammelnachricht je Empfänger.
|
||
- Beide Läufe sind wiederholbar und tragen sich beim Start des Workers je Wehr
|
||
selbst ein.
|
||
|
||
**Mängel und Rückrufe**
|
||
|
||
- Melden darf jedes Mitglied; ein kritischer Mangel sperrt das Gerät sofort.
|
||
- Beim Beheben wird die Sperre aufgehoben — aber nur, wenn kein anderer offener
|
||
Mangel dasselbe Gerät sperrt.
|
||
- Rückrufe suchen betroffene Geräte über Bauart, Modell, Seriennummernbereich
|
||
und Herstellungszeitraum. Das Ergebnis ist ein Vorschlag, keine automatische
|
||
Sperre.
|
||
|
||
**Sonstiges**
|
||
|
||
- Datei-Upload unter `/api/dateien` für Fotos, Unterschriften und
|
||
Fremdprotokolle — als Multipart, nicht Base64 durch eine JSON-Antwort.
|
||
- Der Seed legt 5 Wartungsvorlagen mit echten Prüfvorgaben an (DGUV 112-190,
|
||
DIN 14811, DGUV Vorschrift 3, DIN 14920), dazu die fälligen Termine, ein
|
||
abgeschlossenes Protokoll samt Mangel und einen Beispielrückruf.
|
||
- 79 neue Tests: 47 Unit-Tests zu Fristen, Bewertung und Planung, 10 gegen eine
|
||
echte Datenbank zur Unveränderbarkeit und Mandantentrennung, 22 Playwright-
|
||
Durchläufe über Dashboard, Vorlagen, Wizard, Mängel und Rückrufe.
|
||
|
||
**Nachzügler aus Phase 1**
|
||
|
||
Mit dieser Version wird auch das Geräteformular ausgeliefert, das nach der
|
||
0.2.0 entstanden ist:
|
||
|
||
- **Formular zum Anlegen und Bearbeiten eines Geräts** (`/geraete/neu` und
|
||
`/geraete/<id>/bearbeiten`). Damit ist die letzte offene Lücke aus Phase 1
|
||
geschlossen: Ein Neuzugang lässt sich einzeln erfassen, ohne den Umweg über
|
||
eine Importdatei.
|
||
- Die Lagerung wird als Umschalter geführt statt als zwei Auswahlfelder, die
|
||
man versehentlich beide füllen kann.
|
||
- Prüftermin und Lebensdauerende werden schon beim Tippen ausgerechnet und
|
||
angezeigt — mit denselben Funktionen, mit denen der Server anschließend
|
||
rechnet.
|
||
- Ist nur ein Standort angelegt, wird er vorbelegt.
|
||
- 7 weitere Playwright-Tests zum Formular (nun 58 statt 44, zwei davon unter
|
||
Touch-Emulation begründet übersprungen).
|
||
|
||
Dabei behoben:
|
||
|
||
- **Das Formular stürzte beim ersten Rechnen ab.** Die Vorgabewerte lagen in
|
||
einem `"use client"`-Modul und wurden von einer Server-Komponente importiert;
|
||
dort kommt nur eine Client-Referenz an, kein Objekt. Das Spreaden davon ergab
|
||
lauter `undefined`. Die Werte liegen jetzt in einem eigenen Modul ohne
|
||
`"use client"`.
|
||
- **Das Sternchen an Pflichtfeldern wurde vorgelesen.** Eine Sprachausgabe
|
||
nannte das Feld „Bezeichnung Stern". Das Sternchen trägt jetzt `aria-hidden`;
|
||
dass ein Feld ausgefüllt werden muss, sagt das `required`-Attribut.
|
||
- Fehlermeldungen in Formularen sind eindeutig ansteuerbar (`data-testid`) —
|
||
Next.js hängt einen eigenen Routen-Ansager mit derselben Rolle in jede Seite.
|
||
|
||
### Geändert
|
||
|
||
- Die Datumsarithmetik aus Phase 1 (`utcTag`, `monateAddieren`, `heutigerTag`,
|
||
`tagesabstand`) ist jetzt exportiert. Zwei Umsetzungen derselben Rechnung
|
||
wären früher oder später um einen Tag auseinandergelaufen.
|
||
- Die Seitenleiste zeigt Prüfungen, Mängel und Rückrufe als verfügbar.
|
||
- Die Gerätedetailseite zeigt Prüfhistorie, offene Mängel und anstehende
|
||
Termine — und startet die Prüfung.
|
||
|
||
### Behoben
|
||
|
||
- **Zwei gleichzeitig abgeschlossene Prüfungen zogen dieselbe Protokollnummer.**
|
||
Die eindeutige Bedingung in der Datenbank fing das ab, aber der zweite Prüfer
|
||
bekam einen Datenbankfehler statt eines Protokolls. Die Nummer wird jetzt
|
||
unter einer Transaktionssperre je Wehr und Jahr vergeben — gefunden von den
|
||
E2E-Tests, weil Desktop- und Tablet-Durchlauf parallel prüften.
|
||
- **Der Worker ließ sich außerhalb des Containers nicht starten.** Ihm fehlte
|
||
das Laden der `.env`, und `server-only` warf beim Import unter Node. Beide
|
||
Startbefehle laufen jetzt mit `--conditions=react-server`, womit der leere
|
||
Export dieses Pakets greift.
|
||
|
||
---
|
||
|
||
## [0.2.0] — 2026-09-03
|
||
|
||
Phase 1: Stammdaten. Ab hier führt die Wehr ihren Bestand in FLORIAN — mit
|
||
Standorten, Fahrzeugen samt Beladeordnung, Gerätetypen als Vorgabe und
|
||
Geräten, die per QR-Code am Regal auffindbar sind.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Datenmodell**
|
||
|
||
- `Standort` (Gerätehaus, Lager, Außenstelle, Werkstatt) mit PostGIS-Punkt für
|
||
die Karte ab Phase 6.
|
||
- `Geraeteraum` — die Fächer am Fahrzeug (G1 bis G6, Heck, Dach,
|
||
Mannschaftsraum), in Beladereihenfolge sortierbar.
|
||
- `Geraetetyp` als Vorgabe für Prüfintervall, Prüfvorschrift und Lebensdauer.
|
||
Ein einzelnes Gerät darf davon abweichen; sonst gilt der Typ.
|
||
- `Qualifikationstyp` und `Qualifikation` mit Gültigkeitsdauer und eigener
|
||
Vorwarnzeit je Typ.
|
||
- `Fahrzeug` erweitert um Fahrgestellnummer, DIN-Typkurzzeichen, Erstzulassung,
|
||
Sitzplätze, Tankinhalt, Gesamtmasse und die UVV-Frist.
|
||
- `Geraet` erweitert um Gerätetyp, Lebensdauerkategorie, errechnetes
|
||
Lebensdauerende, Anschaffungspreis und die Lagerung.
|
||
|
||
**Regeln in der Datenbank**
|
||
|
||
- Ein Gerät steht entweder an einem Standort oder ist auf einem Fahrzeug
|
||
verlastet — eine Prüfbedingung setzt das durch, ein Trigger stellt zusätzlich
|
||
sicher, dass ein angegebener Geräteraum auch zum angegebenen Fahrzeug gehört.
|
||
- Row Level Security auf allen neuen Tabellen.
|
||
- Trigram-Indizes für die Volltextsuche über Geräte, Fahrzeuge und Personen.
|
||
|
||
**Funktionen**
|
||
|
||
- Geräteliste mit Volltextsuche, Statusfilter, serverseitiger Sortierung,
|
||
Blättern und Spaltenauswahl. Die Spaltenauswahl bleibt je Browser gespeichert.
|
||
- Massenbearbeitung: Status mehrerer Geräte auf einmal setzen, Geräte umlagern.
|
||
Jede Änderung erzeugt einen eigenen Protokolleintrag am jeweiligen Gerät.
|
||
- QR-Code je Gerät. Der Code zeigt auf `/g/<inventarnummer>` und führt nach der
|
||
Anmeldung direkt zur Gerätedetailseite.
|
||
- Etikettenbogen als PDF über Gotenberg: DIN A4 mit 65 Etiketten
|
||
(38,1 × 21,2 mm). Der Druck kann an einer beliebigen Position beginnen, damit
|
||
angebrochene Bögen weiterverwendet werden können.
|
||
- QR-Scan über die Kamera im Browser, mit Eingabefeld als Rückfallweg.
|
||
- Import aus CSV und Excel: Spalten werden nach den Überschriften automatisch
|
||
zugeordnet, die Vorschau zeigt Zeile für Zeile, was übernommen würde und was
|
||
daran nicht stimmt. Geschrieben wird erst nach Bestätigung.
|
||
- Gerätehistorie und Fahrzeughistorie, gerendert aus dem Audit-Log.
|
||
- Qualifikationen mit Ablaufwarnung, gerechnet mit der Vorwarnzeit des
|
||
jeweiligen Typs.
|
||
- Fahrzeugdetail zeigt die Beladung nach Geräteräumen statt alphabetisch.
|
||
|
||
**Daten und Tests**
|
||
|
||
- Der Seed legt jetzt zusätzlich 3 Standorte, 20 Geräteräume, 14 Gerätetypen
|
||
mit echten Prüfvorgaben (DGUV, DIN, vfdb), 49 Geräte und 107 Qualifikationen
|
||
an. Fristen sind bewusst gestreut, damit die Ampeln etwas zeigen.
|
||
- 104 Vitest-Tests (vorher 32), davon 40 zum Import und 32 zu QR und Fristen.
|
||
- 44 Playwright-Tests (vorher 10) über Desktop und Tablet.
|
||
|
||
### Geändert
|
||
|
||
- **Fristenrechnung auf UTC-Kalenderarithmetik umgestellt.** Vorher wurde über
|
||
`startOfDay` aus date-fns gerechnet, das die Zeitzone des Servers verwendet.
|
||
Ein Prüfdatum kommt aus der Datenbank als UTC-Mitternacht und wurde dabei auf
|
||
den Vortag zurückgeschoben — Fristen wären einen Tag zu früh fällig geworden.
|
||
- `CardTitle` rendert eine echte Überschrift statt eines `div`. Wer mit einer
|
||
Sprachausgabe arbeitet, springt von Überschrift zu Überschrift; Karten
|
||
tauchten in dieser Navigation vorher gar nicht auf.
|
||
- Die Entwicklungsanzeige von Next.js ist abgeschaltet, damit sie nicht in den
|
||
Bildern der Anleitung mitläuft.
|
||
|
||
### Sicherheit
|
||
|
||
- Der Etikettendruck wird protokolliert (`EXPORTIERT`) — damit ist
|
||
nachvollziehbar, wann welche Aufkleber erzeugt wurden.
|
||
- Der Import übernimmt keine Werte, die der Browser geschickt hat: die Datei
|
||
wird beim Übernehmen erneut serverseitig eingelesen und geprüft. Sonst ließe
|
||
sich die Vorschau umgehen.
|
||
|
||
## [0.1.0] — 2026-09-03
|
||
|
||
Phase 0: das Fundament. Ab hier steht ein lauffähiger Stack mit
|
||
Mandantentrennung, Anmeldung, Dateiablage und lückenloser Protokollierung.
|
||
|
||
### Hinzugefügt
|
||
|
||
**Betrieb**
|
||
|
||
- `docker-compose.yml` mit Traefik, App, Worker, PostgreSQL 17 + PostGIS,
|
||
Redis, MinIO und Gotenberg.
|
||
- `docker-compose.override.yml` für die Entwicklung: ohne Traefik, mit
|
||
Mailpit als SMTP-Auffangkorb und Adminer für den Blick in die Datenbank.
|
||
Host-Ports bewusst abseits der Standardwerte (3100, 5442, 6389, 9010/9011),
|
||
damit FLORIAN neben anderen lokalen Stacks laufen kann.
|
||
- Mehrstufiges `docker/Dockerfile`: eigener Benutzer `florian` (UID 1001,
|
||
kein root), `tini` als PID 1, Healthcheck gegen `/api/health`,
|
||
Standalone-Build von Next.js.
|
||
- `docker/entrypoint.sh` wendet ausstehende Migrationen an — nur im
|
||
App-Container, nicht im Worker.
|
||
- `docker/postgres/10-init-app-role.sh` legt beim ersten Start die Rolle
|
||
`florian_app` (`NOBYPASSRLS`) samt Vorgabeprivilegien an.
|
||
- `.env.example` mit einem Kommentar zu jeder einzelnen Variable.
|
||
|
||
**Datenmodell**
|
||
|
||
- Prisma-Schema mit `Organisation`, `Person`, `Rolle`, `Berechtigung`,
|
||
`RolleBerechtigung`, `PersonRolle`, `AusweisToken`, `AuditLog`, `Datei`.
|
||
- `Fahrzeug` und `Geraet` bereits schmal angelegt, damit der Seed eine
|
||
realistische Wehr aufbauen kann. Phase 1 erweitert beide.
|
||
- Migration `20260902000100_rls_policies`: Row Level Security auf allen
|
||
mandantenpflichtigen Tabellen, Funktion `aktuelle_organisation()`,
|
||
Append-only-Trigger auf `audit_log`.
|
||
- Migration `20260902000200_audit_bereinigung`: `UPDATE` auf `audit_log`
|
||
bleibt dauerhaft gesperrt, `DELETE` ist nur im gekennzeichneten
|
||
Aufbewahrungslauf möglich.
|
||
|
||
**Anwendungskern**
|
||
|
||
- `lib/env.ts` — Zod-Validierung der gesamten Umgebung beim Start.
|
||
- `lib/db.ts` — getrennte Verbindungen für Anwendung (RLS aktiv) und System
|
||
(RLS-frei), `fuerOrganisation()` als einziger Weg zu Fachdaten.
|
||
- `lib/authz.ts` — 9 Rollen, 38 Einzelrechte, zentrale Prüffunktionen.
|
||
- `lib/storage.ts` — MinIO, presignte Up- und Downloads, SHA-256-Prüfsumme.
|
||
- `lib/audit.ts` — Protokolleinträge mit Vorher/Nachher-Vergleich, entfernt
|
||
Geheimnisse vor dem Schreiben.
|
||
- `lib/auth.ts` — Auth.js mit OIDC gegen authentik, lokalem Passwort-Login
|
||
und Terminal-Anmeldung über PIN oder RFID.
|
||
- `lib/warteschlange.ts` und `src/worker/` — BullMQ-Gerüst mit den
|
||
Auftragsarten für Fristen, Erinnerungen, PDF und Aufbewahrung.
|
||
- `lib/integrations/` — Mail, Telegram und LLM hinter abschaltbaren
|
||
Schnittstellen.
|
||
- tRPC v11 mit den Prozedurarten `oeffentlich`, `angemeldet` und
|
||
`mitRecht(...)`.
|
||
|
||
**Oberfläche**
|
||
|
||
- Basis-Layout mit Seitenleiste, heller und dunkler Darstellung
|
||
(Systemvorgabe als Standard) und durchgängig deutschen Texten.
|
||
- Anmeldeseite, die genau die eingerichteten Anmeldewege zeigt.
|
||
- Übersicht mit Kennzahlen und dem Sicherheitshinweis zur analogen
|
||
Atemschutzüberwachung.
|
||
- Farbwelt in OKLCH mit festen Statusfarben für Gerätezustände.
|
||
|
||
**Daten und Tests**
|
||
|
||
- Seed mit einer vollständigen Beispielwehr: 15 Personen mit Rollen und
|
||
Terminal-Ausweisen, 3 Fahrzeuge, 40 Geräte mit gemischten Prüffristen.
|
||
- Vitest: 32 Tests, darunter acht, die die Mandantentrennung gegen eine echte
|
||
Datenbank prüfen.
|
||
- Playwright: 10 Tests über Desktop und Tablet.
|
||
- `npm run screenshots` erzeugt die Bilder für die Anleitung.
|
||
|
||
**Dokumentation**
|
||
|
||
- README mit Schnellstart in sechs Zeilen.
|
||
- Anleitung, Roadmap, Aufwandsschätzung, Devlog und Versionierungsregeln.
|