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 (PKbenutzer_email), genau einemandant_id,basis_ms+segment_start_am(nur Pause/Fortsetzen desselben Mandanten). Keine Vordergrund-Historie.- Öffnen eines anderen Mandanten → Konflikt (HTTP 409,
startenverweigert 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_timermigrationsschonend unverändert.aktiver_timerbleibt 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 intimer_segment(nullablemandant_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-onlyleistungszeit-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) jemandant_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)
| Route | Zweck |
|---|---|
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/aufschluesselung | Segment-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/unsubscribe | Web-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 + Cron | VAPID/WebCrypto; Notification-Body PII-arm; nie im Antwortpfad geawaitet (waitUntil, 5-s-Fetch-Timeout, MED-D-276). |
- RBAC:
mandant_bearbeitenje 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 behandeltpush(Notification anzeigen/ersetzen pertag; bei gestopptem Timer die bestehende Notification schließen, MED-D-276) undnotificationclick(→ 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), keinaes128gcm; über die Gateways geht keinerlei PII. Secrets:VAPID_PRIVATE_KEYviawrangler secret put;VAPID_PUBLIC_KEYals[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/verwerfenräumen die Segmente;korrigierenschließ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),
unsubscribeuser-scoped, keinuser_agentmehr (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); undkorrigierengleicht die Segment-Historie an den korrigierten Gesamtwert an (ein geschlossenes Segment über die korrigierte Dauer →aufschluesselung= korrigierterbasisMs). Bewusst NICHT serverseitig auf die Segmentzeit gedeckelt:dauerMinutenbleibt frei editierbar (Kern-Feature — Zuweisung nie getrackter Zeit wie Telefonat). Push-Secretauth/p256dhat-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")
- Datenmodell + Migration (
timer_segment,push_subscription, v50/v51; partieller UNIQUE-Index für offene Segmente v52, MED-D-276) + Design-Doc (dieses Dok). - Timer-Service: Vordergrund-Segmente, Aufschlüsselung, verteiltes Buchen; Repo (d1/memory).
- Server-API: Routen + Push-Senden (
web-push/VAPID) + Cron; RBAC/Audit. - Web-Push-Infra: Service Worker + PWA-Manifest + Subscription-Flow.
- Client: Buchungs-Overlay (editierbar/zuweisbar, geräteübergreifend Stop) + Vordergrund-Meldung + Permission-Flow.
- 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).