chore(bmad): BMAD v6.10 Setup (bmm, de) + PRD/Addendum/Projekt-Kontext (adversarial validiert, revidiert)

This commit is contained in:
Kenearos 2026-07-07 21:58:55 +02:00
parent 6d956c7672
commit 3fdc1b1dd0
256 changed files with 31087 additions and 0 deletions

View file

@ -0,0 +1,59 @@
# PRD Addendum — Technical Depth (Team-Release v1.0)
*Technical-how that does not belong in the PRD but feeds Architecture / Epics / Dev. Authoritative source: `docs/superpowers/specs/2026-07-07-team-multiuser-design.md` + approved plan.*
## Decision rationale (options considered)
- **Passwortlos statt Passwort-Login:** kein Passwort-Speicher/-Reset, geringere Angriffsfläche, weniger Support. Trade-off: E-Mail-Zustellung wird kritischer Pfad (→ SPF/DKIM).
- **Server-Sessions statt JWT:** widerrufbar (Logout, Nutzer-Löschung), kein Secret-Rotation-Aufwand. Trade-off: DB-Lookup pro Request (vernachlässigbar bei better-sqlite3 synchron).
- **Token- UND Session-Hashing (SHA-256):** aus externem Review übernommen — bei DB-Leak wertlos. (Rohwert nur im Link/Cookie.)
- **Kein `cookie-parser`:** `res.cookie()` ist nativ in Express; Lesen via kleiner Helfer → keine Dependency.
- **Verworfen:** `db.run()`-Stil (node-sqlite3-API, existiert nicht in better-sqlite3); CORS (same-origin).
## Schema-Deltas (`server/db.js`)
- Pragma: `db.pragma('foreign_keys = ON')` (sonst wirkt CASCADE nicht).
- `users(id PK, email UNIQUE lowercase, is_admin, created_at)`.
- `login_tokens(token_hash PK, user_id → users ON DELETE CASCADE, expires_at, used_at)`.
- `sessions(id_hash PK, user_id → users ON DELETE CASCADE, expires_at, created_at)`.
- `documents`: neuer zusammengesetzter PK `(user_id, key)`**Table-Rebuild** (SQLite kann PK nicht per ALTER ändern): `documents_v2` anlegen → `INSERT … SELECT <adminId>, …``DROP``RENAME`. Guard: nur wenn `documents` noch keine `user_id`-Spalte hat.
- `history`: `ADD COLUMN user_id` + `UPDATE … WHERE user_id IS NULL`.
- `getDoc(userId,key)` / `putDoc(userId,key,value,now)` — strikt nach `user_id` gefiltert.
## Startup-Reihenfolge (kritisch)
`seedAdmin(ADMIN_EMAIL)` **zuerst** (liefert `adminUserId`) → **dann** `migrateToMultiUser(adminUserId)`. Sonst kann die Migration Alt-Zeilen keinem Admin zuordnen. Beides idempotent, läuft vor `listen`.
## Neue Backend-Dateien
- `server/auth.js``hashToken`, `createLoginToken`/`consumeLoginToken`, `createSession`/`validateSession`/`deleteSession`/`deleteUserSessions`, `seedAdmin`, `migrateToMultiUser`, `authMiddleware`, `adminMiddleware`. better-sqlite3-synchron, kein `async`.
- `server/mailer.js``sendMagicLink(email,url)` via `nodemailer`; ohne SMTP-Config → `console.log(link)` (Dev/Test), kein Wurf.
- `server/ratelimit.js` — In-Memory `Map<key,{count,resetAt}>`; per-Prozess (resettet bei Neustart — für einen Container ok).
## Endpunkte
`POST /api/auth/request` (neutral, rate-limited) · `GET /auth?token=` (einlösen → Cookie → 302) · `POST /api/auth/logout` · `GET /api/auth/me` · `GET/PUT /api/state` (hinter `authMiddleware`, nutzen `req.user.id`) · `GET/POST/DELETE /api/admin/users` (hinter `authMiddleware`+`adminMiddleware`).
Cookie setzen: `res.cookie('session', raw, {httpOnly:true, secure:true, sameSite:'lax', maxAge})`. Lesen: Helfer `readCookie(req,'session')`.
## Frontend-Integrationspunkte
- `index.html`: Login-Overlay am Anfang `<body>` (initial sichtbar); App-Container initial versteckt; in `#tab-settings` Logout-Button + Admin-Sektion (versteckt).
- `app.js` Bootstrap: `GET /api/auth/me` **vor** App-Start; 401 → Overlay; sonst Nutzerwechsel-Check → `DataSync.boot()` → Admin-Sektion bei `isAdmin`.
- `sync.js`: beide `fetch` mit `credentials:'include'`; 401 → Overlay + Sync stopp.
- `storage.js`: `clearUserData()` — nur Datenschlüssel + `pending` leeren (OpenRouter-Key/Modell bleiben). `dienstplan_current_user` hält den eingeloggten Nutzer.
- `sw.js`: Cache-Version bumpen.
## Env-Variablen
`ADMIN_EMAIL`, `APP_BASE_URL`, `SESSION_TTL_DAYS=30`, `RATE_LIMIT_EMAIL=5`, `RATE_LIMIT_WINDOW_MIN=15`, `SMTP_HOST/PORT/SECURE/USER/PASS/FROM`. Secrets nur als Env.
## Deployment-Notizen
- Neue Dependency `nodemailer`. Docker `node:20-slim`, Volume `dienstplan-data:/data`, hinter Caddy.
- SPF + DKIM beim Mail-Provider einrichten (sonst Magic-Links im Spam).
- Server zieht von GitHub `Kenearos/Dienstplan-Pro`; Deploy per SSH-Key `id_ed25519_hetzner`; Container-Env um obige Variablen erweitern.
## Verifikation (Kurz)
Node-Unit-Tests (Token-Hash/Consume, Session validate/delete, Migration idempotent, Rate-Limit, Datentrennung 2 Nutzer, 401/403). Browser-E2E: Server ohne SMTP → Magic-Link in Konsole → kompletter Login-/Trennungs-/Logout-/Nutzerwechsel-Durchlauf. Migration gegen Prod-DB-Kopie.

View file

@ -0,0 +1,249 @@
---
title: Dienstplan-Pro
status: draft
created: 2026-07-07
updated: 2026-07-07
---
# PRD: Dienstplan-Pro — Team-Release v1.0
## 0. Document Purpose
Dieser PRD ist für den Product Owner (Kenearos, zugleich Admin und primärer Nutzer) und die downstream BMAD-Workflows (Architecture, Epics/Stories, Dev). Er definiert das **Was** und **Warum** des v1.0-Release; das technische **Wie** (Hashing, Schema-Migration, Cookie-Handling, Rate-Limit) liegt in `addendum.md`. Bestehende Fähigkeiten (Bonus-Berechnung, Foto-Import, Sync) sind bereits gebaut und live und werden als Kontext erfasst; der release-treibende Kern ist **Mehrbenutzer-Zugang**. Vokabular folgt dem Glossar (§3) verbatim; inferierte Annahmen sind inline `[ASSUMPTION]` markiert und in §9 indexiert. Dieser PRD wurde adversarial validiert (BMAD-Rubrik + adversarial + Edge-Case + Sicherheit/DSGVO); die Reviews liegen als `review-*.md` im selben Ordner.
## 1. Vision
Dienstplan-Pro rechnet Assistenzärzten den Bonus für Wochenend- und Feiertagsdienste nach der NRW-Regelung aus — inklusive der drei Abrechnungs-Varianten und der Feiertags-Verschiebung, die man beim Handrechnen leicht übersieht. Heute ist es ein Single-User-Werkzeug: eine globale Datenbasis, öffentlich ohne Login.
Das v1.0-Release macht Dienstplan-Pro **team-tauglich**: jeder Kollege meldet sich passwortlos per Magic-Link an und hat seine **eigene, getrennte Datenbasis**. Der **Admin** behält seinen bestehenden Team-Blick (mehrere Mitarbeiter); **reguläre Nutzer erfassen nur ihre eigene Person** und sehen nur ihren Bonus. Ein Admin schaltet frei, wer teilnehmen darf. Damit wird aus einem privaten Rechner-Helfer ein kleines, sicheres Team-Tool — ohne Passwort-Management und ohne die bewährte Berechnungslogik zu verändern. Zugleich wird die bisher öffentlich beschreibbare Datenschnittstelle geschlossen.
## 2. Target User
### 2.1 Jobs To Be Done
- **Als regulärer Nutzer (Assistenzarzt)** will ich **meine eigenen** geleisteten Wochenend-/Feiertagsdienste erfassen und meinen Bonus korrekt berechnet bekommen — ohne die Varianten/Feiertagsregeln selbst durchrechnen zu müssen und ohne auf den Admin angewiesen zu sein.
- **Als regulärer Nutzer** will ich, dass **nur ich** meine Daten sehe/bearbeite.
- **Als Kollege** will ich mich **ohne Passwort** anmelden und trotzdem sicher wieder reinkommen.
- **Als Admin (Kenearos)** will ich weiterhin **das ganze Team** erfassen/abrechnen können, steuern **wer** teilnimmt, und meine bestehenden Daten behalten.
- **Als Betreiber** will ich, dass die App im öffentlichen Netz nicht mehr offen beschreibbar ist.
### 2.2 Non-Users (v1)
- Externe/Fremde ohne Freischaltung.
- Lohnbuchhaltung/Vorgesetzte als eigene Rolle — Ergebnisse werden weiter per Export/Bericht geteilt.
- Teams, die **gemeinsam an derselben** Datenbasis arbeiten wollen — v1 ist pro Nutzer isoliert (§5).
### 2.3 Key User Journeys
- **UJ-1. Dr. Alsholi wird freigeschaltet und kommt zum ersten Mal rein.**
- **Persona + Kontext:** regulärer Nutzer, Arbeits-E-Mail vom Admin freigeschaltet, will ohne Passwort loslegen.
- **Entry state:** nicht eingeloggt, öffnet `bonus.pixel-by-design.de`.
- **Path:** Login-Overlay → freigeschaltete E-Mail eingeben → „Link anfordern" → „Prüfe dein Postfach" → Mail öffnen → Magic-Link klicken → **Bestätigungsseite** („Jetzt anmelden"-Button) → bestätigen.
- **Climax:** Sitzung gesetzt; App öffnet sich mit **leerer, eigener** Datenbasis; Sync-Badge „Synchronisiert".
- **Resolution:** angemeldet für ~30 Tage (bzw. bis 8 h Inaktivität).
- **Edge case:** Link abgelaufen/verbraucht → „Link ungültig — neuen anfordern".
- **UJ-2. Kenearos (Admin) rechnet das ganze Team ab.** *(folgt auf UJ-1; Admin-Nutzung.)*
- **Persona + Kontext:** Admin, pflegt wie bisher mehrere Mitarbeiter in seiner eigenen (Admin-)Datenbasis.
- **Path:** Monat wählen → Dienste je Mitarbeiter erfassen (manuell oder **Foto-Import**) → Tab „Berechnung".
- **Climax:** pro Mitarbeiter Gewinner-Variante + Bonus; Summen stimmen mit Handrechnung.
- **Resolution:** Daten in der Admin-Datenbasis gesichert.
- **UJ-3. Dr. Alsholi (regulär) trägt nur sich selbst ein.**
- **Persona + Kontext:** regulärer Nutzer, eingeloggt, will seinen eigenen Monat abrechnen.
- **Path:** Monat wählen → seine eigenen Dienste eintragen → „Berechnung".
- **Climax:** sieht seinen eigenen Bonus; keine fremden Daten sichtbar.
- **Resolution:** nur für ihn gesichert.
- **UJ-4. Kenearos schaltet einen Kollegen frei.**
- **Path:** „Einstellungen" → „Nutzer verwalten" (nur Admin) → E-Mail freischalten → Kollegen informieren (Notzugang: Admin kann bei Bedarf einen Login-Link generieren und out-of-band übergeben, falls die Mail nicht ankommt).
- **Climax:** E-Mail in der Liste; nur diese Person kann sich einloggen.
- **Resolution:** Entfernen später möglich → Sitzungen sofort ungültig.
- **UJ-5. Geteilter Stationsrechner — Nutzerwechsel.**
- **Path:** Kollege A loggt aus (oder Session läuft nach 8 h Inaktivität ab); Kollege B loggt sich ein.
- **Climax:** A's lokale Daten **und** gerätelokaler OpenRouter-Key werden geleert; B sieht frisch nur seine eigenen Daten.
- **Resolution:** keine Vermischung, kein geteilter API-Key.
## 3. Glossary
- **Nutzer** — Person mit freigeschalteter E-Mail und eigener, isolierter **Datenbasis**. Eine E-Mail = ein Nutzer.
- **Regulärer Nutzer** — Nutzer ohne Admin-Recht; erfasst konventionsgemäß nur die **eigene Person** (typisch ein Mitarbeiter = er selbst).
- **Admin** — Nutzer mit dem Recht, die **Allowlist** zu verwalten; pflegt in seiner eigenen Datenbasis das ganze Team (mehrere Mitarbeiter). Admin-Recht betrifft **ausschließlich** die Allowlist — kein Zugriff auf fremde Datenbasen. Mindestens ein Admin existiert immer.
- **Allowlist** — Menge freigeschalteter E-Mails = Zeilen der `users`-Tabelle. Nur diese können einen Magic-Link erhalten.
- **Magic-Link** — einmaliger, kurzlebiger Login-Link (Rohtoken im Link, nur Hash gespeichert). Einlösung per menschlicher Bestätigung erzeugt eine **Sitzung**.
- **Sitzung (Session)** — serverseitig gespeicherter, ablaufender Anmeldezustand; im Browser httpOnly-Cookie (Rohwert), in der DB nur Hash. Läuft nach absoluter Frist **oder** Inaktivitäts-Frist ab.
- **Datenbasis** — die **Dienste**, **Mitarbeiter** und **Urlaubs-Flags** genau eines Nutzers (per `user_id` getrennt).
- **Mitarbeiter** — benannter Eintrag innerhalb der Datenbasis eines Nutzers, für den Dienste erfasst und Boni berechnet werden. Bei regulären Nutzern konventionsgemäß nur sie selbst; beim Admin das Team.
- **Dienst** — Datum mit **Anteil** (1,0 oder 0,5) für einen Mitarbeiter in einem Monat.
- **Urlaubs-Flag** — pro Mitarbeiter/Monat gesetzter Schalter, der die Bonus-Schwellen und Abzüge halbiert (der „Urlaubsmodus" der Berechnung).
- **Slot** — Klassifizierung eines Diensttages: `fr`, `sa`, `so` (Wochenend-Slots, je 450 €) oder `weekday` (250 €), inkl. Feiertags-Verschiebung.
- **Variante** — eine der drei Abrechnungsregeln (V1/V2/V3); pro Mitarbeiter/Monat gewinnt die mit dem höchsten Bonus.
## 4. Features
### 4.1 Authentifizierung per Magic-Link
**Description:** Passwortloser Login. Ein Nutzer gibt seine E-Mail ein; ist sie in der Allowlist, erhält er einen Magic-Link. **Einlösung erfordert eine menschliche Bestätigung** (Bestätigungsseite mit POST), damit automatische Klinik-Mailscanner (Safe-Links/Proofpoint) den Einmal-Token nicht per Prefetch verbrauchen. Keine Passwörter, kein Passwort-Reset. Realizes UJ-1.
**Functional Requirements:**
#### FR-1: Login-Link anfordern (neutral, rate-limited)
Ein Besucher kann per E-Mail-Feld einen Magic-Link anfordern. Realizes UJ-1.
**Consequences (testable):**
- E-Mail in der Allowlist → Login-Token erzeugt, Magic-Link an diese E-Mail gesendet.
- E-Mail NICHT in der Allowlist → nichts gesendet.
- In beiden Fällen dieselbe neutrale `200`-Antwort.
- Eine neue Anfrage invalidiert vorherige noch nicht verbrauchte Tokens desselben Nutzers.
- Enforcement primär **pro E-Mail** (Default 5 / 15 Min → `429`); pro-IP-Limit großzügig (klinikinterne NAT teilt eine IP — mehrere Kollegen dürfen sich nicht gegenseitig aussperren).
#### FR-2: Magic-Link scanner-sicher einlösen → Sitzung
Ein Nutzer kann per Magic-Link eine Sitzung erhalten, ohne dass Mailscanner den Link verbrauchen. Realizes UJ-1.
**Consequences (testable):**
- `GET /auth?token=` liefert eine **Bestätigungsseite**; erst ein expliziter menschlicher POST verbraucht den Token und erzeugt die Sitzung (setzt httpOnly/Secure/SameSite=Lax-Cookie, leitet zur App). Ein reiner GET-Prefetch verbraucht den Token nicht.
- Verbrauchter/abgelaufener Token (>30 Min) → Fehlerseite „Link ungültig/abgelaufen — neuen anfordern".
#### FR-3: Sitzungsdauer, Idle-Timeout & Logout
Ein Nutzer bleibt angemeldet, bis eine Frist abläuft oder er sich abmeldet. Realizes UJ-5.
**Consequences (testable):**
- Absolute Frist `SESSION_TTL_DAYS` (Default 30) **und** Inaktivitäts-Frist `SESSION_IDLE_HOURS` (Default 8) — die zuerst greifende beendet die Sitzung; danach `401` + Login.
- Ein prominenter Logout-Button beendet die Sitzung serverseitig sofort und leert das Cookie.
#### FR-4: Zugangs-Gate auf allen Daten (offline-bewusst)
Alle Datenschnittstellen sind nur mit gültiger Sitzung erreichbar; Offline-Betrieb bleibt möglich.
**Consequences (testable):**
- `GET`/`PUT /api/state` ohne gültige Sitzung → `401`.
- Beim App-Start wird **`401` (ungültige Sitzung → Login-Overlay)** klar von **Netzwerkfehler/Serverausfall (offline → App läuft mit letzter bekannter Sitzung + lokalen Daten weiter)** unterschieden. Offline sperrt die Kernapp nicht aus (Konsistenz mit NFR-8).
#### FR-5: Notzugang (Admin-generierter Link)
Fällt die Mail-Zustellung aus, kann ein Admin einem freigeschalteten Nutzer einen gültigen Login-Link out-of-band bereitstellen.
**Consequences (testable):**
- Ein Admin kann für eine freigeschaltete E-Mail einen einmaligen Login-Link erzeugen/abrufen (übergibt ihn selbst, z.B. per Arbeits-Chat).
- Der Link unterliegt denselben Regeln wie FR-2 (Bestätigung, Ablauf, Einmaligkeit).
### 4.2 Nutzer-Datentrennung
**Description:** Jede Datenbasis ist per `user_id` getrennt; jeder Nutzer — **auch der Admin** — liest/schreibt ausschließlich seine eigene Datenbasis. Realizes UJ-3, UJ-5.
#### FR-6: Strikte Datentrennung pro Nutzer
**Consequences (testable):**
- `GET/PUT /api/state` betrifft ausschließlich die Dokumente des eingeloggten Nutzers — auch für Admins (Admin ist **kein** Daten-Superuser).
- Zwei verschiedene Nutzer sehen disjunkte Datensätze (stehender 2-Nutzer-Isolationstest, siehe SM-2).
#### FR-7: Übernahme der Alt-Daten (sicher & verifiziert)
Die bestehende globale Datenbasis wird beim Umstieg dem Admin zugeordnet — ohne Verlustrisiko.
**Consequences (testable):**
- Der Server bricht bei fehlender/leerer/ungültiger `ADMIN_EMAIL` **laut und ohne Migration** ab (Fail-Fast) — kein verwaister Zustand, keine Totalaussperrung.
- Migration läuft **atomar in einer Transaktion** (documents-Rebuild + history) — ein Abbruch hinterlässt keinen halb-migrierten Zustand; die Ursprungsdaten bleiben bei Fehler unverändert.
- Nach erfolgreicher Migration gehören alle bisherigen Daten dem Admin; kein anderer Nutzer sieht sie.
- Zweiter Start = No-Op (idempotent). `ADMIN_EMAIL` ist nach dem Erst-Boot unveränderlich (Änderung erzeugt keinen zweiten „Alt-Daten"-Empfänger). `[ASSUMPTION: ADMIN_EMAIL wird vor dem ersten Produktiv-Boot korrekt gesetzt und danach nicht geändert.]`
#### FR-8: Nutzer-Isolation im Browser
Beim Nutzerwechsel/Erstlogin im selben Browser werden keine Daten vermischt. Realizes UJ-5.
**Consequences (testable):**
- Bei Login wird geleert, wenn **kein** Nutzer gemerkt ist (Erstfall — verhindert, dass Alt-Daten aus der Login-losen Ära beim ersten regulären Nutzer aufblitzen) **oder** ein anderer Nutzer gemerkt ist.
- Geleert werden die Daten-Schlüssel **und** der gerätelokale OpenRouter-Key/Modell (kein geteilter Kosten-/Secret-Zugang auf geteilten Geräten).
- Ungeflushte `pending`-Änderungen: Bei Re-Login **desselben** Nutzers bleiben sie erhalten und werden geflusht (kein Überschreiben durch den Boot-Pull). Loggt sich ein **anderer** Nutzer ein, während ungeflushte Änderungen offen sind, wird gewarnt/geblockt statt still verworfen.
### 4.3 Admin-Nutzerverwaltung
**Description:** Der Admin pflegt die Allowlist in einem nur für ihn sichtbaren Bereich. Realizes UJ-4.
#### FR-9: E-Mail freischalten
**Consequences (testable):** Nach Freischalten kann diese E-Mail einen Magic-Link erhalten (FR-1); E-Mails normalisiert (lowercase/trim); Duplikate erzeugen keinen zweiten Nutzer.
#### FR-10: Nutzerliste einsehen
**Consequences (testable):** Liste zeigt alle Nutzer inkl. Admin-Kennzeichnung.
#### FR-11: Nutzer entfernen (mit Last-Admin-Schutz)
**Consequences (testable):**
- Nach Entfernen kein Magic-Link mehr; bestehende Sitzungen/Tokens des Nutzers werden mit entfernt (CASCADE) → serverseitiger Zugang sofort entzogen (gerätelokaler Cache wird beim nächsten Nutzerwechsel geleert).
- Ein Admin kann sich **nicht selbst** und **nicht den letzten Admin** entfernen (definierte Fehlermeldung) — die Invariante „mindestens ein Admin existiert immer" gilt.
- `[ASSUMPTION: Die Datenbasis eines entfernten Nutzers wird v1 belassen; siehe §8/DSGVO.]`
#### FR-12: Admin-Gating
**Consequences (testable):** `/api/admin/*` ohne Admin-Recht → `403`, ohne Sitzung → `401`; Admin-Bereich im UI nur bei Admin-Rechten sichtbar.
### 4.4 Bonus-Berechnung *(bestehend, unverändert)*
**Description:** Klassifiziert jeden Dienst in einen Slot (inkl. Feiertags-Verschiebung), berechnet den Bonus über V1/V2/V3, wählt den höchsten. Urlaubs-Flag halbiert Schwellen/Abzüge. Verifiziert gegen Handrechnung (Juni 2026).
#### FR-13: Varianten-Berechnung
**Consequences (testable):** Wochenend-Slots 450 €, Werktage 250 €, Anteile 0,5/1,0 anteilig; Gleichstand → „Schwelle erreicht" vor „nicht", dann niedrigere Varianten-Nummer. (Bestehende Node-Tests decken das ab.)
### 4.5 Dienste-Erfassung & Foto-Import *(bestehend)*
**Description:** Manuelles Eintragen (Datum + Anteil) sowie **Foto-Import** per Vision-LLM (OpenRouter). Namens-Matching entfernt Anrede/Titel; Zielmonat aus dem Live-Dropdown. **DSGVO-Hinweis:** Der Foto-Import überträgt Bilder (Namen + Dienstzeiten) an einen externen LLM — für v1 als bewusst akzeptiertes Risiko geführt (§6.2/§8).
#### FR-14: Foto-Import auf den gewählten Monat
**Consequences (testable):** Import zielt auf den gewählten Monat (nicht auf ein veraltetes Feld); Namen mit Anrede/Titel matchen titellose Mitarbeiter (kein Doppelanlegen).
### 4.6 Synchronisation & Offline *(bestehend, an Auth angepasst)*
**Description:** LocalStorage = synchrone Working-Copy; `DataSync` zieht beim Start den Server-Stand, schiebt Änderungen debounced zurück; `pending`-Flag schützt Offline-Änderungen. Neu: alle Sync-Anfragen tragen die Sitzung.
#### FR-15: Auth-bewusster Sync
**Consequences (testable):**
- `boot()` und `_flush()` senden Credentials; bei `401` (ungültige Sitzung) → Login-Overlay + Sync-Stopp; bei Netzwerkfehler (offline) → App läuft lokal weiter (FR-4).
- Offline getätigte Änderungen gehen nicht verloren (bestehende `pending`-Logik + Race-Fix + FR-8-Regeln zum Nutzerwechsel).
## 5. Non-Goals (Explicit)
- **Keine Passwörter, kein SSO, keine 2FA** — Login nur per Magic-Link.
- **Keine gemeinsame/kollaborative Datenbasis**; jeder Nutzer isoliert (last-write-wins pro Nutzer).
- **Kein Daten-Superuser** — auch der Admin sieht nur seine eigene Datenbasis.
- **Keine Mandanten-/Firmen-Ebene** über den Nutzern.
- **Keine eigene Rolle für Lohnbuchhaltung/Vorgesetzte** in v1.
- **Kein Wechsel der Berechnungslogik**.
- **E-Mail-Änderung eines Nutzers = neuer Nutzer** (Alt-Daten bleiben unter der alten E-Mail; kein automatischer Umzug in v1).
- **Kein Wechsel des Foto-Import-LLM** (OpenRouter bleibt; lokaler KI-Server = späteres Vorhaben).
## 6. MVP Scope
### 6.1 In Scope
- Magic-Link-Login: Anfordern (neutral, rate-limited), scanner-sichere Einlösung (Bestätigung/POST), Sitzung mit absoluter + Idle-Frist, Logout, Admin-Notzugang.
- Datentrennung pro Nutzer (Admin inbegriffen) inkl. sicherer, atomarer, verifizierter Migration + Fail-Fast auf `ADMIN_EMAIL`.
- Admin-Nutzerverwaltung (freischalten/auflisten/entfernen) mit Last-Admin-Schutz.
- Auth-Gate auf `/api/state`; auth-bewusster, offline-fähiger Frontend-Sync; Nutzerwechsel-/Erstlogin-Isolation (inkl. OpenRouter-Key + pending-Schutz).
- E-Mail-Versand per SMTP (Konsolen-Fallback im Dev/Test).
- Sicherheits-Härtungen: Token- & Session-Hashing, `foreign_keys`+CASCADE, Rate-Limit (E-Mail primär, IP großzügig), Cookie-Flags, Token in Access-Logs maskiert.
- **Minimales Audit-Log** (Login/Logout/Admin-Aktionen/401-403-Häufungen, **ohne PII**) — macht die Metriken beobachtbar.
- Release-Hygiene: `.env.example`, `README`-Update, `CHANGELOG`, Version-Tag `v1.0.0`, veraltete `CLAUDE.md`-Regeln aktualisieren, alte Test-Artefakte prüfen/aufräumen.
- **Bericht-Text-Splice-Bug** — eigenes Arbeitspaket mit Akzeptanzkriterium: im Textbericht dürfen Bemerkungs-Zeilen zweier Mitarbeiter nicht mehr ineinanderrutschen (jede Bemerkung dem korrekten Mitarbeiter zugeordnet).
### 6.2 Out of Scope for MVP *(bewusst zurückgestellt)*
- **DSGVO/Rechtstexte** (Datenschutzerklärung, Impressum, Auskunfts-/Löschpfad für Nutzer- und Mitarbeiterdaten, Aufbewahrungsgrenzen, AVV/Consent für den externen Foto-Import-LLM, Breach-Prozess) — **bewusst nicht in v1**; internes Team, Risiko vom Owner (Kenearos) akzeptiert. `[NOTE FOR PM: bewusst akzeptiertes Rechts-/Datenschutzrisiko; bei Ausweitung über das interne Team hinaus zwingend nachzuziehen. Owner: Kenearos.]`
- Passwort-Login / Self-Service-Registrierung.
- „Nutzer zum Admin befördern" über die UI.
- Automatisches Löschen der Datenbasis entfernter Nutzer.
- Wechsel auf den lokalen KI-Server für den Foto-Import (Stufe 3).
## 7. Success Metrics
*Beobachtbar über das minimale Audit-Log (§6.1) bzw. den stehenden Isolationstest — keine PII.*
**Primary**
- **SM-1:** Aktive Team-Nutzung — ≥3 verschiedene Nutzer mit erfolgreichem Login im ersten Monat, **gezählt über die Login-Events des Audit-Logs** bzw. die `users`/`sessions`-Tabellen. Validates FR-1..FR-6.
- **SM-2:** Datentrennung ohne Leck — der **automatisierte 2-Nutzer-Isolationstest** (Node) läuft grün in CI und bei jedem Deploy; keine gemeldeten Vorfälle. Validates FR-6, FR-8.
**Secondary**
- **SM-3:** Reibungsloser Zugang — qualitativ: Kollegen bestätigen, dass Anmelden „einfach funktioniert" (Selbstauskunft/Stichprobe), gestützt auf 401/Fehler-Häufungen im Audit-Log. Validates FR-1, FR-2.
- **SM-4:** Alt-Daten unverändert übernommen — Admin bestätigt nach Migration die Vollständigkeit gegen eine DB-Kopie. Validates FR-7.
**Counter-metrics (do not optimize)**
- **SM-C1:** Login-Komfort nicht auf Kosten der Sicherheit — Sitzungsfristen/Neutralität/Rate-Limit/Scanner-Schutz dürfen nicht gelockert werden, um SM-3 zu verbessern. Counterbalances SM-3.
## 8. Open Questions
1. **DSGVO-Nachzug-Trigger:** Ab wann (mehr als internes Team / externe Nutzer) werden Rechtstexte + Löschpfad Pflicht? Owner: Kenearos. *(v1 bewusst ohne — §6.2.)*
2. **Deployment-Ziel & Orchestrierung** — Tool klären („potctl"? Portainer?); bestimmt den Deploy-Schritt.
3. **SMTP-Postfach** — Zugangsdaten + SPF/DKIM (sonst Spam); harte Voraussetzung vor Live-Schaltung des Magic-Link.
4. Mehrere Admins später nötig? (Beförderung ist v1-Non-Goal.)
## 9. Assumptions Index
- §4.2/FR-7 — `ADMIN_EMAIL` wird vor Erst-Boot korrekt gesetzt und danach nicht geändert.
- §4.3/FR-11 — Datenbasis eines entfernten Nutzers wird v1 belassen.
- §2/§3 — Kollegen nutzen eigene, personengebundene Arbeits-E-Mails (kein geteiltes Postfach); eine E-Mail = ein Nutzer. `[ASSUMPTION inline in §3 Glossar.]`
- §4.6/FR-15 — Ein Nutzer arbeitet meist von einem Gerät zugleich; bei parallelem Multi-Device-Edit desselben Nutzers ist Datenverlust durch last-write-wins ein **bewusst akzeptiertes** Risiko (kein Fremd-Leck). `[ASSUMPTION inline in §5.]`
- §8/#3 — Mail-Zustellung ist mit SPF/DKIM zuverlässig genug (sonst FR-5 Notzugang).

View file

@ -0,0 +1,83 @@
# Adversarial Review — PRD Dienstplan-Pro Team-Release v1.0
*Linse: zynischer/adversarialer Review. Fokus: Klarheit, Vollständigkeit, Umsetzbarkeit des PRD selbst. Technisches "Wie" (Addendum) ist bewusst ausgelagert und wird NICHT als "PRD-Lücke" gewertet.*
**Verdict: REVISE.** Der PRD ist sauber strukturiert und die Auth-Mechanik ist gut durchdacht. Aber er ruht auf zwei ungeklärten Fundamenten (Datenmodell-Bedeutung, E-Mail-Zustellung als einziger Zugangsweg), definiert unmessbare Erfolgsmetriken und hat ein UX-Sackgassen-Problem, das direkt aus der Sicherheitsentscheidung "neutrale 200" folgt. Nichts davon ist ein Blocker im Sinne von "unbaubar", aber alles davon wird ein skeptischer Dev/Architekt zerreißen, bevor er die erste Zeile schreibt.
---
## Kritisch / Hoch
### 1. Kern-Widerspruch: Was erfasst ein einzelner Nutzer eigentlich? (§2.1 vs §2.3/UJ-2 vs Glossar)
Der PRD ist sich uneins, ob ein Nutzer nur **seine eigenen** Dienste erfasst oder die **des ganzen Teams**:
- §2.1 JTBD: "will ich **meine** geleisteten Dienste erfassen" (Singular, eigene Person).
- UJ-2 Climax: "Er sieht **pro Mitarbeiter** die Gewinner-Variante" (Plural — mehrere Mitarbeiter in seiner eigenen Datenbasis).
- Glossar: Datenbasis = Dienste/**Mitarbeiter**/Urlaubs-Flags **eines** Nutzers; Mitarbeiter = benannter Eintrag "innerhalb der Datenbasis eines Nutzers".
Das ist nicht kosmetisch. Wenn jeder Nutzer eine eigene, isolierte Datenbasis mit **mehreren** Mitarbeitern führt (§5 verbietet ja explizit eine gemeinsame Datenbasis), dann müssen 3 Kollegen jeweils **denselben Team-Dienstplan komplett neu privat eintippen**. Das ist massiver Doppelaufwand und untergräbt genau die Rationale des Multi-User-Release. Die Alternative — jeder erfasst nur seine eigene Person — passt nicht zu "pro Mitarbeiter" und nicht zum bestehenden Modell (heute tippt der Admin den ganzen Plan für alle ein). Der PRD muss unmissverständlich sagen, welches der beiden Modelle gilt, sonst bauen Architektur und Dev an der falschen Sache. **Verdächtig: das reale Bedürfnis ist evtl. "Admin rechnet für alle und teilt Berichte" — was der aktuelle Single-User-Zustand bereits kann und den Multi-User-Bedarf teilweise aushöhlt.**
### 2. E-Mail-Zustellung ist Single Point of Failure für den EINZIGEN Login-Weg — und ist ungeprovisioniert (§4.1, §8.3, §9)
Magic-Link ist die einzige Authentifizierung (§5: keine Passwörter, kein SSO, kein Fallback). Damit hängt der gesamte "release-treibende Kern" (§0) an SMTP-Zustellung. Und ausgerechnet die ist:
- §8.3 **offene Frage** (Zugangsdaten + SPF/DKIM "vom Nutzer bereitzustellen") — d.h. beim Release noch nicht existent.
- §9 **bloße Annahme** ("Mail-Zustellung ist zuverlässig genug").
Wenn Mails im Spam landen oder der Provider zickt, ist **niemand** mehr reinzukommen — es gibt keinen Zweitweg. Ein zynischer Architekt fragt: Was ist der Notzugang, wenn SMTP am Release-Tag ausfällt? Der Admin-Konsolen-Fallback (`console.log(link)` laut Addendum) ist nur Dev/Test, nicht produktiv nutzbar. Der PRD verkauft "reibungslosen Zugang" (SM-3) auf einem Fundament, das er selbst als ungebaut und nur angenommen markiert. Mindestens ein produktiver Notzugang (z.B. Admin kann für einen Nutzer manuell einen Link generieren und out-of-band schicken) gehört in Scope oder explizit begründet ausgeschlossen.
### 3. Erfolgsmetriken sind nicht messbar — es gibt keine Instrumentierung (§7)
Der PRD definiert vier Metriken, von denen keine mit dem beschriebenen System messbar ist:
- **SM-1** (≥3 Logins im ersten Monat): Es gibt kein Analytics/Login-Event-Tracking. Woher kommt die Zahl? Manuelles DB-Abfragen? Nirgends spezifiziert.
- **SM-2** (0 Datenleck-Vorfälle): Abwesenheit von Meldungen ≠ Abwesenheit von Lecks. Unfalsifizierbar wie formuliert.
- **SM-3** (Median "E-Mail eingeben → in der App" < 2 Min): Erfordert clientseitiges Timing über den Mailversand hinweg technisch nirgends vorgesehen und faktisch nicht instrumentierbar.
- **SM-4** (Admin bestätigt Alt-Daten): einzige realistisch prüfbare, aber rein manuell.
Entweder gehört minimale Instrumentierung (Login-Zähler o.ä.) in Scope, oder die Metriken müssen ehrlich als "manuell/qualitativ" umformuliert werden. So wie sie dastehen, sind sie Wunschdenken.
### 4. Die "neutrale 200" erzeugt eine Onboarding-Sackgasse — und die versprochene "Einladung" existiert als Feature gar nicht (FR-1, UJ-1, UJ-3)
FR-1: nicht-freigeschaltete E-Mails bekommen dieselbe 200 und "Prüfe dein Postfach", aber **nie eine Mail**. Ein Kollege, dem der Admin sagt "du bist freigeschaltet", der aber z.B. einen Tippfehler in der Allowlist hat (oder noch nicht drin ist), sitzt mit "Prüfe dein Postfach" ewig da, ohne Hinweis warum. Sicherheit gegen Enumeration ist richtig — aber der PRD adressiert die daraus folgende Support-Last/Frustration mit keinem Wort.
Verschärfend: UJ-1 spricht von "klickt direkt den **Einladungslink**", UJ-3 von "ihr den Einladungslink/die Info schicken" — aber **kein FR beschreibt ein Einladungs-/Invite-Mail-Feature**. FR-8 fügt nur die E-Mail zur Allowlist hinzu; der Nutzer muss selbst zur Seite und einen Link anfordern. Die Journeys versprechen einen Einladungsfluss, den die Requirements nicht liefern. Entweder Invite-Feature spezifizieren oder die Journeys ehrlich auf "Admin schaltet frei + teilt manuell mit, Nutzer fordert selbst an" zurechtstutzen.
---
## Mittel
### 5. 15-Minuten-Token kollidiert mit realem E-Mail-Verhalten (FR-2/NFR-3)
Token 15 Min gültig, aber der PRD sorgt sich selbst um Zustelllatenz (SM-3 rechnet sie ein). E-Mail wird oft erst 2060 Min später gelesen. Ergebnis: abgelaufener Link, "Link ungültig", neu anfordern — ein Frustloop, den der Edge-Case in UJ-1 zwar erwähnt, dessen Häufigkeit aber unterschätzt wird. 15 Min ist für passwortlose E-Mail-Links aggressiv kurz. Entweder Wert überdenken oder die Re-Request-Ergonomie als First-Class-Fall behandeln.
### 6. Geteilter Klinik-Rechner + Rate-Limit pro IP = gegenseitige Aussperrung (FR-1/NFR-4 vs UJ-4)
UJ-4 macht den geteilten Stationszimmer-Rechner explizit zum Szenario. NFR-4/FR-1 rate-limiten "pro E-Mail **und pro IP**". In einer Klinik hinter NAT teilen sich alle Kollegen eine öffentliche IP. Mehrere legitime Kollegen, die kurz nacheinander Links anfordern, laufen ins IP-Limit (`RATE_LIMIT_EMAIL=5` / 15 Min laut Addendum) und sperren sich gegenseitig aus. Der PRD stellt beide Anforderungen nebeneinander, ohne den Konflikt zu erkennen.
### 7. OpenRouter-Key überlebt den Nutzerwechsel — Kosten-/Secret-Leck auf geteiltem Rechner (FR-7)
FR-7 löscht beim Nutzerwechsel die Datenschlüssel, lässt aber "gerätelokale Konfiguration (OpenRouter-Key/Modell)" **bewusst stehen**. Auf dem geteilten Praxis-Rechner (UJ-4) bedeutet das: Kollege B nutzt beim Foto-Import stillschweigend Kollege A's **kostenpflichtigen** API-Key. Das widerspricht dem Isolations-Versprechen ("jeder sieht/nutzt nur das Seine") für ein abrechnungsrelevantes Secret. Zumindest als bewusste Entscheidung + Risiko benennen, besser: auf geteilten Geräten auch mitlöschen oder gar nicht gerätelokal halten.
### 8. "last-write-wins bleibt korrekt" ist zu stark für Multi-Device bei 30-Tage-Sessions (§5 vs §9)
§5 behauptet definitiv, LWW "bleibt korrekt, weil ein Nutzer allein auf seinen Daten arbeitet". §9 relativiert: Nutzer arbeitet "**üblicherweise** von einem Gerät zugleich". Die harte Korrektheitsaussage steht auf einer zugegeben nur wahrscheinlichen Annahme. Mit 30-Tage-Sessions ist derselbe Nutzer plausibel auf Handy **und** Klinik-Rechner gleichzeitig eingeloggt; LWW auf ganzen Dokumenten kann dann still Änderungen überschreiben (Datenverlust). "Korrekt" durch "in der Praxis meist ausreichend, akzeptierter Datenverlust bei Parallel-Edit desselben Nutzers" ersetzen — oder das Risiko explizit als bekannt akzeptieren.
### 9. Fehl-konfigurierte ADMIN_EMAIL beim Erst-Boot ist permanent — bedroht "nahtlos übernommen" (FR-6)
FR-6 ordnet alle Alt-Daten dem `ADMIN_EMAIL`-Nutzer zu, und die Migration ist idempotent (läuft nie wieder). Ist die Env beim ersten Start falsch gesetzt (Tippfehler), landen sämtliche Bestandsdaten beim falschen/leeren Nutzer — **ohne im PRD genannten Wiederherstellungspfad**. Das Kern-Versprechen "die bisherige Datenbasis wurde nahtlos übernommen" (§1) hängt an einer einzigen, unumkehrbaren Env-Variable. Der PRD sollte einen Verifikations-/Rücksetz-Schritt fordern (Migration nur nach Bestätigung, oder umkehrbar bis Admin bestätigt hat).
### 10. Scope-Grabbag enthält undefinierte und unfertige Punkte (§6.1, §8.2)
- "**Bericht-Text-Splice-Bug fixen**" steht als In-Scope-Zeile ohne jede Beschreibung, ohne FR, ohne Akzeptanzkriterium. Was ist der Bug? Nicht spezifizierbar = nicht schätzbar = nicht testbar.
- §8.2: das **Deployment-Ziel/Orchestrierung ist offen** ("potctl? Portainer?"). Für ein v1.0-**Release** ist der unklare Ausliefermechanismus eine echte Lücke — der letzte Meter, an dem Releases sterben.
Beides in konkrete, geschätzte Arbeitspakete überführen oder aus dem MVP nehmen.
---
## Niedrig
### 11. Kein Schutz gegen Entfernen des letzten/eigenen Admins (FR-10/§8.5)
FR-10 lässt einen Admin Nutzer entfernen inkl. Sessions (CASCADE). Nichts hindert den (einzigen) Admin daran, sich selbst zu entfernen → sofortiger Verlust aller Verwaltungsfunktionen. Zwar via Neustart (seedAdmin idempotent) teilweise heilbar, aber der PRD sollte einen Guard "letzter Admin nicht entfernbar" fordern. §8.5 ("mehrere Admins später?") streift das Thema, adressiert die Selbst-Aussperrung aber nicht.
### 12. Session-Dauer gleichzeitig festgelegt und offen (FR-3 vs §8.4, SM-C1)
FR-3 setzt Default 30 Tage; §8.4 fragt, ob 30 Tage passen; SM-C1 verbietet, die Dauer für Komfort zu lockern. Kleiner Spannungsbogen: ein sicherheitsrelevanter Parameter ist zugleich verbindlich, offen und mit einer Counter-Metric eingezäunt. Vor Dev entscheiden und OQ4 schließen.
### 13. Formaler Nit: fehlerhafte Trace-Referenz (UJ-2)
UJ-2 trägt "**Realizes UJ-1**" im Kontext — eine Journey kann keine andere Journey "realisieren". Vermutlich Copy-Paste. Traceability-Sauberkeit leidet, wenn solche Marker nicht stimmen.
---
## Was gut ist (fairerweise)
- Auth-Sicherheitsmodell (Hash-only Speicherung, neutraler Endpunkt, kurzlebige Tokens, widerrufbare Server-Sessions, CASCADE) ist konsistent und durchdacht.
- FRs sind fast durchgängig mit testbaren "Consequences" hinterlegt — gute Vorlage für Stories.
- Non-Goals und Assumptions-Index sind explizit; DSGVO-Löschfrage ist ehrlich als offen markiert statt versteckt.
- Trennung PRD/Addendum ist sauber durchgehalten.

View file

@ -0,0 +1,86 @@
# PRD-Review — Edge-Case-Hunter
**Dokument:** `prd.md` (Team-Release v1.0) + `addendum.md`
**Linse:** Unbehandelte Randfälle in den Auth-/Datentrennungs-/Migrations-/Nutzerwechsel-/Offline-Anforderungen.
**Datum:** 2026-07-07
**Verdict:** revise
Methodik: Jeder Flow (Login anfordern → einlösen → Sitzung → Datentrennung → Admin → Migration → Nutzerwechsel → Offline) wurde Schritt für Schritt abgegangen und an jedem Verzweigungs-/Grenzpunkt gefragt „was, wenn hier das Unerwartete passiert?". Kritisiert werden nur Lücken in den **Anforderungen**, nicht im (noch nicht existenten) Code. Technisches „Wie" gehört ins Addendum und wird nicht bemängelt, wo es dort steht.
---
## Kritisch
### K1 — Letzter Admin / Admin entfernt sich selbst → Aussperrung (FR-10, FR-11)
FR-10 erlaubt „Ein Admin kann einen Nutzer entfernen" ohne Ausnahme. Nichts hindert den (einzigen) Admin daran, **sich selbst** oder den **letzten verbliebenen Admin** zu entfernen. Folge: sofortiger Zugangsentzug (CASCADE löscht die eigenen Sitzungen, FR-10) und danach **existiert kein Admin mehr** — die Allowlist kann von niemandem mehr verwaltet werden, kein neuer Nutzer freigeschaltet, kein Admin nachbenannt (Beförderung ist Non-Goal §6.2). Das Team-Tool ist unwiederbringlich verwaltungslos, Wiederherstellung nur per DB-Eingriff.
**Fehlt:** Invariante „mindestens ein Admin muss immer existieren" + Verbot der Selbst-/Letzter-Admin-Entfernung mit definierter Fehlermeldung. §8 Q5 („mehrere Admins später?") berührt das, deckt es aber nicht ab.
### K2 — `ADMIN_EMAIL` fehlt/leer/ungültig beim ersten Boot → kein Admin + verwaiste Alt-Daten (FR-6, Glossar §3, Constraints)
Das Addendum ordnet richtig `seedAdmin` vor `migrateToMultiUser` an. Der PRD sagt aber nirgends, was gilt, wenn `ADMIN_EMAIL` **nicht gesetzt oder leer** ist. Dann legt `seedAdmin` keinen Admin an, `migrateToMultiUser` bekommt keine `adminUserId` → die globalen Alt-Daten werden einer undefinierten/keiner `user_id` zugeordnet (verwaist, für niemanden sichtbar) **und** es existiert kein Admin, der die Allowlist öffnen könnte → Totalaussperrung ab Deploy 1. Die Constraints listen `ADMIN_EMAIL` als Env-Variable, fordern aber kein **Fail-Fast** („Server startet nicht ohne gültige `ADMIN_EMAIL`").
**Fehlt:** Anforderung, dass der Server bei fehlender/leerer `ADMIN_EMAIL` laut und ohne Migration abbricht.
---
## Hoch
### H1 — Offline-Start vs. Auth-Gate: 401 nicht von „Netz nicht erreichbar" unterschieden → NFR-8-Widerspruch
Addendum-Bootstrap: `GET /api/auth/me` **vor** App-Start; „401 → Overlay". NFR-8 verspricht aber, die Kernapp (Erfassen/Rechnen) funktioniert **offline weiter**. Öffnet ein bereits eingeloggter Nutzer die App **ohne Netz** (oder bei kurzem Serverausfall), schlägt `auth/me` mit *Netzwerkfehler* fehl — **nicht** mit 401. Der PRD/Addendum behandelt nur den 401-Fall. Wird jeder Fehlschlag wie „keine Sitzung" behandelt, sperrt das Login-Overlay den Nutzer aus der offline-fähigen Kernapp aus — direkter Widerspruch zu NFR-8. Wird er ignoriert, drohen Stale-Data-Fälle.
**Fehlt:** Anforderung, die „kein Netz / Serverfehler" (App offline weiterlaufen, letzte bekannte Sitzung annehmen) klar von „Sitzung ungültig/401" (Overlay) trennt.
### H2 — E-Mail-Sicherheitsscanner verbrauchen den Einmal-Token vor dem Nutzer (FR-2, NFR-3)
FR-2/NFR-3: Token ist **einmalig** (`used_at`) und wird per `GET /auth?token=` eingelöst. Firmen-/Klinik-Postfächer (Microsoft Safe Links, Proofpoint, Virenscanner) **rufen Links in E-Mails automatisch vorab auf**. Ein solcher GET löst den Token ein und markiert ihn als verbraucht — **bevor** der Mensch klickt. Ergebnis: der Nutzer bekommt „Link ungültig/verbraucht" (FR-2) und kommt nie rein; ein per Design offline-taugliches Team-Tool wird für Corporate-Mail unbrauchbar. Das ist ein bekannter, hochwahrscheinlicher Fall gerade im Zielumfeld (Assistenzärzte mit Klinik-E-Mail).
**Fehlt:** Anforderung, die Scanner-Prefetch toleriert (z. B. Einlösung erfordert menschliche Bestätigung/POST auf der Landing-Page statt reinem GET-Verbrauch).
### H3 — Re-Login nach Sitzungsablauf + offene Offline-Änderungen → Datenverlust (FR-3, FR-7, FR-14)
FR-14 verspricht „Offline getätigte Änderungen gehen nicht verloren" (pending-Flag). Kollidiert aber mit Ablauf/Nutzerwechsel: Läuft die Sitzung ab (401), während lokale `pending`-Änderungen ungeflusht sind, erscheint das Overlay. Loggt sich dann (a) derselbe Nutzer neu ein → `boot()` zieht Server-Stand; ob die pending-Änderungen den Pull überleben oder überschrieben werden, ist **nicht spezifiziert**. Loggt sich (b) am geteilten Praxis-Rechner ein **anderer** Nutzer ein (genau UJ-4), löscht `clearUserData` (FR-7) die pending-Änderungen von Nutzer A **unwiederbringlich**. FR-14 und FR-7 widersprechen sich in diesem Pfad.
**Fehlt:** Anforderung, wie pending-Änderungen bei Re-Login (gleicher Nutzer) erhalten bleiben und was mit ungeflushten Änderungen passiert, wenn am selben Gerät ein anderer Nutzer einloggt (Warnung? Verwerfen? Blockieren?).
### H4 — `ADMIN_EMAIL`-Änderung nach erfolgter Migration → neuer Admin leer, Alt-Daten gestrandet (FR-6)
Die Migration ist idempotent, geschützt durch „nur wenn `documents` noch keine `user_id`-Spalte hat" (Addendum). Wird `ADMIN_EMAIL` bei einem **späteren** Deploy geändert (Tippfehler-Korrektur, Personenwechsel), legt `seedAdmin` einen **zweiten** Admin an, aber die Migration läuft nicht erneut → die Alt-Daten bleiben beim **alten** Admin, der neue Admin startet leer. Es existieren nun zwei Admins mit unklarer Datenzuordnung. FR-6 behandelt nur den Erst-Migrationsfall.
**Fehlt:** Anforderung/Annahme zum Umgang mit einer `ADMIN_EMAIL`-Änderung nach der Migration (bzw. explizites „`ADMIN_EMAIL` ist nach Erst-Boot unveränderlich").
---
## Mittel
### M1 — Gleichzeitige Sitzungen desselben Nutzers auf zwei Geräten → stiller Datenverlust (§4.6, Assumptions §9)
Assumption §9 nimmt an, „ein Nutzer arbeitet üblicherweise von einem Gerät/Session zugleich; last-write-wins ist ausreichend". „Üblicherweise" ist kein Rand. Handy + Stationsrechner gleichzeitig offen (realistisch im Klinikalltag) → last-write-wins auf ganzen Dokumenten überschreibt die Dienste-Eingabe des einen Geräts komplett mit dem Stand des anderen — **ohne Warnung**. Nichts in den Anforderungen begrenzt die Zahl aktiver Sitzungen pro Nutzer oder erkennt konkurrierende Schreibvorgänge. SM-2 zählt nur *fremde* Daten als Leck, deckt diesen Eigen-Datenverlust nicht ab.
**Fehlt:** Bewusste Entscheidung — entweder als Risiko akzeptieren (im PRD benennen) oder eine Erkennungs-/Warn-Anforderung.
### M2 — Geteilte Klinik-IP (NAT) + Pro-IP-Rate-Limit → legitime Kollegen ausgesperrt (FR-1, NFR-4)
NFR-4 rate-limitiert „pro E-Mail **+ IP**". Im Addendum ist nur `RATE_LIMIT_EMAIL=5` konfiguriert — ein separater Pro-IP-Wert fehlt/ist unbenannt. Genau das Zielszenario (mehrere Ärzte hinter **einer** Klinik-NAT-IP, UJ-4 geteilter Rechner) bedeutet: viele Login-Anfragen aus derselben IP. Ist das IP-Limit ähnlich niedrig (5/15 min), wird der 6. Kollege, der in einem Fenster einen Link anfordert, mit 429 abgewiesen, obwohl legitim. Das kollidiert mit SM-1 (≥3 Kollegen im ersten Monat) und SM-3 (reibungsloser Zugang).
**Fehlt:** Getrennte, an geteilte IPs angepasste Dimensionierung des Pro-IP-Limits (oder explizit „Pro-IP-Limit großzügig, Enforcement primär per E-Mail").
### M3 — Nutzerwechsel-Erkennung bei „noch kein gemerkter Nutzer" undefiniert → Stale-Data/Leck-Risiko (FR-7)
FR-7 cleart, wenn der eingeloggte Nutzer „vom zuletzt lokal gemerkten **abweicht**". Der Erstfall (nichts gemerkt, `dienstplan_current_user` leer) ist nicht spezifiziert. Kritisch: Der bisherige Browser des Admins enthält aus der Login-losen Ära bereits globale Daten unter `dienstplan_employees|_duties|_vacation`. Loggt sich dort als **erster** ein *Nicht-Admin* ein, ist kein Vor-Nutzer gemerkt → je nach Auslegung wird **nicht** gecleart → die Alt-Daten des Admins blitzen kurz beim Nicht-Admin auf, bevor der Server-Pull greift. Das ist ein potenzielles Datentrennungs-Leck (SM-2 = 0 Vorfälle).
**Fehlt:** Explizit „bei Login clearen, wenn kein Nutzer gemerkt ODER ein anderer gemerkt ist".
### M4 — Kein Pfad für E-Mail-Änderung eines Nutzers (Glossar §3 „eine E-Mail = ein Nutzer")
Identität = E-Mail. Ändert sich die Arbeits-E-Mail eines Kollegen (Namenswechsel, Domänenwechsel der Klinik), gibt es keinen Anforderungspfad: Der Admin kann nur die neue E-Mail freischalten → das erzeugt einen **brandneuen, leeren** Nutzer; die gesamte Datenbasis unter der alten E-Mail ist gestrandet (und die alte E-Mail bleibt aktiv, bis manuell entfernt). Weder in Features, Non-Goals noch Open Questions erwähnt.
**Fehlt:** Entweder als expliziter Non-Goal deklarieren („E-Mail-Änderung = neuer Nutzer, Alt-Daten manuell") oder als Open Question aufnehmen.
### M5 — Teil-/abgebrochene Migration nicht abgedeckt; „idempotent" ≠ crash-sicher (FR-6)
FR-6 fordert „ein zweiter Serverstart wiederholt die Migration nicht (idempotent)". Der Idempotenz-Guard hängt (Addendum) an der `user_id`-Spalte in `documents`. Bricht der Prozess **zwischen** dem `documents`-Rebuild und dem `history`-Update ab, ist beim Neustart der Guard bereits erfüllt (documents hat user_id) → die Migration wird übersprungen, während `history`-Zeilen dauerhaft `user_id IS NULL` behalten (verwaiste Historie). „Idempotent" laut FR-6 deckt Wiederholung ab, nicht **partiellen Abbruch**.
**Fehlt:** Anforderung, dass die Migration atomar (eine Transaktion) oder wiederaufsetzbar ist, sodass ein Absturz keinen halb-migrierten Zustand hinterlässt.
---
## Niedrig
### N1 — Mehrere offene Login-Tokens gleichzeitig (FR-1, NFR-3)
Fordert ein Nutzer mehrere Links an (erste Mail kam nicht), erzeugt jede Anfrage laut FR-1 einen Token. Nichts sagt, ob eine neue Anfrage ältere unverbrauchte Tokens invalidiert. Ergebnis: bis zu `RATE_LIMIT_EMAIL` gleichzeitig gültige Login-Tokens im 15-Min-Fenster. Meist harmlos (alle einmalig/kurzlebig), aber vergrößert das Zeitfenster für Token-Diebstahl leicht.
**Fehlt (optional):** „Neue Anfrage invalidiert vorherige offene Tokens desselben Nutzers."
### N2 — Lokale Daten eines entfernten Nutzers verbleiben auf dessen Gerät (FR-10, FR-7)
FR-10 entzieht serverseitig sofort den Zugang (CASCADE). Die zuletzt gesyncten Daten liegen aber weiter im LocalStorage des Geräts des entfernten Nutzers (`clearUserData` läuft nur beim *Wechsel*). Auf dem Eigengerät akzeptabel, aber der Vollständigkeit halber im Löschkonzept (§8.1) mitzudenken.
**Fehlt (optional):** Erwähnung im Löschkonzept.
---
## Positiv abgedeckt (kein Handlungsbedarf)
- Token-Reuse / abgelaufener Token (FR-2) — klar behandelt.
- Sofortiger Zugangsentzug bei Nutzerlöschung serverseitig via CASCADE (FR-10, NFR-5).
- Login-Token eines zwischenzeitlich entfernten Nutzers wird durch CASCADE ungültig — implizit gedeckt.
- Neutralität des Request-Endpunkts (FR-1, NFR-4) gegen Allowlist-Enumeration.
- Gerätelokale Config (OpenRouter-Key) überlebt Nutzerwechsel (FR-7).
- SameSite=Lax erlaubt Cookie-Set bei Top-Level-Navigation aus der Mail (FR-2).

View file

@ -0,0 +1,60 @@
# PRD Quality Review — Dienstplan-Pro Team-Release v1.0
## Overall verdict
Ein starker, kohärenter PRD mit klarer These (aus einem öffentlichen Single-User-Tool ein isoliertes Team-Tool machen, ohne Einfachheit oder die verifizierte Rechenlogik zu verlieren) und ungewöhnlich testbaren FRs — die Consequences-Blöcke sind das Rückgrat und tragen. Was ihn von „build-ready" trennt, sind vier gezielte Lücken: die Success Metrics haben kein benanntes Messinstrument (SM-2 „0 Leck-Vorfälle" ist ohne Detektionsmechanismus unfalsifizierbar bis zur Katastrophe), die Migration (FR-6) beschreibt kein Verhalten im Fehlerfall obwohl sie ein destruktiver Table-Rebuild auf Produktivdaten ist, die Daten-Zugriffsgrenze des Admins ist nur ableitbar statt ausgesprochen, und der Assumptions-Index hält die in §0 versprochene Inline-Disziplin nicht ein. Keine davon ist ein Blocker; alle sind eng umrissen und schnell zu schließen. Verdict: **revise**.
## Decision-readiness — strong
Der PRD trifft Entscheidungen als Entscheidungen: Non-Goals (§5) sind hart formuliert („Keine Passwörter, kein SSO, keine 2FA"), das Addendum trägt echte Trade-offs (Passwortlos → E-Mail wird kritischer Pfad; Server-Sessions → DB-Lookup pro Request). Die Open Questions (§8) sind wirklich offen und nicht rhetorisch — Sitzungsdauer, Mehr-Admin-Bedarf, Deployment-Tool. Der `[NOTE FOR PM]` am DSGVO-Löschkonzept (§6.2/§8.1) sitzt an einer echten Spannung, nicht an einem sicheren Checkpoint.
Eine reale Kehrseite: Open Question #1 (Löschkonzept für entfernte Nutzer) ist rechtlich lasttragend — die App speichert ab Tag 1 Personenbezug (Namen + Dienste) — und wird dennoch nach v2 verschoben. Das ist ehrlich geflaggt, aber ein Green-Light-to-Build-PRD sollte hier einen Entscheidungs-Owner und ein Datum tragen, nicht nur „früh klären".
### Findings
- **low** DSGVO-Löschkonzept offen bei Build-Start (§8.1 / §6.2) — Personenbezogene Daten werden ab v1 gespeichert, das „Recht auf Vergessen" ist aber vollständig offen. Gut geflaggt, aber ohne Owner/Deadline. *Fix:* dem `[NOTE FOR PM]` einen Verantwortlichen und ein Zieldatum geben, damit die Entscheidung nicht bis v2 treibt.
## Substance over theater — strong
Kein Furniture. Die vier UJs haben benannte Protagonisten (Dr. Alsholi, Kenearos) und treiben je eine Design-Entscheidung — UJ-4 (geteilter Stationsrechner, Nutzerwechsel) rechtfertigt direkt FR-7. Die Vision (§1) ist produktspezifisch und ließe sich nicht in einen beliebigen anderen PRD einsetzen. Die NFRs sind mit konkreten Schwellen unterlegt (SHA-256, 32 Byte, 15 Min Token, 30-Tage-Session, Rate-Limit pro E-Mail+IP) statt „system must be secure". Keine Persona-, Innovation- oder NFR-Theater-Signale.
## Strategic coherence — strong
Der PRD hat eine These und wettet sichtbar darauf: Datentrennung + passwortloser Zugang als der eine release-treibende Kern, alles Bestehende ausdrücklich als Kontext markiert (§0, §4.44.6 „bestehend"). Feature-Priorisierung folgt der These, nicht „was ist einfach zuerst". Die Success Metrics zielen auf die These (Team-Nutzung, Datentrennung) statt auf Aktivitäts-Vanity-Metriken, und die Counter-Metric (SM-C1: Login-Komfort nicht auf Kosten der Sicherheit) ist vorhanden und sinnvoll gegen SM-3 gerichtet. MVP-Scope-Art ist klar problemlösend.
## Done-ness clarity — adequate
Dies ist die Stärke des PRD und zugleich der Ort der schärfsten Restlücken. Fast alle FRs tragen mindestens eine testbare Consequence mit HTTP-Status oder beobachtbarem Zustand (401/403/429, „Token als verbraucht markiert", „disjunkte Datensätze"). Keine „handles X gracefully"-Weichmacher. Aber:
**FR-6 (Migration)** beschreibt nur den Erfolgspfad (idempotent, Admin sieht Daten). Es ist ein destruktiver `documents`-Table-Rebuild auf Produktivdaten (Addendum: DROP/RENAME). Das produktseitige „Done" muss den Fehlerfall einschließen — kein Datenverlust bei Abbruch. Das Addendum erwähnt „Migration gegen Prod-DB-Kopie" in der Verifikation, aber FR-6 selbst hat keine Fehlerfall-Consequence.
**Admin-Datengrenze:** Das Glossar sagt, der Admin habe „zusätzlich eine eigene Datenbasis wie jeder Nutzer" und FR-5 filtert per `user_id` — also *impliziert* der PRD, dass ein Admin fremde Datenbasen nicht lesen kann. Ausgesprochen wird es nie. Bei einem sicherheitslasttragenden Punkt sollte „Admin hat keine Daten-Superkraft" explizit stehen (FR-11-Consequence oder Non-Goal), nicht nur ableitbar sein.
### Findings
- **medium** Migration ohne Fehlerfall-Done-Bedingung (§4.2 / FR-6) — destruktiver Rebuild auf Produktivdaten, aber keine Consequence für Abbruch/Datenverlust. *Fix:* Consequence ergänzen: „Bei Migrationsfehler bleiben die Ursprungsdaten unverändert; Cutover erst nach erfolgreicher Verifikation gegen eine DB-Kopie."
- **medium** Admin-Daten-Zugriffsgrenze nur ableitbar (§3 Glossar / §4.3 FR-11) — nirgends steht, dass ein Admin fremde Datenbasen *nicht* lesen kann; ein Leser könnte Admin als Daten-Superuser annehmen. *Fix:* explizite FR-11-Consequence oder Non-Goal: „Admin-Recht umfasst ausschließlich die Allowlist; `/api/state` bleibt auch für Admins auf die eigene `user_id` beschränkt."
## Scope honesty — strong
Die Non-Goals (§5) leisten echte Arbeit und decken die Verwechslungsgefahren ab (keine kollaborative Datenbasis, keine Mandanten-Ebene, keine Buchhaltungs-Rolle). §6.2 nennt Out-of-Scope mit Begründung. `[ASSUMPTION]` an FR-10, `[NOTE FOR PM]` an der DSGVO-Frage. Die Open-Items-Dichte (5 Open Questions + 1 NOTE + 4 Assumptions) ist für die Stakes angemessen — zwei der Open Questions (SMTP-Zugangsdaten, Deployment-Orchestrierung) sind operative Ship-Blocker, aber sauber als solche erkennbar. De-Scoping wird offen vorgeschlagen, nicht still gemacht.
Eine Ungenauigkeit im Scope-Anspruch: FR-10 verspricht „sofortiger Zugangsentzug" beim Entfernen eines Nutzers. Serverseitig stimmt das (CASCADE killt Sessions → 401). Aber NFR-8 + FR-7 halten die letzten Daten offline im LocalStorage lesbar, bis ein Nutzerwechsel sie leert. Auf dem geteilten Stationsrechner könnte ein entfernter Nutzer seine zwischengespeicherten Daten offline noch sehen. Kleine Kante, aber der Absolutheits-Anspruch ist leicht überzogen.
### Findings
- **low** „Sofortiger Zugangsentzug" überzeichnet (§4.3 FR-10 vs. NFR-8/FR-7) — Server-Sessions sterben sofort, der Offline-LocalStorage-Cache eines entfernten Nutzers bleibt aber bis zum Nutzerwechsel lesbar. *Fix:* Caveat ergänzen („serverseitiger Zugang sofort entzogen; gerätelokaler Cache wird beim nächsten Nutzerwechsel geleert").
## Downstream usability — strong
Chain-top-PRD (speist Architecture → Epics → Stories), und die Traceability hält: FR-1..FR-14 lückenlos, UJ-1..UJ-4, SM/SM-C, NFR-1..8 alle eindeutig. Cross-Refs lösen auf (`Realizes UJ-x`, `Validates FR-x`, `Counterbalances SM-3`). Stichprobe SM-2 → FR-5, FR-7: existieren und passen. Das Glossar ist vorhanden und die Domänennomen sind über FRs/UJs weitgehend konsistent benutzt. Jeder Abschnitt ist einzeln herausziehbar.
Kleine Glossar-Drift: die Datenbasis wird über „Urlaubs-Flags" definiert (§3), an anderer Stelle heißt dasselbe Konzept „Urlaubsmodus" (§4.4) — kein eigener Glossar-Eintrag, zwei Bezeichnungen. Downstream harmlos, aber notierbar.
## Shape fit — strong
Korrekt kalibriert. Brownfield-Tool mit lasttragender UX (Magic-Link-Flow, Nutzerwechsel) → UJs mit benannten Protagonisten sind hier zurecht schwergewichtig, nicht Overhead. Bestehende Fähigkeiten sind sauber von neuen getrennt („bestehend"-Markierung) und die Brownfield-Referenzen decken sich mit `project-context.md` (better-sqlite3 synchron, `documents`-PK-Rebuild, `foreign_keys` default OFF). Weder über- noch unterformalisiert.
## Mechanical notes
- **Assumptions-Index-Roundtrip (medium):** §0 behauptet „Annahmen sind inline mit `[ASSUMPTION]` markiert und in §9 indexiert." Tatsächlich ist nur die FR-10-Annahme inline getaggt. Die drei übrigen Index-Einträge (§2 eigene Arbeits-E-Mails, §4.6 ein Gerät/Session, §7/SM-3 zuverlässige Mail-Zustellung) haben keinen korrespondierenden Inline-Tag. Roundtrip gebrochen → entweder Inline-Tags nachziehen oder Index auf tatsächlich inline markierte Annahmen reduzieren.
- **Success-Metric-Messbarkeit (medium):** SM-1/SM-2/SM-3 benennen kein Messinstrument. Die App sammelt keine Analytics/Timing; SM-2 („0 Vorfälle, in denen ein Nutzer fremde Daten sieht") hat keinen Detektionsmechanismus — ein Leck wäre still. Für ein 3-Personen-Team ist manuelle Beobachtung (Admin prüft `users`-Tabelle, Selbstauskunft für SM-3) legitim, aber der PRD sollte je Metrik sagen *wie* sie beobachtet wird und für SM-2 den stehenden 2-Nutzer-Isolationstest (Addendum-Verifikation) als laufenden Check benennen.
- **Glossar-Drift (low):** „Urlaubs-Flags" (§3) vs. „Urlaubsmodus" (§4.4) für dasselbe Konzept; kein eigener Glossar-Eintrag.
- **Cross-Ref `§8.1`:** In Constraints referenziert; mappt auf Open Question #1 — auflösbar, aber die Open Questions sind als flache Liste ohne `§8.x`-Nummerierung geführt. Konsistente Nummerierung würde den Verweis wasserdicht machen.

View file

@ -0,0 +1,135 @@
# PRD-Review — Linse: Sicherheit + Datenschutz (DSGVO, Klinik-Kontext)
**Reviewer-Fokus:** Personenbezogene Daten in einem öffentlich erreichbaren Dienst.
**Betrachtet:** `prd.md` (Kritik-Ziel), `addendum.md` + `project-context.md` (nur als Kontext).
**Grundregel dieses Reviews:** Das technische „Wie" (Hashing, Cookie-Handling, Rate-Limit-Implementierung) gehört ins Addendum und wird hier **nicht** als PRD-Lücke gewertet. Bewertet werden Klarheit, Vollständigkeit und Umsetzbarkeit des PRD auf der Ebene *Was/Warum* — speziell, ob die Sicherheits-/Datenschutz-NFRs für einen live erreichbaren Klinik-Dienst mit Personendaten ausreichen.
**Verdict: REVISE** — Die Auth-/Isolations-NFRs (NFR-1 bis NFR-5) sind für den Login-/Session-/Enumeration-Teil solide und testbar. Aber der PRD betrachtet Datenschutz fast ausschließlich als *Datentrennung zwischen eingeloggten Nutzern*. Der eigentliche DSGVO-Kern eines Klinik-Dienstes — **die betroffenen Dritten (Mitarbeiter-Namen), das Löschkonzept, die Weitergabe an einen externen LLM und die Pflicht-Transparenz (Datenschutzerklärung/Rechtsgrundlage)** — fehlt oder ist auf „v2/offen" geschoben. Für einen bereits **live** öffentlich erreichbaren Dienst sind das keine v2-Themen.
---
## 1. Was gut ist (kein Handlungsbedarf)
- **NFR-1/NFR-3 (Token-/Session-Hashing, 32-Byte-Zufall, 15-Min-Einmal-Token):** angemessen, testbar.
- **NFR-2 (Cookie-Flags httpOnly/Secure/SameSite=Lax):** korrekt für diesen Anwendungsfall.
- **NFR-4 (neutrale Response + Rate-Limit gegen Enumeration):** die Enumeration-Klasse ist erkannt und mit FR-1 („beide Fälle 200") + `429` sauber adressiert.
- **NFR-5 (foreign_keys + CASCADE):** verhindert verwaiste Sessions/Tokens beim Nutzer-Löschen → sofortiger Zugangsentzug (FR-10) ist konsistent.
- **FR-4 (Auth-Gate auf allen Datenschnittstellen):** schließt die zentrale Schwachstelle (offen beschreibbare API) — das Kern-Ziel des Release.
Der Login-/Session-/Enumeration-Block ist gut. Die Findings betreffen die **Datenschutz-Schicht darüber**, nicht die Auth-Mechanik.
---
## 2. Findings
### F1 — CRITICAL — Betroffene Dritte (Mitarbeiter-Namen) kommen im Datenschutz-Konzept nicht vor
**Section:** §3 Glossary (Mitarbeiter) / §4.2 / Constraints (Privacy/DSGVO)
Das Datenmodell speichert **personenbezogene Daten von Personen, die keine Nutzer sind**: Ein Nutzer legt „Mitarbeiter" (echte Kollegen, benannt) an und erfasst deren Dienste + berechnet deren Bonus (UJ-2: „sieht pro Mitarbeiter die Gewinner-Variante und den Bonus"). Name + Dienstzeiten + abgeleitete Vergütung eines Dritten = klar personenbezogene Daten nach DSGVO.
Der PRD behandelt „Datenschutz" durchgängig nur als **Isolation zwischen eingeloggten Nutzern** (FR-5, FR-7, SM-2). Die eigentlichen *Betroffenen* (die Mitarbeiter) haben:
- keine Rechtsgrundlage (Art. 6),
- keinen Weg, Auskunft/Löschung zu verlangen (Art. 15/17),
- keine Information, dass sie verarbeitet werden (Art. 13/14).
**Suggestion:** Im PRD explizit benennen, dass zwei Datensubjekt-Klassen existieren (Nutzer *und* die erfassten Mitarbeiter). Mindestens: (a) Rechtsgrundlage/Zweckbindung als Constraint aufnehmen, (b) klären, ob Mitarbeiter-Namen wirklich Klarnamen sein müssen oder pseudonymisiert werden können (Datenminimierung, Art. 5), (c) das Löschkonzept (F2) muss auch die Mitarbeiter-Ebene abdecken.
---
### F2 — CRITICAL — Löschkonzept / Recht auf Vergessen auf „v2/offen" geschoben, obwohl der Dienst live ist
**Section:** §4.3/FR-10, §6.2 Out of Scope, §8.1 Open Questions, §9 Assumptions
FR-10 + §9 legen fest: Die Datenbasis eines *entfernten* Nutzers wird v1 **belassen** (nicht gelöscht). §6.2 schiebt automatisches Löschen nach v2, §8.1 markiert es als offene Frage („DSGVO/Recht auf Vergessen"). Für einen **bereits öffentlich live** laufenden Dienst mit Personendaten ist das kein v2-Thema: Art. 17 DSGVO ist eine Betriebspflicht ab Tag 1, nicht ein Feature-Wunsch.
Konkret entsteht ein Zustand, in dem personenbezogene Daten (des entfernten Nutzers **und** der von ihm erfassten Mitarbeiter, siehe F1) unbegrenzt und ohne definierten Löschpfad im System bleiben.
**Suggestion:** Für v1 mindestens einen **manuellen, dokumentierten Löschpfad** in Scope nehmen (Admin kann Datenbasis eines entfernten Nutzers hart löschen; oder Export + Löschung). Es muss keine Automatik sein — aber „gar kein Weg" ist für einen Live-Dienst nicht vertretbar. §8.1 von „offen" zu einer Entscheidung machen, bevor Architecture startet.
---
### F3 — HIGH — Weitergabe von Klinik-Dienstplänen an externen LLM (OpenRouter) ohne DSGVO-Behandlung
**Section:** §4.5 Foto-Import (bestehend), Constraints (Privacy/DSGVO)
Der Foto-Import schickt ein Foto des **Papier-Dienstplans** (enthält Kollegen-Namen + komplette Dienstzeiten = Personendaten mehrerer Betroffener) an einen **externen Vision-LLM-Dienst (OpenRouter)**. Der PRD führt das als „bestehend, unverändert" und schiebt in §6.2 nur den *Wechsel* auf einen lokalen KI-Server nach „Stufe 3".
Datenschutzrechtlich ist das eine Auftragsverarbeitung / potenzielle Drittland-Übermittlung von Personendaten. Der PRD adressiert weder AVV, noch Zweck/Einwilligung, noch was mit dem hochgeladenen Bild beim Anbieter passiert. NFR-8 („KI nie im kritischen Pfad") behandelt nur *Verfügbarkeit*, nicht *Datenschutz*.
**Suggestion:** Im PRD (Constraints oder Open Questions) als Datenschutz-Risiko explizit benennen, auch wenn die Funktion technisch unverändert bleibt. Mindestens ein Hinweis/Consent im UI vor dem Upload und eine Entscheidung, ob Klinik-Roster überhaupt an einen externen US-LLM gehen dürfen — sonst ist die „bestehende" Funktion im nun multi-user, öffentlichen Kontext ein blinder Fleck.
---
### F4 — HIGH — Keine Datenschutzerklärung / Rechtsgrundlage / Transparenz für den öffentlichen Dienst
**Section:** §6 MVP Scope (Release-Hygiene), Constraints
`bonus.pixel-by-design.de` ist ein **öffentlich erreichbarer** Dienst, der Personendaten verarbeitet. Der PRD listet unter Release-Hygiene README/CHANGELOG/Version-Tag, aber **keine Datenschutzerklärung, kein Impressum, keine Angabe der Rechtsgrundlage** (Art. 13 DSGVO Informationspflicht; §5 TMG/DDG Impressumspflicht). Für einen Dienst, der ab v1 „team-tauglich" beworben und von Kollegen genutzt wird, ist das eine harte Lücke.
**Suggestion:** Datenschutzerklärung + Impressum als v1-Release-Artefakt in §6.1 aufnehmen (Pflicht, nicht optional). Rechtsgrundlage (vermutlich berechtigtes Interesse / Einwilligung) als Constraint festhalten.
---
### F5 — HIGH — 30-Tage-Session auf dem geteilten Klinik-Rechner ohne Idle-/Re-Auth-Timeout
**Section:** §2.3 UJ-4, FR-3 (SESSION_TTL_DAYS=30), §8.4 Open Questions
UJ-4 beschreibt explizit den **geteilten Praxis-Rechner im Stationszimmer**. Die Isolation (FR-7) greift nur beim *aktiven Nutzerwechsel* (anderer Login) oder *expliziten Logout*. Passiert weder — Nutzer A geht einfach weg — bleibt A's Session bis zu **30 Tage** aktiv und httpOnly-Cookie-gebunden am Klinik-Terminal. Jeder, der danach den Browser öffnet, ist als A eingeloggt und sieht A's Personendaten.
Der PRD hat keinen Idle-Timeout / keine absolute Re-Auth für den Shared-Device-Fall. §8.4 fragt nur allgemein nach der Sitzungsdauer, ohne das Shared-Terminal-Risiko zu benennen. SM-C1 verbietet zwar, Sicherheit für Komfort zu senken — aber der Default 30 Tage *ist* bereits die komfort-optimierte Wahl für genau das Risiko-Szenario aus UJ-4.
**Suggestion:** Für den Shared-Device-Fall ein Idle-Timeout (z. B. Session-Ablauf nach X Stunden Inaktivität) oder einen deutlich sichtbaren, obligatorischen Logout-Flow fordern. §8.4 um das konkrete Shared-Terminal-Risiko erweitern, damit die Sitzungsdauer-Entscheidung informiert getroffen wird.
---
### F6 — MEDIUM — SM-2 („0 Leak-Vorfälle") ist ohne Audit-/Nachweis-Anforderung nicht messbar
**Section:** §7 SM-2, NFRs
SM-2 („0 Vorfälle, in denen ein Nutzer fremde Daten sieht") ist die **primäre Sicherheits-Metrik**, aber es gibt keine NFR/FR für **Zugriffs-Logging, Anomalie-Erkennung oder Nachvollziehbarkeit**. Ohne serverseitiges Audit-Log kann ein Leck weder erkannt noch die Metrik validiert werden — SM-2 wäre nur „uns ist nichts aufgefallen". Für einen Klinik-Dienst ist Nachvollziehbarkeit (wer hat wann auf welche Datenbasis zugegriffen) auch DSGVO-relevant (Rechenschaftspflicht, Art. 5 Abs. 2) und Voraussetzung für die Meldepflicht bei Datenpannen (F7).
**Suggestion:** Eine NFR für minimales, datensparsames Zugriffs-/Auth-Audit-Log ergänzen (Login, Logout, Admin-Aktionen, 401/403-Häufungen), damit SM-2 überhaupt prüfbar wird. Kein PII in den Logs (siehe auch F8).
---
### F7 — MEDIUM — Kein Datenpannen-/Breach-Prozess (Art. 33/34 DSGVO)
**Section:** §8 Open Questions, Constraints
Der PRD nennt Backups (NFR-6) und Zugangsentzug, aber keinen **Incident-/Meldeprozess** für den Fall eines Datenlecks. Bei Personendaten fordert Art. 33/34 DSGVO die Meldung binnen 72 h. Für einen öffentlich erreichbaren Dienst, dessen Kern-Metrik „0 Leaks" ist, gehört zumindest ein Verweis darauf, wer im Leak-Fall was tut, in die Constraints oder Open Questions.
**Suggestion:** Als Open Question / Constraint aufnehmen: Wer ist Verantwortlicher, wie wird eine Datenpanne erkannt (siehe F6) und gemeldet.
---
### F8 — MEDIUM — Roh-Login-Token im URL-Query-String landet in Access-/Proxy-Logs
**Section:** NFR-1, Constraints („Keine personenbezogenen Daten in URLs/Query-Strings")
NFR-1 sagt zu Recht: Rohwert existiert „ausschließlich im Link bzw. Cookie". Der Login-Link ist aber `GET /auth?token=…` (Addendum) — der **Roh-Token steht im URL-Query-String**. Damit landet er in Caddy-Access-Logs, evtl. Proxy-/CDN-Logs, Browser-History und `Referer`-Headern. Der Constraint des PRD selbst verlangt „keine … Daten in URLs/Query-Strings" — ein Bearer-Credential im Query-String verletzt den Geist dieser Regel, auch wenn der Token nur 15 Min gültig ist.
Das ist kein reines Addendum-Detail, weil der PRD einen **expliziten Constraint** dazu aufstellt, den das eigene Design bricht. Der Constraint sollte präzisiert werden (gilt er auch für kurzlebige Credentials? Müssen Access-Logs Token maskieren?).
**Suggestion:** Constraint schärfen: Access-/Proxy-Logs dürfen Login-Token nicht im Klartext protokollieren (Masking), oder der Constraint wird explizit auf „Roh-Token im Link ist eine bewusste, akzeptierte Ausnahme" reduziert. Entscheidung im PRD festhalten, nicht offen lassen.
---
### F9 — LOW — Admin ist alleiniger Single Point of Full Control, ohne besonderen Schutz
**Section:** §3 Glossary (Admin), FR-8..FR-11, §8.5
Der Admin (per `ADMIN_EMAIL`) steuert die gesamte Allowlist und hat eine eigene Datenbasis. Da es keine Passwörter/2FA gibt (bewusst), ist der Zugang zum Admin-Postfach der einzige Schutz der kompletten Zugangskontrolle. Kompromittiertes Admin-Postfach = vollständige Übernahme (neue Nutzer freischalten, Zugangsentzug). §8.5 fragt nur nach *mehreren* Admins, nicht nach dem *Schutz* des einen.
**Suggestion:** Als Risiko/Constraint benennen. Nichts Schweres nötig, aber die Abhängigkeit „Sicherheit des gesamten Systems = Sicherheit eines E-Mail-Postfachs" sollte bewusst akzeptiert und dokumentiert sein.
---
### F10 — LOW — Keine Retention-/Aufbewahrungsgrenze (Storage Limitation, Art. 5)
**Section:** NFR-6, §4.2, Addendum `history`
Dienste- und `history`-Daten wachsen unbegrenzt; es gibt keine Aufbewahrungs-/Purge-Regel. DSGVO Art. 5 (Speicherbegrenzung) verlangt, Personendaten nicht länger als nötig zu halten. Für v1 vermutlich unkritisch (kleines Team), aber sollte als bewusste Entscheidung stehen, nicht implizit „für immer".
**Suggestion:** Als Open Question aufnehmen: Wie lange werden Dienst-/History-Daten aufbewahrt.
---
## 3. Zusammenfassung
Die Auth-Mechanik (Magic-Link, Session-Hashing, Enumeration-Neutralität, Rate-Limit, CASCADE) ist für dieses Projekt gut und testbar spezifiziert — der Login-Teil braucht keine Nacharbeit. Die Lücken liegen eine Ebene höher, im **Datenschutz-Konzept für einen Live-Klinik-Dienst**:
- **Blocker für „live mit Personendaten":** Löschkonzept (F2), betroffene Dritte (F1), fehlende Datenschutzerklärung/Rechtsgrundlage (F4), externer LLM (F3).
- **Betriebsrisiken:** Shared-Terminal-Session (F5), fehlendes Audit → SM-2 nicht messbar (F6), kein Breach-Prozess (F7), Token im Query-String vs. eigener Constraint (F8).
Empfehlung: F1F4 vor dem Start der Architecture-Phase entscheiden (mind. manueller Löschpfad + Datenschutzerklärung in v1-Scope), F5F8 als NFR/Open-Question nachziehen.

View file

@ -0,0 +1,54 @@
# Project Context — Dienstplan-Pro
*Lean, LLM-optimized brownfield context. Loaded as a persistent fact by BMAD workflows.*
## What it is
Deutschsprachige Progressive Web App (PWA) zur Berechnung von **Bonuszahlungen für Wochenend- und Feiertagsdienste** nach NRW-Regelung (Psychiatrie 2011). Live unter `https://bonus.pixel-by-design.de`. Sprache durchgehend Deutsch (UI, Doku, Kommentare); Variablen-/Funktionsnamen Englisch.
## Tech-Stack
- **Frontend:** Vanilla JS (ES6-Klassen an `window`), HTML5, CSS3, LocalStorage. Kein Build-Step, kein Framework.
- **Backend (seit „Stufe 1"):** Node 20 (CommonJS) + Express 4 + `better-sqlite3` 11 (synchron, WAL). Ein Prozess serviert statisches Frontend **und** `/api/*`.
- **Tests:** `node --test` (kein Framework). Alt-Artefakte `test.html`/`test-suite.js` (Browser) noch vorhanden, teils veraltet.
- **Deployment:** Docker (`node:20-slim`) hinter Caddy (Let's Encrypt TLS) auf Hetzner; SQLite-DB + Backups auf Docker-Volume `dienstplan-data` (`/data`). Server zieht Code von **GitHub** (`Kenearos/Dienstplan-Pro`), nicht vom lokalen Forgejo-Origin. Deploy-Key: `~/.ssh/id_ed25519_hetzner`. Service-Worker mit `skipWaiting`+`clients.claim` (self-updating).
## Kernmodule (Frontend)
| Datei | Klasse/Rolle |
|---|---|
| `app.js` | `DienstplanApp` — UI-Orchestrierung, Event-Handling, Bootstrap (`DOMContentLoaded``DataSync.boot()``new DienstplanApp()``imageImporter`) |
| `calculator.js` | `BonusCalculator` — orchestriert Tag-Klassifizierung + 3 Varianten, wählt Gewinner |
| `variants.js` | reine Funktionen: `classify` (Slot fr/sa/so/weekday inkl. Feiertags-Verschiebung), `variant1/2/3` |
| `holidays.js` | `HolidayProvider` — NRW-Feiertage 20252030 (handgepflegt) |
| `storage.js` | `DataStorage` — synchron, LocalStorage; Keys `dienstplan_employees|_duties|_vacation`, plus gerätelokaler OpenRouter-Key/Modell |
| `sync.js` | `DataSync` — hält LocalStorage (Working-Copy) und Server-DB synchron; `pending`-Flag schützt Offline-Änderungen; last-write-wins auf ganzen Dokumenten |
| `image-import.js` | `ImageImporter` — Foto→Dienste via OpenRouter Vision-LLM; Namens-Matching, Monat aus Live-Dropdown |
## Backend (`server/`)
- `db.js` — better-sqlite3, WAL + `synchronous=NORMAL`; Tabellen `documents(key PK, value, updated_at)` + `history`; `getDoc(key)`/`putDoc(key,value,now)` (putDoc snapshottet Alt-Wert in history).
- `index.js` — Express; `GET /api/health`, `GET/PUT /api/state` (liest/schreibt die 3 Dokumente employees/duties/vacation), statisches Frontend.
- `backup.js` — tägliches SQLite-Online-Backup (`db.backup()`), letzte 14.
## Geschäftslogik (verifiziert korrekt)
- **Sätze:** Wochenend-Slot (fr/sa/so) = 450 €, Werktag = 250 €. Dienst-Anteil 1,0 oder 0,5.
- **Tag-Klassifizierung:** echte Fr/Sa/So gewinnen immer; MoDo: Feiertag→so, Tag-vor-Feiertag→fr, Sandwich→sa.
- **3 Varianten** (V1 fr+so-Pool+Werktage, V2 sa+Werktage, V3 Wochenend-Pool) mit eigenen Schwellen/Abzügen (Freitag-Priorität); Rechner nimmt die mit dem höchsten Bonus. Urlaubsmodus halbiert Schwellen+Abzüge.
## Datenmodell (aktuell, Single-User)
LocalStorage-Working-Copy ↔ Server-Dokumente. `documents` global (kein Nutzerbezug). Datum als ISO-String, intern `T12:00:00` gegen Zeitzonen-Kanten.
## Constraints / Gotchas
- `better-sqlite3` ist **synchron** — kein `db.run()` (das ist node-sqlite3), immer `db.prepare().run()`/`db.exec()`.
- SQLite `foreign_keys` ist **standardmäßig AUS** — für CASCADE explizit `PRAGMA foreign_keys=ON`.
- SQLite kann **PK nicht per ALTER ändern** → Table-Rebuild.
- Deploy ohne Volume `dienstplan-data` = Datenverlust.
- CI-Pipeline (`.github/workflows/ci.yml`) mit OPA/Rego-Policy-Gates (`secrets_in_env`, `deploy_rules`, `image_provenance` …) aus einem Org-Standard.
## Reifegrad
„Stufe 1" (Server-Persistenz + Backup + Sync) ist gebaut, getestet, live. Bild-Import funktioniert (Juni-Erkennung + Berechnung gegen Handrechnung verifiziert). Aktuell **öffentlich ohne Login** — das ist der zentrale Punkt des v1.0-Team-Release.