E-mail notification digests #95
Labels
No Label
area:auth
area:docs
area:export
area:ops
area:storage
area:supply-chain
auth
backend
blocked
collab
deployment
docs
effort:L
effort:M
effort:S
frontend
plugins
qa
vs-nfd
vs-nfd:blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: stwaidele/dorfteich#95
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Users who are not logged in daily still learn about changes — batched, never mail-per-edit.
Scope
Digest job (scheduler): per user with unread notifications older than the batch window (default 1 h, user setting: immediate-ish hourly | daily | off), one localized mail summarizing grouped notifications (per pond → per page, actor and counts) via the outbox; unsubscribe link (signed, no login required) landing on the notification settings; delivery marks notifications as mailed (not read).
Acceptance criteria
Technical notes
mail_outbox), ADR 0007 (signed tokens), ADR 0012 (mail i18n).Dependencies
Depends on #12, #94.
Size: ~1.5 days
Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add
deanden), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.Implemented in
9674c0b+9ee9bbe(pipeline green, 8/8; migration20260712010000_digest_frequencyauto-applied on Test + Int).User setting —
digestFrequency(hourlydefault |daily|off) on the profile PATCH, in/auth/me, and as a select next to the watch preferences in the account settings.Digest job — a new
notification-digestmaintenance job (15 min cadence, visible and manually triggerable in the #86 system panel) runsDigestService.runOnce(now): for every user with unread, not-yet-mailed notifications whose oldest entry exceeds their cadence window, it sends exactly one localized mail (user's locale, de/en, texts in the sharedmailscatalog) via the mail outbox — grouped per pond → per page with change/comment counts and actor names, never bodies. Sending stamps the whole batchmailed_at— neverread_at.Permission re-check at send time — every entry is re-checked against page read permission (one lookup per distinct page); entries the user can no longer read are dropped from the mail but still marked handled, so revoked content cannot queue forever. If nothing readable remains, no mail goes out at all.
Unsubscribe — every digest carries a signed, single-purpose link (
HMAC(purpose ∥ payload), 90 d expiry, ADR 0007 pattern):GET /notifications/unsubscribe?token=…verifies in constant time, flips the setting tooff, and renders a session-free, localized confirmation page through the shared HTML shell — no cookie is ever set, and the token grants nothing else. Tampered/garbage/absent tokens → 400.Acceptance criteria (in
digest.e2e.db.test.ts, 4 tests):mailed).runOnce(now + 25 h)(time travel via the injectable clock) → one mail.set-cookieon the response, setting flips to off; tampered and garbage tokens rejected.Follow-up commit
9ee9bbe: the system-panel pack's hard-coded job count now includes the fifth job.