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

8.6 KiB
Raw Blame History

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] #{{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 = <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: 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.