Files
daily-notification-plugin/doc/directives/0002-Daily-Notification-Plugin-Recommendations.md
Matthew Raymer 0bb5a8d218 feat(android)!: implement Phase 2 Android core with WorkManager + AlarmManager + SQLite
- Add complete SQLite schema with Room database (content_cache, schedules, callbacks, history)
- Implement WorkManager FetchWorker with exponential backoff and network constraints
- Add AlarmManager NotifyReceiver with TTL-at-fire logic and notification delivery
- Create BootReceiver for automatic rescheduling after device reboot
- Update AndroidManifest.xml with necessary permissions and receivers
- Add Room, WorkManager, and Kotlin coroutines dependencies to build.gradle

feat(callback-registry)!: implement callback registry with circuit breaker

- Add CallbackRegistryImpl with HTTP, local, and queue callback support
- Implement circuit breaker pattern with exponential backoff retry logic
- Add CallbackEvent interface with structured event types
- Support for exactly-once delivery semantics with retry queue
- Include callback status monitoring and health checks

feat(observability)!: add comprehensive observability and health monitoring

- Implement ObservabilityManager with structured logging and event codes
- Add performance metrics tracking (fetch, notify, callback times)
- Create health status API with circuit breaker monitoring
- Include log compaction and metrics reset functionality
- Support for DNP-* event codes throughout the system

feat(web)!: enhance web implementation with new functionality

- Integrate callback registry and observability into web platform
- Add mock implementations for dual scheduling methods
- Implement performance tracking and structured logging
- Support for local callback registration and management
- Enhanced error handling and event logging

BREAKING CHANGE: New Android dependencies require Room, WorkManager, and Kotlin coroutines
2025-09-22 09:02:04 +00:00

256 lines
8.7 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.
# 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 12)**
- 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 23)** — event codes, status endpoints, history compaction
4. **iOS Parity (Week 34)** — 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> | 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