Update project plan documentation

Introduce new file types and update versioning strategy for documents.
Refine LLM provider configuration and naming.
This commit is contained in:
2026-04-15 19:33:03 +02:00
parent 9c50c003a3
commit 0011569e1b
+24 -15
View File
@@ -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 `<pre>`
- [ ] 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. |