Files
Webklar-Kundenbereich/docs/superpowers/specs/2026-09-30-push-zeitmail-design.md
T

164 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<token>?min=<minuten>`: eine
Bestätigungsseite mit vorausgefüllter Dauer.
5. Klick auf „Speichern“ sendet `POST /zeit/<token>` (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 = `<payload>.<signatur>`.
- 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.