# Daily Notification Plugin — Phase 2 Recommendations (v3) > This directive assumes Phase 1 (API surface + tests) is complete and aligns with the current codebase. It focuses on **platform implementations**, **storage/TTL**, **callbacks**, **observability**, and **security**. --- ## 1) Milestones & Order of Work 1. **Android Core (Week 1–2)** - Fetch: WorkManager (`Constraints: NETWORK_CONNECTED`, backoff: exponential) - Notify: AlarmManager (or Exact alarms if permitted), NotificationManager - Boot resilience: `RECEIVE_BOOT_COMPLETED` receiver reschedules jobs - Shared SQLite schema + DAO layer (Room recommended) 2. **Callback Registry (Week 2)** — shared TS interface + native bridges 3. **Observability & Health (Week 2–3)** — event codes, status endpoints, history compaction 4. **iOS Parity (Week 3–4)** — BGTaskScheduler + UNUserNotificationCenter 5. **Web SW/Push (Week 4)** — SW events + IndexedDB (mirror schema), periodic sync fallback 6. **Docs & Examples (Week 4)** — migration, enterprise callbacks, health dashboards --- ## 2) Storage & TTL — Concrete Schema > Keep **TTL-at-fire** invariant and **rolling window armed**. Use normalized tables and a minimal DAO. ### SQLite (DDL) ```sql CREATE TABLE IF NOT EXISTS content_cache ( id TEXT PRIMARY KEY, fetched_at INTEGER NOT NULL, -- epoch ms ttl_seconds INTEGER NOT NULL, payload BLOB NOT NULL, meta TEXT ); CREATE TABLE IF NOT EXISTS schedules ( id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('fetch','notify')), cron TEXT, -- optional: cron expression clock_time TEXT, -- optional: HH:mm enabled INTEGER NOT NULL DEFAULT 1, last_run_at INTEGER, next_run_at INTEGER, jitter_ms INTEGER DEFAULT 0, backoff_policy TEXT DEFAULT 'exp', state_json TEXT ); CREATE TABLE IF NOT EXISTS callbacks ( id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK (kind IN ('http','local','queue')), target TEXT NOT NULL, -- url_or_local headers_json TEXT, enabled INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS history ( id INTEGER PRIMARY KEY AUTOINCREMENT, ref_id TEXT, -- content or schedule id kind TEXT NOT NULL, -- fetch/notify/callback occurred_at INTEGER NOT NULL, duration_ms INTEGER, outcome TEXT NOT NULL, -- success|failure|skipped_ttl|circuit_open diag_json TEXT ); CREATE INDEX IF NOT EXISTS idx_history_time ON history(occurred_at); CREATE INDEX IF NOT EXISTS idx_cache_time ON content_cache(fetched_at); ``` ### TTL-at-fire Rule - On notification fire: `if (now > fetched_at + ttl_seconds) -> skip (record outcome=skipped_ttl)`. - Maintain a **prep guarantee**: ensure a fresh cache entry for the next window even after failures (schedule a fetch on next window). --- ## 3) Android Implementation Sketch ### WorkManager for Fetch ```kotlin class FetchWorker( appContext: Context, workerParams: WorkerParameters ) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result = withContext(Dispatchers.IO) { val start = SystemClock.elapsedRealtime() try { val payload = fetchContent() // http call / local generator dao.upsertCache(ContentCache(...)) logEvent("DNP-FETCH-SUCCESS", start) Result.success() } catch (e: IOException) { logEvent("DNP-FETCH-FAILURE", start, e) Result.retry() } catch (e: Throwable) { logEvent("DNP-FETCH-FAILURE", start, e) Result.failure() } } } ``` **Constraints**: `Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()` **Backoff**: `setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)` ### AlarmManager for Notify ```kotlin fun scheduleExactNotification(context: Context, triggerAtMillis: Long) { val alarmMgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val pi = PendingIntent.getBroadcast(context, REQ_ID, Intent(context, NotifyReceiver::class.java), FLAG_IMMUTABLE) alarmMgr.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pi) } class NotifyReceiver : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent?) { val cache = dao.latestCache() if (cache == null) return if (System.currentTimeMillis() > cache.fetched_at + cache.ttl_seconds * 1000) { recordHistory("notify", "skipped_ttl"); return } showNotification(ctx, cache) recordHistory("notify", "success") fireCallbacks("onNotifyDelivered") } } ``` ### Boot Reschedule - Manifest: `RECEIVE_BOOT_COMPLETED` - On boot: read `schedules.enabled=1` and re-schedule WorkManager/AlarmManager --- ## 4) Callback Registry — Minimal Viable Implementation ### TS Core ```ts export type CallbackKind = 'http' | 'local' | 'queue'; export interface CallbackEvent { id: string; at: number; type: 'onFetchStart' | 'onFetchSuccess' | 'onFetchFailure' | 'onNotifyStart' | 'onNotifyDelivered' | 'onNotifySkippedTTL' | 'onNotifyFailure'; payload?: unknown; } export type CallbackFunction = (e: CallbackEvent) => Promise | void; ``` ### Delivery Semantics - **Exactly-once attempt per event**, persisted `history` row - **Retry**: exponential backoff with cap; open **circuit** per `callback.id` on repeated failures - **Redaction**: apply header/body redaction before persisting `diag_json` ### HTTP Example ```ts async function deliverHttpCallback(cb: CallbackRecord, event: CallbackEvent) { const start = performance.now(); try { const res = await fetch(cb.target, { method: 'POST', headers: { 'content-type': 'application/json', ...(cb.headers ?? {}) }, body: JSON.stringify(event), }); recordHistory(cb.id, 'callback', 'success', start, { status: res.status }); } catch (err) { scheduleRetry(cb.id, event); // capped exponential recordHistory(cb.id, 'callback', 'failure', start, { error: String(err) }); } } ``` --- ## 5) Observability & Health - **Event Codes**: `DNP-FETCH-*`, `DNP-NOTIFY-*`, `DNP-CB-*` - **Health API** (TS): `getDualScheduleStatus()` returns `{ nextRuns, lastOutcomes, cacheAgeMs, staleArmed, queueDepth }` - **Compaction**: nightly job to prune `history` > 30 days - **Device Debug**: Android broadcast to dump status to logcat for field diagnostics --- ## 6) Security & Permissions - Default **HTTPS-only** callbacks, opt-out via explicit dev flag - Android: runtime gate for `POST_NOTIFICATIONS`; show rationale UI for exact alarms (if requested) - **PII/Secrets**: redact before persistence; never log tokens - **Input Validation**: sanitize HTTP callback targets; enforce allowlist pattern (e.g., `https://*.yourdomain.tld` in prod) --- ## 7) Performance & Battery - **±Jitter (5m)** for fetch; coalesce same-minute schedules - **Retry Caps**: ≤ 5 attempts, upper bound 60 min backoff - **Network Guards**: avoid waking when offline; use WorkManager constraints to defer - **Back-Pressure**: cap concurrent callbacks; open circuit on sustained failures --- ## 8) Tests You Can Add Now - **TTL Edge Cases**: past/future timezones, DST cutovers - **Retry & Circuit**: force network failures, assert capped retries + circuit open - **Boot Reschedule**: instrumentation test to simulate reboot and check re-arming - **SW/IndexedDB**: headless test verifying cache write/read + TTL skip --- ## 9) Documentation Tasks - API reference for new **health** and **callback** semantics - Platform guides: Android exact alarm notes, iOS background limits, Web SW lifecycle - Migration note: why `scheduleDualNotification` is preferred; compat wrappers policy - “Runbook” for QA: how to toggle jitter/backoff; how to inspect `history` --- ## 10) Acceptance Criteria (Phase 2) - Android end-to-end demo: fetch → cache → TTL check → notify → callback(s) → history - Health endpoint returns non-null next run, recent outcomes, and cache age - iOS parity path demonstrated on simulator (background fetch + local notif) - Web SW functional on Chromium + Firefox with IndexedDB persistence - Logs show structured `DNP-*` events; compaction reduces history size as configured - Docs updated; examples build and run --- ## 11) Risks & Mitigations - **Doze/Idle drops alarms** → prefer WorkManager + exact when allowed; add tolerance window - **iOS background unpredictability** → encourage scheduled “fetch windows”; document silent-push optionality - **Web Push unavailable** → periodic sync + foreground fallback; degrade gracefully - **Callback storms** → batch events where possible; per-callback rate limit --- ## 12) Versioning - Release as `1.1.0` when Android path merges; mark wrappers as **soft-deprecated** in docs - Keep zero-padded doc versions in `/doc/` and release notes linking to them