Spec: Push-Zeitmail (Zeit für Git-Pushes per E-Mail eintragen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7c2027d3f
commit
6f4be30c18
1 file changed
+163
@@ -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/<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.
|
||||
Reference in new issue
Block a user