Phase 1: Stammdatenverwaltung

Ab hier fuehrt die Wehr ihren Bestand in FLORIAN.

Datenmodell:
- Standort, Geraeteraum, Geraetetyp, Qualifikationstyp und Qualifikation
- Fahrzeug um Fahrgestellnummer, DIN-Typ, Erstzulassung, Sitzplaetze,
  Tankinhalt und UVV-Frist erweitert
- Geraet um Geraetetyp, Lebensdauerkategorie, errechnetes Lebensdauerende,
  Anschaffungspreis und die Lagerung erweitert
- Ein Geraet steht entweder an einem Standort oder ist auf einem Fahrzeug
  verlastet. Eine Pruefbedingung setzt das durch, ein Trigger stellt zusaetzlich
  sicher, dass ein angegebener Geraeteraum zum angegebenen Fahrzeug gehoert.
- RLS auf allen neuen Tabellen, Trigram-Indizes fuer die Volltextsuche

Fachlogik:
- lib/stammdaten/fristen.ts rechnet Prueftermine, Lebensdauer und den
  abgeleiteten Geraetestatus. Der Geraetetyp gibt vor, das Geraet darf
  abweichen.
- lib/stammdaten/qr.ts und qr-parser.ts erzeugen und lesen die Codes
- lib/stammdaten/etiketten.ts baut den A4-Bogen mit 65 Etiketten
- lib/stammdaten/import.ts liest CSV und Excel, erkennt deutsche Datums- und
  Zahlenformate, Windows-1252 und Dubletten innerhalb der Datei
- lib/pdf.ts spricht Gotenberg an

Oberflaeche:
- Geraeteliste mit Volltextsuche, Statusfilter, serverseitiger Sortierung,
  Blaettern, Spaltenauswahl und Massenbearbeitung
- Geraetedetail mit QR-Code, Pruefvorgabe aus dem Geraetetyp und Verlauf
- Fahrzeugliste und -detail mit Beladung nach Beladeordnung
- Mitgliederliste nach Abteilung, Mitgliedsdetail mit Qualifikationen
- Qualifikationen mit Ablaufwarnung je Vorwarnzeit des Typs
- Import-Assistent mit Zuordnung und Vorschau vor dem Uebernehmen
- QR-Scanner ueber die Kamera, mit Eingabefeld als Rueckfallweg

Behoben:
- Fristenrechnung lief ueber startOfDay aus date-fns und damit in der Zeitzone
  des Servers. Ein DATE aus der Datenbank kommt als UTC-Mitternacht und wurde
  dadurch auf den Vortag geschoben — Fristen waeren einen Tag zu frueh faellig
  geworden. Jetzt eigene Kalenderarithmetik auf UTC-Bestandteilen.
- ctx.sitzung blieb in tRPC-Prozeduren nullbar, weil t.middleware() die
  Verengung der vorigen Middleware verwirft. Middleware jetzt direkt an .use().
- CardTitle war ein div und damit keine Ueberschrift. Wer mit einer
  Sprachausgabe arbeitet, springt von Ueberschrift zu Ueberschrift; Karten
  tauchten in dieser Navigation nicht auf.

Daten und Tests:
- Seed um 3 Standorte, 20 Geraeteraeume, 14 Geraetetypen mit echten
  Pruefvorgaben, 49 Geraete und 107 Qualifikationen erweitert
- 104 Vitest-Tests (vorher 32), 44 Playwright-Tests (vorher 10)
- Anleitung um sieben bebilderte Abschnitte erweitert, Changelog, Devlog,
  Aufwandsschaetzung und Roadmap fortgeschrieben

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-03 08:31:08 +02:00
co-authored by Claude Opus 5
parent 4b3021bb50
commit 26b62a3477
79 changed files with 11738 additions and 217 deletions
+239 -10
View File
@@ -1,11 +1,10 @@
# Anleitung
Bedienung von FLORIAN, Stand Phase 0 (Version 0.1.0). Die Bilder zeigen die
Bedienung von FLORIAN, Stand Phase 1 (Version 0.2.0). Die Bilder zeigen die
Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt.
> Was hier noch fehlt — Mitglieder, Geräte, Prüfungen — kommt in den
> folgenden Phasen dazu. Der Stand ist bewusst schmal: Phase 0 baut das
> Fundament, nicht die Fachmodule. Siehe [ROADMAP.md](ROADMAP.md).
> Prüfungen, Einsätze, Atemschutz und die Kiosk-Oberfläche kommen in den
> folgenden Phasen dazu. Siehe [ROADMAP.md](ROADMAP.md).
---
@@ -94,7 +93,231 @@ nicht geblendet werden.
---
## 4. Rollen und was sie dürfen
## 4. Geräte
![Geräteliste](screenshots/03-geraeteliste-light.png)
Die Geräteliste ist die Ansicht, die am häufigsten offen ist. Von links nach
rechts steht dort, was man beim Suchen braucht: Inventarnummer, was es ist, von
wem, wo es liegt, in welchem Zustand und wann es wieder geprüft werden muss.
### Suchen und filtern
**Das Suchfeld** durchsucht Inventarnummer, Bezeichnung, Hersteller, Modell und
Seriennummer gleichzeitig. Es genügt ein Teil — „press" findet alle
Pressluftatmer. Gesucht wird erst, wenn man kurz aufhört zu tippen; das
verhindert eine Abfrage bei jedem Anschlag.
**Alle Zustände** schränkt auf einen Status ein. Praktisch, wenn man wissen
will, was gerade fällig oder gesperrt ist.
**Spalten** blendet Spalten ein und aus. Zwölf stehen zur Auswahl, sechs sind
voreingestellt. Die Auswahl bleibt im Browser gespeichert — der Gerätewart
schaut auf Prüftermine, der Kassier auf Anschaffungspreise, und beide finden
ihre Einstellung beim nächsten Mal wieder.
**Sortiert** wird durch einen Klick auf die Spaltenüberschrift, ein zweiter
Klick dreht die Richtung um.
### Farben und Fristen
| Farbe | Bedeutung |
|---|---|
| grün | einsatzbereit |
| gelb | Prüfung fällig, oder eine Frist läuft in den nächsten 30 Tagen ab |
| rot | gesperrt, ausgesondert oder überfällig |
| grau | in Reparatur |
Hinter jedem Datum steht, wie viele Tage noch bleiben — „in 12 Tagen" sagt mehr
als ein Datum, das man erst im Kopf ausrechnen muss. Was vorbei ist, steht als
„seit 62 Tagen" da.
Die Farbe allein trägt nie die Information: daneben steht immer Klartext.
### Mehrere Geräte auf einmal
Über die Kästchen links lassen sich Geräte auswählen. Sobald mindestens eines
ausgewählt ist, erscheint eine Leiste mit zwei Möglichkeiten:
- **Etiketten drucken** erzeugt den Etikettenbogen als PDF.
- **Status setzen auf …** ändert den Zustand aller ausgewählten Geräte. Der
häufigste Handgriff überhaupt: Nach einer Sammelprüfung stehen zwanzig Geräte
auf einmal wieder auf einsatzbereit.
Jede Änderung wird einzeln protokolliert — am jeweiligen Gerät, nicht als
Sammelvorgang, den später niemand mehr zuordnen kann.
---
## 5. Ein einzelnes Gerät
![Gerätedetail](screenshots/04-geraet-light.png)
### Woher das Prüfintervall kommt
Steht am Gerät kein eigenes Intervall, gilt die Vorgabe des Gerätetyps. Das
Bild zeigt es: „alle 12 Monate (aus dem Gerätetyp)". So muss man bei vierzig
Pressluftatmern nicht vierzigmal dasselbe eintragen. Schreibt der Hersteller
für eine bestimmte Charge etwas anderes vor, wird der Wert am Gerät gesetzt und
gewinnt dann.
Dasselbe gilt für die Lebensdauer. Ob sie ab Herstellung oder ab
Indienststellung zählt, hängt von der Vorschrift ab: Seile und Druckbehälter
altern ab Herstellung, Schutzkleidung meist ab Indienststellung.
### QR-Code
Rechts steht der QR-Code des Geräts. **Etikett drucken** erzeugt daraus einen
Aufkleber für den Geräteraum.
Der Code zeigt auf `.../g/<Inventarnummer>`. Wer ihn scannt und angemeldet ist,
landet direkt auf dieser Seite. Wer nicht angemeldet ist, landet zuerst auf der
Anmeldung und danach hier — **der Aufkleber ist keine Berechtigung.** Wer ihn
fotografiert, bekommt dadurch keinen Zugang.
Bewusst steht die Inventarnummer im Code und nicht die interne Kennung: Eine
UUID wäre 36 Zeichen lang, das Muster entsprechend fein, und auf einem
38 × 21 mm großen Aufkleber im staubigen Geräteraum wird das unzuverlässig.
### Verlauf
Unten steht, was mit dem Gerät passiert ist — direkt aus dem Audit-Log. Es gibt
bewusst keine zweite Historie: zwei Quellen könnten auseinanderlaufen.
---
## 6. Etiketten drucken
Der Bogen ist auf das gängige Format mit **65 Etiketten je DIN-A4-Seite**
(38,1 × 21,2 mm, 5 Spalten × 13 Zeilen) ausgelegt — den gibt es in jedem
Schreibwarenladen und er passt in jeden Laserdrucker.
Auf jedem Etikett stehen drei Dinge: der QR-Code, die Inventarnummer zum
Ablesen ohne Handy, und die Bezeichnung. Bei verlasteten Geräten kommt der
Funkrufname des Fahrzeugs dazu.
Der Druck kann an einer beliebigen Position des Bogens beginnen. Wer drei
Etiketten braucht, muss so keinen ganzen Bogen opfern.
> Erzeugt wird das PDF von Gotenberg, einem eigenen Container im Stack. Kommt
> die Meldung „Der Etikettenbogen ließ sich nicht erzeugen", läuft er nicht:
> `docker compose ps` zeigt es.
---
## 7. Gerät scannen
Über **Scannen** in der Geräteliste öffnet sich der QR-Scanner. Der Browser
fragt beim ersten Mal nach der Kamerafreigabe. Das Bild verlässt das Gerät
nicht — ausgewertet wird direkt im Browser.
Zwei Dinge sind bewusst so gebaut:
- **Es geht immer auch ohne Kamera.** Darunter steht ein Eingabefeld für die
Inventarnummer. Wenn die Freigabe verweigert wurde oder das Etikett zerkratzt
ist, tippt man sie ein.
- **Fremde Codes brechen nichts ab.** Der Barcode des Herstellers klebt oft
direkt daneben. Wird er erkannt, kommt ein Hinweis statt einer Fehlermeldung.
---
## 8. Fahrzeuge
![Fahrzeugdetail](screenshots/06-fahrzeug-light.png)
Die Übersicht zeigt die Fahrzeuge als Kacheln — eine Wehr hat drei bis zehn,
nicht dreihundert. Auf jeder Kachel stehen der nächste TÜV-Termin und die Zahl
der verlasteten Geräte.
Auf der Detailseite steht die **Beladung nach Geräteräumen**, in der Reihenfolge
der Beladeordnung: G1 bis G6, Heck, Dach, Mannschaftsraum. Genau so sucht der
Gerätewart — nicht „wo ist die Motorsäge", sondern „was ist in G3".
Beim Anlegen eines neuen Fahrzeugs werden diese Räume auf Wunsch gleich
mitangelegt. Ein Mannschaftstransportwagen bekommt weniger Fächer als ein
Löschfahrzeug.
Ein Gerät liegt **entweder** an einem Standort **oder** auf einem Fahrzeug.
Beides gleichzeitig lässt die Datenbank nicht zu — das ist keine Formalität,
sondern verhindert, dass ein Gerät zweimal gezählt wird.
Was verlastet ist, aber keinem Raum zugeordnet wurde, erscheint unter „Ohne
Raumangabe" statt stillschweigend zu verschwinden.
---
## 9. Mitglieder und Qualifikationen
![Qualifikationen](screenshots/08-qualifikationen-light.png)
Die Mitgliederliste ist nach Abteilung gruppiert — Aktive, Jugendfeuerwehr,
Passive. Eine durchlaufende alphabetische Liste vermischt Gruppen, die in der
Wehr nichts miteinander zu tun haben.
Auf der Seite **Qualifikationen** steht oben, was abgelaufen ist oder demnächst
abläuft. Das ist der Grund, warum jemand diese Seite öffnet.
**Gewarnt wird mit der Vorwarnzeit des jeweiligen Typs**, nicht mit einer
pauschalen Frist. Eine G26.3-Untersuchung braucht 120 Tage Vorlauf, weil erst
ein Termin beim Betriebsarzt gefunden werden muss; eine jährliche Unterweisung
kommt mit deutlich weniger aus. Einsatzrelevante Qualifikationen sind
gekennzeichnet.
Das Ablaufdatum wird beim Speichern aus der Gültigkeitsdauer des Typs
gerechnet — lässt sich aber überschreiben. Auf einer Urkunde steht manchmal
etwas anderes, als die Regel hergäbe.
---
## 10. Daten importieren
![Import-Assistent](screenshots/09-import-light.png)
Der Import geht in drei Schritten, und der mittlere ist der eigentliche Punkt.
### Schritt 1: Datei wählen
CSV oder Excel (`.xlsx`). Die erste Zeile muss die Spaltenüberschriften
enthalten. Höchstens 10 MB und 5.000 Zeilen.
Erkannt werden die Formate, die in der Praxis herauskommen:
| Fall | Wird verstanden |
|---|---|
| Trennzeichen | Semikolon, Komma, Tabulator, senkrechter Strich |
| Zeichensatz | UTF-8 (auch mit BOM) und Windows-1252 |
| Datum | 02.09.2026, 2.9.26, 2026-09-02, und die Seriennummer aus Excel |
| Zahlen | 1.234,56 und 1,234.56, mit und ohne Eurozeichen |
| Ja/Nein | ja, j, x, 1, wahr — und die Gegenstücke |
### Schritt 2: Zuordnung prüfen
FLORIAN rät die Zuordnung anhand der Überschriften. „Inv.-Nr." landet bei
Inventarnummer, „Fabrikat" bei Hersteller. Jede Zuordnung lässt sich ändern,
und die Vorschau passt sich sofort an.
**Die Vorschau ist der Grund, warum es diesen Zwischenschritt gibt.** Sie zeigt
Zeile für Zeile, was übernommen würde, und markiert rot, was nicht stimmt:
fehlende Pflichtfelder, unlesbare Daten, Inventarnummern, die in der Datei
doppelt vorkommen. Eine falsch zugeordnete Spalte verteilt sonst Seriennummern
auf Inventarnummern, und das merkt man erst Wochen später am Regal.
### Schritt 3: Übernehmen
Übernommen wird nur, was fehlerfrei ist. Zeilen mit Fehlern bleiben in der
Datei und lassen sich nach der Korrektur erneut einlesen.
**Vorhandene Datensätze aktualisieren** entscheidet, was mit Datensätzen
passiert, die es schon gibt: überspringen (Vorgabe) oder überschreiben.
Erkannt werden sie über die Inventarnummer, bei Mitgliedern über die
Mitgliedsnummer oder die E-Mail-Adresse.
Zum Schluss steht da, wie viele Datensätze angelegt, aktualisiert und
übersprungen wurden. Der ganze Vorgang steht auch im Protokoll.
---
## 11. Rollen und was sie dürfen
Neun Rollen, jede mit einem festen Satz Rechte. Die Zuordnung steht zentral in
`src/lib/authz.ts` und wird beim Seed in die Datenbank gespiegelt — eine Wehr
@@ -123,7 +346,7 @@ Das Terminal selbst ist nur die Tür.
---
## 5. Anmeldung über authentik einrichten
## 12. Anmeldung über authentik einrichten
authentik läuft **getrennt** von FLORIAN und ist nicht Teil dieses Stacks.
@@ -176,7 +399,7 @@ ganze Wehr aus.
---
## 6. Terminal-Anmeldung im Gerätehaus
## 13. Terminal-Anmeldung im Gerätehaus
Die Oberfläche dazu kommt in Phase 4; der Anmeldeweg steht bereits.
@@ -198,7 +421,7 @@ Huber), PIN `471203`.
---
## 7. Was protokolliert wird
## 14. Was protokolliert wird
Jede schreibende Änderung erzeugt einen Eintrag mit Zeitpunkt, Person,
betroffenem Datensatz und den geänderten Feldern im Vorher-Nachher-Vergleich.
@@ -218,11 +441,12 @@ einzige Weg, auf dem etwas aus dem Log verschwindet.
dem Schreiben herausgenommen.
Einsehbar ist der Log mit dem Recht `system.auditlog`, das Kommandant und
Administrator haben. Die Ansicht dazu kommt in Phase 1.
Administrator haben. Am Gerät und am Fahrzeug ist der Verlauf bereits auf der
jeweiligen Detailseite sichtbar.
---
## 8. Wenn etwas nicht geht
## 15. Wenn etwas nicht geht
| Beobachtung | Wahrscheinliche Ursache | Was hilft |
|---|---|---|
@@ -231,6 +455,11 @@ Administrator haben. Die Ansicht dazu kommt in Phase 1.
| Listen sind leer, obwohl Daten da sind | Kein Mandantenkontext gesetzt | Das ist der Schutzmechanismus. Der Zugriff muss über `fuerOrganisation()` laufen. |
| Dateien lassen sich nicht hochladen | MinIO nicht erreichbar oder Bucket fehlt | `http://localhost:9011` öffnen; `docker compose logs minio-init` |
| Der Knopf für authentik fehlt | Issuer, Client-ID oder Secret nicht gesetzt | Alle drei sind nötig, sonst wird der Weg ausgeblendet |
| Etikettenbogen lässt sich nicht erzeugen | Gotenberg läuft nicht | `docker compose ps`, dann `docker compose up -d gotenberg` |
| Kamera bleibt schwarz | Freigabe abgelehnt, oder die Seite läuft nicht über HTTPS | Der Browser fragt nur bei HTTPS oder localhost. Sonst die Inventarnummer eintippen |
| Import meldet fehlende Pflichtfelder | Keine Spalte ist Inventarnummer oder Bezeichnung zugeordnet | In Schritt 2 die Zuordnung von Hand setzen |
| Umlaute im Import verstümmelt | Die Datei ist weder UTF-8 noch Windows-1252 | In Excel als „CSV UTF-8" speichern |
| Gerät lässt sich nicht speichern | Standort **und** Fahrzeug gesetzt, oder keines von beidem | Genau eines von beiden angeben |
| `docker compose up` startet Traefik mit | Override-Datei wurde nicht mitgeladen | In Produktion `-f docker-compose.yml` angeben |
Zustand aller Randsysteme auf einen Blick: