Notification generation and in-app notification center #94

Closed
opened 2026-07-04 14:52:51 +02:00 by fable-5 · 1 comment
Collaborator

Context

Watchers are notified about page changes and new comments; the center makes it visible in-app.

Scope

Notification generation: on version snapshot creation (#41 — meaningful change unit, not per keystroke) and on new comments, fan out to watchers (excluding the actor) into notifications; API: list (paginated, unread first), mark read/all-read; UI: bell icon with unread badge, dropdown center (actor, page, type, time; click navigates and marks read); low-frequency polling (30 s) — no push infrastructure in v1.

Acceptance criteria

  • editing a watched page (session close) notifies the watcher, not the editor (e2e with two users)
  • comment notifications link to the page with the comments panel open
  • mark-all-read clears the badge; unread survives reload (server state)
  • notification generation respects page read permission at delivery time (revoked watcher gets nothing — test)

Technical notes

  • data-model.md §notifications, ADR 0013 (version = change unit), permissions.md.

Dependencies

Depends on #41, #91, #93.

Size: ~2 days


Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add de and en), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.

## Context Watchers are notified about page changes and new comments; the center makes it visible in-app. ## Scope Notification generation: on version snapshot creation (#41 — meaningful change unit, not per keystroke) and on new comments, fan out to watchers (excluding the actor) into `notifications`; API: list (paginated, unread first), mark read/all-read; UI: bell icon with unread badge, dropdown center (actor, page, type, time; click navigates and marks read); low-frequency polling (30 s) — no push infrastructure in v1. ## Acceptance criteria - [ ] editing a watched page (session close) notifies the watcher, not the editor (e2e with two users) - [ ] comment notifications link to the page with the comments panel open - [ ] mark-all-read clears the badge; unread survives reload (server state) - [ ] notification generation respects page read permission at delivery time (revoked watcher gets nothing — test) ## Technical notes - data-model.md §notifications, ADR 0013 (version = change unit), permissions.md. ## Dependencies Depends on #41, #91, #93. **Size**: ~2 days --- *Conventions: English code/comments, clear human-readable code, no hard-coded UI strings (ADR 0012, add `de` **and** `en`), permission checks only via the shared guard (docs/architecture/permissions.md). Read the referenced ADRs before starting.*
fable-5 added this to the M9 — Comments & notifications milestone 2026-07-04 14:52:51 +02:00
fable-5 added the
backend
frontend
labels 2026-07-04 14:52:51 +02:00
Author
Collaborator

Implemented in 67fb01f (pipeline green, 8/8; migration 20260712000000_notifications auto-applied on Test + Int).

GenerationNotificationsService.fanoutPageEvent(type, pageId, actorIds): everyone watching the page or its pond, minus all actors, and per watcher a read-permission re-check at delivery time (a revoked watcher gets nothing). Payload is denormalized (page/pond names + slugs, actor display names) so the center renders without joins. Never throws — a notification failure cannot break the write that caused it. Sources:

  • Version snapshots (#41's change unit): named snapshots hook in the api's VersionsService; the automatic session-close snapshots live in the collab server, which has no permission resolution — it now announces them over a new pg NOTIFY channel (page_version_created, the reverse direction of the established api→collab LISTEN/NOTIFY bus), consumed by a dedicated LISTEN client in the api (VersionEventListener, own pg connection since Prisma cannot hold LISTEN; auto-reconnect; inert under NODE_ENV=test).
  • New comments hook in CommentsService.create.

APIGET /notifications (paginated, unread first via nulls-first ordering, with unreadCount), POST /notifications/:id/read (owner-scoped, 404 for foreign ids), POST /notifications/read-all.

UI — bell in the top bar with an unread badge, 30 s polling (no push in v1); the dropdown lists actor/page/type/time, entries navigate and mark themselves read, plus mark-all-read. Comment notifications deep-link with ?comments=1, which the page now honors by opening the comments panel on load. New notifications namespace (de+en). mailed_at is already on the table for the #95 digests.

Acceptance criteria (in notifications.e2e.db.test.ts, 4 tests):

  • watcher notified, editor not — fan-out with the owner as actor: watcher unread=1 with the actor's name in the payload, the actor's own list stays empty. (The two-user browser flow rides the #96 pack; the collab NOTIFY path is exercised live by the CI collab pack since the listener is active in the e2e stack.)
  • comment notifications link to the page with the panel open — payload carries pond+page slugs; the bell builds …?comments=1 and the page opens the panel.
  • mark-all-read clears the badge; unread survives reload — single read flips ordering (read entries sink below unread), read-all → unreadCount 0 on a fresh request (server state).
  • delivery-time permission re-check — reader granted via API, watch set, grant revoked via API → the next fan-out produces nothing for them. Test lesson: grants must be created and revoked through the API — raw rows bypass the permission cache in both directions.
Implemented in `67fb01f` (pipeline green, 8/8; migration `20260712000000_notifications` auto-applied on Test + Int). **Generation** — `NotificationsService.fanoutPageEvent(type, pageId, actorIds)`: everyone watching the **page or its pond**, minus all actors, and per watcher a **read-permission re-check at delivery time** (a revoked watcher gets nothing). Payload is denormalized (page/pond names + slugs, actor display names) so the center renders without joins. Never throws — a notification failure cannot break the write that caused it. Sources: - **Version snapshots** (#41's change unit): named snapshots hook in the api's `VersionsService`; the **automatic session-close snapshots live in the collab server**, which has no permission resolution — it now announces them over a new pg NOTIFY channel (`page_version_created`, the reverse direction of the established api→collab LISTEN/NOTIFY bus), consumed by a dedicated LISTEN client in the api (`VersionEventListener`, own pg connection since Prisma cannot hold LISTEN; auto-reconnect; inert under NODE_ENV=test). - **New comments** hook in `CommentsService.create`. **API** — `GET /notifications` (paginated, **unread first** via nulls-first ordering, with `unreadCount`), `POST /notifications/:id/read` (owner-scoped, 404 for foreign ids), `POST /notifications/read-all`. **UI** — bell in the top bar with an unread badge, **30 s polling** (no push in v1); the dropdown lists actor/page/type/time, entries navigate and mark themselves read, plus mark-all-read. **Comment notifications deep-link with `?comments=1`**, which the page now honors by opening the comments panel on load. New `notifications` namespace (de+en). `mailed_at` is already on the table for the #95 digests. **Acceptance criteria** (in `notifications.e2e.db.test.ts`, 4 tests): - *watcher notified, editor not* — fan-out with the owner as actor: watcher unread=1 with the actor's name in the payload, the actor's own list stays empty. (The two-user browser flow rides the #96 pack; the collab NOTIFY path is exercised live by the CI collab pack since the listener is active in the e2e stack.) - *comment notifications link to the page with the panel open* — payload carries pond+page slugs; the bell builds `…?comments=1` and the page opens the panel. - *mark-all-read clears the badge; unread survives reload* — single read flips ordering (read entries sink below unread), read-all → unreadCount 0 on a fresh request (server state). - *delivery-time permission re-check* — reader granted via API, watch set, grant revoked via API → the next fan-out produces nothing for them. Test lesson: grants must be created **and** revoked through the API — raw rows bypass the permission cache in both directions.
Sign in to join this conversation.
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: stwaidele/dorfteich#94
No description provided.