diff --git a/docs/projektplan.md b/docs/projektplan.md index 846d557..5ebd410 100644 --- a/docs/projektplan.md +++ b/docs/projektplan.md @@ -381,7 +381,10 @@ POST /web/cases/{case_id}/recordings/delete → Einzel-Aufnahme hart löschen analysis_input.json) invalidieren — Auto-Trigger regeneriert beim nächsten View-Load POST /web/cases/undo-delete → letzte Lösch-Batch wiederherstellen -POST /web/cases/bulk → Bulk-Aktionen (analyze / delete, admin-only) +POST /web/cases/bulk → Bulk-Aktionen (analyze / delete, admin-only, + Check am Entry-Handler) +POST /web/cases/purge-closed → Geschlossene Fälle hart löschen (admin-only, + `confirm=yes` Pflicht, emittiert CasePurged) GET /web/audio/{user}/{case_id}/{filename} → Audio-Streaming (Cookie-Auth; Arzt oder Admin; HTTP-Range-Support, `Accept-Ranges: bytes`, 206 Partial Content für HTML5-Seeking) @@ -640,19 +643,30 @@ Serverseitig gerendertes HTML + Formular-Submits. Live-Updates via SSE sind impl ``` Meine Fälle — dr_mueller ──────────────────────────────────────────── -Offen +Heute (2/5) ← open_count / total_count 08:14 │ ⏳ Empfangen (transkribiere ...) - 09:32 │ ✓ Transkribiert [Ansehen] [Analysieren] [Reset] - 11:05 │ ✓ Transkribiert [Ansehen] [Analysieren] [Reset] - [Bulk: alle markierten analysieren] [Bulk: alle markierten löschen] + 09:32 │ „Kniegelenk, re." (3 Aufnahmen) ← Titel-Link → Fall-Übersicht + Zusammenfassung: Röntgen rechtes … Recordings-Link → case_recordings + (Analyse-HTML, CSS-line-clamp auf + preview_lines, click → expand) + [Analysieren] + ... + [Bulk: alle markierten analysieren] ← nur sichtbar für Admins + [Bulk: alle markierten löschen] -Abgeschlossen - 2026-04-06 08:14 „Kniegelenk, re." [Öffnen] [Reset] [Löschen] - 2026-04-05 14:20 „Hypertonie Grad 2" [Öffnen] [Reset] [Löschen] +Gestern (0/2) + 2026-04-20 „Hypertonie Grad 2" [Öffnen] [Löschen] + +Abgeschlossen ausblenden/anzeigen (Toggle) +[Alte abgeschlossene Fälle endgültig löschen] ← purge-closed-Form, admin-only Zuletzt gelöscht [Undo letzten Batch] ``` +**Gruppenzählung:** Pro Datum-Gruppe zeigen wir `open_count/total_count`. Bei `show_closed=false` kommen bereits geschlossene Fälle nicht als Zeilen vor, werden aber in `total_count` über eine separate `closed_extra`-Map eingerechnet (`count_closed_by_date` läuft einmal über die Case-Verzeichnisse). Im `show_closed=true`-Modus sind geschlossene Fälle als Zeilen sichtbar — `closed_extra` ist dann leer, die Summen stimmen über den regulären Iterations-Pfad. Ergebnis: konsistente Anzeige beider Modi, ohne doppelte Zählpfade. + +**Analyse-Preview in der Liste:** Wenn ein Fall ein `document.md` hat, rendert der Server das gesamte Markdown durch dieselbe `md_to_html`-Pipeline wie die Fall-Übersicht (`analysis_html`). Das gerenderte HTML wird direkt unter der Case-Zeile eingehängt; CSS-`line-clamp` (`preview_lines` aus `users.toml`) klemmt es auf N Zeilen. Ein Klick auf die Zusammenfassung togglet eine `.expanded`-Klasse, die den Clamp aufhebt — kein separater Server-Roundtrip, keine zweite gerenderte Variante. Entscheidung: **kein** server-seitiges Truncaten, damit es keine Diskrepanz zwischen Preview und Full-View gibt. + Fall-Übersicht (case_page, Ist-Stand seit 2026-04-19): ``` Fall 09:32 — „Kniegelenk, re." ← Oneliner als Titel @@ -675,7 +689,7 @@ Fall 09:32 — 3 Aufnahmen [← Zurück zur Übersicht] ``` -**Admin-spezifische Aktionen:** `Reset` und die Bulk-Aktionen sind serverseitig auf `role = "admin"` eingegrenzt (Handler prüfen `AuthenticatedUser::is_admin()`). Die Templates erhalten `is_admin: bool` aus dem ViewModel und blenden Nicht-Admin-User die entsprechenden Buttons aus (Defense-in-Depth). +**Admin-spezifische Aktionen:** `Reset`, die Bulk-Aktionen und `purge-closed` sind serverseitig auf `role = "admin"` eingegrenzt. Der Admin-Check sitzt jeweils am *Entry-Handler* (`bulk.rs::handle_bulk` + `case_actions.rs::handle_purge_closed`), nicht pro Sub-Action — so kann kein späterer Branch die Prüfung vergessen. Die Templates erhalten `is_admin: bool` aus dem ViewModel und blenden Nicht-Admin-User die entsprechenden Buttons und die Purge-Form aus (Defense-in-Depth). „Löschen" legt einen `.deleted`-Batch-Marker an. Der Fall verschwindet aus den Listen, kann aber über „Zuletzt gelöscht → Undo" en bloc wiederhergestellt werden. Physisches Entfernen (Cleanup) ist geplant, aber noch nicht implementiert — der Ordner bleibt bis auf Weiteres bestehen. @@ -803,6 +817,9 @@ slug = "dr_mueller" api_key = "..." web_password = "$2b$12$..." role = "doctor" +preview_lines = 2 # optional; CSS line-clamp für die Analyse-Preview + # in der Fall-Liste. Default 2. Wirkt nur visuell; + # der Server rendert immer das vollständige HTML. [user.whisper] # optional; missing block = Service-Defaults language = "de" @@ -835,6 +852,13 @@ Der Transkriptions-Worker reicht `[user.whisper]` pro Upload an den Whisper-Serv - ASR-typische Fehler (phonetische Verwechslungen, zerschnittene Komposita) aktiv korrigieren. Bei Unsicherheit Original behalten. - Unsichere Stellen / korrigierte Tokens / unklare Zahlen mit `==text==` umschließen. Diese Markierungen werden später vom Gazetteer nicht mehr angefasst. Keine anderen Annotation-Formen erlaubt. +**Semantik des Oneliner-Prompts (Priority-Order, strikt top-down):** +1. **Explizites Label:** Enthält das Transkript `Bezeichnung: X` oder `Fall-ID: X`, wird `X` verbatim als Label übernommen. +2. **Medizin-Entity:** Diagnose, Körperregion, Organ, Bildgebungs-Modalität oder zentraler Befund aus dem Transkript. +3. **Fallback:** Markantestes Substantiv / Ereignis / Kontext — **nur** wenn keine Medizin-Entity vorhanden ist. + +**Silence-Rule (eng gefasst):** Nur wenn das Transkript komplett leer ist oder keine verwertbaren Wörter enthält, gibt das Modell null Zeichen aus. Platzhalter wie „empty", „nothing", „-", „N/A" sind verboten. Frühere Versionen erlaubten die Silence-Rule als Default-Eskape für „nicht-medizinische Transkripte" — das führte zu still aussehenden Fällen, obwohl Wörter vorhanden waren. Die Fallback-Regel fängt diesen Fall jetzt ab, die Silence-Rule feuert nur noch bei echt leerem Input. + **Geplant für Phase 4 (noch nicht implementiert):** Preset-System für „Neu generieren" (Arztbrief / Kürzer / Formeller / Diagnosen / Medikamente). Konfigurierbar vermutlich via `prompts.toml` — Format ist noch nicht festgelegt. Bis dahin gibt es nur den einen Default-Prompt. **Entwurf (für spätere Umsetzung):** @@ -1127,7 +1151,8 @@ wiremock = "0.6" - [x] Einzel-Transkripte: `GET /web/cases/{case_id}/recordings` rendert `case_recordings.html` (ein Eintrag pro Aufnahme, Audio-Link pro Transkript). - [x] Audio-Streaming: `GET /web/audio/{user}/{case_id}/{filename}` (Cookie-Auth, Arzt eigene Dateien oder Admin) — HTTP-Range-Requests, `Accept-Ranges: bytes`, 206 Partial Content, Duration-Sidecar `{ts}.duration.txt` für HTML5-Player mit Seeking - [x] Fall analysieren — Button in der Fall-Übersicht -- [x] Bulk-Aktionen (alle markierten analysieren / löschen) über `POST /web/cases/bulk` — **admin-only** (`AuthenticatedUser::is_admin()` auf `role == "admin"`). +- [x] Bulk-Aktionen (alle markierten analysieren / löschen) über `POST /web/cases/bulk` — **admin-only** (`AuthenticatedUser::is_admin()` auf `role == "admin"`, Check am Entry-Handler). +- [x] Purge-Closed (`POST /web/cases/purge-closed`, `confirm=yes` Pflicht) — **admin-only**, entfernt geschlossene Fälle hart, emittiert `CaseEventKind::CasePurged` pro entferntem Case. - [x] Soft-Delete mit Undo letzter Batch (ohne Bestätigungsdialog) - [ ] `ValidCaseId`-Extractor: UUID-Validierung als Axum-Extractor (aktuell inline in Handlern) - [x] SSE-Endpunkt (`GET /web/events`) — `events`-Modul + `routes::events`, 15 s Keep-Alive, Non-Admins slug-gefiltert, Admins ungefiltert, `Lagged`-Recovery durch Reload @@ -1140,7 +1165,7 @@ wiremock = "0.6" **Admin-Features (integriert in die Arzt-UI)** - [x] `hash-password` CLI (`cargo run --bin hash-password`) — erzeugt bcrypt-Hashes für `users.toml`, mit `toml_edit`-Schreibzugriff ohne Kommentarverlust. -- [x] Admin-Gate auf destruktiven Aktionen: `POST /web/cases/{id}/reset` und `POST /web/cases/bulk` prüfen `AuthenticatedUser::is_admin()` (`role == "admin"`). Templates erhalten `is_admin: bool` aus dem ViewModel und blenden die entsprechenden Buttons für Nicht-Admins aus (Defense-in-Depth). +- [x] Admin-Gate auf destruktiven Aktionen: `POST /web/cases/{id}/reset`, `POST /web/cases/bulk` und `POST /web/cases/purge-closed` prüfen `AuthenticatedUser::is_admin()` (`role == "admin"`). Check sitzt jeweils am Entry-Handler (statt pro Sub-Action in `bulk.rs`), sodass neue Bulk-Operationen automatisch geschützt sind. Templates erhalten `is_admin: bool` aus dem ViewModel und blenden Buttons + Purge-Form für Nicht-Admins aus (Defense-in-Depth). - [~] Das frühere separate Admin-Log (`GET /web/` als flache Cross-User-Liste) ist mit dem Case-Pages-Refactor (2026-04-19) entfernt worden. Admin-übergreifende Sichten sind aktuell nicht implementiert; bei Bedarf als Phase-4-TODO wieder einziehen. ### Phase 5 — Clients (Erfassung + Review) @@ -1339,7 +1364,7 @@ Alle Einträge beziehen sich auf den Ist-Stand im Repository. Die ursprüngliche | Änderung | Original | Aktuell | Grund | |---|---|---|---| | Browser-Login | Nur Passwort-Formular gegen `users.toml` | Zusätzlich Magic-Link: `POST /api/auth/magic-link` (API-Key) → `GET /web/magic?token=…` (60 s TTL, one-time-use, `return_to` auf `/web/`-Pfade whitelisted, `Referrer-Policy: no-referrer`) | Der Desktop-Client hat den API-Key ohnehin, das Passwort separat einzutippen ist Friktion ohne Sicherheitsgewinn. Flow nutzt den vorhandenen `AuthenticatedUser`-Extractor, sodass Policy an einer Stelle bleibt. | -| Admin-Gating für destruktive Aktionen | `Reset`/`Bulk` für jeden eingeloggten Arzt | `AuthenticatedUser::is_admin()` (`role == "admin"`) — Handler für `POST /web/cases/{id}/reset` und `POST /web/cases/bulk` rejecten Nicht-Admins, Templates blenden Buttons via `is_admin: bool`-Feld in den ViewModels aus (Defense-in-Depth) | Reset wirft alle Transkripte weg, Bulk kann viele Fälle löschen. Admin-Gate schützt vor Fat-Finger während der Entwicklung und bleibt auf Dauer sinnvoll. | +| Admin-Gating für destruktive Aktionen | `Reset`/`Bulk` für jeden eingeloggten Arzt | `AuthenticatedUser::is_admin()` (`role == "admin"`) — Handler für `POST /web/cases/{id}/reset`, `POST /web/cases/bulk` und `POST /web/cases/purge-closed` rejecten Nicht-Admins. Der Admin-Check sitzt am *Entry-Handler* (`bulk.rs::handle_bulk`, nicht pro Sub-Action wie `bulk_analyze`/`bulk_delete`), damit eine neu hinzugefügte Bulk-Operation nicht versehentlich ungeschützt bleibt. Templates blenden Buttons und die Purge-Form via `is_admin: bool`-Feld aus (Defense-in-Depth). | Reset wirft alle Transkripte weg, Bulk kann viele Fälle löschen, Purge entfernt geschlossene Fälle hart. Admin-Gate schützt vor Fat-Finger während der Entwicklung und bleibt auf Dauer sinnvoll. Authorization am Rand (Entry-Handler statt Sub-Action) ist robuster gegen Code-Drift. | ### Client-Architektur