Version 1.0.0 — Freigabe für den Echtbetrieb (#17)
Pruefung / pruefung (push) Canceled after 0s
Pruefung / oberflaeche (push) Canceled after 0s

This commit was merged in pull request #17.
This commit is contained in:
2026-09-06 20:08:16 +00:00
parent 14cc4de9b8
commit 3b20abcb23
11 changed files with 799 additions and 30 deletions
+78
View File
@@ -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 —
+32 -1
View File
@@ -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
+6 -2
View File
@@ -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.
---
+101 -1
View File
@@ -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
---
+283
View File
@@ -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/<Ordner>
```
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.*
+29 -9
View File
@@ -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.
---
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "florian",
"version": "0.12.0",
"version": "1.0.0",
"private": true,
"description": "FLORIAN — Feuerwehr-Verwaltungsplattform",
"engines": {
+8 -4
View File
@@ -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()]);
+8 -5
View File
@@ -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()]);
+15 -3
View File
@@ -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;
});
afterAll(async () => {
if (erreichbar) {
/** Alles entfernen, was dieser Test anlegt. */
async function aufraeumen(): Promise<void> {
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 aufraeumen();
await system.$disconnect();
});
Executable
+234
View File
@@ -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