Zum Hauptinhalt springen

Zeiterfassung — Vordergrund-Segmente & Live-Push (MED-D-275, MED-OP-TIME-4)

Kernaussage. Der Zeiterfassungs-Timer wird vom mandantengebundenen Einzeltimer zum durchlaufenden Timer je Benutzer, der mitschreibt, welcher Mandant wann im Vordergrund war (Segment-Historie). Ein Buchungs-Overlay zeigt die Aufschlüsselung je Mandant, macht sie editierbar/zuweisbar (auch an nie geöffnete Mandanten, z. B. Telefonat) und erlaubt geräteübergreifendes Stoppen/Buchen. Eine fortlaufend aktualisierte Web-Push-Benachrichtigung zeigt den offenen Fall + das Buchungsziel und führt per Klick ins Overlay. Grundlage bleibt der serverautoritative Timer (MED-D-172).

Zielgruppen: IT-Dev/Architektur (Datenmodell, API) · IT-Ops/DevOps (VAPID-Secrets, Cron) · CISO/Security (DSGVO/Push) · Anwender (Bedienlogik). True North: #2 (was ist offen/fällig — korrekte Zeitbuchung) & #3 (lückenlose, rechtssichere Doku der geleisteten Zeit).

1. Warum (Nutzer-Anforderung)

Nutzer: „Timer soll immer tracken, welche Mandanten offen waren. Bei Klick auf den Timer soll eine Seite angezeigt werden, welche Mandanten in der Erfassungszeit wie lange im Vordergrund waren. Zeiten sollen editierbar/zuweisbar sein, ggf. auch an gar nicht geöffnete Mandanten (z. B. Telefonat). Zusätzlich eine fortlaufend aktualisierte Benachrichtigung (welcher Fall offen ist / worauf gebucht wird); Klick führt zum Overlay; so kann ein Timer auch gestoppt werden, dessen Seite nicht mehr offen oder auf einem anderen Gerät offen ist."

2. Ist-Zustand (vor MED-D-275)

  • aktiver_timer = eine Zeile je Benutzer (PK benutzer_email), genau eine mandant_id, basis_ms + segment_start_am (nur Pause/Fortsetzen desselben Mandanten). Keine Vordergrund-Historie.
  • Öffnen eines anderen Mandanten → Konflikt (HTTP 409, starten verweigert das Überschreiben). Der Timer „wandert" nicht mit.
  • Keine Web-Push-/Service-Worker-/PWA-Infrastruktur. Cron existiert (stündlich, wrangler.toml).

3. Entscheidungen

  • D-a — Durchlaufender Timer statt Konflikt. Der Timer ist ein kontinuierlicher Clock je Benutzer. Ein Vordergrund-Wechsel schließt das offene Segment und öffnet ein neues — kein Konflikt mehr. (Löst das MED-D-172-„anderer Mandant blockiert"-Verhalten ab; die Zeit geht nicht mehr verloren, sondern wird je Mandant protokolliert.)
  • D-b — Segmente als neue Entität, aktiver_timer migrationsschonend unverändert. aktiver_timer bleibt der Master-Clock (basis_ms/segment_start_am = Gesamt-Pause/Resume; mandant_id = aktueller/ letzter Vordergrund-Mandant, weiterhin NOT NULL → keine riskante NULL-Migration ephemerer Laufzeit-Timer). Die Segment-Historie lebt in timer_segment (nullable mandant_id = „nicht zugeordnet").
  • D-c — „Nicht zugeordnet" ist ein First-class-Bucket. Zeit ohne Vordergrund-Mandant (Cockpit, weg, Telefonat) = Segment mit mandant_id = NULL. Im Overlay manuell einem beliebigen Mandanten zuweisbar.
  • D-d — Buchen = Aufteilen in mehrere leistungszeit-Zeilen. Beim Buchen aggregiert der Server die (ggf. editierten) Segmente je Mandant und schreibt je Mandant eine append-only leistungszeit-Zeile (G-4). Nicht zugeordnete Rest-Zeit muss vor dem Buchen zugewiesen oder verworfen werden.
  • D-e — Web-Push realistisch (kein Sekunden-Tick). Eine Notification wird bei Vordergrund-Wechsel und periodisch per Cron neu gesendet (gleicher tag → ersetzt). Inhalt PII-arm (Fall-Kurzname/ID, kein Klarname), Klick → Buchungs-Overlay. Geräteübergreifendes Stoppen folgt aus dem serverautoritativen Timer (die Aktion läuft server-seitig, unabhängig vom Ursprungsgerät/-Tab).

4. Datenmodell (neu)

timer_segment push_subscription
id TEXT PK (ULID) id TEXT PK (ULID)
benutzer_email TEXT (Ref R7) benutzer_email TEXT (Ref R7)
mandant_id TEXT NULL (Ref R1; endpoint TEXT UNIQUE
NULL = nicht zugeordnet) p256dh TEXT (Public-Key des Clients)
von_ms INTEGER auth TEXT (Auth-Secret des Clients)
bis_ms INTEGER NULL angelegt_am INTEGER
(NULL = offenes Segment) letzte_nutzung INTEGER
quelle TEXT ('vordergrund'|
'manuell')
angelegt_am INTEGER
  • Invariante: je Benutzer höchstens ein offenes Segment (bis_ms IS NULL) — es ist der aktuelle Vordergrund. Wechsel/Pause schließt es (bis_ms = jetzt), Öffnen/Fortsetzen legt ein neues an. Hart durchgesetzt über einen partiellen UNIQUE-Index (WHERE bis_ms IS NULL, Migration v52, MED-D-276) → auch bei gleichzeitigen Vordergrund-Wechseln bleibt es bei einem offenen Segment.
  • Abgeleitet, nie doppelt persistiert (G-2): Dauer je Mandant = Σ ((bis_ms ?? jetzt) − von_ms) je mandant_id — das offene Segment (bis_ms IS NULL) zählt bis jetzt; Gesamt = aktiver_timer-Clock. Rundung bleibt Client-seitig (wie MED-D-172).
  • Push-Subscription PII-arm (G-6, MED-D-276): kein user_agent — für die Zustellung genügen Endpoint
    • Krypto-Keys; Geräte-Metadaten werden gar nicht erst gespeichert (Datensparsamkeit).
  • Data-Dictionary (G-8): neue Felder zuerst ins Weltmodell (Weltmodell-Data-Dictionary.md).

5. API (neu/geändert)

RouteZweck
POST /api/timer/vordergrund {mandantId|null}Vordergrund-Wechsel: offenes Segment schließen, neues öffnen (kein Konflikt). Ersetzt die 409-Semantik von start. Nur explizit null oder ein nicht-leerer String — malformte Werte → 400 (MED-D-276).
GET /api/timer/aufschluesselungSegment-Aufschlüsselung je Mandant + „nicht zugeordnet" + Gesamt (abgeleitet).
POST /api/timer/buchen-verteilt {zeilen:[{mandantId,dauerMinuten,taetigkeit,abrechenbar,auftragRef}], weiterlaufen}Mehrere leistungszeit-Zeilen aus den (editierten) Segmenten; danach Segmente räumen + Timer zurücksetzen/beenden. Geräteübergreifend (kein mandantId-Guard des Ursprungs).
POST /api/timer/buchen (einzeln)Direkt-Buchung nur, wenn alle erfasste Zeit demselben Mandanten gehört; verteilt sich Zeit über mehrere Mandanten → 409 {verteilen:true} (MED-D-276) → Client leitet ins verteilte Overlay (keine Fehlzuordnung).
POST /api/push/subscribe / POST /api/push/unsubscribeWeb-Push-Subscription je Gerät registrieren/entfernen (PII-arm). subscribe validiert den Endpoint (HTTPS + Provider-Allowlist, Egress-Kontrolle); unsubscribe user-scoped (MED-D-276).
(intern) Push-Senden bei Vordergrund-Wechsel + Stopp-Übergängen + CronVAPID/WebCrypto; Notification-Body PII-arm; nie im Antwortpfad geawaitet (waitUntil, 5-s-Fetch-Timeout, MED-D-276).
  • RBAC: mandant_bearbeiten je Zielmandant beim Buchen (unverändert). Vordergrund-Meldung an einen Mandanten, den man nicht sehen darf → abgelehnt. Audit (G-4): die Buchung (leistungszeit.erfasst) bleibt auditiert; Segment-Wechsel bleiben PII-arm/unauditiert (kein Sekunden-Rauschen, wie Timer-Ticks).

6. Web-Push-Infrastruktur (neu)

  • Client: schlanker, handgeschriebener /sw.js + manifest.webmanifest (PWA) — kein @angular/service-worker/ngsw-config.json (bewusst, um Build-/Caching-Komplexität zu vermeiden). Der Service Worker behandelt push (Notification anzeigen/ersetzen per tag; bei gestopptem Timer die bestehende Notification schließen, MED-D-276) und notificationclick (→ Buchungs-Overlay). Permission-Flow opt-in (Nutzer entscheidet aktiv; ohne Erlaubnis läuft alles weiter, nur ohne Push).
  • Server: daten-loses Web-Push, VAPID-JWT via WebCrypto (ES256, analog DocuSign-JWT) — kein Node-web-push (nicht Workers-kompatibel), kein aes128gcm; über die Gateways geht keinerlei PII. Secrets: VAPID_PRIVATE_KEY via wrangler secret put; VAPID_PUBLIC_KEY als [vars] (an den Client ausgeliefert). Dormant ohne Secret (Feature-Flag: ohne VAPID kein Push, App unverändert nutzbar — analog DocuSign-Adapter). Zustellung mit 5-s-Timeout, nie im Antwortpfad geawaitet (waitUntil); abgelaufene Subscriptions (404/410) werden gelöscht.
  • Trigger: Vordergrund-Wechsel + Stopp-Übergänge (Pause/Korrektur/Verwerfen/Buchen) (Event) + Cron (periodische Auffrischung). Kein Sekunden-Tick (Web-Grenze, D-e).

6a. Härtung MED-D-276 (CodeRabbit-Nachlese zu #313, in-PR vor Merge)

  • Segment-Konsistenz aller Timer-Aktionen: starten öffnet ein Segment; buchen/verwerfen räumen die Segmente; korrigieren schließt das offene Segment. So bleibt die (segment-abgeleitete) Aufschlüsselung konsistent — keine verlorene Anfangszeit, keine Doppelzählung.
  • Keine Fehlzuordnung (Direkt-Buchung): verteilt sich Zeit über mehrere Mandanten, verweigert die Einzel-Buchung (verteilen_noetig → 409 {verteilen:true}) und der Client führt ins verteilte Overlay.
  • Push als Egress-Kontrolle: Endpoint-Allowlist (HTTPS + FCM/Mozilla/Apple/WNS), unsubscribe user-scoped, kein user_agent mehr (G-6), non-blocking Zustellung mit Timeout.
  • Rest-Zeit-Schutz (D-d-Schärfung): das verteilte Overlay zeigt alle erfasste Zeit inkl. „nicht zugeordnet"; erst die bewusste Zuordnung/Buchung räumt die Segmente. Bis zum server-seitigen Remainder-Guard bleibt die zugehörige Feature-Zeile bewusst als teilweise (🟡) geführt.
  • Nebenläufigkeit Buchung ↔ Vordergrund-Wechsel (Runde 3, MED-OP-TIME-5): die Buchung beansprucht den Timer atomar (claim-by-delete, keine Doppelbuchung) und räumt danach nur die zum Lesezeitpunkt vorhandenen Segmente (loescheTimerSegmenteMitIds); der Timer wird nur neu angelegt, wenn nicht schon ein nebenläufiger Vordergrund-Wechsel einen angelegt hat (dessen jüngere Nutzerabsicht gewinnt). Damit kann ein gleichzeitiger geräteübergreifender Vordergrund-Wechsel weder sein Segment noch seinen Timer verlieren. Residual (dokumentiert, akzeptiert): ein Sub-Millisekunden-Fenster im „create-if-absent" bleibt (PK-Upsert, letzter gewinnt) — die vollständige Atomarität des GESAMTEN Übergangs (Claim → Leistungszeit → Segment-Räumung → Timer-Neuanlage) erfordert ein transaktionales/CAS-Repo-Primitive über mehrere Statements; als eigener Schritt geführt (MED-OP-TIME-5, kein Buchungs-/Abrechnungsfehler, nur ein selten verlierbares In-Progress-Segment, das der Nutzer neu erfassen kann). Runde 4: zusätzlich schließt der Buchungs-weiterlaufen-Pfad ein etwaiges verwaistes offenes Segment eines nebenläufigen Wechsels VOR dem Neuanlegen (sonst UNIQUE-Index-Verletzung NACH bereits geschriebener Leistungszeit); und korrigieren gleicht die Segment-Historie an den korrigierten Gesamtwert an (ein geschlossenes Segment über die korrigierte Dauer → aufschluesselung = korrigierter basisMs). Bewusst NICHT serverseitig auf die Segmentzeit gedeckelt: dauerMinuten bleibt frei editierbar (Kern-Feature — Zuweisung nie getrackter Zeit wie Telefonat). Push-Secret auth/p256dh at-rest: bei daten-losem Push ungenutzt → vor Live dropen oder verschlüsseln (analog MED-KB-6); dormant, keine Subscriptions in Prod.

7. Compliance (DE/AT/CH — proaktiv, CLAUDE.md-Pflicht)

  • DSGVO/revDSG: Push-Subscriptions (Endpoint + Keys je Gerät) sind personenbeziehbar → Einwilligung (Notification-Permission + dokumentierter Zweck/Aufbewahrung), Speicherung PII-arm (G-6), Löschung bei Abmeldung/Rollenverlust/abgelaufener Subscription (410 vom Push-Dienst → Zeile löschen).
  • Dritt-Gateways: Zustellung über Browser-Push-Dienste (Google/Mozilla/Apple/Microsoft) — Notification- Inhalt PII-arm (Fall-ID/Kurzname statt Klarname). Vermerk in Risikoregister.md + Compliance.md (neue RISK-Zeile, AVV-Hinweis). VAPID-Keys als Secrets (kein Repo-PII, G-5).
  • Aufbewahrung: Segmente sind flüchtige Arbeitsdaten bis zum Buchen (dann → leistungszeit-Ledger, GoBD-relevant); ungebuchte Segmente werden beim Buchen/Verwerfen geräumt.

8. Slices (in EINER PR — Nutzer wählte „alles zusammen")

  1. Datenmodell + Migration (timer_segment, push_subscription, v50/v51; partieller UNIQUE-Index für offene Segmente v52, MED-D-276) + Design-Doc (dieses Dok).
  2. Timer-Service: Vordergrund-Segmente, Aufschlüsselung, verteiltes Buchen; Repo (d1/memory).
  3. Server-API: Routen + Push-Senden (web-push/VAPID) + Cron; RBAC/Audit.
  4. Web-Push-Infra: Service Worker + PWA-Manifest + Subscription-Flow.
  5. Client: Buchungs-Overlay (editierbar/zuweisbar, geräteübergreifend Stop) + Vordergrund-Meldung + Permission-Flow.
  6. Tests + Doku-Kaskade (Vitest, CHANGELOG/HANDOFF/Decision-Log/Feature-/Test-Liste/Risiko/Compliance).

9. Offene Punkte

  • MED-OP-TIME-4 (Rollout dieses Features) · OP-DM-8 (realer Push-Adapter dockt an die BenachrichtigungProvider-Abstraktion) · Cron-Feinheit (stündlich zu grob für Minuten-Auffrischung — Intervall/Alarm als Folgefrage).