From 0011569e1b5ddef1cea24058157d4de874ea949c Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 15 Apr 2026 19:33:03 +0200 Subject: [PATCH] Update project plan documentation Introduce new file types and update versioning strategy for documents. Refine LLM provider configuration and naming. --- docs/projektplan.md | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/docs/projektplan.md b/docs/projektplan.md index 4b955f7..aba0416 100644 --- a/docs/projektplan.md +++ b/docs/projektplan.md @@ -479,16 +479,19 @@ Freitext: [________________________] ├── open/ │ └── {case_id}/ │ ├── {UTC-timestamp}.m4a - │ ├── {UTC-timestamp}.txt ← Transkript (existiert erst nach Transkription) - │ └── oneliner.txt + │ ├── {UTC-timestamp}.transcript.txt ← Transkript (existiert erst nach Transkription) + │ ├── oneliner.txt + │ ├── analysis_input_v1.json ← nach Klick auf „Abschließen" + │ └── document_v1.md ← nach erfolgreicher LLM-Analyse └── done/ └── {case_id}/ ├── {UTC-timestamp}.m4a ← RETENTION_AUDIO_DAYS - ├── {UTC-timestamp}.txt ← RETENTION_TRANSCRIPT_DAYS + ├── {UTC-timestamp}.transcript.txt ← RETENTION_TRANSCRIPT_DAYS + ├── analysis_input_v1.json + ├── analysis_input_v2.json ├── document_v1.md ├── document_v2.md - ├── document_v3.md - ├── current → document_v3.md + ├── document_v3.md ← höchste Version gewinnt (kein Symlink) └── .remove ← Marker: zum Entfernen markiert (Lazy Cleanup am Folgetag) /var/log/recorder/ @@ -532,11 +535,12 @@ OLLAMA_URL=http://localhost:11434 OLLAMA_MODEL=gemma3:4b OLLAMA_KEEP_ALIVE=0 -# LLM Provider +# LLM Provider (OpenAI-kompatibel, z.B. Ionos) LLM_URL=https://openai.ionos.com/openai LLM_API_KEY=... LLM_MODEL=... LLM_TEMPERATURE=0 +LLM_TIMEOUT_SECONDS=180 # Session SESSION_TIMEOUT_HOURS=8 @@ -753,14 +757,14 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 - [x] Deployed auf minerva:9001, `WHISPER_URL` in `server/.env` umgestellt ### Phase 3 — Fallverwaltung -- [ ] Drei States: Empfangen → Transkribiert → Ausgewertet -- [ ] State-Übergang: erst "Transkribiert" wenn alle Aufnahmen eines Falls fertig +- [x] Drei States: Empfangen → Transkribiert → Ausgewertet — rein aus FS abgeleitet (keine State-Datei, siehe Abweichungen) +- [x] State-Übergang: erst "Transkribiert" wenn alle Aufnahmen eines Falls fertig — `compute_flags` in `user_web.rs` - [ ] Concurrency-Schutz: RwLock + Pending-Zähler pro Fall (Upload-Handler und Worker synchronisieren) -- [ ] "Abschließen" nur im State "Transkribiert" erlaubt -- [ ] Aufnahmen nach case_id zusammenführen -- [ ] Chronologische Sortierung nach UTC-Zeitstempel -- [ ] Ionos LLM → Dokument generieren (bei Fallabschluss) -- [ ] Versionierung (document_vN.md + current Symlink) +- [x] "Abschließen" nur im State "Transkribiert" erlaubt — `handle_close_case` in `routes/case_actions.rs` +- [x] Aufnahmen nach case_id zusammenführen — `analysis_input_v{N}.json` mit allen Transkripten +- [x] Chronologische Sortierung nach UTC-Zeitstempel — lexikographische Filename-Sortierung +- [x] Externes LLM → Dokument generieren (bei Fallabschluss) — `analyze/`-Modul, OpenAI-kompatibel (siehe Abweichungen: generischer Name `llm`) +- [x] Versionierung `document_vN.md` — v1 implementiert; **kein `current`-Symlink** (siehe Abweichungen: höchste v{N} gewinnt) - [ ] Undo-Logik - [ ] Fall zum Entfernen markieren (.remove Marker, Lazy Cleanup am Folgetag) - [ ] Upload-Deduplizierung (gleiche case_id + Timestamp → ignorieren) @@ -778,8 +782,8 @@ axum-extra = { version = "0.12", features = ["cookie"] } # passend zu axum 0.8 - [ ] Vanilla-JS EventSource-Client - [ ] Übersicht mit drei States (Empfangen/Transkribiert/Ausgewertet) — aktuell: zwei Sektionen (Offen/Abgeschlossen) via FS-Layout, States kommen mit Phase 3 - [x] Fall-Detail: Transkripte pro Aufnahme einsehen (`GET /web/cases/{case_id}`, read-only, IDOR-geschützt via Session-Slug) -- [ ] Fall abschließen / Alle abschließen -- [ ] Dokumentansicht +- [x] Fall abschließen — Button im Detail-View; „Alle abschließen"-Bulk-Action noch offen +- [x] Dokumentansicht — `GET /web/cases/{id}/document`, rendert höchste `document_v{N}.md` in `
`
 - [ ] Nachtrag-Hinweis bei Aufnahmen nach Abschluss (⚠ "N neue Aufnahmen seit Abschluss")
 - [ ] Neu generieren (Preset + Freitext, Freitext max. 500 Zeichen)
 - [ ] Undo-Button
@@ -899,3 +903,8 @@ axum-extra = { version = "0.12", features = ["cookie"] }   # passend zu axum 0.8
 | Admin-Log vs. Arzt-UI | Nur Arzt-UI geplant | Zusätzlich frühes Admin-Log unter `/web/` (flache Liste aller Fälle, Transkripte, Oneliner) | Gebaut, bevor das Arzt-UI (Session, States, Fall-Detail) existiert, um die Pipeline während Entwicklung inspizieren zu können. Soll bleiben, aber später hinter `role = "admin"` geschützt; das Arzt-UI wird separat entwickelt. |
 | Hotwords | Nicht vorgesehen | Per-User-Feld `[user.whisper].hotwords` bleibt **im Code**, wird aber **nicht als Feature angeboten** | Erstes Ad-hoc-Experiment sah 8/8 vs. 3/8 aus. Regress-Lauf über 10 kuratierte Fixtures (`scripts/regress_whisper.sh`) widerlegte das: ohne Hotwords 12/257 Wortfehler (~4.7% WER), mit Hotwords 14/257 — leicht schlechter. Hotwords schluckten sogar Funktionswörter wie „Beginn mit" und „mittels". Kein belegter Nutzen, dafür zusätzliche Bedienkomplexität pro Arzt. KISS: wir lassen die Leitung durchverdrahtet (kein Code-Rückbau), aber bewerben/konfigurieren es nicht. Re-evaluiert, sobald ein konkreter Fachvokabular-Bedarf auftaucht. |
 | Case-State-Machine | Nicht vorgesehen | **Verworfen**: explizite State-Machine + `state.json`-Sidecar (Received/Transcribing/Transcribed/Evaluating/Evaluated/Closed/Failed) wurde gebaut und nach Live-Test wieder entfernt. FS bleibt einzige Wahrheitsquelle. | Der Sidecar führte sofort zu Datendrift: bei jeder Schema-/Code-Änderung waren Bestandsfälle inkonsistent (State sagt „fertig", aber `.m4a` ohne `.transcript.txt` vorhanden). Recovery hätte Self-Healing-Checks gegen die FS-Invariante gebraucht — womit `state.json` zur bloßen Cache-Kopie wird und ihre eigentliche Daseinsberechtigung verliert. Erkenntnis in `docs/projektplan.md` → „Design-Prinzipien: Filesystem ist SoT" festgehalten. |
+| Dokument-Versionierung | `document_vN.md` + `current`-Symlink | Nur `document_vN.md`; höchste `N` gewinnt (`find_latest_document` scannt Ordner) | Symlink-Swap wäre ein zusätzlicher atomarer Schritt mit eigenem Crash-Pfad; Undo wäre `rm current + symlink new` statt einfachem `rm document_v{max}.md`. Max-Scan ist O(n), bei n ≤ 10 Versionen vernachlässigbar. Weniger bewegte Teile. |
+| Analyse-Input-Format | Zusammengeführter Markdown-Text | JSON-Datei `analysis_input_v{N}.json` mit strukturierten Metadaten | Persistenz und LLM-Prompt entkoppelt: Datei speichert `version`, `last_recording_mtime` (für zukünftige Nachtrag-Detection), `recordings[{recorded_at, text}]`. Prompt wird zur Call-Zeit aus dem JSON gerendert. Schema-Evolution bleibt billig, ohne Prompt-Format zu brechen. |
+| LLM-Provider-Naming | `ionos.rs`, `IonosError`, `IonosSettings` | `llm.rs`, `LlmError`, `LlmSettings` — generischer Name im Code | Ionos ist OpenAI-API-kompatibel und jederzeit durch Azure OpenAI, OpenAI direkt oder Together.ai ersetzbar. Code-Identifier sollen anbieter-agnostisch bleiben; konkreter Anbieter lebt nur in `.env` (`LLM_URL`, `LLM_API_KEY`). |
+| Analyse-Queue | `mpsc::channel(depth=32)` (bounded) | `mpsc::unbounded_channel` | `AnalyzeJob` ist ~110 Bytes (Path + u32). Selbst 100.000 Jobs wären 11 MB — realistisch unerreichbar bei 3 Ärzten × wenigen Abschlüssen/Tag. Persistenz lebt in `analysis_input_v{N}.json`, nicht in der Queue. Unbounded spart den 503-Pfad und den ganzen Queue-Full-Recovery-Mechanismus. |
+| LLM-Gate im UI | Nicht vorgesehen | Abschließen-Button nur sichtbar, wenn `LLM_URL`/`LLM_API_KEY`/`LLM_MODEL` alle gesetzt sind; sonst Hinweis „LLM-Analyse nicht konfiguriert". Handler hat denselben Guard (503 Defense-in-Depth). | Ohne konfigurierten Provider würde der Close zur Laufzeit an einem `reqwest`-Fehler scheitern — verwirrend. `Config::llm_configured()` macht das Gate explizit, Tests prüfen beide Pfade. |