8.6 KiB
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
- Gitea-Webhook (
server/routes/webhook/gitea.js, Phase 5) ruftcreateGitPushWorksheetauf. - Nur wenn das Ergebnis
created: trueist (also nicht bei System-Pusher, Duplikat oder fehlendem Ticket), wirdsendPushTimeMail(...)aufgerufen – nach der Projekt-Sperre, ohneawaitauf das Webhook-Ergebnis (fire-and-forget mit.catch→console.error). Ein Mailfehler ändert die Webhook-Antwort nicht. - Die Mail geht an die zugeordnete Person (siehe Empfänger).
- Klick auf einen Zeit-Knopf öffnet
GET {publicBaseUrl}/zeit/<token>?min=<minuten>: eine Bestätigungsseite mit vorausgefüllter Dauer. - 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:shortcodeunddisplayName. Das ist dieselbe Regel wiecanEditWorksheetTimesin WOMS (knso → KNSO → Kenso Grimm). - Treffer mit leerer
emailoder kein Treffer → keine Mail,console.warnmit Pusher-Namen. - Mehrere Treffer → erster Treffer mit E-Mail (Reihenfolge nach
$createdAt).
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}}(ohnemin, 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; FallbackSESSION_SECRETwie beiPREVIEW_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 (
needsTimeEntryfalsch, d. h.startTimegesetzt undtotalTime > 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
/zeiteinhä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.