Files
crowd-funder-for-time-pwa/doc/notification-system-plan.md
Matthew Raymer 79b226e7d2 docs: apply documentation references model agents directive to notification system docs
- Remove duplicate sync checklists from notification-system-plan.md
- Fix markdown table formatting (remove double pipes) in executive summary
- Streamline cross-document references and eliminate redundant content
- Consolidate canonical ownership statements across all three docs
- Improve document structure and readability per directive guidelines

Files modified:
- doc/notification-system-executive-summary.md
- doc/notification-system-implementation.md
- doc/notification-system-plan.md

Compliance: Follows @docs/documentation_references_model_agents.mdc directive
for eliminating redundancy, centralizing context, and optimizing reference placement.
2025-09-07 10:51:27 +00:00

335 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TimeSafari Notification System — Strategic Plan
**Status:** 🚀 Active plan
**Date:** 2025-09-05T05:09Z (UTC)
**Author:** Matthew Raymer
**Scope:** v1 (inapp orchestrator) now; path to v2 (native plugin) next
**Goal:** We **will deliver** 1..M local notifications/day with content **prefetched** so messages **will display offline**. We **will support** onlinefirst (API→DB→Schedule) with offlinefirst fallback. The system **will enhance** TimeSafari's community-building mission by keeping users connected to gratitude, gifts, and collaborative projects through timely, relevant notifications.
> **Implementation Details:** See `notification-system-implementation.md` for detailed code, database schemas, and integration specifics.
> **Canonical Ownership:** This document owns Goals, Tenets, Platform behaviors, Acceptance criteria, and Test cases.
---
## 1) Versioning & Intent
- **v1 (InApp Orchestrator):** We **will implement** multidaily local notifications, online/offline flows, templating, SQLite persistence, and eventing **inside the app** using Capacitor Local Notifications.
- **v2 (Plugin):** We **will extract** adapters to a Capacitor/Native plugin to gain native schedulers (WorkManager/AlarmManager; BGTask+UNUserNotificationCenter), native HTTP, and native SQLite **with the same TypeScript API**.
> We **will retain** the existing web push + Service Worker foundation; the system **will add** reliable local scheduling on mobile and a unified API across platforms.
---
## 2) Design Tenets
- **Reliability:** OSlevel delivery once scheduled; no reliance on JS being alive at fire time.
- **Freshness:** Prefer onlinefirst within a short prefetch window; degrade gracefully to cached content with TTL.
- **Extractable:** Clean interfaces (Scheduler, DataStore, Callbacks) so v2 **will swap** adapters without API changes.
- **Simplicity:** Oneshot notifications per slot; rolling window scheduling to respect platform caps.
- **Observability:** Persist deliveries and errors; surface minimal metrics; enable ACKs.
- **Privacy-First:** Follow TimeSafari's privacy-preserving architecture; user-controlled visibility and data sovereignty.
- **Community-Focused:** Enhance TimeSafari's mission of connecting people through gratitude, gifts, and collaborative projects.
---
## 3) Architecture Overview
```
Application (Vue/TS)
├─ NotificationOrchestrator (core state)
│ ├─ Scheduler (adapter)
│ ├─ DataStore (adapter)
│ └─ Callbacks (adapter)
└─ UI (settings, status)
Adapters
├─ V1: SchedulerCapacitor, DataStoreSqlite, CallbacksHttp
└─ V2: SchedulerNative, DataStoreNativeSqlite, CallbacksNativeHttp
**Scheduler Adapter:** All notification arming must go through the Scheduler adapter to honor platform timing semantics (exact alarms vs. windowed fallback).
Platform
├─ iOS/Android: LocalNotifications (+ native bridges later)
├─ Web: Service Worker + Push (kept)
└─ Electron: OS notifications (thin adapter)
```
**Execution modes (concise):**
- **OnlineFirst:** wake near slot → fetch (ETag, timeout) → persist → schedule; on failure → OfflineFirst.
- **OfflineFirst:** read last good payload from SQLite; if beyond TTL → skip notification (no retry).
---
## 4) Public API (Shared by v1 & v2)
**Core Types & Interface:** See Implementation document for complete API definitions, type interfaces, and design decisions.
> **Storage semantics:** `'shared'` = app DB; `'private'` = plugin-owned/native DB (v2). (No functional difference in v1.)
> **Slot Identity & Scheduling Policy**
> • **SlotId** uses canonical `HHmm` and remains stable across timezone changes.
> • **Lead window:** default `prefetchLeadMinutes = 20`; no retries once inside the lead.
> • **TTL policy:** When offline and content is beyond TTL, **we will skip** the notification (no "(cached)" suffix).
> • **Idempotency:** Duplicate "scheduled" deliveries are prevented by a unique index on `(slot_id, fire_at, status='scheduled')`.
> • **Time handling:** Slots will follow **local wall-clock** time across TZ/DST; `slotId=HHmm` stays constant and we will **recompute fire times** on offset change.
---
## 5) Data Model & Retention (SQLite)
**Tables:** `notif_contents`, `notif_deliveries`, `notif_config` (see Implementation document for complete schema)
**Retention:** We **will keep** ~14 days of contents/deliveries (configurable) and **will prune** via a simple daily job that runs on app start/resume. We **will prune** daily but **will not** VACUUM by default on mobile; disk compaction is deferred.
**Payload handling:** We **will template** `{title, body}` **before** scheduling; we **will not** mutate at delivery time.
---
## 6) Scheduling Policy & Slot Math
- **Oneshot per slot** per day (nonrepeating).
- **Rolling window:** today's remaining slots; seed tomorrow where platform limits allow.
- **TZ/DST safe:** We **will recompute** local walltimes on app resume and whenever timezone/offset changes; then **reschedule**.
- **Android exactness:** If exact alarms are unavailable or denied, we **will use** `setWindow` semantics via the scheduler adapter.
- **iOS pending cap:** We **will keep** pending locals within typical caps (~64) by limiting the window and canceling/rearming as needed.
- **Electron rolling window:** On Electron we **will schedule** the **next occurrence per slot** by default; depth (today+tomorrow) **will be** enabled only when auto-launch is on, to avoid drift while the app is closed.
---
## 7) Timing & Network Requirements
**Summary:** The notification system uses lightweight, ETag-aware content fetching with single attempts inside lead windows. All timing constants and detailed network policies are defined in the Implementation document.
**Key Policies:**
- **Lead policy:** The lead window governs **online-first fetch attempts**, not arming. We **will arm** locals **whenever the app runs**, using the freshest available payload.
- **TTL policy:** If offline and content is beyond TTL, we will **skip** the notification (no "cached" suffix).
- **Idempotency:** Duplicate "scheduled" rows are prevented by a unique index on `(slot_id, fire_at, status='scheduled')`.
- **Wall-clock rule:** Slots will follow **local wall-clock** across TZ/DST; `slotId=HHmm` stays constant and we will **recompute fire times** on offset change.
- **Resume debounce:** On app resume/open we will **debounce** pipeline entry points by **30s** per app session to avoid burst fetches.
- **No scheduled background network in v1 (mobile):** Local notifications **will deliver offline once armed**, but **we will not** run timed network jobs when the app is terminated. **Network prefetch will occur only while the app is running** (launch/resume/inside lead). Server-driven push (Web SW) and OS background schedulers are a **v2** capability.
**Platform-Specific Network Access:**
- **iOS:** Foreground/recently backgrounded only; no JS wake when app is killed
- **Android:** Exact alarms vs. windowed triggers based on permissions
- **Web:** Service Worker for push notifications only
- **Electron:** App-running only; no background network access
**Optional Background Prefetch (v1):**
- **Background Runner (optional, v1):** We **will integrate** Capacitor's Background Runner to **opportunistically prefetch** content on iOS/Android when the OS grants background time. This **will not** provide clock-precise execution and **will not** run after user-terminate on iOS. It **will not** be treated as a scheduler. We **will continue** to *arm* local notifications via our rolling window regardless of Runner availability. When Runner fires near a slot (inside `prefetchLeadMinutes`), it **will** refresh content (ETag, 12s timeout) and, behind a flag, **may** cancel & re-arm that slot with the fresh template if within TTL. If no budget or failure, the previously armed local **will** still deliver.
**Implementation Details:** See Implementation document for complete timing constants table, network request profiles, and platform-specific enforcement.
---
## 8) Platform Essentials
**iOS**
- Local notifications **will** fire without background runtime once scheduled. NSE **will not** mutate locals; delivery-time enrichment requires remote push (future).
- **Category ID**: `TS_DAILY` with default `OPEN` action
- **Background budget** is short and OSmanaged; any prefetch work **will complete** promptly.
- **Mobile local notifications will route via action listeners (not the service worker)**.
- Background Runner **will** offer **opportunistic** network wake (no guarantees; short runtime; iOS will not run after force-quit). Locals **will** still deliver offline once armed.
**Android**
- Exact alarms on **API 31+** may require `SCHEDULE_EXACT_ALARM`. If exact access is missing on API 31+, we will use a **windowed trigger (default ±10m)** and surface a settings deep-link.
- **We will deep-link users to the exact-alarm settings when we detect denials.**
- **Channel defaults**: ID `timesafari.daily`, name "TimeSafari Daily", importance=high (IDs never change)
- Receivers for reboot/time change **will be handled** by v2 (plugin); in v1, rearming **will occur** on app start/resume.
- **Mobile local notifications will route via action listeners (not the service worker)**.
- Background Runner **will** offer **opportunistic** network wake (no guarantees; short runtime; iOS will not run after force-quit). Locals **will** still deliver offline once armed.
**Web**
- Requires registered Service Worker + permission; can deliver with browser closed. **Web will not offline-schedule**.
- Service Worker click handlers are for **web push only**; **mobile locals bypass the SW**.
- SW examples use `/sw.js` as a placeholder; **wire this to your actual build output path** (e.g., `sw_scripts/notification-click.js` or your combined bundle).
- **Note**: Service workers are **intentionally disabled** in Electron (`src/main.electron.ts`) and web uses VitePWA plugin for minimal implementation.
**Electron**
- We **will use** native OS notifications with **best-effort scheduling while the app is running**; true background scheduling will be addressed in v2 (native bridges).
**Electron delivery strategy (v1 reality + v2 path)**
We **will deliver** desktop notifications while the Electron app is running. True **background scheduling when the app is closed** is **out of scope for v1** and **will be addressed** in v2 via native bridges. We **will adopt** one of the following options (in order of fit to our codebase):
**In-app scheduler + auto-launch (recommended now):** Keep the orchestrator in the main process, **start on login** (tray app, hidden window), and use the **Electron `Notification` API** for delivery. This requires no new OS services and aligns with our PlatformServiceFactory/mixin patterns.
**Policy (v1):** If the app is **not running**, Electron will **not** deliver scheduled locals. With **auto-launch enabled**, we **will achieve** near-mobile parity while respecting OS sleep/idle behavior.
**UX notes:** On Windows we **will set** `appUserModelId` so toasts are attributed correctly; on macOS we **will request** notification permission on first use.
**Prerequisites:** We **will require** Node 18+ (global `fetch`) or we **will polyfill** via `undici` for content fetching in the main process.
---
## 9) Template Engine Contract
**Supported tokens:** `{{headline}}`, `{{summary}}`, `{{date}}` (YYYY-MM-DD), `{{time}}` (HH:MM).
**Escaping:** HTML-escape all injected values.
**Limits:** Title ≤ 50 chars; Body ≤ 200 chars; truncate with ellipsis.
**Fallback:** Missing token → `"[Content]"`.
**Mutation:** We **will** render templates **before** scheduling; no mutation at delivery time on iOS locals.
---
## 10) Integration with Existing TimeSafari Infrastructure
**Database:** We **will integrate** with existing migration system in `src/db-sql/migration.ts` following the established `MIGRATIONS` array pattern
**Settings:** We **will extend** existing Settings type in `src/db/tables/settings.ts` following the established type extension pattern
**Platform Service:** We **will leverage** existing PlatformServiceMixin database utilities following the established mixin pattern
**Service Factory:** We **will follow** the existing `PlatformServiceFactory` singleton pattern for notification service creation
**Capacitor:** We **will integrate** with existing deep link system in `src/main.capacitor.ts` following the established initialization pattern
**Service Worker:** We **will extend** existing service worker infrastructure following the established `sw_scripts/` pattern (Note: Service workers are intentionally disabled in Electron and have minimal web implementation via VitePWA plugin)
**API:** We **will use** existing error handling from `src/services/api.ts` following the established `handleApiError` pattern
**Logging:** We **will use** existing logger from `src/utils/logger` following the established logging patterns
**Platform Detection:** We **will use** existing `process.env.VITE_PLATFORM` patterns (`web`, `capacitor`, `electron`)
**Vue Architecture:** We **will follow** Vue 3 + vue-facing-decorator patterns for component integration (Note: The existing `useNotifications` composable in `src/composables/useNotifications.ts` is currently stub functions with eslint-disable comments and needs implementation)
**State Management:** We **will integrate** with existing settings system via `PlatformServiceMixin.$saveSettings()` for notification preferences (Note: TimeSafari uses PlatformServiceMixin for all state management, not Pinia stores)
**Identity System:** We **will integrate** with existing `did:ethr:` (Ethereum-based DID) system for user context
**Testing:** We **will follow** Playwright E2E testing patterns established in TimeSafari
**Database Architecture:** We **will support** platform-specific database backends:
- **Web**: Absurd SQL (SQLite via IndexedDB) via `WebPlatformService` with worker pattern
- **Capacitor**: Native SQLite via `CapacitorPlatformService`
- **Electron**: Native SQLite via `ElectronPlatformService` (extends CapacitorPlatformService)
---
## 11) Error Taxonomy & Telemetry
**Error Codes:** `FETCH_TIMEOUT`, `ETAG_NOT_MODIFIED`, `SCHEDULE_DENIED`, `EXACT_ALARM_MISSING`, `STORAGE_BUSY`, `TEMPLATE_MISSING_TOKEN`, `PERMISSION_DENIED`.
**Event Envelope:** `code, slotId, whenMs, attempt, networkState, tzOffset, appState, timestamp`.
---
## 12) Permission UX & Channels/Categories
- We **will request** notification permission **after** user intent (e.g., settings screen), not on first render.
- **Android:** We **will create** a stable channel ID (e.g., `timesafari.daily`) and **will set** importance appropriately.
- **iOS:** We **will register** categories for optional actions; grouping may use `threadIdentifier` per slot/day.
---
## 13) Eventing & Telemetry
**Error Codes:** `FETCH_TIMEOUT`, `ETAG_NOT_MODIFIED`, `SCHEDULE_DENIED`, `EXACT_ALARM_MISSING`, `STORAGE_BUSY`, `TEMPLATE_MISSING_TOKEN`, `PERMISSION_DENIED`.
**Event Envelope:** `code, slotId, whenMs, attempt, networkState, tzOffset, appState, timestamp`.
**Implementation:** See Implementation document for complete error taxonomy, event logging envelope, ACK payload format, and telemetry events.
---
## 14) Feature Flags & Config
**Key Flags:** `scheduler`, `mode`, `prefetchLeadMinutes`, `ttlSeconds`, `iosCategoryIdentifier`, `androidChannelId`, `prefetchRunner`, `runnerRearm`.
**Storage:** Feature flags **will reside** in `notif_config` table as key-value pairs, separate from user settings.
**Implementation:** See Implementation document for complete feature flags table with defaults and descriptions.
---
## 15) Acceptance (Definition of Done) → Test Cases
### Explicit Test Checks
- **App killed → locals fire**: Configure slots at 8:00, 12:00, 18:00; kill app; verify notifications fire at each slot on iOS/Android
- **ETag 304 path**: Server returns 304 → keep previous content; locals fire with cached payload
- **ETag 200 path**: Server returns 200 → update content and re-arm locals with fresh payload
- **Offline + beyond TTL**: When offline and content > 24h old → skip notification (no "(cached)" suffix)
- **iOS pending cap**: Respect ~64 pending limit; cancel/re-arm as needed within rolling window
- **Exact-alarm denied**: Android permission absent → windowed schedule (±10m) activates; UI shows fallback hint
- **Permissions disabled** → we will record `SCHEDULE_DENIED` and refrain from queuing locals.
- **Window fallback** → when exact alarm is absent on Android, verify target fires within **±10m** of slot time (document as an E2E expectation).
- **Timezone change**: On TZ/DST change → recompute wall-clock times; cancel & re-arm all slots
- **Lead window respect**: No retries attempted once inside 20min lead window
- **Idempotency**: Multiple `runFullPipelineNow()` calls don't create duplicate scheduled deliveries
- **Cooldown guard**: `deliverStoredNow()` has 60s cooldown to prevent double-firing
### Electron-Specific Test Checks
- **Electron running (tray or window) → notifications fire** at configured slots using Electron `Notification`
- **Electron not running →** no delivery (documented limitation for v1)
- **Start on Login enabled →** after reboot + login, orchestrator **will re-arm** slots and deliver
- **Template limits honored** (Title ≤ 50, Body ≤ 200) on Electron notifications
- **SW scope** not used for Electron (click handlers are **web only**)
- **Windows appUserModelId** set correctly for toast attribution
- **macOS notification permission** requested on first use
### Timing-Verifiable Test Checks
- **iOS/Android (app killed):** locals will fire at their slots; no network activity at delivery time.
- **iOS/Android (resume inside lead):** exactly **one** online-first attempt occurs; if fetch completes within **12s** → content updated; otherwise offline policy applies.
- **Android (no exact access):** observed delivery is within **±10 min** of slot time.
- **Web push:** SW push event fetch runs once with **12s** timeout; if it times out, the push still displays (from payload).
- **Electron (app running):** timer-based locals fire on time; on reboot with **Start on Login**, orchestrator re-arms on first run.
- **TTL behavior:** offline & stale → **skip** (no notification posted).
- **ETag path:** with `304`, last payload remains; no duplicate scheduling rows (unique index enforced).
- **Cooldown:** calling `deliverStoredNow` twice within **60s** for same slot doesn't produce two notifications.
- **Closed app, armed earlier** → locals fire at slot; title/body match last rendered content (proves "render at schedule time" + adapter API).
- **Closed app, timezone change before slot** → on next resume, app recomputes and re-arms; already armed notifications will still fire on original wall-time
- **Mobile closed-app, no background network:** Arm at Thours; kill app; verify locals fire with last rendered text; confirm **no** network egress at delivery.
- **Web push as network scheduler:** Send push with empty payload → SW fetches within 12s timeout → shows correct text; confirm behavior with browser closed.
- **Electron app not running:** No delivery; with **Start on Login**, after reboot first run fetches and re-arms; subsequent slots fire.
- **Runner fires in background (Android/iOS):** With Runner enabled and app backgrounded for ≥30 min, at least one prefetch **will** occur; content cache **will** update; already-armed locals **will** still fire on time.
- **Runner re-arm (flagged):** If `runnerRearm=true` and Runner fires inside lead with fresh content + within TTL, the system **will** cancel & re-arm the next slot; delivered text **will** match fresh template.
---
## 16) Test Matrix (Essentials)
- **Android:** exact vs inexact branch, Doze/App Standby behavior, reboot/time change, permission denial path, deeplink to exactalarm settings.
- **iOS:** BG fetch budget limits, pending cap windowing, local notification delivery with app terminated, category actions.
- **Web:** SW lifecycle, push delivery with app closed, click handling, no offline scheduling.
- **Crosscutting:** ETag/304 behavior, TTL policy, templating correctness, event queue drain, SQLite retention job.
---
## 17) Migration & Rollout Notes
- We **will keep** existing web push flows intact.
- We **will introduce** the orchestrator behind a feature flag, initially with a small number of slots.
- We **will migrate** settings to accept multiple times per day.
- We **will document** platform caveats inside uservisible settings (e.g., Android exact alarms, iOS cap).
---
## 18) Security & Privacy
- Tokens **will reside** in Keystore/Keychain (mobile) and **will be injected** at request time; they **will not** be stored in SQLite.
- Optionally, SQLCipher at rest for mobile; redaction of PII in logs; payload size caps.
- Content **will be** minimal (title/body); sensitive data **will not be** embedded.
---
## 19) NonGoals (Now)
- Complex action sets and rich media on locals (kept minimal).
- Deliverytime mutation of local notifications on iOS (NSE is for remote).
- Full analytics pipeline (future enhancement).
---
## 20) Cross-Document Synchronization
**Canonical Ownership:**
- **This document (Plan):** Goals, Tenets, Platform behaviors, Acceptance criteria, Test cases
- **Implementation document:** API definitions, Database schemas, Adapter implementations, Code examples
**Synchronization Requirements:**
- API code blocks must be identical between Plan §4 and Implementation §3
- Feature flags must match between Plan §13 and Implementation §15 defaults
- Test cases must align between Plan §14 acceptance criteria and Implementation examples
- Error codes must match between Plan §11 taxonomy and Implementation error handling
- Slot/TTL/Lead policies must be identical between Plan §4 semantics and Implementation §9 logic
---
*This strategic plan focuses on features and futuretense deliverables, avoids implementation details, and preserves a clear path from the inapp orchestrator (v1) to native plugin (v2).*