diff --git a/CHANGELOG.md b/CHANGELOG.md index 293204a..a7c220b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,84 @@ Alle nennenswerten Änderungen an FLORIAN. Format nach 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 — diff --git a/README.md b/README.md index 6516302..1fad810 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ # FLORIAN — Feuerwehr-Verwaltungsplattform +**Version 1.0.0** — freigegeben für den Echtbetrieb. + Selbstgehostete Verwaltungssoftware für Freiwillige Feuerwehren. Läuft im Gerätehaus, nicht in der Cloud. Keine Telemetrie, keine externen CDNs. +> **Wer FLORIAN benutzen und nicht bauen will:** +> [**Kurzanleitung**](docs/KURZANLEITUNG.md) — alles Wichtige nach Rollen +> sortiert, auf ein paar Seiten. + ## Schnellstart ```bash @@ -43,7 +49,8 @@ Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und | Datei | Inhalt | |---|---| -| [docs/ANLEITUNG.md](docs/ANLEITUNG.md) | Bedienung mit Bildschirmfotos | +| [docs/KURZANLEITUNG.md](docs/KURZANLEITUNG.md) | **Der Einstieg.** Nach Rollen sortiert, ein paar Seiten | +| [docs/ANLEITUNG.md](docs/ANLEITUNG.md) | Die vollständige Bedienung mit Bildschirmfotos, zum Nachschlagen | | [docs/ROADMAP.md](docs/ROADMAP.md) | Phasenplan und was jeweils entsteht | | [docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md) | Aufwand je Phase, Schätzung gegen Ist | | [docs/DEVLOG.md](docs/DEVLOG.md) | Entwicklungstagebuch mit Zeiterfassung | @@ -1655,6 +1662,30 @@ ohne Traefik. In Produktion terminiert Traefik TLS und verteilt auf Der App-Container wendet beim Start ausstehende Migrationen an; der Worker tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren. +### Aktualisieren + +```bash +./update.sh --pruefen # nachsehen, ob es etwas Neues gibt +./update.sh # einspielen +``` + +Der Ablauf ist so gebaut, dass er an jeder Stelle abbrechen darf, ohne eine +halb aktualisierte Anlage zu hinterlassen: + +1. Nachsehen, ob überhaupt etwas Neues da ist — sonst endet es hier. +2. **Sichern.** Vor allem anderen; ein Update ist der häufigste Anlass, eine + Sicherung zu brauchen. +3. Neue Fassung holen und die Abbilder bauen. Schlägt das fehl, läuft die alte + Fassung unverändert weiter — es wurde noch nichts umgeschaltet. +4. Umschalten. Der App-Container spielt dabei die Migrationen ein. +5. Nachsehen, ob die Anwendung wieder antwortet. Tut sie es nach zweieinhalb + Minuten nicht, stehen die letzten Protokollzeilen da und die Befehle, mit + denen der vorherige Stand zurückkommt. + +Vorher prüft das Skript, dass Docker, git und die `.env` da sind und dass im +Verzeichnis keine unverbuchten Änderungen liegen. `--ohne-sicherung` gibt es +für Testinstallationen; im Betrieb gehört es nicht benutzt. + ## Datenschutz - Keine Telemetrie, keine externen CDNs, keine Google Fonts. Es werden nur diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md index cb90676..5469d95 100644 --- a/docs/ANLEITUNG.md +++ b/docs/ANLEITUNG.md @@ -1,7 +1,11 @@ # Anleitung -Bedienung von FLORIAN, Stand Phase 11 (Version 0.12.0). Die Bilder zeigen die -Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt. +Bedienung von FLORIAN, Version 1.0.0. Die Bilder zeigen die Beispielwehr aus +dem Seed und werden mit `npm run screenshots` neu erzeugt. + +> **Das hier ist zum Nachschlagen, nicht zum Durchlesen.** Wer anfangen will, +> nimmt die [Kurzanleitung](KURZANLEITUNG.md) — nach Rollen sortiert, auf ein +> paar Seiten. --- diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 5831627..241628b 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -24,7 +24,107 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte | Phase 9 — Kassenverwaltung | 4:10 h | 1 | 04.09.2026 | | Phase 10 — Mandantenfähigkeit | 4:20 h | 1 | 05.09.2026 | | Phase 11 — Freigabereife | 2:40 h | 1 | 05.09.2026 | -| **Summe** | **35:30 h** | **14** | | +| 1.0.0 — Freigabe | 0:50 h | 1 | 06.09.2026 | +| **Summe** | **36:20 h** | **15** | | + +--- + +## 2026-09-06 · 1.0.0 — Freigabe + +**09:20 – 10:10 · 0:50 h · Version 1.0.0** + +### Was entstanden ist + +Kein Modul. Zwei Dinge, die zum Betrieb gehören, und eine Entscheidung. + +| Was | Wofür | +|---|---| +| `update.sh` | Aktualisieren auf einem Linux-Server, in einem Befehl | +| `docs/KURZANLEITUNG.md` | Der Einstieg für die Wehr, nach Rollen sortiert | +| Version 1.0.0 | Ab hier semantische Versionierung | + +### Entscheidungen + +**Das Update-Skript sichert, bevor es irgendetwas anfasst.** Ein Update ist +der häufigste Anlass, eine Sicherung zu brauchen — und der Moment, in dem +niemand daran denkt. `--ohne-sicherung` gibt es, aber mit einer Warnung und +nicht als Vorgabe. + +**Die Reihenfolge ist so gewählt, dass ein Abbruch harmlos bleibt.** Holen und +Bauen passieren, bevor umgeschaltet wird: schlägt der Bau fehl, läuft die alte +Fassung unverändert weiter. Erst danach wird umgeschaltet, und ab da nennt die +Fehlerbehandlung die Befehle für den Rückweg. + +**Die Kurzanleitung ist aufgabenorientiert, nicht modulorientiert.** Die große +Anleitung hat 63 Kapitel und ist nach Funktionsbereichen sortiert — richtig +zum Nachschlagen, unbrauchbar zum Anfangen. Die Kurzanleitung fragt stattdessen +„was willst du tun": ein Gerät prüfen, einen Kontoauszug einlesen, einen +Termin absagen. + +**Sie erklärt bei jedem heiklen Punkt das Warum.** Warum ein abgeschlossenes +Prüfprotokoll nicht änderbar ist, warum gegengebucht statt korrigiert wird, +warum der Atemschutzhinweis stehen bleibt. Wer den Grund kennt, sucht nicht +nach dem Weg drumherum. + +**Die Roadmap hakt nicht alles ab.** Zwei Freigabevoraussetzungen sind +organisatorisch und stehen aus — die Abnahme des Löschkonzepts und die +Schulung. Sie stehen mit ihrem Stand dort, statt stillschweigend als erledigt +zu gelten. Eine Freigabe, die abhakt, was nicht geprüft wurde, ist keine. + +### Wo es gehakt hat + +**Die Ursache der Testflackerei gefunden — nach elf Phasen.** + +Beim abschließenden Durchlauf fielen wieder einmal fünf, dann sechs, dann ein +Test aus, in wechselnder Besetzung. Dieses Mal habe ich nicht weitergemacht, +sondern fünf Läufe hintereinander mitgeschrieben: betroffen waren immer +Zusicherungen über Trigger — „Wiki-Fassungen bleiben", „Mandantentrennung", +„abgeschlossene Prüfprotokolle". + +`ki-datenbank.test.ts` und `atemschutz-datenbank.test.ts` schalteten beim +Aufräumen ihre Trigger mit `ALTER TABLE ... DISABLE TRIGGER USER` 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 also die eine Datei abräumte, prüfte +die andere gerade, dass genau dieser Trigger greift, und sah keinen. + +`SET LOCAL session_replication_role = 'replica'` bewirkt dasselbe, aber nur in +der eigenen Transaktion. Danach: fünf Läufe hintereinander 601 grün. + +Rückwirkend erklärt das eine Reihe von Ausfällen, die ich als Flackern +abgetan habe — sie waren keines. Die Lehre ist unbequem: „läuft einzeln durch, +also Umgebung" ist eine bequeme Erklärung und oft die falsche. + +| Weiteres | Ursache | Lösung | +|---|---|---| +| Ein Datenbanktest ließ sich nur einmal ausführen | `mandanten-datenbank.test.ts` räumte nur am Ende auf; ein abgebrochener Lauf legte den nächsten lahm | Räumt jetzt auch am Anfang auf — dasselbe Muster wie in Phase 10 bei `ki-datenbank` | + +### Abweichung von der Vorgabe + +**`update.sh` ist nicht auf einem Linux-Server erprobt.** Entwickelt wurde +unter Windows; geprüft sind die Syntax (`bash -n`), die Hilfe, die +Argumentbehandlung und der Wächter gegen unverbuchte Änderungen — alles unter +Git Bash. Der eigentliche Ablauf (fetch, build, up, Gesundheitsprüfung) läuft +erst dort, wo FLORIAN wirklich steht. + +Das ist keine Nachlässigkeit, sondern die Grenze dieser Umgebung. Beim ersten +echten Update gehört `./update.sh --pruefen` vorangestellt: es holt nichts und +ändert nichts, zeigt aber, ob Docker, git und die `.env` gefunden werden. + +### Nachweise + +- `npm run typecheck` — ohne Ausgabe +- `npm run lint` — ohne Befund +- `npm run test` — 601 Tests grün +- `bash -n update.sh` — Syntax in Ordnung +- `./update.sh --help` und `--quatsch` — Ausgabe und Ablehnung wie vorgesehen +- Der Wächter gegen unverbuchte Änderungen greift (an einem schmutzigen + Arbeitsverzeichnis geprüft) +- Alle acht Bilder der Kurzanleitung existieren +- `npm run test` **fünfmal hintereinander** — jedes Mal 601 grün. Das war + nach elf Phasen der erste Lauf, bei dem ich das behaupten kann +- `npm run test:e2e` — **268 grün, kein Fehlschlag, kein Flackern.** Ebenfalls + der erste vollständig saubere Durchlauf in diesem Projekt +- `npm run build` — sauber --- diff --git a/docs/KURZANLEITUNG.md b/docs/KURZANLEITUNG.md new file mode 100644 index 0000000..c6e76d6 --- /dev/null +++ b/docs/KURZANLEITUNG.md @@ -0,0 +1,283 @@ +# FLORIAN — Kurzanleitung + +Alles, was man am Anfang braucht, auf ein paar Seiten. Nach Rollen sortiert: +such dir den Abschnitt, der zu dir gehört, und lies den Rest später. + +Ausführlich steht alles in der [Anleitung](ANLEITUNG.md) — 63 Kapitel, zum +Nachschlagen, nicht zum Durchlesen. + +--- + +## Für alle + +### Anmelden + +FLORIAN läuft im Browser. Die Adresse bekommst du vom Kommandanten. + +![Anmeldung](screenshots/01-anmeldung-light.png) + +Nach der Anmeldung landest du auf der Übersicht: was ansteht, was fällig ist, +was gemeldet wurde. + +**Das Passwort gehört nach der ersten Anmeldung geändert.** Oben rechts auf +deinen Namen. + +### Was du überall findest + +| Links in der Leiste | Wofür | +|---|---| +| **Übersicht** | Die Startseite: Fristen, Mängel, nächste Termine | +| **Termine** | Übungsabende, Lehrgänge, Versammlungen — mit Rückmeldung | +| **Dienstausweis** | Dein Ausweis mit QR-Code, auch fürs Handy | +| **Wissen** | Merkblätter und Dienstanweisungen der Wehr | + +Du siehst nur, was zu deiner Rolle gehört. Fehlt dir etwas, sag dem +Kommandanten Bescheid — Rollen werden dort vergeben. + +### Zu einem Termin zurückmelden + +**Termine** öffnen, den Termin anklicken, **Zusagen** oder **Absagen**. Fertig. +Wer absagt, kann einen Grund angeben; das hilft dem Ausbilder bei der Planung. + +### Am Terminal im Gerätehaus + +Am Wandtablet meldest du dich mit **PIN oder Ausweiskarte** an — nicht mit +E-Mail und Passwort. Deine PIN bekommst du vom Kommandanten. + +![Terminal](screenshots/25-kiosk-kacheln-light.png) + +Dort geht: anmelden zum Dienst, einen Mangel melden, eine Fahrt eintragen, +Schutzkleidung zur Reinigung geben. + +--- + +## Für den Gerätewart + +Das ist der Bereich, in dem FLORIAN am meisten abnimmt. + +### Der wichtigste Bildschirm: Prüfungen + +![Fristen](screenshots/11-fristen-light.png) + +Unter **Prüfungen** steht, was wann fällig wird. Rot heißt überfällig, gelb +heißt demnächst. Die Fristen rechnet FLORIAN selbst aus den Prüfintervallen +der Gerätetypen aus — du musst nichts nachhalten. + +### Ein Gerät prüfen + +1. **Prüfungen** → das fällige Gerät anklicken → **Prüfung durchführen**. +2. Die Checkliste durchgehen. Sie kommt aus der Wartungsvorlage des Gerätetyps. +3. Ergebnis eintragen, unterschreiben, abschließen. + +![Prüfprotokoll](screenshots/18-pruefprotokoll-light.png) + +Danach entsteht ein **Prüfprotokoll als PDF** — das ist der Nachweis gegenüber +der Unfallkasse. + +> **Achtung:** Ein abgeschlossenes Protokoll lässt sich nicht mehr ändern. Das +> ist Absicht — ein nachträglich änderbarer Nachweis wäre keiner. Ist etwas +> falsch, wird eine neue Prüfung erfasst. + +### Ein Gerät anlegen + +**Geräte** → **Gerät anlegen**. Pflicht sind nur Inventarnummer und +Bezeichnung; der Rest kann nachkommen. + +Wähle möglichst einen **Gerätetyp** — daran hängen Prüfintervall und +Lebensdauer, und ohne Typ rechnet FLORIAN keine Frist. + +### Einen Mangel bearbeiten + +Mängel kommen meist vom Terminal — jemand meldet sie beim Einrücken. + +**Mängel** → anklicken → jemandem zuweisen → nach der Behebung **erledigen**. +Ein Gerät mit einem schweren Mangel wird automatisch gesperrt. + +### Was noch dazugehört + +| Wo | Wofür | +|---|---| +| **Rückrufe** | Herstellerrückrufe: welche eurer Geräte betroffen sind | +| **Lebensdauer** | Helme, Schläuche, Schutzkleidung mit Höchstalter | +| **Fahrzeuge** | Beladung nach Geräteräumen | + +--- + +## Für den Atemschutzgerätewart + +> **Der wichtigste Satz zuerst:** Die Überwachungstafel am Fahrzeug bleibt +> führend. FLORIAN ergänzt sie, ersetzt sie nicht. Das steht auch dauerhaft +> über jeder Ansicht, und es soll dort stehen. + +### Wer darf unter Pressluftatmer? + +![Atemschutz](screenshots/31-atemschutz-light.png) + +**Atemschutz** zeigt jeden Träger mit seinem Stand: G26, Belastungsübung, +Einsatzübung. Wer eine der Voraussetzungen nicht erfüllt, steht rot da — mit +Klartext, woran es liegt, nicht nur mit einer Farbe. + +### Eine G26 eintragen + +**Atemschutz** → **Nachweise** → Person wählen → G26 erfassen. Untersuchungs­datum, +Ergebnis und Gültigkeit reichen; den Befund speichert FLORIAN bewusst nicht. + +Dasselbe für Belastungsübungen. + +### Eine Überwachung führen + +**Atemschutz** → **Überwachung eröffnen**. Trupp zusammenstellen, Druckwerte +in Intervallen eintragen, Rückzugsdruck im Blick behalten. FLORIAN rechnet den +Rückzugsdruck und warnt, bevor es knapp wird. + +![Überwachung](screenshots/33-atemschutz-ueberwachung-light.png) + +Nach dem Ende entsteht ein vollständiges Protokoll als PDF. + +> Solange ein Trupp im Einsatz ist, lässt sich die Überwachung nicht beenden. + +--- + +## Für den Kommandanten + +### Mitglieder und Rollen + +**Mitglieder** → Person anklicken → Rollen vergeben. + +Neun Rollen, jede mit festem Rechteumfang. Die wichtigsten: Gerätewart, +Atemschutzgerätewart, Kassier, Schriftführer, Ausbilder. + +**Jemand kommt nicht hinein?** Dann hat die Person eine Akte, aber kein Konto. +Konten verwaltet, wer den Server betreibt — unter *Wehren und Konten*. + +### Einen Einsatz erfassen + +**Einsätze** → **Einsatz anlegen**. Die sechs Zeitstempel (Alarmierung, +Ausrücken, Eintreffen, Einsatzende, Einrücken, Einsatzbereitschaft), die +beteiligten Fahrzeuge und die Mannschaft. + +![Einsatz](screenshots/22-einsatz-light.png) + +Daraus entsteht der **Einsatzbericht als PDF** und die Statistik fürs Jahr. + +### Kostenersatz abrechnen + +Fehlalarm einer Brandmeldeanlage, Öleinsatz mit Verursacher, vermieteter +Schulungsraum: **Kasse** → **Rechnungen** → *Rechnung schreiben*. Die Nummer +vergibt FLORIAN fortlaufend. + +### Wenn jemand Auskunft über seine Daten verlangt + +Kommt selten vor, hat aber eine Frist von einem Monat. + +**Mitglieder** → Person → **Datenauskunft** → *Auskunft erstellen*. Daraus +entsteht ein PDF mit allem, was über die Person gespeichert ist. Ausdrucken +und aushändigen, fertig. + +--- + +## Für den Kassier + +![Kasse](screenshots/45-kasse-light.png) + +### Buchen + +**Kasse** → **Buchungen** → *Buchung erfassen*. Der Betrag ist immer positiv; +ob Einnahme oder Ausgabe, sagen die beiden Knöpfe daneben. + +FLORIAN schlägt eine Kategorie vor — meist die richtige, weil er nachsieht, +wie ihr dieselbe Sache zuletzt gebucht habt. + +### Kontoauszug einlesen + +Statt fünfzig Umsätze abzutippen: im Online-Banking den **Umsatzexport** +herunterladen (CAMT.053 oder MT940), dann **Kasse** → **Buchungen** → +*Kontoauszug*. + +Es entstehen **Entwürfe**, keine fertigen Buchungen — durchsehen musst du sie. +Umsätze, die schon in der Kasse stehen, erkennt FLORIAN und hakt sie nicht +noch einmal ab. + +### Beiträge + +Einmal im Jahr: **Kasse** → **Beiträge** → *Beitragslauf*. Vorher steht da, wie +viele Beiträge entstehen — und wer keinen passenden Satz hat. + +Zahlungen hakst du in der Liste ab; dabei entsteht gleich die Buchung. + +### Festschreiben — und was das bedeutet + +Eine neue Buchung ist erst einmal ein **Entwurf** und änderbar. Mit +*Festschreiben* wird sie unveränderbar. + +Ist danach etwas falsch, wird **gegengebucht**: eine zweite Buchung in die +andere Richtung, mit Begründung. Beide bleiben stehen. Das sieht nach mehr +Aufwand aus und ist genau der Punkt — eine verschwundene Buchung kann ein +Kassenprüfer nicht prüfen, eine berichtigte schon. + +### Kassenprüfung + +**Kasse** → **Abschluss** → *Kassenprüfung anlegen*. Links steht der Bestand +laut Buch, rechts trägst du ein, was gezählt wurde. + +Bleibt eine Differenz, muss sie erklärt werden — sonst lässt sich der Abschluss +nicht festschreiben. In fünf Jahren weiß sonst niemand mehr, woher sie kam. + +Danach entsteht der **Kassenprüfbericht als PDF** mit Unterschriftsfeldern. + +--- + +## Für den, der den Server betreibt + +### Sichern + +```bash +npm run sicherung +``` + +Sichert Datenbank **und** Dateien. Das Ergebnis gehört auf ein anderes Gerät — +eine Sicherung auf derselben Festplatte hilft gegen einen Festplattenfehler +nicht. + +### Einmal im Jahr proben + +```bash +npm run wiederherstellen -- --quelle sicherungen/ +``` + +So passiert nichts, es wird nur angezeigt, was geschähe. Erst mit `--wirklich` +wird zurückgespielt. + +> Eine Sicherung, die nie zurückgespielt wurde, ist eine Vermutung. Der Ordner +> kann leer sein, der Auszug abgeschnitten — man erfährt es sonst an dem Tag, +> an dem man es nicht erfahren will. + +### Aktualisieren + +```bash +./update.sh --pruefen # nachsehen, ob es etwas Neues gibt +./update.sh # einspielen +``` + +Das Skript sichert von sich aus, bevor es etwas anfasst, und sagt bei einem +Fehler, wie der vorherige Stand zurückkommt. + +--- + +## Wenn etwas nicht geht + +| Was | Woran es meist liegt | +|---|---| +| **Anmeldung geht nicht** | Nach fünf Fehlversuchen ist das Konto fünf Minuten gesperrt. Kurz warten. | +| **Ein Menüpunkt fehlt** | Die Rolle fehlt. Der Kommandant vergibt sie unter *Mitglieder*. | +| **Ein PDF entsteht nicht** | Es wird im Hintergrund erzeugt; nach ein paar Sekunden die Seite neu laden. | +| **Das Terminal meldet niemanden an** | PIN falsch, oder das Tablet ist nicht mehr eingerichtet. | +| **Eine Buchung lässt sich nicht ändern** | Sie ist festgeschrieben. Gegenbuchen. | + +Steht die Antwort nicht dabei: [Anleitung, letztes Kapitel](ANLEITUNG.md). + +--- + +*FLORIAN läuft auf einem Server, den die Feuerwehr selbst betreibt. Es gibt +keine Telemetrie, keine Cloud und keine Weitergabe an Dritte — die Daten +bleiben im Gerätehaus.* diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e7c8fe2..50a58f2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -21,7 +21,7 @@ Phase 8 ████████████████████ fertig Phase 9 ████████████████████ fertig 0.10.0 Phase 10 ████████████████████ fertig 0.11.0 Phase 11 ████████████████████ fertig 0.12.0 -Freigabe ░░░░░░░░░░░░░░░░░░░░ offen 1.0.0 +Freigabe ████████████████████ fertig 1.0.0 ``` --- @@ -310,17 +310,37 @@ also genau die Probe, wegen der 1.0.0 sie verlangt. **Die Wehr kann jetzt:** eine Selbstauskunft erteilen, ihren Bestand sichern und ihn nachweislich zurückholen. -## 1.0.0 — Freigabe +## 1.0.0 — Freigabe · am 06.09.2026 erteilt Kein neues Modul, sondern eine Entscheidung: die Wehr setzt FLORIAN im -Echtbetrieb ein. Voraussetzungen: +Echtbetrieb ein. -- Datenübernahme aus der bisherigen Verwaltung abgeschlossen und abgeglichen -- ~~Sicherungskonzept steht und ist einmal wiederhergestellt worden~~ — mit Phase 11 erledigt -- Schulung von Kommandant, Gerätewart und Atemschutzgerätewart erfolgt -- Löschkonzept nach DSGVO abgenommen (die Auskunftsseite dazu gibt es seit Phase 11) -- Der Hinweis zur analogen Atemschutzüberwachung wird bewusst und - dokumentiert abgeschaltet — oder bewusst stehen gelassen +**Damit das nicht zur Formalie wird, hier der Stand jeder Voraussetzung — auch +der offenen.** Eine Freigabe, die alles abhakt, was ohnehin nicht geprüft +wurde, ist keine. + +| Voraussetzung | Stand | +|---|---| +| Datenübernahme aus der bisherigen Verwaltung | **entfällt.** Die Wehr pflegt den Bestand von Hand ein; es gibt kein Altsystem, aus dem übernommen würde. Für Mitglieder, Fahrzeuge und Geräte steht der Import-Assistent bereit, falls sich das ändert. | +| Sicherungskonzept steht und ist einmal wiederhergestellt worden | **erledigt** mit Phase 11. Die Probe hat dabei einen Fehler gefunden, der sonst erst im Notfall aufgefallen wäre. | +| Löschkonzept nach DSGVO | **technisch vollständig** seit Phase 11: Auskunft nach Art. 15, Anonymisierung im Aufbewahrungslauf, konfigurierbare Fristen. Die Abnahme durch die Wehr ist ein organisatorischer Schritt und steht aus. | +| Schulung von Kommandant, Gerätewart und Atemschutzgerätewart | **steht aus.** Sie läuft nach der Freigabe; Grundlage ist die [Kurzanleitung](KURZANLEITUNG.md). | +| Hinweis zur analogen Atemschutzüberwachung | **bewusst stehen gelassen.** `ATEMSCHUTZ_HINWEIS=true`. FLORIAN bleibt damit ergänzend; die Tafel am Fahrzeug ist führend. Das Abschalten verlangt ohnehin einen Kommandantenbeschluss samt Fundstelle. | + +Die beiden offenen Punkte sind **organisatorisch, nicht technisch**: an der +Software hängt nichts davon ab. Sie stehen hier, damit später nachlesbar ist, +was zum Zeitpunkt der Freigabe erledigt war und was nicht. + +### Was 1.0.0 bedeutet — und was nicht + +**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. + +**Bedeutet nicht:** dass keine Fehler mehr da sind. Es bedeutet, dass die +Wehr echte Daten hineingeben kann, ohne befürchten zu müssen, sie beim +nächsten Update zu verlieren. --- diff --git a/package.json b/package.json index 956a2f1..f73958a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "florian", - "version": "0.12.0", + "version": "1.0.0", "private": true, "description": "FLORIAN — Feuerwehr-Verwaltungsplattform", "engines": { diff --git a/tests/unit/atemschutz-datenbank.test.ts b/tests/unit/atemschutz-datenbank.test.ts index 5dd9aad..917d659 100644 --- a/tests/unit/atemschutz-datenbank.test.ts +++ b/tests/unit/atemschutz-datenbank.test.ts @@ -77,11 +77,15 @@ afterAll(async () => { // Protokollzeilen lassen sich nicht loeschen — die Trigger sitzen auf // den Tabellen. Fuer das Aufraeumen der Testdaten werden sie kurz // abgeschaltet; im Betrieb passiert das nie. - await tx.$executeRawUnsafe('ALTER TABLE "druckmessung" DISABLE TRIGGER USER'); - await tx.$executeRawUnsafe('ALTER TABLE "atemschutz_ereignis" DISABLE TRIGGER USER'); + // Trigger fuer das Aufraeumen stilllegen — sitzungslokal. + // + // NICHT ueber `ALTER TABLE ... DISABLE TRIGGER`: das ist eine + // Schemaänderung und gilt fuer alle. Vitest faehrt die Testdateien + // parallel, und waehrend hier abgeraeumt wurde, pruefte anderswo + // eine Datei, dass genau dieser Trigger greift — und sah keinen. + // `session_replication_role` wirkt nur in dieser Transaktion. + await tx.$executeRawUnsafe("SET LOCAL session_replication_role = 'replica'"); await tx.organisation.deleteMany({ where: { slug: SLUG } }); - await tx.$executeRawUnsafe('ALTER TABLE "druckmessung" ENABLE TRIGGER USER'); - await tx.$executeRawUnsafe('ALTER TABLE "atemschutz_ereignis" ENABLE TRIGGER USER'); }); } await Promise.allSettled([app.$disconnect(), system.$disconnect()]); diff --git a/tests/unit/ki-datenbank.test.ts b/tests/unit/ki-datenbank.test.ts index 06c3246..22db6e4 100644 --- a/tests/unit/ki-datenbank.test.ts +++ b/tests/unit/ki-datenbank.test.ts @@ -81,12 +81,15 @@ afterAll(async () => { if (erreichbar) { await system.$transaction(async (tx) => { await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; - // Der Unveränderlichkeits-Trigger sitzt auf ki_vorgang. Für das - // Aufräumen der Testdaten wird er kurz abgeschaltet; im Betrieb - // passiert das nie. - await tx.$executeRawUnsafe('ALTER TABLE "ki_vorgang" DISABLE TRIGGER USER'); + // Trigger fuer das Aufraeumen stilllegen — sitzungslokal. + // + // NICHT ueber `ALTER TABLE ... DISABLE TRIGGER`: das ist eine + // Schemaänderung und gilt fuer alle. Vitest faehrt die Testdateien + // parallel, und waehrend hier abgeraeumt wurde, pruefte anderswo + // eine Datei, dass genau dieser Trigger greift — und sah keinen. + // `session_replication_role` wirkt nur in dieser Transaktion. + await tx.$executeRawUnsafe("SET LOCAL session_replication_role = 'replica'"); await tx.organisation.deleteMany({ where: { slug: SLUG } }); - await tx.$executeRawUnsafe('ALTER TABLE "ki_vorgang" ENABLE TRIGGER USER'); }); } await Promise.allSettled([app.$disconnect(), system.$disconnect()]); diff --git a/tests/unit/mandanten-datenbank.test.ts b/tests/unit/mandanten-datenbank.test.ts index 23d3ce6..fd6cdda 100644 --- a/tests/unit/mandanten-datenbank.test.ts +++ b/tests/unit/mandanten-datenbank.test.ts @@ -43,6 +43,15 @@ beforeAll(async () => { return; } + // Erst aufraeumen, dann anlegen. + // + // Aufraeumen nur am Ende reicht nicht: bricht ein Lauf ab — abgebrochene + // Sitzung, Fehler in einer anderen Datei, geschlossenes Terminal —, bleiben + // die Datensaetze stehen und der naechste Lauf scheitert an einer + // Eindeutigkeit, die mit dem Geprueften nichts zu tun hat. Das kostet dann + // zehn Minuten Fehlersuche an der falschen Stelle. + await aufraeumen(); + const a = await system.organisation.upsert({ where: { slug: SLUG_A }, update: {}, @@ -91,14 +100,17 @@ beforeAll(async () => { konto = benutzer.id; }); +/** Alles entfernen, was dieser Test anlegt. */ +async function aufraeumen(): Promise { + await system.$transaction(async (tx) => { + await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; + await tx.benutzer.deleteMany({ where: { email: { endsWith: "@test-mandant.de" } } }); + await tx.organisation.deleteMany({ where: { slug: { in: [SLUG_A, SLUG_B] } } }); + }); +} + afterAll(async () => { - if (erreichbar) { - await system.$transaction(async (tx) => { - await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; - await tx.benutzer.deleteMany({ where: { email: { endsWith: "@test-mandant.de" } } }); - await tx.organisation.deleteMany({ where: { slug: { in: [SLUG_A, SLUG_B] } } }); - }); - } + if (erreichbar) await aufraeumen(); await system.$disconnect(); }); diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..9bb83ef --- /dev/null +++ b/update.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# ============================================================================= +# FLORIAN aktualisieren. +# +# ./update.sh neue Fassung holen und einspielen +# ./update.sh --pruefen nur nachsehen, ob es etwas Neues gibt +# ./update.sh --ohne-sicherung für Testinstallationen +# +# Der Ablauf ist so gebaut, dass er an jeder Stelle abbrechen darf, ohne die +# Wehr mit einer halb aktualisierten Anlage zurückzulassen: +# +# 1. Nachsehen, ob überhaupt etwas Neues da ist. +# 2. SICHERN — Datenbank und Dateien. Vor allem anderen. +# 3. Neue Fassung holen und Abbilder bauen. +# 4. Umschalten. Der App-Container spielt dabei die Migrationen ein. +# 5. Nachsehen, ob die Anwendung wieder antwortet. +# +# Geht Schritt 3 schief, läuft die alte Fassung weiter — es wurde nichts +# angefasst. Geht Schritt 4 oder 5 schief, steht in der Ausgabe, mit welchem +# Befehl sich der vorherige Stand zurückholen lässt. +# +# Voraussetzungen: Linux, Docker mit Compose-Erweiterung, git, ein .env im +# selben Verzeichnis. +# ============================================================================= +set -Eeuo pipefail + +VERZEICHNIS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$VERZEICHNIS" + +COMPOSE=(docker compose -f docker-compose.yml) +NUR_PRUEFEN=false +OHNE_SICHERUNG=false + +for argument in "$@"; do + case "$argument" in + --pruefen) NUR_PRUEFEN=true ;; + --ohne-sicherung) OHNE_SICHERUNG=true ;; + -h|--help) + sed -n '3,20p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Unbekannte Angabe: $argument" >&2 + echo "Bekannt sind: --pruefen, --ohne-sicherung, --help" >&2 + exit 2 + ;; + esac +done + +# --- Ausgabe ---------------------------------------------------------------- +# Farben nur, wenn wirklich ein Terminal dranhängt. In einem Cron-Protokoll +# sind Steuerzeichen nur Müll. +if [ -t 1 ]; then + ROT=$'\033[31m'; GRUEN=$'\033[32m'; GELB=$'\033[33m'; FETT=$'\033[1m'; AUS=$'\033[0m' +else + ROT=''; GRUEN=''; GELB=''; FETT=''; AUS='' +fi + +schritt() { echo; echo "${FETT}==> $*${AUS}"; } +hinweis() { echo " $*"; } +warnung() { echo "${GELB} $*${AUS}"; } +fehler() { echo "${ROT}FEHLER: $*${AUS}" >&2; } + +# Wo wir standen, bevor etwas verändert wurde — für die Rückkehr. +STAND_VORHER="" +SICHERUNG="" + +abbruch() { + local zeile=$1 + echo + fehler "Abbruch in Zeile $zeile." + echo + if [ -n "$STAND_VORHER" ]; then + echo "Der vorherige Stand war: $STAND_VORHER" + echo "Zurueck damit:" + echo " git checkout $STAND_VORHER" + echo " ${COMPOSE[*]} up -d --build" + fi + if [ -n "$SICHERUNG" ]; then + echo + echo "Die Sicherung von vorhin liegt in: $SICHERUNG" + echo "Zurueckspielen (ueberschreibt den Bestand):" + echo " npm run wiederherstellen -- --quelle $SICHERUNG --wirklich" + fi + exit 1 +} +trap 'abbruch $LINENO' ERR + +# --- Voraussetzungen -------------------------------------------------------- +schritt "Voraussetzungen" + +for werkzeug in git docker; do + if ! command -v "$werkzeug" >/dev/null 2>&1; then + fehler "$werkzeug ist nicht installiert." + exit 1 + fi +done + +if ! docker compose version >/dev/null 2>&1; then + fehler "Docker Compose fehlt (die Erweiterung 'docker compose', nicht docker-compose)." + exit 1 +fi + +if [ ! -f .env ]; then + fehler "Es gibt keine .env in $VERZEICHNIS." + hinweis "Fuer eine Erstinstallation: cp .env.example .env und ausfuellen." + exit 1 +fi + +# Ein Update mit unverbuchten Aenderungen wuerde sie beim Holen ueberschreiben +# oder in einem Konflikt enden. Beides mitten in einem Update ist unschoen. +if [ -n "$(git status --porcelain)" ]; then + fehler "Im Verzeichnis liegen Aenderungen, die nicht eingecheckt sind." + git status --short | head -20 + echo + hinweis "Bitte erst klaeren. Die .env gehoert nicht ins Git und stoert hier nicht." + exit 1 +fi + +hinweis "Docker, git und .env sind da." + +# --- Gibt es etwas Neues? --------------------------------------------------- +schritt "Nach einer neuen Fassung sehen" + +ZWEIG="$(git rev-parse --abbrev-ref HEAD)" +STAND_VORHER="$(git rev-parse HEAD)" + +git fetch --quiet origin "$ZWEIG" + +STAND_NEU="$(git rev-parse "origin/$ZWEIG")" + +if [ "$STAND_VORHER" = "$STAND_NEU" ]; then + hinweis "Alles aktuell — nichts zu tun." + hinweis "Stand: $(git log -1 --format='%h %s')" + exit 0 +fi + +ANZAHL="$(git rev-list --count "$STAND_VORHER..$STAND_NEU")" +hinweis "$ANZAHL neue Aenderung(en):" +git log --oneline --no-decorate "$STAND_VORHER..$STAND_NEU" | head -20 | sed 's/^/ /' + +if [ "$NUR_PRUEFEN" = true ]; then + echo + hinweis "Nur nachgesehen — es wurde nichts geaendert." + hinweis "Zum Einspielen: ./update.sh" + exit 0 +fi + +# --- Sichern ---------------------------------------------------------------- +# Vor allem anderen, und ohne Umgehung im Regelfall. Ein Update ist der +# haeufigste Anlass, eine Sicherung zu brauchen. +if [ "$OHNE_SICHERUNG" = true ]; then + schritt "Sicherung uebersprungen" + warnung "--ohne-sicherung ist gesetzt. Bei einem Fehler gibt es kein Zurueck." +else + schritt "Sichern" + + if ! command -v npm >/dev/null 2>&1; then + fehler "npm fehlt — ohne das laeuft das Sicherungsskript nicht." + hinweis "Entweder Node installieren oder bewusst mit --ohne-sicherung fahren." + exit 1 + fi + + npm run --silent sicherung + SICHERUNG="$(find sicherungen -maxdepth 1 -mindepth 1 -type d | sort | tail -1)" + hinweis "Liegt in: $SICHERUNG" +fi + +# --- Holen und bauen -------------------------------------------------------- +# Bis hierher ist nichts umgeschaltet: schlaegt der Bau fehl, laeuft die alte +# Fassung unveraendert weiter. +schritt "Neue Fassung holen" +git merge --ff-only "origin/$ZWEIG" +hinweis "Jetzt auf: $(git log -1 --format='%h %s')" + +schritt "Abbilder bauen" +hinweis "Das dauert ein paar Minuten." +"${COMPOSE[@]}" build + +# --- Umschalten ------------------------------------------------------------- +# Der App-Container spielt beim Start die ausstehenden Migrationen ein; der +# Worker tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig +# migrieren. +schritt "Umschalten" +"${COMPOSE[@]}" up -d --remove-orphans + +# --- Nachsehen -------------------------------------------------------------- +schritt "Nachsehen, ob die Anwendung antwortet" + +ADRESSE="http://localhost:3000/api/health" +if grep -q '^APP_URL=' .env; then + ADRESSE="$(grep '^APP_URL=' .env | head -1 | cut -d= -f2- | tr -d '"'"'"' ')/api/health" +fi + +hinweis "Frage $ADRESSE ..." + +GESUND=false +for versuch in $(seq 1 30); do + if curl -fsS --max-time 5 "$ADRESSE" >/dev/null 2>&1; then + GESUND=true + break + fi + sleep 5 + [ $((versuch % 6)) -eq 0 ] && hinweis " noch nicht da, warte weiter ($((versuch * 5)) s) ..." +done + +if [ "$GESUND" != true ]; then + fehler "Die Anwendung antwortet nach zweieinhalb Minuten nicht." + echo + echo "Die letzten Zeilen aus dem Protokoll:" + "${COMPOSE[@]}" logs --tail 40 app || true + echo + echo "Zurueck auf den vorherigen Stand:" + echo " git checkout $STAND_VORHER" + echo " ${COMPOSE[*]} up -d --build" + [ -n "$SICHERUNG" ] && echo " npm run wiederherstellen -- --quelle $SICHERUNG --wirklich" + exit 1 +fi + +# Ab hier ist nichts mehr rueckgaengig zu machen, was das Skript angerichtet +# haette — deshalb keine Fehlerfalle mehr, die zum Rueckbau raet. +trap - ERR + +schritt "Fertig" +echo "${GRUEN} FLORIAN laeuft auf dem Stand $(git log -1 --format='%h %s')${AUS}" +echo +hinweis "Zustand der Dienste:" +"${COMPOSE[@]}" ps --format ' {{.Service}}\t{{.Status}}' 2>/dev/null || "${COMPOSE[@]}" ps + +if [ -n "$SICHERUNG" ]; then + echo + hinweis "Die Sicherung von vorhin liegt in $SICHERUNG." + hinweis "Sie kann weg, sobald ein paar Tage nichts aufgefallen ist." +fi