Notification bell: received + sent peering transfers in a dropdown, with mark-all-read #147

Closed
opened 2026-08-09 02:00:25 +00:00 by Cordy · 1 comment
Owner

From the #106 dogfood, decided with Nikola: the first successful A↔B transfer arrived silently — the #105 inbox dot is too subtle, and for unscoped admins (who don't use the Personal nav) it is invisible entirely.

Spec (Nikola's): a notification bell left of the user circle in the top bar. Dropdown lists what arrived (peering deliveries into the user's Inbox) and what was sent out (their outbound transfers, with status). A "Mark all read" button clears in bulk.

Implementation sketch:

  • No new event framework: derive the feed from what already exists. Outbound = the sender's transfers store (GET /api/v1/peering/transfers). Inbound = listing /home/<user>/Inbox/*/ via the unscoped store (works for admins too), newest first.
  • Read state = a per-user lastSeen timestamp in a tiny atomic-JSON store (house pattern), not per-item flags: "Mark all read" sets it to now; unread = items newer than it. Honest and cheap.
  • GET /api/v1/notifications{ items: [{kind: received|sent, file, peer, at, status}], unread: N } (capped, newest first). POST /api/v1/notifications/read-all.
  • Bell shows a dot when unread > 0 (consistent with the #105 decision: dot, no count). Poll on the existing UI cadence.
  • i18n en/de/fr/it.
From the #106 dogfood, decided with Nikola: the first successful A↔B transfer arrived silently — the #105 inbox dot is too subtle, and for unscoped admins (who don't use the Personal nav) it is invisible entirely. **Spec (Nikola's):** a notification bell **left of the user circle** in the top bar. Dropdown lists what **arrived** (peering deliveries into the user's Inbox) and what was **sent out** (their outbound transfers, with status). A **"Mark all read"** button clears in bulk. **Implementation sketch:** - No new event framework: derive the feed from what already exists. Outbound = the sender's transfers store (`GET /api/v1/peering/transfers`). Inbound = listing `/home/<user>/Inbox/*/` via the unscoped store (works for admins too), newest first. - Read state = a per-user **lastSeen timestamp** in a tiny atomic-JSON store (house pattern), not per-item flags: "Mark all read" sets it to now; unread = items newer than it. Honest and cheap. - `GET /api/v1/notifications` → `{ items: [{kind: received|sent, file, peer, at, status}], unread: N }` (capped, newest first). `POST /api/v1/notifications/read-all`. - Bell shows a dot when `unread > 0` (consistent with the #105 decision: dot, no count). Poll on the existing UI cadence. - i18n en/de/fr/it.
Cordy closed this issue 2026-08-09 02:09:36 +00:00
Author
Owner

Shipped in v0.4.11 (PR #148), live on both dogfood instances.

What was built:

  • Bell button left of the user circle, with the house navdot when there are unread items. Hidden automatically on instances where the feed endpoint doesn't exist.
  • Dropdown (same menu pattern as the user menu): received items — "file arrived from peer" — and sent items — "file sent to peer" with the transfer state chip text and failure reason. Newest first, capped at 20; times via the existing fmtTime/trState helpers. i18n en/de/fr/it.
  • Mark all read advances a per-user watermark; unread = items newer than it.

Architecture choice: no event framework. The feed is a view over state that already exists — received from the /home/<user>/Inbox/<peer>/ tree (read via the unscoped store, so it works for unscoped admins, whose own browsing never touches /home), sent from the outbound transfers store. The only new persistence is internal/notify: one timestamp per user, atomic JSON beside the other runtime state (notify-seen.json), round-trip tested. On the dogfood PVCs the read-state survives redeploys.

Endpoints: GET /api/v1/notifications{items, unread}; POST /api/v1/notifications/read-all → 204. Poll on the existing 60s cadence.

Verified: gofmt/vet clean, full go test ./... incl. the new watermark test, node --check both blocks; image cordy/cairn:v0.4.11 Running 1/1 on enc and beta.

Shipped in **v0.4.11** (PR #148), live on both dogfood instances. **What was built:** - **Bell button** left of the user circle, with the house `navdot` when there are unread items. Hidden automatically on instances where the feed endpoint doesn't exist. - **Dropdown** (same menu pattern as the user menu): received items — "*file* arrived from *peer*" — and sent items — "*file* sent to *peer*" with the transfer state chip text and failure reason. Newest first, capped at 20; times via the existing `fmtTime`/`trState` helpers. i18n en/de/fr/it. - **Mark all read** advances a per-user watermark; unread = items newer than it. **Architecture choice:** no event framework. The feed is a *view* over state that already exists — received from the `/home/<user>/Inbox/<peer>/` tree (read via the **unscoped** store, so it works for unscoped admins, whose own browsing never touches `/home`), sent from the outbound transfers store. The only new persistence is `internal/notify`: one timestamp per user, atomic JSON beside the other runtime state (`notify-seen.json`), round-trip tested. On the dogfood PVCs the read-state survives redeploys. Endpoints: `GET /api/v1/notifications` → `{items, unread}`; `POST /api/v1/notifications/read-all` → 204. Poll on the existing 60s cadence. Verified: gofmt/vet clean, full `go test ./...` incl. the new watermark test, `node --check` both blocks; image `cordy/cairn:v0.4.11` Running 1/1 on enc and beta.
Sign in to join this conversation.
No labels
No project
No assignees
1 participant
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: Cordy/Cairn#147
No description provided.