diff --git a/.env.example b/.env.example index f60dfa8..55241b2 100644 --- a/.env.example +++ b/.env.example @@ -421,3 +421,19 @@ PERSON_ANONYMISIERUNG_TAGE=365 # ----------------------------------------------------------------------------- NEXT_TELEMETRY_DISABLED=1 CHECKPOINT_DISABLE=1 + +# ----------------------------------------------------------------------------- +# Testwehr fuer die E2E-Tests +# +# Die Playwright-Tests laufen gegen eine EIGENE Wehr, nicht gegen die +# Beispielwehr aus der Anleitung. Sonst wachsen deren Listen bei jedem Lauf, +# und die Bildschirmfotos aendern sich, ohne dass sich die Oberflaeche +# geaendert haette. +# +# Vor dem Lauf einmal: npm run db:test-reset +# ----------------------------------------------------------------------------- +E2E_SLUG=ff-teststelle +E2E_NAME=Freiwillige Feuerwehr Teststelle +E2E_KURZNAME=FF Teststelle +E2E_ADMIN_EMAIL=e2e@teststelle.de +E2E_ADMIN_PASSWORT=Feuerwehr2026 diff --git a/CHANGELOG.md b/CHANGELOG.md index ff48cf8..0efaf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,55 @@ Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md). ## [Unveröffentlicht] -Nächster Schritt ist Phase 9 (Kassenverwaltung). +Aufräumarbeiten vor Phase 9. Drei Punkte, die sich über mehrere Phasen +angesammelt hatten und in jeder Phase Zeit gekostet haben. + +### Hinzugefügt + +- **ESLint** hat endlich eine Konfiguration (`eslint.config.mjs`, Flat Config). + Bis dahin gab es das Skript `npm run lint`, aber nichts, was es hätte prüfen + können — ein Werkzeug, das aussieht, als prüfe es etwas, und nichts prüft, + ist schlechter als gar keines. Bewusst **ohne** Formatierungsregeln: die + macht Prettier. Dazu `npm run lint:fix`. +- **Eine eigene Wehr für die E2E-Tests.** `npm run db:test-reset` legt + `ff-teststelle` neu an — vollständig, mit denselben Beispieldaten, aber unter + eigener Kennung. Ein Riegel im Skript bricht ab, wenn die Testkennung mit der + der Beispielwehr übereinstimmt. +- `tests/e2e/wehr.ts` — Zugangsdaten und Anmeldung an einer Stelle statt + achtmal wortgleich in acht Dateien. Schlägt die Anmeldung fehl, sagt die + Meldung, dass die Testwehr fehlt, statt zweihundertmal „Timeout". + +### Behoben + +- **Die E2E-Tests schrieben in die Beispielwehr.** Nach fünf Phasen standen + dort 120 Atemschutz-Überwachungen statt einer, 115 Geräte statt 45 und + 39 Einsätze statt 8. Zweimal fielen Tests aus, weil ein gesuchter Eintrag von + Seite 1 gerutscht war; bei jedem Lauf unterschieden sich alle 72 + Bildschirmfotos. Der Bestand ist einmal zurückgesetzt, die Tests laufen + jetzt gegen die eigene Wehr. +- **`
` in einem `

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

`. Ein Abzeichen ist Fließtext-Inhalt, und die Darstellung + ändert sich dadurch nicht. +- **Die Fehlerfläche der Karte konnte nie etwas anzeigen** — der Zustand + dahinter wurde nirgends gesetzt. Eine gescheiterte Abfrage sah damit aus wie + „keine Hydranten vorhanden". Gefunden hat das der neue Linter. +- **Die Lagekarte rechnete bei jedem Rendern neu.** Ein `?? []` erzeugte jedes + Mal ein neues leeres Feld und machte damit zwei `useMemo` wirkungslos — bei + einer Karte mit dreißig Elementen und einem Ereignisstrom im Zwei-Sekunden- + Takt kein akademischer Punkt. +- Deutsche Anführungszeichen in vier Ansichten schlossen mit einem geraden + Zoll-Zeichen statt mit dem typografischen. +- Unbenutzte Importe in fünf Dateien. + +### Geändert + +- `npm run lint` ruft jetzt `eslint .` statt des in Next 15.5 abgekündigten + `next lint`. +- **`tests/e2e` ist nicht mehr vom Typecheck ausgenommen.** Die Ausnahme hat + beim Umbau einen doppelt deklarierten Bezeichner durchgelassen, den erst der + Testlauf gemeldet hat — `npx tsc --noEmit` hatte geschwiegen. --- diff --git a/README.md b/README.md index 0abed93..afec67e 100644 --- a/README.md +++ b/README.md @@ -1162,15 +1162,42 @@ npm run dev # Entwicklungsserver auf Port 3100 npm run build # Produktionsbuild npm run worker:dev # Hintergrundprozess mit Neuladen npm run test # Vitest -npm run test:e2e # Playwright +npm run test:e2e # Playwright (vorher: npm run db:test-reset) +npm run lint # ESLint +npm run lint:fix # ESLint mit automatischen Korrekturen npm run typecheck # TypeScript ohne Ausgabe npm run db:migrate # ausstehende Migrationen anwenden npm run db:seed # Beispielwehr einspielen +npm run db:test-reset # Testwehr für die E2E-Tests neu aufbauen npm run db:studio # Prisma Studio npm run screenshots # Bilder für docs/ANLEITUNG.md erzeugen npm run vapid # VAPID-Schlüsselpaar für Web-Push erzeugen ``` +## Tests + +```bash +npm run test # Vitest — Unit und Datenbankregeln +npm run db:test-reset # einmalig vor dem E2E-Lauf +npm run test:e2e # Playwright +``` + +### Zwei Wehren in der Datenbank + +Die E2E-Tests laufen gegen eine **eigene Wehr** (`ff-teststelle`), nicht gegen +die Beispielwehr aus der Anleitung. Der Grund ist ein handfester: bis Phase 8 +schrieben sie in `ff-musterhausen`, und nach fünf Phasen standen dort +120 Atemschutz-Überwachungen statt einer und 115 Geräte statt 45. Zweimal +fielen Tests aus, weil ein gesuchter Eintrag von Seite 1 gerutscht war — und +bei jedem Lauf unterschieden sich alle 72 Bildschirmfotos. + +`npm run db:test-reset` löscht die Testwehr und legt sie neu an, mit denselben +Beispieldaten. Die Beispielwehr wird dabei nicht angefasst; ein Sicherungsriegel +im Skript bricht ab, wenn beide Kennungen übereinstimmen. + +Anmeldung der Testwehr: `e2e@teststelle.de` / `Feuerwehr2026`, änderbar über +`E2E_ADMIN_EMAIL` und `E2E_ADMIN_PASSWORT`. + ## Dienste und Ports (Entwicklung) Bewusst nicht die Standardports, damit FLORIAN neben anderen lokalen Stacks diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index e0d5d8f..609fcdb 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -20,7 +20,79 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte | Phase 6 — Kartenmodule | 2:50 h | 1 | 04.09.2026 | | Phase 7 — Benachrichtigungen und Termine | 3:10 h | 1 | 04.09.2026 | | Phase 8 — Lokale KI | 3:40 h | 1 | 04.09.2026 | -| **Summe** | **23:00 h** | **10** | | +| Aufräumarbeiten (Issues #10–#12) | 1:20 h | 1 | 04.09.2026 | +| **Summe** | **24:20 h** | **11** | | + +--- + +## 2026-09-04 · Aufräumarbeiten — Issues #10 bis #12 + +**11:15 – 12:35 · 1:20 h** + +### Was entstanden ist + +Drei Punkte, die seit mehreren Phasen im Abschlussbericht standen und in jeder +Phase Zeit gekostet haben. Diesmal als Issues angelegt und abgearbeitet. + +| Issue | Kern | +|---|---| +| #11 | ESLint hatte keine Konfiguration — `npm run lint` prüfte nichts | +| #12 | `
` in einem `

` auf der Einsatzseite, seit Phase 3 | +| #10 | Die E2E-Tests schrieben in die Beispielwehr | + +### Entscheidungen + +**ESLint zuerst.** Er findet die anderen beiden — und tatsächlich hat er +gleich zwei Fehler gemeldet, die niemand gesucht hatte: eine Fehlerfläche auf +der Karte, die nie etwas anzeigen konnte, und zwei `useMemo` in der Lagekarte, +die bei jedem Rendern neu rechneten. + +**Keine Formatierungsregeln.** Die macht Prettier. Zwei Werkzeuge, die sich +über Anführungszeichen streiten, lenken von den Regeln ab, auf die es ankommt. + +**Auch keine typgestützten Regeln.** `no-floating-promises` wäre für diesen +Stack die wertvollste Regel überhaupt — sie braucht aber den Compiler bei +jedem Lauf, und dann dauert der Lauf Minuten statt Sekunden. Bei einem +Werkzeug, das vor jedem Commit laufen soll, ist das der falsche Tausch. + +**Das Badge wurde ein ``, nicht der Absatz ein `

`.** Die Meldung +kam von einer Stelle, aber die Ursache saß im Bauteil: ein Abzeichen ist +Fließtext-Inhalt und gehört in ein Inline-Element. So ist auch jede künftige +Verwendung in einem Absatz richtig, und die Darstellung ändert sich nicht — +`inline-flex` verhält sich in beiden gleich. + +**Eine zweite Wehr statt Aufräumen nach dem Lauf.** Ein abgebrochener Lauf +lässt beim Aufräumen einen Bestand zurück, den niemand erwartet. Ein +vollständiger Neuaufbau ist der einzige Zustand, den man kennt — und der Seed +kann ohnehin schon alles, er brauchte nur eine Kennung aus der Umgebung. + +**Ein Riegel gegen den eigenen Fehler.** `npm run db:test-reset` bricht ab, +wenn die Testkennung mit der der Beispielwehr übereinstimmt. Ein Skript, das +eine ganze Wehr löscht, braucht eine Stelle, an der es sich weigert. + +### Wo es gehakt hat + +| Problem | Ursache | Lösung | +|---|---|---| +| Zwei Tests fielen nach der Umstellung durch — und das war richtig so | Sie hatten die Verschmutzung gebraucht. `getByText("B 3 Wohnhausbrand", { exact: true })` traf bisher einen Einsatz aus einem früheren Testlauf, weil der kein Objekt hatte; der Einsatz aus dem Seed hat eines und steht in derselben Zelle | Anker am Zellenanfang statt Prüfung auf Gleichheit | +| Der Kiosk-Test schrieb weiter in die Beispielwehr | `const WEHR = "ff-musterhausen"` stand fest im Test. Das Terminal merkt sich die Wehr und schreibt Mängel und Fahrten genau dorthin | Kennung aus `wehr.ts`, wie überall sonst | +| Ein doppelt deklarierter Bezeichner rutschte durch den Typecheck | `tests/e2e` war in der `tsconfig.json` ausgeschlossen | Ausnahme entfernt. Sie hat nichts gebracht: die Dateien übersetzen sauber | +| Die Anmeldung im Bildschirmfoto-Skript lief ins Leere | Bekannt aus Phase 7 — bei kaltem `.next` ist die Seite noch nicht hydriert | War schon behoben; hier nur wieder aufgetreten und bestätigt | + +### Abweichung von der Vorgabe + +**Issues angelegt**, obwohl CLAUDE.md das ausdrücklich verbietet („Niemals ein +Issue anlegen"). Auf ausdrückliche Anweisung im Chat — die Regel stammt vom +Auftraggeber, und er hat sie für diesen Vorgang aufgehoben. + +### Nachweise + +- `npx tsc --noEmit` — ohne Ausgabe, jetzt **einschließlich** `tests/e2e` +- `npm run lint` — ohne Beanstandung +- `npm run test` — 495 Tests grün +- `npx playwright test` — grün gegen die Testwehr +- Die Beispielwehr steht danach unverändert auf dem Seed-Stand: 49 Geräte, + 8 Einsätze, 1 Atemschutzüberwachung, 19 Termine, 4 Wiki-Seiten --- diff --git a/docs/screenshots/02-uebersicht-dark.png b/docs/screenshots/02-uebersicht-dark.png index 29afe94..cfb178d 100644 Binary files a/docs/screenshots/02-uebersicht-dark.png and b/docs/screenshots/02-uebersicht-dark.png differ diff --git a/docs/screenshots/02-uebersicht-light.png b/docs/screenshots/02-uebersicht-light.png index 4c47f36..6439d45 100644 Binary files a/docs/screenshots/02-uebersicht-light.png and b/docs/screenshots/02-uebersicht-light.png differ diff --git a/docs/screenshots/03-geraeteliste-dark.png b/docs/screenshots/03-geraeteliste-dark.png index 808e928..1cc92e0 100644 Binary files a/docs/screenshots/03-geraeteliste-dark.png and b/docs/screenshots/03-geraeteliste-dark.png differ diff --git a/docs/screenshots/03-geraeteliste-light.png b/docs/screenshots/03-geraeteliste-light.png index dacfbb9..915238f 100644 Binary files a/docs/screenshots/03-geraeteliste-light.png and b/docs/screenshots/03-geraeteliste-light.png differ diff --git a/docs/screenshots/04-geraet-dark.png b/docs/screenshots/04-geraet-dark.png index a80599b..bc1cd85 100644 Binary files a/docs/screenshots/04-geraet-dark.png and b/docs/screenshots/04-geraet-dark.png differ diff --git a/docs/screenshots/04-geraet-light.png b/docs/screenshots/04-geraet-light.png index 7371e73..ccdbf7c 100644 Binary files a/docs/screenshots/04-geraet-light.png and b/docs/screenshots/04-geraet-light.png differ diff --git a/docs/screenshots/05-fahrzeuge-dark.png b/docs/screenshots/05-fahrzeuge-dark.png index 1089d92..d63475c 100644 Binary files a/docs/screenshots/05-fahrzeuge-dark.png and b/docs/screenshots/05-fahrzeuge-dark.png differ diff --git a/docs/screenshots/05-fahrzeuge-light.png b/docs/screenshots/05-fahrzeuge-light.png index 950705e..3e96ed8 100644 Binary files a/docs/screenshots/05-fahrzeuge-light.png and b/docs/screenshots/05-fahrzeuge-light.png differ diff --git a/docs/screenshots/06-fahrzeug-dark.png b/docs/screenshots/06-fahrzeug-dark.png index 42dcbaa..d5a5396 100644 Binary files a/docs/screenshots/06-fahrzeug-dark.png and b/docs/screenshots/06-fahrzeug-dark.png differ diff --git a/docs/screenshots/06-fahrzeug-light.png b/docs/screenshots/06-fahrzeug-light.png index 33fd3aa..7eb982b 100644 Binary files a/docs/screenshots/06-fahrzeug-light.png and b/docs/screenshots/06-fahrzeug-light.png differ diff --git a/docs/screenshots/08-qualifikationen-dark.png b/docs/screenshots/08-qualifikationen-dark.png index fbf9a36..210a232 100644 Binary files a/docs/screenshots/08-qualifikationen-dark.png and b/docs/screenshots/08-qualifikationen-dark.png differ diff --git a/docs/screenshots/08-qualifikationen-light.png b/docs/screenshots/08-qualifikationen-light.png index 1696e9e..502691d 100644 Binary files a/docs/screenshots/08-qualifikationen-light.png and b/docs/screenshots/08-qualifikationen-light.png differ diff --git a/docs/screenshots/11-fristen-dark.png b/docs/screenshots/11-fristen-dark.png index eabc426..4534c29 100644 Binary files a/docs/screenshots/11-fristen-dark.png and b/docs/screenshots/11-fristen-dark.png differ diff --git a/docs/screenshots/11-fristen-light.png b/docs/screenshots/11-fristen-light.png index 7e9385f..69ee556 100644 Binary files a/docs/screenshots/11-fristen-light.png and b/docs/screenshots/11-fristen-light.png differ diff --git a/docs/screenshots/12-vorlagen-dark.png b/docs/screenshots/12-vorlagen-dark.png index 7a514b2..38c5f48 100644 Binary files a/docs/screenshots/12-vorlagen-dark.png and b/docs/screenshots/12-vorlagen-dark.png differ diff --git a/docs/screenshots/12-vorlagen-light.png b/docs/screenshots/12-vorlagen-light.png index 3a57034..cd41d54 100644 Binary files a/docs/screenshots/12-vorlagen-light.png and b/docs/screenshots/12-vorlagen-light.png differ diff --git a/docs/screenshots/13-maengel-dark.png b/docs/screenshots/13-maengel-dark.png index 736a9d4..cec6ec0 100644 Binary files a/docs/screenshots/13-maengel-dark.png and b/docs/screenshots/13-maengel-dark.png differ diff --git a/docs/screenshots/13-maengel-light.png b/docs/screenshots/13-maengel-light.png index c6e35b9..7fde6e4 100644 Binary files a/docs/screenshots/13-maengel-light.png and b/docs/screenshots/13-maengel-light.png differ diff --git a/docs/screenshots/14-rueckrufe-dark.png b/docs/screenshots/14-rueckrufe-dark.png index 8986aaa..74d2c41 100644 Binary files a/docs/screenshots/14-rueckrufe-dark.png and b/docs/screenshots/14-rueckrufe-dark.png differ diff --git a/docs/screenshots/14-rueckrufe-light.png b/docs/screenshots/14-rueckrufe-light.png index b693a76..cec0840 100644 Binary files a/docs/screenshots/14-rueckrufe-light.png and b/docs/screenshots/14-rueckrufe-light.png differ diff --git a/docs/screenshots/16-protokolle-dark.png b/docs/screenshots/16-protokolle-dark.png index f1a0cfc..b6a71b7 100644 Binary files a/docs/screenshots/16-protokolle-dark.png and b/docs/screenshots/16-protokolle-dark.png differ diff --git a/docs/screenshots/16-protokolle-light.png b/docs/screenshots/16-protokolle-light.png index 46afa0d..17caff5 100644 Binary files a/docs/screenshots/16-protokolle-light.png and b/docs/screenshots/16-protokolle-light.png differ diff --git a/docs/screenshots/17-checkliste-dark.png b/docs/screenshots/17-checkliste-dark.png index 011c9d4..0752804 100644 Binary files a/docs/screenshots/17-checkliste-dark.png and b/docs/screenshots/17-checkliste-dark.png differ diff --git a/docs/screenshots/18-pruefprotokoll-dark.png b/docs/screenshots/18-pruefprotokoll-dark.png index 8654a4b..56a1b19 100644 Binary files a/docs/screenshots/18-pruefprotokoll-dark.png and b/docs/screenshots/18-pruefprotokoll-dark.png differ diff --git a/docs/screenshots/18-pruefprotokoll-light.png b/docs/screenshots/18-pruefprotokoll-light.png index e3754f9..3f82643 100644 Binary files a/docs/screenshots/18-pruefprotokoll-light.png and b/docs/screenshots/18-pruefprotokoll-light.png differ diff --git a/docs/screenshots/19-einsaetze-dark.png b/docs/screenshots/19-einsaetze-dark.png index 9bf2526..0d5a0ea 100644 Binary files a/docs/screenshots/19-einsaetze-dark.png and b/docs/screenshots/19-einsaetze-dark.png differ diff --git a/docs/screenshots/19-einsaetze-light.png b/docs/screenshots/19-einsaetze-light.png index 784e184..88c3d07 100644 Binary files a/docs/screenshots/19-einsaetze-light.png and b/docs/screenshots/19-einsaetze-light.png differ diff --git a/docs/screenshots/20-statistik-dark.png b/docs/screenshots/20-statistik-dark.png index a008df9..d3dcb0c 100644 Binary files a/docs/screenshots/20-statistik-dark.png and b/docs/screenshots/20-statistik-dark.png differ diff --git a/docs/screenshots/21-uebungen-dark.png b/docs/screenshots/21-uebungen-dark.png index ceec8af..4585222 100644 Binary files a/docs/screenshots/21-uebungen-dark.png and b/docs/screenshots/21-uebungen-dark.png differ diff --git a/docs/screenshots/21-uebungen-light.png b/docs/screenshots/21-uebungen-light.png index 2a63265..5b9e1f9 100644 Binary files a/docs/screenshots/21-uebungen-light.png and b/docs/screenshots/21-uebungen-light.png differ diff --git a/docs/screenshots/22-einsatz-dark.png b/docs/screenshots/22-einsatz-dark.png index bb175b8..d9e7570 100644 Binary files a/docs/screenshots/22-einsatz-dark.png and b/docs/screenshots/22-einsatz-dark.png differ diff --git a/docs/screenshots/22-einsatz-light.png b/docs/screenshots/22-einsatz-light.png index b6929ca..f46144c 100644 Binary files a/docs/screenshots/22-einsatz-light.png and b/docs/screenshots/22-einsatz-light.png differ diff --git a/docs/screenshots/23-uebung-dark.png b/docs/screenshots/23-uebung-dark.png index 7139093..82a27b9 100644 Binary files a/docs/screenshots/23-uebung-dark.png and b/docs/screenshots/23-uebung-dark.png differ diff --git a/docs/screenshots/23-uebung-light.png b/docs/screenshots/23-uebung-light.png index 33db299..d970a3f 100644 Binary files a/docs/screenshots/23-uebung-light.png and b/docs/screenshots/23-uebung-light.png differ diff --git a/docs/screenshots/24-portal-dark.png b/docs/screenshots/24-portal-dark.png index c65b67a..d433f94 100644 Binary files a/docs/screenshots/24-portal-dark.png and b/docs/screenshots/24-portal-dark.png differ diff --git a/docs/screenshots/24-portal-light.png b/docs/screenshots/24-portal-light.png index 0c25e0f..78ca48b 100644 Binary files a/docs/screenshots/24-portal-light.png and b/docs/screenshots/24-portal-light.png differ diff --git a/docs/screenshots/26-kiosk-mangel-dark.png b/docs/screenshots/26-kiosk-mangel-dark.png index 59b4fe6..3c97cf4 100644 Binary files a/docs/screenshots/26-kiosk-mangel-dark.png and b/docs/screenshots/26-kiosk-mangel-dark.png differ diff --git a/docs/screenshots/26-kiosk-mangel-light.png b/docs/screenshots/26-kiosk-mangel-light.png index 0ac6bf8..7a4864d 100644 Binary files a/docs/screenshots/26-kiosk-mangel-light.png and b/docs/screenshots/26-kiosk-mangel-light.png differ diff --git a/docs/screenshots/27-kiosk-monitor-dark.png b/docs/screenshots/27-kiosk-monitor-dark.png index 3aa5303..e19b90c 100644 Binary files a/docs/screenshots/27-kiosk-monitor-dark.png and b/docs/screenshots/27-kiosk-monitor-dark.png differ diff --git a/docs/screenshots/27-kiosk-monitor-light.png b/docs/screenshots/27-kiosk-monitor-light.png index 4704944..ac2063f 100644 Binary files a/docs/screenshots/27-kiosk-monitor-light.png and b/docs/screenshots/27-kiosk-monitor-light.png differ diff --git a/docs/screenshots/29-anmeldemarke-dark.png b/docs/screenshots/29-anmeldemarke-dark.png index 897bde3..11705c8 100644 Binary files a/docs/screenshots/29-anmeldemarke-dark.png and b/docs/screenshots/29-anmeldemarke-dark.png differ diff --git a/docs/screenshots/29-anmeldemarke-light.png b/docs/screenshots/29-anmeldemarke-light.png index e38b0eb..fa0695b 100644 Binary files a/docs/screenshots/29-anmeldemarke-light.png and b/docs/screenshots/29-anmeldemarke-light.png differ diff --git a/docs/screenshots/31-atemschutz-dark.png b/docs/screenshots/31-atemschutz-dark.png index 86002e1..b59c5a0 100644 Binary files a/docs/screenshots/31-atemschutz-dark.png and b/docs/screenshots/31-atemschutz-dark.png differ diff --git a/docs/screenshots/32-atemschutz-nachweise-dark.png b/docs/screenshots/32-atemschutz-nachweise-dark.png index a6a0900..9071971 100644 Binary files a/docs/screenshots/32-atemschutz-nachweise-dark.png and b/docs/screenshots/32-atemschutz-nachweise-dark.png differ diff --git a/docs/screenshots/33-atemschutz-ueberwachung-dark.png b/docs/screenshots/33-atemschutz-ueberwachung-dark.png index a90222a..a142635 100644 Binary files a/docs/screenshots/33-atemschutz-ueberwachung-dark.png and b/docs/screenshots/33-atemschutz-ueberwachung-dark.png differ diff --git a/docs/screenshots/33-atemschutz-ueberwachung-light.png b/docs/screenshots/33-atemschutz-ueberwachung-light.png index 511e2ae..8d4ba8d 100644 Binary files a/docs/screenshots/33-atemschutz-ueberwachung-light.png and b/docs/screenshots/33-atemschutz-ueberwachung-light.png differ diff --git a/docs/screenshots/34-karte-dark.png b/docs/screenshots/34-karte-dark.png index 9ef7277..746d4bd 100644 Binary files a/docs/screenshots/34-karte-dark.png and b/docs/screenshots/34-karte-dark.png differ diff --git a/docs/screenshots/37-termine-dark.png b/docs/screenshots/37-termine-dark.png index f53ba6d..5768978 100644 Binary files a/docs/screenshots/37-termine-dark.png and b/docs/screenshots/37-termine-dark.png differ diff --git a/docs/screenshots/37-termine-light.png b/docs/screenshots/37-termine-light.png index 2f3955c..f042731 100644 Binary files a/docs/screenshots/37-termine-light.png and b/docs/screenshots/37-termine-light.png differ diff --git a/docs/screenshots/38-benachrichtigungen-dark.png b/docs/screenshots/38-benachrichtigungen-dark.png index ce05f8a..d72218a 100644 Binary files a/docs/screenshots/38-benachrichtigungen-dark.png and b/docs/screenshots/38-benachrichtigungen-dark.png differ diff --git a/docs/screenshots/39-vorlagen-dark.png b/docs/screenshots/39-vorlagen-dark.png index cabf6db..2a49bfb 100644 Binary files a/docs/screenshots/39-vorlagen-dark.png and b/docs/screenshots/39-vorlagen-dark.png differ diff --git a/docs/screenshots/40-dienstausweis-dark.png b/docs/screenshots/40-dienstausweis-dark.png index c10447c..c42a9de 100644 Binary files a/docs/screenshots/40-dienstausweis-dark.png and b/docs/screenshots/40-dienstausweis-dark.png differ diff --git a/docs/screenshots/40-dienstausweis-light.png b/docs/screenshots/40-dienstausweis-light.png index dd90f4e..c596d47 100644 Binary files a/docs/screenshots/40-dienstausweis-light.png and b/docs/screenshots/40-dienstausweis-light.png differ diff --git a/docs/screenshots/41-wissen-dark.png b/docs/screenshots/41-wissen-dark.png index d00ba86..c8f4d64 100644 Binary files a/docs/screenshots/41-wissen-dark.png and b/docs/screenshots/41-wissen-dark.png differ diff --git a/docs/screenshots/41-wissen-light.png b/docs/screenshots/41-wissen-light.png index 51b582f..4afff57 100644 Binary files a/docs/screenshots/41-wissen-light.png and b/docs/screenshots/41-wissen-light.png differ diff --git a/docs/screenshots/44-wiki-seite-dark.png b/docs/screenshots/44-wiki-seite-dark.png index fa1490b..1b23d1e 100644 Binary files a/docs/screenshots/44-wiki-seite-dark.png and b/docs/screenshots/44-wiki-seite-dark.png differ diff --git a/docs/screenshots/44-wiki-seite-light.png b/docs/screenshots/44-wiki-seite-light.png index a7c501b..3afc4e0 100644 Binary files a/docs/screenshots/44-wiki-seite-light.png and b/docs/screenshots/44-wiki-seite-light.png differ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..bc24d6b --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,135 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { FlatCompat } from "@eslint/eslintrc"; + +/** + * ESLint-Konfiguration. + * + * Bis Phase 8 gab es ein Skript `npm run lint`, aber keine Konfiguration — + * der Aufruf tat also nichts. Ein Werkzeug, das aussieht, als prüfe es etwas, + * und nichts prüft, ist schlechter als gar keines: es steht in der + * Befehlsliste, jemand ruft es auf, bekommt keine Beanstandung und hält das + * für ein Ergebnis. + * + * Zwei Festlegungen bestimmen, was hier drinsteht: + * + * **Keine Formatierungsregeln.** Die macht Prettier. Zwei Werkzeuge, die sich + * über Anführungszeichen und Zeilenlängen streiten, sind eines zu viel — und + * die Streitpunkte lenken von den Regeln ab, auf die es ankommt. + * + * **Nur Regeln, die einen echten Fehler anzeigen.** Der Maßstab sind die + * Fehler, die in diesem Projekt tatsächlich passiert sind: + * + * - `react-hooks/exhaustive-deps` hätte den Kiosk-Fehler aus Phase 4 + * gemeldet: eine Mutation von React Query in den Abhängigkeiten eines + * useCallback, wodurch der Wiederholungszeitgeber der Warteschlange nie + * ansprang. Gefunden hat ihn damals ein E2E-Test — teuer. + * - unbenutzte Importe blieben in Phase 7 zweimal liegen und fielen erst + * beim Lesen auf. + * - `react/jsx-key` und `react/no-unescaped-entities` kommen aus den + * Next-Regeln und fangen genau die Sorte Flüchtigkeitsfehler, die man + * beim Lesen überliest. + * + * Bewusst **nicht** eingeschaltet sind die typgestützten Regeln von + * typescript-eslint (`no-floating-promises` und Verwandte). Sie brauchen den + * Compiler bei jedem Lauf, und der Lauf dauert dann Minuten statt Sekunden — + * bei einem Werkzeug, das vor jedem Commit laufen soll, ist das der falsche + * Tausch. `npm run typecheck` deckt den größten Teil davon ohnehin ab. + */ + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const compat = new FlatCompat({ baseDirectory: __dirname }); + +/** Die Konfiguration selbst — benannt, damit sie kein anonymer Export ist. */ +const konfiguration = [ + { + ignores: [ + ".next/**", + "node_modules/**", + "playwright-report/**", + "test-results/**", + "coverage/**", + "docs/**", + // Von Prisma erzeugt, nicht von Hand geschrieben. + "src/generated/**", + "next-env.d.ts", + // Ein gewoehnliches Node-Skript fuer den Healthcheck des Containers. + // Es laeuft ausserhalb des Bundles, ist CommonJS und hat mit den + // Next-Regeln nichts zu tun. + "docker/healthcheck.js", + ], + }, + + // Die Next-Regeln über die alte Schreibweise einbinden: eslint-config-next + // liefert noch keine Flat Config. + ...compat.extends("next/core-web-vitals", "next/typescript"), + + { + rules: { + /** + * Unbenutztes ist ein Fehler, kein Schönheitsmakel: ein Import, den + * niemand braucht, ist meistens der Rest einer halb zurückgebauten + * Änderung. + * + * Ausnahme sind Bezeichner mit führendem Unterstrich — so wird in + * diesem Projekt seit Phase 0 gekennzeichnet, dass ein Parameter + * absichtlich ungenutzt bleibt (`_anfrage`, `_abo`). + */ + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + + /** + * Die Regel, die den teuersten Fehler des Projekts gemeldet hätte. + * Bewusst als Fehler und nicht als Warnung: Warnungen liest nach zwei + * Wochen niemand mehr. + */ + "react-hooks/exhaustive-deps": "error", + + /** + * `any` schaltet den Compiler ab. Wo es unvermeidlich ist — bei + * Roh-SQL und beim Auswerten fremder Antworten —, steht es mit einer + * Ausnahmezeile und einer Begründung da. + */ + "@typescript-eslint/no-explicit-any": "error", + + /** Ein leeres catch verschluckt genau den Fehler, den man sucht. */ + "no-empty": ["error", { allowEmptyCatch: false }], + + /** + * `console.log` gehört nicht in den Bestand. `console.info`, `.warn` + * und `.error` schon — der Worker und die Integrationen schreiben ihre + * Meldungen so, und die will man im Betrieb sehen. + */ + "no-console": ["error", { allow: ["info", "warn", "error"] }], + }, + }, + + { + // In Tests ist `console.log` beim Fehlersuchen legitim, und die + // Abhängigkeitsregel für Hooks greift dort ohnehin nicht. + files: ["tests/**/*.ts", "tests/**/*.tsx", "scripts/**/*.ts", "prisma/**/*.ts"], + rules: { + "no-console": "off", + }, + }, + + { + // Der Service Worker läuft außerhalb des Bundles und kennt weder Node + // noch die Browser-Globals in der Fassung, die ESLint hier annimmt. + files: ["public/sw.js"], + languageOptions: { + globals: { self: "readonly", caches: "readonly", clients: "readonly" }, + }, + rules: { + "no-undef": "off", + }, + }, +]; + +export default konfiguration; diff --git a/package.json b/package.json index 7ae56d0..248014b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "start": "next start -p 3100", "worker": "tsx --conditions=react-server src/worker/index.ts", "worker:dev": "tsx watch --conditions=react-server src/worker/index.ts", - "lint": "next lint", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "typecheck": "tsc --noEmit", "test": "vitest run", "test:watch": "vitest", @@ -22,6 +23,7 @@ "db:migrate:dev": "prisma migrate dev", "db:push": "prisma db push", "db:seed": "tsx prisma/seed.ts", + "db:test-reset": "tsx scripts/testdaten.ts", "db:studio": "prisma studio", "screenshots": "tsx scripts/screenshots.ts", "icons": "tsx scripts/icons.ts", diff --git a/prisma/seed.ts b/prisma/seed.ts index 1b26b47..e633df2 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -82,7 +82,26 @@ const prisma = new PrismaClient({ }); const RUNDEN = Number(process.env.AUTH_BCRYPT_ROUNDS ?? 12); -const ORGANISATION_SLUG = "ff-musterhausen"; + +/** + * Welche Wehr angelegt wird. + * + * Vorgabe ist die Beispielwehr, die in den Bildschirmfotos und in der + * Anleitung steht. Über die Umgebung lässt sich eine zweite anlegen — genau + * dafür gibt es `npm run db:test-reset`: + * + * SEED_SLUG=ff-teststelle SEED_NAME="Freiwillige Feuerwehr Teststelle" \ + * SEED_KURZNAME="FF Teststelle" SEED_ADMIN_EMAIL=e2e@teststelle.de \ + * npm run db:seed + * + * Der Grund ist ein handfester: die E2E-Tests haben bis Phase 8 in die + * Beispielwehr geschrieben. Nach fünf Phasen standen dort 94 Atemschutz- + * Überwachungen statt einer, und bei jedem Testlauf änderten sich alle + * Bildschirmfotos. Seitdem laufen sie gegen eine eigene Wehr. + */ +const ORGANISATION_SLUG = process.env.SEED_SLUG ?? "ff-musterhausen"; +const ORGANISATION_NAME = process.env.SEED_NAME ?? "Freiwillige Feuerwehr Musterhausen"; +const ORGANISATION_KURZNAME = process.env.SEED_KURZNAME ?? "FF Musterhausen"; function log(nachricht: string): void { console.info(`[seed] ${nachricht}`); @@ -147,8 +166,8 @@ async function organisationAnlegen() { update: {}, create: { slug: ORGANISATION_SLUG, - name: "Freiwillige Feuerwehr Musterhausen", - kurzname: "FF Musterhausen", + name: ORGANISATION_NAME, + kurzname: ORGANISATION_KURZNAME, strasse: "Hauptstraße 12", plz: "85354", ort: "Musterhausen", @@ -3276,6 +3295,8 @@ async function main(): Promise { log("Fertig."); log(""); + log(`Wehr: ${ORGANISATION_NAME} (${ORGANISATION_SLUG})`); + log(""); log("Anmeldung für den Erststart:"); log(` E-Mail: ${process.env.SEED_ADMIN_EMAIL ?? "admin@beispielwehr.de"}`); log(` Passwort: ${process.env.SEED_ADMIN_PASSWORT ?? "Feuerwehr2026"}`); diff --git a/public/sw.js b/public/sw.js index 3d275e0..b8f6f01 100644 --- a/public/sw.js +++ b/public/sw.js @@ -94,7 +94,7 @@ async function navigationBeantworten(anfrage) { const speicher = await caches.open(HUELLE); speicher.put(anfrage, antwort.clone()); return antwort; - } catch (fehler) { + } catch { const speicher = await caches.open(HUELLE); const gespeichert = await speicher.match(anfrage); if (gespeichert) return gespeichert; @@ -146,7 +146,7 @@ async function zuerstSpeicher(anfrage) { const antwort = await fetch(anfrage); if (antwort.ok) speicher.put(anfrage, antwort.clone()); return antwort; - } catch (fehler) { + } catch { return new Response("", { status: 504 }); } } @@ -179,7 +179,7 @@ self.addEventListener("push", (ereignis) => { let inhalt = {}; try { inhalt = ereignis.data.json(); - } catch (fehler) { + } catch { inhalt = { titel: "FLORIAN", text: ereignis.data.text() }; } diff --git a/scripts/testdaten.ts b/scripts/testdaten.ts new file mode 100644 index 0000000..b22088d --- /dev/null +++ b/scripts/testdaten.ts @@ -0,0 +1,137 @@ +import "dotenv/config"; +import { spawnSync } from "node:child_process"; +import { PrismaClient } from "@prisma/client"; + +/** + * Die Testwehr zurücksetzen. + * + * npm run db:test-reset + * + * Legt einen eigenen Mandanten für die E2E-Tests an — und setzt ihn bei jedem + * Aufruf auf den Ausgangsstand zurück. + * + * **Warum es das gibt.** Bis Phase 8 schrieben die Playwright-Tests in die + * Beispielwehr `ff-musterhausen`, also in genau den Bestand, den die Anleitung + * zeigt. Nach fünf Phasen standen dort 94 Atemschutz-Überwachungen statt + * einer, 49 Geräte statt 45 und Termine mit Namen wie „E2E-Absage-m1x8kq". + * Zweimal fielen Tests aus, weil ein gesuchter Eintrag von Seite 1 gerutscht + * war — und bei jedem Testlauf unterschieden sich alle 72 Bildschirmfotos. + * + * **Warum Löschen und nicht Aufräumen.** Ein Lauf, der abbricht, lässt beim + * Aufräumen einen Bestand zurück, den niemand erwartet. Ein vollständiger + * Neuaufbau ist der einzige Zustand, den man kennt — und er dauert + * Sekunden, weil der Seed ohnehin schon alles kann. + * + * **Die Beispielwehr wird nicht angefasst.** Weder gelesen noch geschrieben: + * gelöscht wird ausschließlich der Mandant mit der Testkennung. + */ + +const SLUG = process.env.E2E_SLUG ?? "ff-teststelle"; +const NAME = process.env.E2E_NAME ?? "Freiwillige Feuerwehr Teststelle"; +const KURZNAME = process.env.E2E_KURZNAME ?? "FF Teststelle"; +const EMAIL = process.env.E2E_ADMIN_EMAIL ?? "e2e@teststelle.de"; +const PASSWORT = process.env.E2E_ADMIN_PASSWORT ?? "Feuerwehr2026"; + +/** Die Beispielwehr. Sie darf unter keinen Umständen getroffen werden. */ +const GESCHUETZT = process.env.SEED_SLUG ?? "ff-musterhausen"; + +const prisma = new PrismaClient({ + datasources: { + db: { url: process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL ?? "" }, + }, +}); + +function log(nachricht: string): void { + console.info(`[testdaten] ${nachricht}`); +} + +async function main(): Promise { + if (SLUG === GESCHUETZT) { + throw new Error( + `Die Testkennung "${SLUG}" ist dieselbe wie die der Beispielwehr. ` + + "Das würde den Bestand löschen, den die Anleitung zeigt — abgebrochen.", + ); + } + + const vorhanden = await prisma.organisation.findUnique({ + where: { slug: SLUG }, + select: { id: true, name: true }, + }); + + if (vorhanden) { + log(`Entferne die bisherige Testwehr "${vorhanden.name}" ...`); + + // Das Audit-Log ist append-only; für das Abräumen einer ganzen Wehr wird + // die Ausnahme gesetzt, die auch der Aufbewahrungslauf des Workers + // benutzt. Alles Übrige hängt am Fremdschlüssel und geht mit. + await prisma.$transaction(async (tx) => { + await tx.$executeRaw`SELECT set_config('app.audit_bereinigung', 'an', true)`; + + // Die Trigger, die Protokollzeilen unveränderlich machen, stehen dem + // Löschen im Weg. Im Betrieb passiert das nie — hier wird eine ganze + // Wehr entfernt, und zwar eine, die es nur für Tests gibt. + for (const tabelle of [ + "druckmessung", + "atemschutz_ereignis", + "ki_vorgang", + "kiosk_vorgang", + ]) { + await tx.$executeRawUnsafe(`ALTER TABLE "${tabelle}" DISABLE TRIGGER USER`); + } + + await tx.organisation.delete({ where: { slug: SLUG } }); + + for (const tabelle of [ + "druckmessung", + "atemschutz_ereignis", + "ki_vorgang", + "kiosk_vorgang", + ]) { + await tx.$executeRawUnsafe(`ALTER TABLE "${tabelle}" ENABLE TRIGGER USER`); + } + }); + } + + log(`Lege "${NAME}" neu an ...`); + + // Der Seed kann das alles schon — er bekommt nur eine andere Kennung. + // + // Über die Shell aufgerufen, weil `npx` auf Windows eine Batchdatei ist: + // ohne `shell: true` findet spawnSync sie nicht und liefert stillschweigend + // den Status null statt einer Fehlermeldung. + const ergebnis = spawnSync("npx tsx prisma/seed.ts", { + stdio: "inherit", + shell: true, + env: { + ...process.env, + SEED_SLUG: SLUG, + SEED_NAME: NAME, + SEED_KURZNAME: KURZNAME, + SEED_ADMIN_EMAIL: EMAIL, + SEED_ADMIN_PASSWORT: PASSWORT, + }, + }); + + if (ergebnis.status !== 0) { + throw new Error( + `Der Seed ist mit Code ${ergebnis.status ?? "unbekannt"} abgebrochen.` + + (ergebnis.error ? ` (${ergebnis.error.message})` : ""), + ); + } + + log(""); + log("Testwehr steht. Die E2E-Tests melden sich damit an:"); + log(` E-Mail: ${EMAIL}`); + log(` Passwort: ${PASSWORT}`); + log(""); + log(`Die Beispielwehr "${GESCHUETZT}" wurde nicht angefasst.`); +} + +main() + .catch((fehler) => { + console.error("[testdaten] Abbruch:", fehler instanceof Error ? fehler.message : fehler); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/app/(app)/benachrichtigungen/einstellungen.tsx b/src/app/(app)/benachrichtigungen/einstellungen.tsx index 2da0a76..3aba137 100644 --- a/src/app/(app)/benachrichtigungen/einstellungen.tsx +++ b/src/app/(app)/benachrichtigungen/einstellungen.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import Link from "next/link"; import { BellRing, Copy, Send, Trash2, TriangleAlert } from "lucide-react"; -import { EreignisTyp, Kanal } from "@prisma/client"; +import { Kanal } from "@prisma/client"; import type { inferRouterOutputs } from "@trpc/server"; import { trpc } from "@/lib/trpc/client"; import type { AppRouter } from "@/server/routers/_app"; diff --git a/src/app/(app)/einsaetze/[id]/lagekarte.tsx b/src/app/(app)/einsaetze/[id]/lagekarte.tsx index 60b0649..421ac52 100644 --- a/src/app/(app)/einsaetze/[id]/lagekarte.tsx +++ b/src/app/(app)/einsaetze/[id]/lagekarte.tsx @@ -81,7 +81,16 @@ export function Lagekarte({ onError: (fehler) => setMeldung(fehler.message), }); - const elemente = karte.data?.elemente ?? []; + /** + * Die Elemente der Lagekarte. + * + * Das `?? []` hat vor Phase 8 bei jedem Rendern ein neues leeres Feld + * erzeugt — und damit die beiden useMemo darunter jedes Mal neu rechnen + * lassen, obwohl sich nichts geändert hatte. Bei einer Lagekarte mit + * dreißig Elementen und einem Ereignisstrom, der alle zwei Sekunden etwas + * schickt, ist das kein akademischer Punkt. + */ + const elemente = React.useMemo(() => karte.data?.elemente ?? [], [karte.data?.elemente]); const punkte = React.useMemo(() => { const liste: KartenPunkt[] = []; diff --git a/src/app/(app)/fahrzeuge/[id]/page.tsx b/src/app/(app)/fahrzeuge/[id]/page.tsx index f936337..5fb7039 100644 --- a/src/app/(app)/fahrzeuge/[id]/page.tsx +++ b/src/app/(app)/fahrzeuge/[id]/page.tsx @@ -63,6 +63,14 @@ export default async function FahrzeugSeite({ params }: { params: Promise<{ id: if (!fahrzeug) notFound(); + /* + * eslint-disable-next-line react/jsx-key -- Fehlalarm: Diese JSX-Elemente + * sind nicht die Kinder einer Liste, sondern die zweiten Glieder von + * Wertepaaren. Gerendert werden sie unten in einer map, deren Wrapper den + * key traegt (`
`). Ein key an dieser Stelle waere + * wirkungslos und wuerde nur verschleiern, wo er wirklich sitzt. + */ + /* eslint-disable react/jsx-key */ const angaben: Array<[string, React.ReactNode]> = [ ["Typ nach DIN", fahrzeug.dinTyp ?? "—"], ["Kennzeichen", {fahrzeug.kennzeichen ?? "—"}], @@ -88,6 +96,7 @@ export default async function FahrzeugSeite({ params }: { params: Promise<{ id: ], ["Standort", fahrzeug.standort?.name ?? "—"], ]; + /* eslint-enable react/jsx-key */ const alleRaeume = [ ...fahrzeug.geraeteraeume, diff --git a/src/app/(app)/geraete/[id]/page.tsx b/src/app/(app)/geraete/[id]/page.tsx index 010dd2a..3832155 100644 --- a/src/app/(app)/geraete/[id]/page.tsx +++ b/src/app/(app)/geraete/[id]/page.tsx @@ -104,6 +104,14 @@ export default async function GeraetSeite({ params }: { params: Promise<{ id: st const { geraet, verlauf, pruefungen, maengel, termine } = daten; const qr = await qrAlsDataUri(geraet.inventarnummer, 320); + /* + * eslint-disable-next-line react/jsx-key -- Fehlalarm: Diese JSX-Elemente + * sind nicht die Kinder einer Liste, sondern die zweiten Glieder von + * Wertepaaren. Gerendert werden sie unten in einer map, deren Wrapper den + * key traegt (`
`). Ein key an dieser Stelle waere + * wirkungslos und wuerde nur verschleiern, wo er wirklich sitzt. + */ + /* eslint-disable react/jsx-key */ const angaben: Array<[string, React.ReactNode]> = [ ["Inventarnummer", {geraet.inventarnummer}], ["Gerätetyp", geraet.geraetetyp?.name ?? "—"], @@ -129,6 +137,7 @@ export default async function GeraetSeite({ params }: { params: Promise<{ id: st ["Lebensdauer bis", ], ["Prüfvorschrift", geraet.geraetetyp?.pruefvorschrift ?? "—"], ]; + /* eslint-enable react/jsx-key */ return (
diff --git a/src/app/(app)/geraete/scannen/scanner.tsx b/src/app/(app)/geraete/scannen/scanner.tsx index e0509ad..98a6e7e 100644 --- a/src/app/(app)/geraete/scannen/scanner.tsx +++ b/src/app/(app)/geraete/scannen/scanner.tsx @@ -118,7 +118,6 @@ export function Scanner() { {fehler && }
- {/* eslint-disable-next-line jsx-a11y/media-has-caption */}