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 */}