Files
daily-notification-plugin/docs/archive/2025-legacy-doc/directives/0002-Daily-Notification-Plugin-Recommendations.md
Matthew Raymer c39bd7cec6 docs: Consolidate documentation structure (139 files, zero information loss)
Consolidate all markdown documentation into organized structure per
CONSOLIDATION_DIRECTIVE. All files preserved (canonical, merged, or archived).

- docs/integration/ - Integration documentation (7 files)
- docs/platform/ios/ - iOS platform docs (12 files)
- docs/platform/android/ - Android platform docs (9 files)
- docs/testing/ - Testing documentation (15 files)
- docs/design/ - Design & research (5 files)
- docs/ai/ - AI/ChatGPT artifacts (7 files)
- docs/archive/2025-legacy-doc/ - Historical docs (17 files)

- Integration: Root INTEGRATION_GUIDE.md → docs/integration/
- Platform: Separated iOS and Android into platform/ subdirectories
- Testing: Consolidated all testing docs to docs/testing/
- Legacy: Archived entire doc/ directory to archive/
- AI: Moved all ChatGPT artifacts to docs/ai/

- Added docs/00-INDEX.md - Central navigation hub
- Added docs/CONSOLIDATION_SOURCE_MAP.md - Complete audit trail
- Added docs/CONSOLIDATION_COMPLETE.md - Consolidation summary
- Updated README.md with links to documentation index

- All 139 files have destinations (see CONSOLIDATION_SOURCE_MAP.md)
- Zero information loss (all files preserved)
- Archive preserves original structure
- Index provides clear navigation

- 87 files moved/created/updated
- Root-level docs consolidated
- Legacy doc/ directory archived
- Test app docs remain with test apps (indexed)

Ref: CONSOLIDATION_DIRECTIVE
Author: Matthew Raymer
2025-12-18 09:13:18 +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