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:
+37
-12
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user