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
This commit is contained in:
255
doc/directives/0002-Daily-Notification-Plugin-Recommendations.md
Normal file
255
doc/directives/0002-Daily-Notification-Plugin-Recommendations.md
Normal file
@@ -0,0 +1,255 @@
|
||||
|
||||
# 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> | 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
|
||||
Reference in New Issue
Block a user