# 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] #{{woid}} {{kunde}} – {{anzahl_commits}} Commit(s)` (Team-Betreffe tragen ein `[…]`-Präfix, siehe test/mailTemplates.test.js) - 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}}` (Text, je Zeile Kurz-SHA + Titel, max. 10 Einträge + „… und N weitere“; Platzhalter werden HTML-escaped, daher ohne Links) - 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. ## 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: einfacher In-Memory-Zähler, 30 Anfragen / 15 min je IP für `/zeit` (danach HTTP 429). 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.