# Devlog
Entwicklungstagebuch mit Zeiterfassung. Jeder Arbeitsabschnitt bekommt einen
Eintrag: wann, wie lange, was entstanden ist, und was dabei schiefging.
Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte
„tatsächlich" der [Aufwandsschätzung](AUFWANDSSCHAETZUNG.md).
## Zeitübersicht
| Phase | Sitzungszeit | Sitzungen | abgeschlossen |
|---|---:|---:|---|
| Phase 0 — Fundament | 1:00 h | 1 | 03.09.2026 |
| Phase 1 — Stammdaten | 1:15 h | 1 | 03.09.2026 |
| Phase 1 — Nachtrag Geräteformular | 0:50 h | 1 | 03.09.2026 |
| Phase 2 — Wartung und Prüfung | 2:25 h | 1 | 03.09.2026 |
| Phase 3 — Einsätze und Übungen | 2:10 h | 1 | 03.09.2026 |
| Phase 4 — Kiosk-PWA im Gerätehaus | 3:00 h | 1 | 03.09.2026 |
| Phase 5 — Atemschutz nach FwDV 7 | 2:40 h | 1 | 03.09.2026 |
| 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 |
| 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
---
## 2026-09-04 · Phase 8 — Lokale KI
**07:30 – 11:10 · 3:40 h · Version 0.9.0**
### Was entstanden ist
Ein Wiki, das man befragen kann, ein Diktat, ein Berichtsassistent, eine
Belegerfassung — und zwei Protokolle, die belegen, was davon eine Maschine
getan hat.
| Bereich | Dateien |
|---|---|
| Rechnen | `lib/ki/zerlegen.ts`, `beleg-lesen.ts`, `anweisungen.ts`, `wiki.ts`, `vorgang.ts` |
| Kanäle | `lib/integrations/llm.ts` (um Einbettungen und Bilder erweitert), `whisper.ts` |
| Server | `server/routers/ki/wiki.ts`, `assistent.ts` |
| Oberfläche | `app/(app)/wiki/`, `belege/`, `ki-protokoll/`, `protokoll/`; `components/ki/` |
| Hintergrund | `worker/ki.ts`, zwei neue Auftragsarten |
| Betrieb | `docker/postgres/Dockerfile` (PostGIS **und** pgvector), Ollama und faster-whisper im Profil `ki` |
| Tests | 73 Unit, 14 E2E je Browser |
### Entscheidungen
**Nichts Maschinelles gelangt ungeprüft in einen Nachweis.** Das ist die Regel
aus Phase-8.md, und sie bestimmt den ganzen Aufbau: jede KI-Ausgabe ist zuerst
ein eigener Datensatz und nicht ein Feld im Zieldokument. Es gibt keine
Funktion, die eine Ausgabe erzeugt und gleich übernimmt — wer das umgehen will,
muss Code schreiben, und das fällt in einer Durchsicht auf.
Die Datenbank hält gegen: Eingabe, Ausgabe, Modell und Art sind nach dem
Anlegen unveränderlich, gelöscht wird nicht, und über einen Vorschlag wird
genau einmal entschieden. Eine Absicht im Code ist eine Absicht; ein Trigger
ist eine Zusicherung.
**Kein Rückfall auf einen Anbieter.** Fehlt der eigene Endpunkt, fehlt die
Funktion. Ein Einsatzbericht spricht über Menschen in ihrer schlechtesten
Stunde — solche Texte gehören nirgendwo anders hin als ins Gerätehaus. Und ein
Rückfall, der „nur im Notfall" greift, ist genau der Fall, in dem niemand
hinsieht.
**Ohne Modell bleibt alles andere benutzbar.** Das war der größte Einzelposten
der Phase und steht in keiner Aufwandsschätzung. Vier Hinweise an vier Stellen,
eine zweite Suche, die immer funktioniert, und vierzehn E2E-Tests, die genau
diesen Zustand absichern — der Testlauf hat keinen Ollama daneben, und das ist
Absicht.
**Drei Modelle statt einem.** Text, Einbettungen und Bilder sind einzeln
konfigurierbar, weil ein Rechner ohne Grafikkarte das Einbettungsmodell noch
flott schafft, das Textmodell zäh und das Vision-Modell praktisch nicht. Eine
Wehr kann so ihr Wiki durchsuchbar machen, ohne Berichte schreiben zu lassen.
**Zerlegt wird an den Überschriften, nicht nach Zeichenzahl.** Weil die
Fundstelle später unter der Antwort steht, und eine Fundstelle soll ein Kapitel
sein. Dazu kommt die Überschriftenkette in den Einbettungstext: ein Abschnitt
über „Rückzugsdruck" ohne das Wort „Atemschutz" darüber findet sich sonst
nicht, wenn jemand nach Atemschutz fragt.
**Markdown wird ohne HTML gerendert.** Der Text kommt von Anwendern. Jeder Weg
über `dangerouslySetInnerHTML` wäre ein Weg für ein `