Update project plan documentation

Add details about the `purge-closed` endpoint, including its admin-only
nature and confirmation requirement.
Clarify the admin-only checks for bulk actions, emphasizing that they
are now performed at the entry handler level.
Refine the explanation of the case list display, particularly the
`open_count/total_count` grouping and how closed cases are handled.
Introduce the concept of analysis preview in the case list, explaining
its implementation using CSS `line-clamp` and avoiding server-side
truncation.
Enhance the description of admin
This commit is contained in:
2026-04-21 21:59:48 +02:00
parent 1182f4817e
commit ba6aea06e5
+37 -12
View File
@@ -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