Phase 8: Lokale KI
Wiki mit semantischer Suche ueber pgvector, Diktat ueber faster-whisper, Berichtsassistent und Belegerfassung — alles gegen selbst betriebene Modelle. Dazu zwei Protokollansichten: was die Maschine getan hat und was Menschen getan haben. Nichts, was ein Modell erzeugt, gelangt ungeprueft in einen Nachweis. Jede Ausgabe ist zuerst ein eigener Datensatz, gekennzeichnet, mit zwei gleich grossen Knoepfen davor. Die Datenbank haelt gegen: Eingabe, Ausgabe und Modell sind unveraenderlich, geloescht wird nicht, entschieden wird einmal. Kein Rueckfall auf einen Anbieter: fehlen die Container, fehlt die Funktion. Dafuer bleibt alles andere ohne Modell vollstaendig benutzbar — geprueft von vierzehn E2E-Tests, die ohne laufendes Modell durchlaufen. Postgres bringt jetzt PostGIS und pgvector mit (eigenes Image), Ollama und faster-whisper stehen im Compose-Profil "ki". 73 neue Unit-Tests, 14 neue E2E-Tests, Version 0.9.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@@ -1,10 +1,9 @@
|
||||
# Anleitung
|
||||
|
||||
Bedienung von FLORIAN, Stand Phase 7 (Version 0.8.0). Die Bilder zeigen die
|
||||
Bedienung von FLORIAN, Stand Phase 8 (Version 0.9.0). Die Bilder zeigen die
|
||||
Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt.
|
||||
|
||||
> Die lokale KI und die Kassenverwaltung kommen in den folgenden Phasen dazu.
|
||||
> Siehe [ROADMAP.md](ROADMAP.md).
|
||||
> Die Kassenverwaltung kommt in Phase 9 dazu. Siehe [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -1502,7 +1501,210 @@ wenn sein Code technisch noch stimmt.
|
||||
|
||||
---
|
||||
|
||||
## 43. Rollen und was sie dürfen
|
||||
## 43. Das Wiki der Wehr
|
||||
|
||||

|
||||
|
||||
Unter **Wissen** steht, was die Wehr nachschlagen soll: Dienstvorschriften,
|
||||
Merkblätter, Ausbildungsunterlagen. Also das, was heute als PDF im Ordner steht
|
||||
und im Einsatz niemand findet.
|
||||
|
||||
Lesen darf jeder. Eine Wehr, in der nur die Führung nachschlagen kann, hat kein
|
||||
Wiki, sondern einen Aktenschrank.
|
||||
|
||||
### Zwei Suchen, und beide werden gebraucht
|
||||
|
||||
| Suche | Findet | Braucht ein Sprachmodell |
|
||||
|---|---|---|
|
||||
| **Wortsuche** (das Feld) | was dasteht — „FwDV 7" findet die Seite mit genau dieser Zeichenfolge | nein |
|
||||
| **Frage** (der Kasten oben) | was gemeint ist — „wie lange hält ein Schlauch" findet den Abschnitt über Ablegereife | ja |
|
||||
|
||||
Die Wortsuche zeigt nicht nur den Seitennamen, sondern die **Textstelle**. So
|
||||
sieht man beim Suchen, ob es die richtige Stelle ist, ohne die Seite zu öffnen.
|
||||
|
||||
### Eine Seite schreiben
|
||||
|
||||
**Seite anlegen**, dann Titel, Bereich und Inhalt. Der Inhalt ist Markdown:
|
||||
|
||||
```
|
||||
## Eine Überschrift
|
||||
|
||||
Ein Absatz.
|
||||
|
||||
- Ein Aufzählungspunkt
|
||||
- Noch einer
|
||||
|
||||
| Spalte | Spalte |
|
||||
|---|---|
|
||||
| Wert | Wert |
|
||||
```
|
||||
|
||||
Die **Überschriften sind wichtiger, als sie aussehen**: an ihnen wird die Seite
|
||||
für die Suche zerlegt, und sie stehen später als Fundstelle unter einer
|
||||
Antwort. Eine Seite ohne Überschriften ist ein einziger großer Block und liefert
|
||||
entsprechend grobe Fundstellen.
|
||||
|
||||
Unter dem Textfeld steht beim Bearbeiten, in wie viele Abschnitte zerlegt wird.
|
||||
|
||||
### Fundstelle angeben
|
||||
|
||||
Das Feld **Fundstelle** ist freiwillig und trotzdem das wichtigste: Es steht
|
||||
unter jeder Antwort, die aus dieser Seite gebaut wird.
|
||||
|
||||
> „Zusammenfassung nach FwDV 7, Ausgabe 2002" sagt einem Leser, woran er ist.
|
||||
> Eine Antwort ohne Herkunft ist eine Behauptung.
|
||||
|
||||
Abschreiben sollten Sie eine Dienstvorschrift übrigens nicht — die sind
|
||||
urheberrechtlich geschützt. Was hier hineingehört, ist die **eigene
|
||||
Zusammenfassung** mit Verweis auf die Fundstelle, so wie ein Merkblatt der Wehr.
|
||||
|
||||
### Entwurf oder veröffentlicht
|
||||
|
||||
Ein **Entwurf** wird nicht durchsucht und nicht für Antworten herangezogen. Das
|
||||
ist der Zustand für eine Seite, an der noch gearbeitet wird — sie steht dann nur
|
||||
für die, die bearbeiten dürfen, in der Liste.
|
||||
|
||||
### Jede Änderung wird aufgehoben
|
||||
|
||||
Beim Speichern wird die vorherige Fassung vollständig weggeschrieben. Über
|
||||
**Verlauf** lässt sich jede frühere Fassung im Wortlaut ansehen.
|
||||
|
||||
Das ist keine Bequemlichkeit: eine Dienstvorschrift, die sich unbemerkt ändert,
|
||||
ist schlimmer als keine. Deshalb lässt sich eine Fassung auch nicht löschen.
|
||||
|
||||
---
|
||||
|
||||
## 44. Fragen stellen
|
||||
|
||||
> **Beantwortet wird ausschließlich aus den hinterlegten Seiten.** Steht die
|
||||
> Antwort dort nicht, sagt FLORIAN genau das — statt etwas Plausibles zu
|
||||
> erfinden.
|
||||
|
||||
Im Kasten oben auf der Wissensseite eine Frage eintippen und **Fragen** drücken.
|
||||
Nach ein paar Sekunden steht die Antwort da — in einem gestrichelten Rahmen mit
|
||||
dem Vermerk **Maschinell erzeugt**.
|
||||
|
||||
Darunter stehen die **Fundstellen**, aus denen die Antwort gebaut wurde. Das
|
||||
sind Verweise: ein Klick führt auf die Seite. Lesen Sie sie, bevor Sie sich auf
|
||||
die Antwort verlassen — sie sind das Belegte, die Antwort ist die
|
||||
Zusammenfassung.
|
||||
|
||||
Findet FLORIAN keine passende Stelle, wird das Modell **gar nicht erst gefragt**.
|
||||
Dann steht dort, dass es in den Unterlagen nicht steht. Das ist die richtige
|
||||
Antwort und keine Fehlermeldung.
|
||||
|
||||
### Wenn kein Modell eingerichtet ist
|
||||
|
||||
Dann steht statt des Kastens ein Hinweis. Die Wortsuche funktioniert weiter,
|
||||
und die ist für „FwDV 7" oder „Ablegereife" ohnehin die bessere.
|
||||
|
||||
### Der Hinweis „Suchstand ist älter als der Text"
|
||||
|
||||
Steht auf einer Seite, die geändert, aber noch nicht neu eingebettet wurde. Bis
|
||||
das nachgeholt ist, findet die Frage-Funktion noch die vorherige Fassung —
|
||||
gelesen wird auf der Seite natürlich die aktuelle.
|
||||
|
||||
Der Hintergrundprozess holt das von selbst nach, spätestens im nächtlichen Lauf.
|
||||
Wer nicht warten will, drückt **Jetzt nachziehen**.
|
||||
|
||||
---
|
||||
|
||||
## 45. Diktieren statt tippen
|
||||
|
||||
Auf der Einsatzseite unter **Bericht** → **Beim Schreiben helfen lassen**.
|
||||
|
||||
Der Weg, den es in jeder Wehr schon gibt: jemand spricht auf der Rückfahrt in
|
||||
sein Telefon, und daraus wird abends ein Bericht. Neu ist nur, dass abends schon
|
||||
ein Text dasteht.
|
||||
|
||||
1. **Diktat aufnehmen** drücken. Der Browser fragt nach dem Mikrofon.
|
||||
2. Sprechen. Der Zähler läuft mit.
|
||||
3. **Fertig** drücken. Die Aufnahme wird übertragen und erkannt — bei einer
|
||||
langen Aufnahme dauert das ein bis zwei Minuten. Die Seite kann offen
|
||||
bleiben, der Text erscheint von selbst.
|
||||
4. Text lesen, dann **In den Bericht übernehmen** oder **Verwerfen**.
|
||||
|
||||
> **Bitte gegenlesen.** Fachbegriffe, Ortsnamen und Fahrzeugbezeichnungen
|
||||
> erkennt kein Modell zuverlässig. Die Aufnahme bleibt abgelegt, bis Sie
|
||||
> entschieden haben — wer dem Text nicht traut, hört nach.
|
||||
|
||||
Das Mikrofon fragt der Browser nur über **HTTPS** oder auf `localhost` ab.
|
||||
Bleibt der Knopf ohne Wirkung, liegt es fast immer daran.
|
||||
|
||||
### Aus Stichpunkten einen Entwurf
|
||||
|
||||
Im selben Kasten. Stichpunkte eintippen — oder das Diktat dorthin übernehmen —
|
||||
und **Entwurf erstellen** drücken.
|
||||
|
||||
```
|
||||
Angriffstrupp unter PA ins EG
|
||||
Brand in der Küche, Fettbrand
|
||||
Löschdecke, kein Wasser
|
||||
Belüftung mit Überdrucklüfter
|
||||
Übergabe an Polizei
|
||||
```
|
||||
|
||||
Daraus wird ein Fließtext für die Akte. Was das Modell **nicht** bekommt:
|
||||
Zeitstempel, Einsatznummer und Fahrzeuge. Die stehen schon als Felder im Bericht
|
||||
und stünden sonst zweimal da — beim zweiten Mal womöglich falsch.
|
||||
|
||||
Der Entwurf landet **nicht** im Berichtsfeld. Er steht daneben, gekennzeichnet,
|
||||
mit zwei gleich großen Knöpfen. Erst **Übernehmen** schreibt ihn ins Feld — und
|
||||
auch dann ist er nur ein Textvorschlag im Formular. Gespeichert wird er, wenn
|
||||
Sie **Bericht speichern** drücken.
|
||||
|
||||
> Verantwortlich ist, wer freigibt. Ein Einsatzbericht ist ein Nachweis, und im
|
||||
> Zweifel liest ihn ein Gericht.
|
||||
|
||||
---
|
||||
|
||||
## 46. Belege erfassen
|
||||
|
||||

|
||||
|
||||
Unter **Belege**: **Beleg fotografieren** drücken. Auf dem Handy öffnet sich
|
||||
direkt die Kamera.
|
||||
|
||||
Was das Modell erkennt, steht danach in **änderbaren Feldern**. Was es nicht
|
||||
erkannt hat, bleibt leer und ist **gelb hinterlegt** — das ist eine
|
||||
Aufforderung. Ein geratener Betrag wäre eine falsche Buchung, die niemand
|
||||
bemerkt.
|
||||
|
||||
Prüfen Sie gegen das Foto, das links daneben steht. Dann **Gegenzeichnen**.
|
||||
|
||||
> Mit dem Gegenzeichnen übernehmen Sie, was **dort steht** — nicht, was die
|
||||
> Maschine gelesen hat. Wenn Sie den Betrag berichtigen, ist der berichtigte
|
||||
> Betrag der Ihre. Beides bleibt nachvollziehbar: das KI-Protokoll hält fest,
|
||||
> was ursprünglich erkannt wurde.
|
||||
|
||||
Ohne Vision-Modell funktioniert alles bis auf das Auslesen: der Beleg wird
|
||||
fotografiert und abgelegt, die Angaben tragen Sie von Hand ein.
|
||||
|
||||
Die Buchung daraus kommt mit der Kasse in Phase 9. Hier entsteht erst einmal ein
|
||||
sauber erfasster Beleg mit Foto — und der ist schon mehr wert als ein
|
||||
Schuhkarton.
|
||||
|
||||
### Das KI-Protokoll
|
||||
|
||||

|
||||
|
||||
Für Kommandant und Administrator: unter **KI-Protokoll** steht jede Befragung
|
||||
eines Modells — was hineinging, was herauskam, wer entschieden hat.
|
||||
|
||||
Die Zeile **Offen** ist die wichtigste. Sie zählt die Ausgaben, die erzeugt und
|
||||
nie angesehen wurden. Steht dort eine große Zahl, benutzt jemand den Assistenten
|
||||
und liest nicht gegen.
|
||||
|
||||
Eingabe, Ausgabe und Modell lassen sich nachträglich nicht ändern, und löschen
|
||||
kann man einen Vorgang gar nicht. Das ist der Zweck der Liste: sie soll belegen,
|
||||
was war.
|
||||
|
||||
Daneben unter **Protokoll** steht dasselbe für Menschen: jede Änderung an jedem
|
||||
Datensatz, seit dem ersten Tag.
|
||||
|
||||
---
|
||||
|
||||
## 47. 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
|
||||
@@ -1514,10 +1716,10 @@ kann sie also nachträglich zuschneiden, ohne dass jemand Code ändert.
|
||||
| **Kommandant** | alles innerhalb der eigenen Wehr, einschließlich Rollen vergeben und Protokoll einsehen |
|
||||
| **Gerätewart** | Fahrzeuge und Geräte anlegen, ändern, aussondern; Prüfungen planen und durchführen |
|
||||
| **Atemschutzgerätewart** | zusätzlich Atemschutzgeräte, Träger und die Überwachung nach FwDV 7 |
|
||||
| **Kassier** | Buchungen und Kassenabschluss (Modul noch nicht geplant, siehe Roadmap) |
|
||||
| **Kassier** | Buchungen und Kassenabschluss (Modul in Phase 9), Belege erfassen und gegenzeichnen |
|
||||
| **Schriftführer** | Mitglieder pflegen, Einsätze und Berichte erfassen, den Terminkalender führen |
|
||||
| **Ausbilder** | Übungen und Übungstermine planen, Teilnahme, Anwesenheit und Qualifikationen erfassen |
|
||||
| **Mitglied** | eigene Daten, Listen lesen, Termine sehen und sich zurückmelden, eigene Benachrichtigungswege einrichten |
|
||||
| **Mitglied** | eigene Daten, Listen lesen, Termine sehen und sich zurückmelden, eigene Benachrichtigungswege einrichten, im Wiki lesen |
|
||||
| **Terminal** | nur anmelden und erfassen — nie verwalten |
|
||||
|
||||
Zwei Dinge sind bewusst so gebaut:
|
||||
@@ -1536,7 +1738,7 @@ sind nur die Textvorlagen, und die hängen an einem eigenen Recht.
|
||||
|
||||
---
|
||||
|
||||
## 44. Anmeldung über authentik einrichten
|
||||
## 48. Anmeldung über authentik einrichten
|
||||
|
||||
authentik läuft **getrennt** von FLORIAN und ist nicht Teil dieses Stacks.
|
||||
|
||||
@@ -1589,7 +1791,7 @@ ganze Wehr aus.
|
||||
|
||||
---
|
||||
|
||||
## 45. PIN und Ausweiskarte verwalten
|
||||
## 49. PIN und Ausweiskarte verwalten
|
||||
|
||||
Wie sich am Terminal angemeldet wird, steht in Kapitel 24. Hier geht es darum,
|
||||
was dahinter liegt.
|
||||
@@ -1612,7 +1814,7 @@ Huber), PIN `471203`.
|
||||
|
||||
---
|
||||
|
||||
## 46. Was protokolliert wird
|
||||
## 50. Was protokolliert wird
|
||||
|
||||
Jede schreibende Änderung erzeugt einen Eintrag mit Zeitpunkt, Person,
|
||||
betroffenem Datensatz und den geänderten Feldern im Vorher-Nachher-Vergleich.
|
||||
@@ -1637,7 +1839,7 @@ jeweiligen Detailseite sichtbar.
|
||||
|
||||
---
|
||||
|
||||
## 47. Wenn etwas nicht geht
|
||||
## 51. Wenn etwas nicht geht
|
||||
|
||||
| Beobachtung | Wahrscheinliche Ursache | Was hilft |
|
||||
|---|---|---|
|
||||
@@ -1677,6 +1879,14 @@ jeweiligen Detailseite sichtbar.
|
||||
| Eine Vorlage lässt sich nicht speichern | Ein Platzhalter gehört nicht zu diesem Anlass | Die Meldung nennt ihn und listet die erlaubten auf |
|
||||
| Ein Termin lässt sich nicht löschen | Es liegen schon Rückmeldungen vor | Absagen statt löschen — dann sehen die Angemeldeten den Grund |
|
||||
| Die Zusage wurde zur Warteliste | Die Plätze sind belegt | Sagt jemand ab, rücken Sie von selbst nach und werden benachrichtigt |
|
||||
| Der Kasten „Frage an die Unterlagen" fehlt | Kein Sprachmodell eingerichtet | `LLM_ENABLED=true` und `LLM_BASE_URL`; Container mit `docker compose --profile ki up -d`. Die Wortsuche geht trotzdem |
|
||||
| Die Frage antwortet „steht nicht in den Unterlagen" | Entweder steht es wirklich nicht drin — oder die Seite ist noch nicht eingebettet | Auf der Seite steht dann „Suchstand ist älter als der Text". **Jetzt nachziehen** drücken |
|
||||
| „Das Modell hat nach 120 Sekunden nicht geantwortet" | Ohne Grafikkarte ist das normal | Kleineres Modell (`qwen2.5:7b`) oder `LLM_TIMEOUT` hochsetzen |
|
||||
| „Das Modell liefert 1024 Dimensionen, die Datenbank erwartet 768" | Ein anderes Einbettungsmodell eingetragen | Entweder `nomic-embed-text` benutzen oder die Spalte per Migration ändern — siehe README, Phase 8 |
|
||||
| Der Diktat-Knopf bewirkt nichts | Der Browser fragt das Mikrofon nur über HTTPS oder auf localhost ab | Über die richtige Adresse aufrufen und die Freigabe erteilen |
|
||||
| Das Diktat bleibt bei „Wird erkannt …" stehen | Der Hintergrundprozess läuft nicht, oder Whisper ist nicht erreichbar | `docker compose ps`, dann `docker compose --profile ki up -d whisper`. Die Aufnahme bleibt erhalten |
|
||||
| Beim Beleg bleiben alle Felder leer | Kein Vision-Modell, oder das Foto ist unscharf | Angaben von Hand eintragen. Für die Erkennung `LLM_VISION_MODELL` setzen und das Modell holen |
|
||||
| Die Migration bricht mit „type vector does not exist" ab | Das Postgres-Image bringt pgvector noch nicht mit | `docker compose build postgres`, dann `docker compose up -d postgres` und erneut migrieren |
|
||||
|
||||
Zustand aller Randsysteme auf einen Blick:
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ Faktoren, die in die Schätzung eingehen:
|
||||
| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | **2:40 h** |
|
||||
| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | **2:50 h** |
|
||||
| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | **3:10 h** |
|
||||
| 8 | Lokale KI | 9–12 PT | 2,5–3 h | offen |
|
||||
| 8 | Lokale KI | 9–12 PT | 2,5–3 h | **3:40 h** |
|
||||
| 9 | Kassenverwaltung | 8–11 PT | 2–3 h | offen |
|
||||
| | **Summe** | **123–164 PT** | **28–37 h** | |
|
||||
|
||||
@@ -368,7 +368,7 @@ Zeilen samt Maskierung und Zeilenumbruch nach 75 Oktetten.
|
||||
|
||||
## Phase 8 — Lokale KI
|
||||
|
||||
**Vergleichsaufwand: 9–12 PT**
|
||||
**Vergleichsaufwand: 9–12 PT** · **Tatsächlich: 3:40 h Sitzungszeit**
|
||||
|
||||
| Baustein | PT |
|
||||
|---|---:|
|
||||
@@ -378,10 +378,28 @@ Zeilen samt Maskierung und Zeilenumbruch nach 75 Oktetten.
|
||||
| Wiki mit pgvector, semantische Suche, RAG mit Quellenangabe | 3,5 |
|
||||
| Belegerfassung über ein Vision-Modell, immer bestätigen | 2,0 |
|
||||
|
||||
Der Client aus Phase 0 (`lib/integrations/llm.ts`) ist bereits vorhanden. Die
|
||||
Kennzeichnungspflicht — jede KI-Ausgabe ist als solche erkennbar und muss
|
||||
bestätigt werden, bevor sie in ein Dokument mit Nachweischarakter gelangt —
|
||||
kostet wenig Zeit, ist aber nicht verhandelbar.
|
||||
Der Client aus Phase 0 (`lib/integrations/llm.ts`) war bereits vorhanden — er
|
||||
konnte allerdings nur Text und musste um Einbettungen und Bilder erweitert
|
||||
werden.
|
||||
|
||||
Die Kennzeichnungspflicht war mit „kostet wenig Zeit" zu günstig eingeschätzt.
|
||||
Sie kostet wenig, wenn man sie als Abzeichen an einer Ansicht versteht. Als
|
||||
Zusicherung — eigener Datensatz je Ausgabe, unveränderlich, genau eine
|
||||
Entscheidung, Trigger und REVOKE dahinter, zwei Protokollansichten — ist sie
|
||||
ein guter halber Personentag, der in der Aufstellung gar nicht vorkommt.
|
||||
|
||||
Zwei weitere Posten fehlen in der Schätzung:
|
||||
|
||||
**Das eigene Postgres-Image.** pgvector und PostGIS gibt es nicht zusammen; es
|
||||
gibt kein offizielles Image mit beidem. Ein Dockerfile, ein Neubau, ein Neustart
|
||||
des Dienstes — eine halbe Stunde, aber sie stand nirgends.
|
||||
|
||||
**Der Betrieb ohne Modell.** Der größte Einzelposten dieser Phase, und in der
|
||||
Aufstellung steht davon keine Zeile. Eine Wehr setzt FLORIAN auf, lange bevor
|
||||
sie über eine Grafikkarte nachdenkt — also muss jede Ansicht ohne Modell
|
||||
vollständig benutzbar bleiben und ehrlich sagen, was fehlt. Das sind vier
|
||||
verschiedene Hinweise an vier Stellen, eine zweite Suche, die immer
|
||||
funktioniert, und vierzehn E2E-Tests, die genau diesen Zustand absichern.
|
||||
|
||||
## Phase 9 — Kassenverwaltung
|
||||
|
||||
|
||||
@@ -19,7 +19,120 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte
|
||||
| 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 |
|
||||
| **Summe** | **19:20 h** | **9** | |
|
||||
| Phase 8 — Lokale KI | 3:40 h | 1 | 04.09.2026 |
|
||||
| **Summe** | **23:00 h** | **10** | |
|
||||
|
||||
---
|
||||
|
||||
## 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 `<script>` in einer
|
||||
Dienstvorschrift; der Renderer baut React-Elemente, und ein spitzes
|
||||
Klammernpaar bleibt ein spitzes Klammernpaar.
|
||||
|
||||
**HNSW statt IVFFlat.** IVFFlat muss auf einem gefüllten Bestand trainiert
|
||||
werden und liefert auf einer frisch angelegten Tabelle schlechte Treffer. Ein
|
||||
Wiki wächst Seite für Seite — es gibt nie den Moment, an dem man den Index
|
||||
einmal aufbaut.
|
||||
|
||||
**Ein eigenes Postgres-Image.** pgvector und PostGIS gibt es nicht zusammen.
|
||||
Die Alternative wäre gewesen, den Kosinusabstand in TypeScript zu rechnen: geht
|
||||
bei zweihundert Wiki-Seiten, wird bei zweitausend zäh, und verlegt eine Aufgabe
|
||||
in die Anwendung, für die die Datenbank einen Index hat.
|
||||
|
||||
**Die Audit-Log-Ansicht kam mit dazu.** Sie stand seit Phase 0 als ausgegrauter
|
||||
Eintrag in der Navigation, markiert mit „ab Phase 8". Mit dem KI-Protokoll
|
||||
daneben wäre es schief geworden: die Wehr könnte nachlesen, was die Maschine
|
||||
getan hat, aber nicht, was Menschen getan haben. Der Router dafür existierte
|
||||
seit Phase 0 — es fehlte nur die Seite.
|
||||
|
||||
### Wo es gehakt hat
|
||||
|
||||
| Problem | Ursache | Lösung |
|
||||
|---|---|---|
|
||||
| Ein kurzer Abschnitt wurde unter eine fremde Überschrift gehängt | Die Regel „zu kurz → an den vorherigen anhängen" kannte keine Kapitelgrenze. Unter einer Antwort hätte dann eine Fundstelle gestanden, in der der zitierte Satz gar nicht vorkommt | Zusammengezogen wird nur innerhalb desselben Überschriftenpfads. Gefunden hat es der Test, nicht das Ausprobieren |
|
||||
| `prisma migrate dev` wollte die Datenbank zurücksetzen | Die zurückgerollte Migration aus Phase 7 stand noch in `_prisma_migrations` und galt als „applied but missing" | Die Zeile entfernt. Ein `migrate resolve --rolled-back` räumt sie nicht weg |
|
||||
| Prisma wollte wieder die Volltextindizes abräumen | Bekannt seit Phase 1, achte Migration in Folge | 27 DropIndex-Blöcke entfernt, im Kopf der Migration vermerkt |
|
||||
| Die abgeleiteten Typen der Oberfläche waren `{}` | `ReturnType<typeof useQuery>["data"]` funktioniert bei tRPC nicht | `inferRouterOutputs<AppRouter>` — wie schon in Phase 7 |
|
||||
|
||||
### Abweichung von der Vorgabe
|
||||
|
||||
**Das Wiki hat keine Bilder.** Phase-8.md nennt sie nicht, aber man erwartet
|
||||
sie in einem Wiki. Sie fehlen bewusst: ein Bild in einer Seite wäre entweder
|
||||
eine Datei in MinIO mit eigener Rechteprüfung beim Ausliefern, oder ein Verweis
|
||||
auf einen fremden Server — und Letzteres verbietet die Grundregel, dass FLORIAN
|
||||
nichts von außen nachlädt. Nachrüstbar, wenn eine Wehr es vermisst.
|
||||
|
||||
**Kein Streaming der Modellantwort.** Der Bericht erscheint am Stück, nicht Wort
|
||||
für Wort. Das sähe moderner aus, ist hier aber falsch: der Text soll gelesen und
|
||||
gegengezeichnet werden, nicht beim Entstehen bewundert. Und ein halb geschriebener
|
||||
Entwurf, den jemand vorzeitig übernimmt, wäre genau die Lücke, die diese Phase
|
||||
schließen soll.
|
||||
|
||||
### Nachweise
|
||||
|
||||
- `npx tsc --noEmit` — ohne Ausgabe
|
||||
- `npm run test` — 495 Tests grün, davon 21 neue gegen eine echte Datenbank
|
||||
- `npx playwright test tests/e2e/wissen.spec.ts` — 14 grün, ohne laufendes Modell
|
||||
- `npm run build` — 56 Routen
|
||||
- `npm run db:seed` — vier Wiki-Seiten mit eigenem Text
|
||||
- `docker compose build postgres` — PostGIS 3.5 und pgvector 0.8.6 im selben Image
|
||||
- `docker compose config` — Ollama und faster-whisper im Profil `ki` prüfen durch
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ Phase 4 ████████████████████ fertig
|
||||
Phase 5 ████████████████████ fertig 0.6.0
|
||||
Phase 6 ████████████████████ fertig 0.7.0
|
||||
Phase 7 ████████████████████ fertig 0.8.0
|
||||
Phase 8 ░░░░░░░░░░░░░░░░░░░░ als Nächstes 0.9.0
|
||||
Phase 9 ░░░░░░░░░░░░░░░░░░░░ offen 0.10.0
|
||||
Phase 8 ████████████████████ fertig 0.9.0
|
||||
Phase 9 ░░░░░░░░░░░░░░░░░░░░ als Nächstes 0.10.0
|
||||
Freigabe ░░░░░░░░░░░░░░░░░░░░ offen 1.0.0
|
||||
```
|
||||
|
||||
@@ -194,19 +194,47 @@ selbst gehostet.
|
||||
Wer seine Wehr trotzdem über WhatsApp erreichen will, hat den Weg schon: die
|
||||
Gruppe, die es ohnehin gibt. Ein Link zum Termin hineinkopiert kostet nichts.
|
||||
|
||||
## Phase 8 — Lokale KI · als Nächstes
|
||||
## Phase 8 — Lokale KI · fertig
|
||||
|
||||
Alles gegen Ollama, kein externer Anbieter, komplett abschaltbar.
|
||||
Alles gegen selbst betriebene Modelle, kein externer Anbieter, komplett
|
||||
abschaltbar.
|
||||
|
||||
- Diktat: Aufnahme im Browser → faster-whisper → Transkript im Berichtsfeld
|
||||
- Berichtsassistent: aus Stichpunkten einen Entwurf, immer als Vorschlag
|
||||
- Wiki mit pgvector: semantische Suche mit Quellenangabe
|
||||
- Belegerfassung: Foto einer Quittung → Vorschlag für Betrag, Datum, Händler
|
||||
- Wiki mit pgvector: Dienstvorschriften und Ausbildungsunterlagen als Markdown,
|
||||
Wortsuche und semantische Suche nebeneinander, Antwort mit Fundstellenangabe
|
||||
- Diktat: Aufnahme im Browser, faster-whisper, Transkript als Vorschlag
|
||||
- Berichtsassistent: aus Stichpunkten ein Einsatzberichtsentwurf, immer mit
|
||||
Übernehmen/Verwerfen
|
||||
- Belegerfassung: Foto einer Quittung, Vorschlag für Betrag, Datum, Händler —
|
||||
nicht Erkanntes bleibt leer statt geraten zu werden
|
||||
- KI-Protokoll und Audit-Log-Ansicht nebeneinander
|
||||
- Eigenes Postgres-Image mit PostGIS **und** pgvector
|
||||
|
||||
Jede KI-Ausgabe ist gekennzeichnet und muss bestätigt werden, bevor sie in ein
|
||||
Dokument mit Nachweischarakter gelangt.
|
||||
**Danach kann die Wehr:** ihr Wissen ablegen und befragen, Berichte diktieren
|
||||
statt tippen und Belege fotografieren statt abzuschreiben — ohne dass eine
|
||||
Zeile Text das Gerätehaus verlässt.
|
||||
|
||||
## Phase 9 — Kassenverwaltung
|
||||
### Die Regel, die den Aufbau bestimmt
|
||||
|
||||
Jede KI-Ausgabe ist zuerst ein eigener Datensatz und nicht ein Feld im
|
||||
Zieldokument. Sie ist gekennzeichnet, sie steht daneben statt mittendrin, und
|
||||
sie braucht eine Entscheidung, bevor etwas mit ihr geschieht. Die Datenbank
|
||||
hält gegen: Eingabe, Ausgabe und Modell sind unveränderlich, gelöscht wird
|
||||
nicht, und über einen Vorschlag wird genau einmal entschieden.
|
||||
|
||||
Ohne das ließe sich in einem Jahr nicht mehr sagen, welcher Satz in einem
|
||||
Einsatzbericht von einem Menschen stammt.
|
||||
|
||||
### Kein Rückfall auf einen Anbieter
|
||||
|
||||
Fehlen die Container, fehlt die Funktion — sie wird nicht durch einen fremden
|
||||
Dienst ersetzt. Das ist die einzige Bauart, bei der der Satz „die Daten bleiben
|
||||
im Haus" auch dann noch stimmt, wenn der eigene Server aus ist.
|
||||
|
||||
Dafür bleibt **alles andere ohne Modell vollständig benutzbar**: Wiki lesen und
|
||||
schreiben, Wortsuche, Belege ablegen, beide Protokolle. Wo etwas fehlt, steht
|
||||
ein Satz, der sagt was fehlt und wie es ohne geht.
|
||||
|
||||
## Phase 9 — Kassenverwaltung · als Nächstes
|
||||
|
||||
Am 03.09.2026 ergänzt. Die Rolle `KASSIER` war seit Phase 0 angelegt, ein Modul
|
||||
dazu fehlte in der ursprünglichen Planung. Spezifikation: `Phase-9.md`.
|
||||
|
||||
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 431 KiB After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 438 KiB After Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 808 KiB After Width: | Height: | Size: 808 KiB |
|
Before Width: | Height: | Size: 831 KiB After Width: | Height: | Size: 830 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 323 KiB |
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 988 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
|
Before Width: | Height: | Size: 229 KiB After Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 888 KiB After Width: | Height: | Size: 930 KiB |
|
Before Width: | Height: | Size: 904 KiB After Width: | Height: | Size: 946 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 719 KiB After Width: | Height: | Size: 760 KiB |
|
Before Width: | Height: | Size: 724 KiB After Width: | Height: | Size: 766 KiB |
|
Before Width: | Height: | Size: 378 KiB After Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 539 KiB After Width: | Height: | Size: 559 KiB |
|
Before Width: | Height: | Size: 541 KiB After Width: | Height: | Size: 563 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 888 KiB After Width: | Height: | Size: 991 KiB |
|
Before Width: | Height: | Size: 907 KiB After Width: | Height: | Size: 1012 KiB |
|
Before Width: | Height: | Size: 368 KiB After Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 333 KiB |
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 329 KiB |