Pages · Health Debug
X1 — Notifications — Session Card
Module: X1 Notifications · Type: cross-cutting X-module (not one of the 8 protocol engines) Closes: Divergence #3 (Notification Center working on iOS, entirely missing on Android) Planning artifact only — no production code. Read alongside
docs/plan/13-execution-modules-and-sessions.md(the framework this card implements),03-protocol-engines.md,04-backend-togo.md,12-legacy-reference-map.md,02-platform-divergences.md, and the relevant platform PRD (05 web, 06 chrome, 07 apple, 08 android, 09 desktop, 11 mcp).
Purpose
X1 builds the notification stack end-to-end: the backend decides what to notify and when (server-side, from Protocol Engine hooks and windows — never a client), stamps notification_items rows that carry a message key plus a vars JSON blob beside the sentence (never interpolated server-side, because Arabic reorders), routes each item through a per-user SSE hub that suppresses push when a live stream is already open, and — critically — finally wires actual delivery that the legacy build never shipped: FCM for Android/Chrome, APNs for Apple, backed by device-token storage. Every client then renders a notification center and handles its platform's push/local-scheduling in a thin, presentation-only way. This is the module that turns "we compute alerts but never deliver them" into working smart alerts across every surface, and it closes Divergence #3 by giving Android the full notification infrastructure it has always lacked.
Feature parameters relevant to this module (exact — do not invent)
Notification rules & routing (legacy precedents — PENDING ratification, not spec-fixed; reuse notify.go / internal/notifier/ / internal/realtime/route.go as reference, do not port wholesale):
- In-process sweeper cadence: 15-minute sweep (legacy
internal/notifier/). - Notice rules: caffeine-window-opened alert 20 min; GERD/shutdown warning 30 min; weigh-in prompt only 07:00–11:00; one notice per kind per user per local calendar day.
- Routing decision (
internal/realtime/route.goprecedent): live SSE stream open → suppress push; else most-recent device within a 12-hour window → primary device → none — every decision carries aReason. - Content:
notification_itemsstore the source sentence +varsJSON separately; the client does ICU rendering. Never interpolate server-side. - Delivery targets: FCM (Android + Chrome), APNs (Apple). Legacy registered no FCM/APNs tokens — delivery is net-new here.
Underlying engine constants that generate notifications (owned by the engines, NOT recomputed here — X1 only fans them out):
- Hydration: 250 ml units, 5000 ml/day max, 30-second cooldown.
- Caffeine Block: no caffeine first 90 minutes post-wakeup.
- GERD Window: 4-hour pre-sleep window (water, chamomile, anise only).
- Medication Grace: 60-minute window.
- Trigger Families: categorical Gout / IBS-GERD / Fatty Liver (Safe vs Trigger-bearing).
- Cycle: 3 consecutive logged cycles before predicting; fails safe on irregularity.
- Kinetic Pomodoro (legacy precedent, PENDING ratification): 25 min work / 5 min short break / 15 min long break / long break every 4 cycles / daily target 8.
Color / i18n (strict): alerts render #FF3333 red glow in dark (#1A1A1A bg); light-mode alert red is genuinely undefined — PENDING DESIGN. All strings ICU Message Format, en/ar, full Arabic RTL (mirrored), no concatenation.
Session table
Surface order within the module (doc 13 §4): backend → web → mcp → chrome → apple → android → desktop. Backend must land before any client session. The backend cell is split into two clearly-bounded slices (doc 13 §1: "when a cell is too big, one clearly-bounded slice of it") because delivery is externally blocked on credentials only Fady can supply.
| Session ID | Surface | Scope (this one session builds) | Entry deps | Done criteria (doc 13 §5 template) | Plan docs to read |
|---|---|---|---|---|---|
S-<date>-X1-backend-01-NN | backend | Notification core. Notifications plugin/subscriber registered on the kernel hook bus; rules that react to engine hooks (entry.logged, engine window transitions) + in-process 15-min sweeper producing notification_items rows (message key + vars JSON stored beside the sentence, never interpolated; read-state; engine/kind; user_id). Per-user SSE hub (resource-name events, no state → clients refetch) + push-suppression routing (live stream open → suppress; else most-recent device 12h → primary → none, each with a Reason). Actual push send is stubbed here (a routing decision only), landed in backend-02. Endpoints: GET /api/v1/notifications (list unread), PATCH /api/v1/notifications (mark read). | M0.1–M0.4 (foundation backend); M1 Hydration backend (needs at least one engine emitting hooks/entries to notify on — doc 13 §4 "X1 after M1 exists") | Backend-engine template: plugin registered on kernel; migration expand-contract, every column NOT NULL with a default; endpoints return computed state; unit tests encode every reused notice/routing constant literally (15-min sweep, one-per-kind-per-day, 12h device window, suppress-on-live-stream) and pass; integration test vs real Postgres (idempotent sweep replay, dedup races, suppression decision); no notification/routing constant leaks outside the plugin core/. | 13, 04, 03, 12, 02 |
S-<date>-X1-backend-02-NN | backend | Actual delivery (net-new). device_tokens storage + register/deregister endpoints (POST/DELETE /api/v1/devices/token); FCM adapter (Android/Chrome) + APNs adapter (Apple) driven by backend-01's routing decision; credentials (Firebase project + google-services.json; APNs auth key) loaded from the secrets vault, not code. Turns the backend-01 "suppress vs. push" decision into a real send. | backend-01; EXTERNAL: Fady must supply the Firebase project / google-services.json / APNs key (secrets vault) — likely opens blocked until then | Backend-engine template + a delivery smoke-check: device_tokens migration expand-contract, every column NOT NULL+default; register/deregister endpoints owner-scoped (foreign token 404 not 403); a documented end-to-end send to a real FCM + APNs token succeeds once credentials exist; no secret in code/logs. | 13, 04, 10, 02, 12 |
S-<date>-X1-web-01-NN | web | Notification center slide-over + unread badge. Renders ['notifications'] from /api/v1/notifications, mark-as-read PATCH, unread count badge; ICU-renders vars beside the sentence; browser Notification API accessed only through the web platform adapter (so Electron can substitute). Consumes the SSE hub for live invalidation (falls back to refetch-on-focus). | backend-01; M0.5–M0.7 (web shell, theme engine, i18n engine); M1 web (card pattern) | Client-engine template: renders server notification state and submits mark-read intent only; thin-client/SSOT audit passes (no rule/window/timing logic client-side — grep finds no protocol constant); both themes render per color spec (#FF3333 glow dark; light alert = PENDING DESIGN); Arabic RTL mirrors; strings ICU, none concatenated. | 13, 05, 01, 02, 12 |
S-<date>-X1-mcp-01-NN | mcp | list_notifications tool. Returns unread notification items verbatim from GET /api/v1/notifications via personal-access-token auth. | backend-01; M0.8 (mcp skeleton) | MCP template: tool calls the real API with PAT auth, returns server state verbatim, holds no protocol logic and no AI keys; excluded tools stay excluded (no complete_break, no notification-creation tool — MCP reads notifications, never fabricates them). | 13, 11, 04 |
S-<date>-X1-chrome-01-NN | chrome | chrome.notifications from the service worker. SW polls/subscribes, raises chrome.notifications, renders unread badge, honors server-side notification prefs (GET/PUT /prefs/notifications, cached for display only). Web Push/FCM registration + token handling depends on backend-02. | backend-01 (list/prefs) + backend-02 (for push/token registration); chrome M0/M1 baseline | Client-engine template: SW renders server-emitted notifications and submits mark-read/token intents only; thin-client audit passes (no timing/rules in SW); ICU-rendered content, Arabic included; no protocol constant in extension code. | 13, 06, 02, 12 |
S-<date>-X1-apple-01-NN | apple | Notification center screen + local schedulers + push handling. Renders the notifications list; schedules LOCAL notifications at server-provided fire times (mine the legacy 11 local notification schedulers for structure, not logic — fire times come from the API, never derived locally); APNs registration + token push to backend-02; foreground/background push handling. | backend-01 + backend-02 (APNs); apple M0/M1 baseline | Client-engine template: renders server notification state, schedules only at server-provided times, submits mark-read/token intents only; thin-client audit passes (no window/timing derivation in Swift); both themes; Arabic RTL mirrors; ICU strings, none concatenated. | 13, 07, 02, 12 |
S-<date>-X1-android-01-NN | android | FULL notification infrastructure (Divergence #3 — entirely missing in legacy). core/notifications: NotificationManager + channels partitioned by engine concern; WorkManager/AlarmManager exact local alarms armed from the backend notification schedule (fire under Doze); notification center screen; FCM registration + token push to backend-02; fan-out to handheld (Wear/Auto consume the same layer). | backend-01 + backend-02 (FCM); android M0/M1 baseline | Client-engine template + explicit Divergence #3 closure: renders server notifications, arms alarms at server-provided instants only, submits mark-read/token intents; thin-client audit passes (no rules/windows in Kotlin); scheduled reminders fire under Doze at server instants; both themes on a physical device; full Arabic RTL; ICU strings, none concatenated; channel taxonomy stubbed (names PENDING DESIGN). | 13, 08, 02, 12 |
S-<date>-X1-desktop-01-NN | desktop | Native OS notifications with the window closed. Electron main-process OS notifications via the platform adapter (substituting the web adapter), working when the main window is closed/tray-only; reuses the web notification-center components for the in-app inbox. | backend-01; X1-web-01 (reuses components); desktop M0/M1 baseline | Client-engine template: renders server notifications, submits mark-read intent only; OS notification path goes through the platform adapter (no browser Notification in shared code); thin-client audit passes; both themes; Arabic RTL; ICU strings, none concatenated; notification copy/iconography PENDING DESIGN. | 13, 09, 05, 02 |
Notes & gotchas
- Legacy precedents worth reusing (reference only — greenfield rebuild, nothing ported wholesale):
notify.gorules and cadences (20-min caffeine, 30-min GERD, 07:00–11:00 weigh-in, one-per-kind-per-day); theinternal/notifier/15-min sweeper; theinternal/realtime/route.gosuppression ladder (live SSE → most-recent device 12h → primary → none, each with aReason); the iOS 11 local notification schedulers (structure to mine for apple-01, fire times still come from the server); thenotification_items+vars-beside-the-sentence pattern. All of these are PENDING ratification, not spec-fixed — treat as candidate answers, confirm before coding. - Liability to avoid (root cause of the divergences): never let a client compute what/when to notify. The engines emit; X1 fans out.
varsare stored beside the sentence and rendered by the client's ICU catalog — do not interpolate server-side (Arabic reorders). A single grep for protocol constants must find nothing in any client notification code. - The delivery gap is the whole point: legacy computed routing but registered no FCM/APNs tokens, so nothing was ever delivered. backend-02 is where that finally ships — and it is the one session gated on an external human dependency (Fady's Firebase project /
google-services.json/ APNs key). Expect backend-02 to open blocked; all client push-handling can be scaffolded against backend-01's list/SSE surface, with real end-to-end push verified only once backend-02 unblocks. - This module resolves a standing open question: docs 02/05/06/08/09 all flagged "push provider / push-vs-poll unspecified." X1 fixes it: per-user SSE for live suppression + FCM/APNs for push. Update those open-question lists when backend-01/02 land.
- PENDING DESIGN (do not invent): notification-center UI, alert grouping, and in-app inbox presentation (all surfaces); Android channel taxonomy and user-facing channel names; desktop notification copy/iconography; light-mode alert red (genuinely undefined in the color spec); Auto/handheld "upcoming constraint" lead times (proposed backend-configured).
- PENDING ratification (not design): the reused legacy notice timings and Pomodoro intervals above — carry them as defaults, flag for Fady's sign-off.
- Ordering: backend-01 requires M1 (Hydration) backend to exist so there is a real engine emitting hooks to notify on (doc 13 §4). Within the module keep surface order backend → web → mcp → chrome → apple → android → desktop.
Handoff reminder
At the end of every X1 session: write one session-handoff memory to the healthdebug brain — retain the content in the doc 13 §6 structure, then memory/edit to attach metadata (category=session-handoff, plus project=healthdebug, module=X1, surface, status, next, era=greenfield, status(record)=current, title, tags). Use a unique source_ref: handoff/X1-notifications/<surface>/<n> (never reuse — reuse supersedes). Then update the single build-ledger memory (source_ref: build-ledger, edited in place) so one recall shows the whole frontier and the recommended next session. Any architectural decision or new open question also gets its own classified memory and, if it changes the plan, an edit to the relevant plan doc. See doc 13 §6 and docs/brain/BRAIN-INDEX.md.