From 6f4be30c18ca0d1a7aa5714218ec6f68240d8a7e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 13:20:52 +0000 Subject: [PATCH] =?UTF-8?q?Spec:=20Push-Zeitmail=20(Zeit=20f=C3=BCr=20Git-?= =?UTF-8?q?Pushes=20per=20E-Mail=20eintragen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-09-30-push-zeitmail-design.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-30-push-zeitmail-design.md diff --git a/docs/superpowers/specs/2026-09-30-push-zeitmail-design.md b/docs/superpowers/specs/2026-09-30-push-zeitmail-design.md new file mode 100644 index 0000000..6797dad --- /dev/null +++ b/docs/superpowers/specs/2026-09-30-push-zeitmail-design.md @@ -0,0 +1,163 @@ +# Push-Zeitmail: Zeit für Git-Pushes per E-Mail eintragen + +Stand: 2026-09-30 · Status: Entwurf zur Freigabe + +## Ziel + +Nach jedem Git-Push auf ein Projekt mit Ticket bekommt die pushende Person eine E-Mail, damit das +Nachtragen der Arbeitszeit nicht vergessen wird. Die Mail enthält alle Infos zu Ticket und Push und +bietet Schnell-Knöpfe, mit denen die Zeit ohne Login eingetragen wird, sowie einen Knopf +„Ticket öffnen“ (WOMS). + +Erfolg: Kein Git-Push bleibt mehr ohne Zeit („orange“) liegen, weil das Eintragen in der Mail +zwei Klicks kostet. + +## Entscheidungen (mit dem Nutzer abgestimmt) + +- Mailprogramme im Team sind gemischt → **keine Formulare in der Mail**, sondern Links + (Schnell-Knöpfe), die in jedem Mailprogramm funktionieren. +- **Eine Mail pro Push** (keine Bündelung, keine Tagesmail). +- **Keine Erinnerung** nach 24 h (später ergänzbar). +- Umsetzung komplett im **Kundenportal-Server**; WOMS bleibt unverändert. + +## Ablauf + +1. Gitea-Webhook (`server/routes/webhook/gitea.js`, Phase 5) ruft `createGitPushWorksheet` auf. +2. Nur wenn das Ergebnis `created: true` ist (also nicht bei System-Pusher, Duplikat oder fehlendem + Ticket), wird `sendPushTimeMail(...)` aufgerufen – **nach** der Projekt-Sperre, ohne `await` auf + das Webhook-Ergebnis (fire-and-forget mit `.catch` → `console.error`). Ein Mailfehler ändert die + Webhook-Antwort nicht. +3. Die Mail geht an die zugeordnete Person (siehe Empfänger). +4. Klick auf einen Zeit-Knopf öffnet `GET {publicBaseUrl}/zeit/?min=`: eine + Bestätigungsseite mit vorausgefüllter Dauer. +5. Klick auf „Speichern“ sendet `POST /zeit/` (Formular, `minutes`) → Arbeitsblatt wird + aktualisiert → Erfolgsseite. + +Der GET speichert **nie** etwas: Virenscanner und Link-Vorschauen öffnen Mail-Links automatisch. + +## Empfänger + +Neue Funktion `findEmployeeForPusher(pusher)` in `server/services/pushTimeMail.js`: + +- Kandidatennamen aus dem Webhook: `pusher.username`, `pusher.login`, `pusher.full_name`. +- Vergleich (ohne Groß-/Kleinschreibung, getrimmt) gegen jedes `employees`-Dokument: `shortcode` + und `displayName`. Das ist dieselbe Regel wie `canEditWorksheetTimes` in WOMS + (knso → KNSO → Kenso Grimm). +- Treffer mit leerer `email` oder kein Treffer → keine Mail, `console.warn` mit Pusher-Namen. +- Mehrere Treffer → erster Treffer mit E-Mail (Reihenfolge nach `$createdAt`). + +## Mail + +Neue Team-Vorlage `server/mail-templates/T15 Zeit eintragen.html` im Stil der bestehenden +T-Vorlagen (Kopfkommentar, Platzhalter, Dark-Mode-Regeln), gerendert mit `renderMail('T15', …)` +und versendet über den bestehenden Mailer (neue Exportfunktion `sendStaffMail(to, message)` in +`mailer.js`, gleicher Transport/Absender wie die übrigen Team-Mails). + +- Betreff: `Zeit eintragen: #{{woid}} {{kunde}} – {{anzahl_commits}} Commit(s)` +- Platzhalter: + - `{{vorname}}`: Name des Mitarbeiters + - Ticket: `{{woid}}`, `{{kunde}}`, `{{ticket_titel}}`, `{{ticket_beschreibung}}` (auf + 300 Zeichen gekürzt), `{{ticket_status}}` (deutsche Bezeichnung) + - Push: `{{repo}}`, `{{branch}}`, `{{push_zeit}}` (dd.mm.yyyy hh:mm Berlin), `{{commits}}` + (Liste: Kurz-SHA + Titel, jeweils auf Gitea verlinkt; HTML-sicher erzeugt, max. 10 Einträge + + „… und N weitere“) + - Knöpfe: `{{link_15}}`, `{{link_30}}`, `{{link_45}}`, `{{link_60}}`, `{{link_90}}`, + `{{link_120}}`, `{{link_180}}`, `{{link_andere}}` (ohne `min`, Feld leer) + - `{{ticket_link}}` = `https://ticket.webklar.com/tickets?woid={{woid}}` (bestehender Deep-Link) +- Knopfbeschriftung: 15 Min · 30 Min · 45 Min · 1 Std · 1,5 Std · 2 Std · 3 Std · Andere Zeit; + darunter ein zweiter, schlichter Knopf „Ticket öffnen“. +- Textfassung (für Mailprogramme ohne HTML) listet die Links als Zeilen. + +Falls `renderMail` für Listen-Platzhalter kein HTML zulässt, wird die Commit-Liste als Text mit +Zeilenumbrüchen übergeben (wie `{{bericht}}` in T14). + +## Token + +Zustandslos und signiert, damit Links einen Neustart überleben: + +- Inhalt: `{ w: worksheetId, e: employeeDocId, x: ablaufUnixSekunden }`, base64url-JSON. +- Signatur: HMAC-SHA256 mit `PUSH_TIME_SECRET` (neue Umgebungsvariable, min. 32 Zeichen; Fallback + `SESSION_SECRET` wie bei `PREVIEW_SESSION_SECRET`). Token = `.`. +- Gültigkeit: 14 Tage. +- Prüfung mit `timingSafeEqual`; ungültig/abgelaufen/manipuliert → Seite „Link ungültig oder + abgelaufen“ + Link zu WOMS, HTTP 400/410. +- Der Token berechtigt ausschließlich dazu, die Zeit genau dieses Arbeitsblatts einzutragen. Kein + Login, keine Session, keine anderen Daten → die Mitarbeiter-2FA wird nicht umgangen. +- „Einmaligkeit“ entsteht über den Datenzustand: Hat das Arbeitsblatt schon eine Zeit, wird nicht + gespeichert (siehe unten). + +## Bestätigungsseite (`server/routes/pushTime.js`) + +Server-gerendertes HTML (kein SPA), WEBklar-Look (Farben/Schrift wie die Mailvorlagen), mobil +tauglich, `Cache-Control: no-store`, `X-Robots-Tag: noindex`, `Referrer-Policy: no-referrer`. + +GET `/zeit/:token`: +- Token prüfen, Arbeitsblatt laden. Fehlt es → „Nicht mehr vorhanden“. +- Arbeitsblatt muss `serviceType === 'GIT'` sein, sonst ungültig. +- Schon erledigt (`needsTimeEntry` falsch, d. h. `startTime` gesetzt und `totalTime > 0`) → + „Schon eingetragen: 45 Min“ + „Ticket öffnen“. +- Sonst: Ticketnummer, Kunde, Repo, Push-Zeit, Commit-Titel; Zahlenfeld „Minuten“ (ganze Minuten, + 1–720, vorausgefüllt aus `?min=`), Knopf „Speichern“, Link „Ticket öffnen“. +- Kein CSRF-Token nötig: Der POST wirkt nur mit dem geheimen Token in der URL und kann nur dieses + eine Arbeitsblatt ausfüllen. + +POST `/zeit/:token` (urlencoded, `minutes`): +- Token prüfen, Minuten validieren (ganze Zahl 1–720, sonst Seite mit Fehlermeldung). +- Unter `withLocks(['ws-time:' + worksheetId])`: Arbeitsblatt neu laden; schon erledigt → + „Schon eingetragen“ (kein Überschreiben). +- Update wie „Zeit nachtragen“ in WOMS: + - `startDate`/`startTime` = Endzeit (`endDate`/`endTime`) minus Minuten, Berlin-Zeit, über + Mitternacht hinweg korrekt (Datum wechselt). + - `totalTime` = Minuten, `isComment` = `false`. +- Erfolgsseite: „Eingetragen: 1 Std auf #100118“ + „Ticket öffnen“. +- Rate-Limit: bestehender Limiter-Stil, z. B. 30 Anfragen / 15 min je IP für `/zeit`. + +Hinweis Abo: Das Stundenkonto im Portal liest `totalTime` live – eine eingetragene Zeit auf einem +Abo-Ticket zählt damit sofort, genau wie beim Eintragen in WOMS. + +## Dateien + +- neu `server/services/pushTimeToken.js` – `createPushTimeToken`, `verifyPushTimeToken` +- neu `server/services/pushTimeMail.js` – `findEmployeeForPusher`, `buildPushTimeMail`, + `sendPushTimeMail` +- neu `server/services/pushTimeEntry.js` – `startFromEnd(endDate, endTime, minutes)`, + `needsTimeEntry(ws)`, `applyPushTime(worksheetId, minutes)` +- neu `server/routes/pushTime.js` – GET/POST `/zeit/:token` +- neu `server/mail-templates/T15 Zeit eintragen.html` +- geändert `server/routes/webhook/gitea.js` – Mailaufruf nach Phase 5 +- geändert `server/services/mailer.js` – `sendStaffMail` +- geändert `server/config.js` – `pushTime.secret`, `pushTime.ttlDays`, `womsBaseUrl` + (`https://ticket.webklar.com`) +- geändert Server-Einstieg – Route `/zeit` einhängen (vor dem SPA-Fallback) +- geändert `docker-compose`/`.env` – `PUSH_TIME_SECRET` (setzt der Nutzer bzw. Fallback greift) + +`createGitPushWorksheet` gibt zusätzlich die für die Mail nötigen Daten zurück (`woid`, Ticket-Doc, +Datum/Uhrzeit), damit nichts doppelt geladen wird. + +## Fehlerfälle + +| Fall | Verhalten | +|---|---| +| Pusher nicht zuordenbar / ohne E-Mail | keine Mail, `console.warn` | +| SMTP-Fehler | `console.error`, Webhook-Antwort unverändert | +| Arbeitsblatt in WOMS schon ausgefüllt | Seite „Schon eingetragen“ | +| Arbeitsblatt gelöscht | Seite „Nicht mehr vorhanden“ | +| Token manipuliert/abgelaufen | Seite „Link ungültig oder abgelaufen“ | +| Minuten ungültig | Formular erneut mit Fehlermeldung | +| Zwei Klicks gleichzeitig | Sperre je Arbeitsblatt, der zweite sieht „Schon eingetragen“ | + +## Tests + +- Unit: Token (gültig, abgelaufen, falsche Signatur, anderes Secret), `findEmployeeForPusher` + (Kürzel, Anzeigename, Groß/klein, kein Treffer, ohne E-Mail), `startFromEnd` (normal, über + Mitternacht, Monats-/Jahreswechsel), `needsTimeEntry`, Mail-Render T15 (alle Platzhalter ersetzt, + HTML-Escaping der Commit-Titel, Kürzung). +- Route: GET/POST mit gemocktem Appwrite (offen, erledigt, gelöscht, falscher Token, ungültige + Minuten, GET speichert nie). +- Webhook: Mail nur bei `created: true`, Mailfehler bricht Webhook nicht ab. +- Abnahme: ein echter Test-Push auf ein Test-Repo nach dem Livegang (mit Nutzer-OK). + +## Nicht enthalten + +Bündelung mehrerer Pushes, Erinnerungsmails, Bearbeiten schon eingetragener Zeiten per Mail, +Änderungen an WOMS.