# Reminder Alert Archival Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Deliver alarm-style reminder popups with cancel/snooze actions, 30s timeout auto-snooze, overlap handling, and archived/gray lifecycle consistency across Android and iOS. **Architecture:** Keep scheduling local on device (Flutter local notifications), persist user reminder actions with an app-side outbox for eventual backend sync, and use backend PATCH update for archive status as the source of truth. Add a backend safety net job to auto-archive expired active events so app-terminated scenarios still converge. Implement shared reminder payload and action handler with platform-specific notification configuration (Android full-screen intent, iOS category actions). **Tech Stack:** Flutter (`flutter_local_notifications`, existing calendar service), FastAPI schedule-items API, SQLAlchemy service layer, uv/pytest, Dart tests. --- ### Task 1: Update protocol and state semantics first **Files:** - Modify: `docs/protocols/` (create a new reminder interaction protocol doc or extend existing schedule protocol doc) - Modify: `docs/runtime/runtime-route.md` **Step 1: Write failing doc checks (manual checklist as fail-first gate)** ```text Checklist fails until all are documented: 1) cancel action semantics 2) snooze +10 minutes semantics 3) timeout(30s) = ignore -> snooze 4) overlap aggregation semantics 5) archive + gray render semantics 6) iOS degraded behavior note ``` **Step 2: Run verification of checklist** Run: manual review (expect FAIL before edits) **Step 3: Write minimal protocol spec** Include exact payload keys and action enum: ```json { "eventId": "uuid", "title": "string", "startAt": "iso8601", "endAt": "iso8601|null", "timezone": "IANA", "location": "string|null", "notes": "string|null", "color": "#RRGGBB|null", "mode": "single|aggregate", "aggregateIds": ["uuid"] } ``` Actions: - `cancel`: archive target events and stop reminders - `snooze_10m`: reschedule +10m, stop when `now >= endAt` - `timeout_30s`: same as `snooze_10m` **Step 4: Verify checklist passes** Run: manual review (expect PASS) **Step 5: Commit** ```bash git add docs/protocols docs/runtime/runtime-route.md git commit -m "docs: define reminder interaction protocol and lifecycle semantics" ``` ### Task 2: Add frontend reminder action models and payload codec **Files:** - Create: `apps/lib/features/calendar/reminders/models/reminder_payload.dart` - Create: `apps/lib/features/calendar/reminders/models/reminder_action.dart` - Test: `apps/test/features/calendar/reminders/models/reminder_payload_test.dart` **Step 1: Write the failing test** ```dart test('round-trips payload with single and aggregate modes', () { final payload = ReminderPayload(...); expect(ReminderPayload.fromJson(payload.toJson()), payload); }); ``` **Step 2: Run test to verify it fails** Run: `flutter test test/features/calendar/reminders/models/reminder_payload_test.dart` Expected: FAIL (type/file missing) **Step 3: Write minimal implementation** Implement immutable model + json codec + enum parser. **Step 4: Run test to verify it passes** Run: `flutter test test/features/calendar/reminders/models/reminder_payload_test.dart` Expected: PASS **Step 5: Commit** ```bash git add apps/lib/features/calendar/reminders/models apps/test/features/calendar/reminders/models/reminder_payload_test.dart git commit -m "feat: add reminder payload and action models" ``` ### Task 3: Refactor local notification service for action-capable reminders (Android + iOS) **Files:** - Modify: `apps/lib/core/notifications/local_notification_service.dart` - Modify: `apps/lib/main.dart` - Modify: `apps/ios/Runner/AppDelegate.swift` - Test: `apps/test/core/notifications/local_notification_service_test.dart` **Step 1: Write failing tests** ```dart test('uses alarm-style Android details with actions and timeout', () async {}); test('uses Darwin category actions for cancel/snooze', () async {}); test('encodes payload in notification details', () async {}); ``` **Step 2: Run tests to verify failure** Run: `flutter test test/core/notifications/local_notification_service_test.dart` Expected: FAIL **Step 3: Write minimal implementation** Implement: - Android notification actions: `cancel`, `snooze_10m` - `timeoutAfter: 30000` - payload serialization - iOS `DarwinNotificationCategory` + action identifiers - initialize callback registration for action responses Also keep existing full-screen alarm setup and exact alarm fallback behavior. **Step 4: Run tests to verify pass** Run: `flutter test test/core/notifications/local_notification_service_test.dart` Expected: PASS **Step 5: Commit** ```bash git add apps/lib/core/notifications/local_notification_service.dart apps/lib/main.dart apps/ios/Runner/AppDelegate.swift apps/test/core/notifications/local_notification_service_test.dart git commit -m "feat: support actionable reminder notifications on android and ios" ``` ### Task 4: Implement reminder action executor + local outbox for eventual consistency **Files:** - Create: `apps/lib/features/calendar/reminders/reminder_action_executor.dart` - Create: `apps/lib/features/calendar/reminders/reminder_outbox_store.dart` - Modify: `apps/lib/features/calendar/data/services/calendar_service.dart` - Test: `apps/test/features/calendar/reminders/reminder_action_executor_test.dart` **Step 1: Write failing tests** ```dart test('cancel archives remotely and cancels local reminders', () async {}); test('network failure writes outbox item and keeps local state updated', () async {}); test('snooze reschedules +10m and stops after endAt', () async {}); ``` **Step 2: Run tests (expect FAIL)** Run: `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart` **Step 3: Write minimal implementation** Rules: - Cancel: local cancel now, enqueue archive API job, best-effort immediate PATCH - Snooze: schedule at `now + 10m`, if `next >= endAt` then archive path - Timeout action uses same path as snooze **Step 4: Re-run tests (expect PASS)** Run: `flutter test test/features/calendar/reminders/reminder_action_executor_test.dart` **Step 5: Commit** ```bash git add apps/lib/features/calendar/reminders apps/lib/features/calendar/data/services/calendar_service.dart apps/test/features/calendar/reminders/reminder_action_executor_test.dart git commit -m "feat: add reminder action executor with offline outbox" ``` ### Task 5: Add startup reconciliation (replay outbox + rebuild reminders) **Files:** - Modify: `apps/lib/core/startup/auth_session_bootstrapper.dart` - Modify: `apps/lib/main.dart` - Test: `apps/test/core/startup/auth_session_bootstrapper_test.dart` **Step 1: Write failing tests** ```dart test('replays pending reminder actions after login', () async {}); test('rebuilds reminders after outbox replay', () async {}); ``` **Step 2: Run tests (expect FAIL)** Run: `flutter test test/core/startup/auth_session_bootstrapper_test.dart` **Step 3: Write minimal implementation** In authenticated startup flow: 1) replay outbox 2) fetch events with overlap semantics (active and not ended) 3) rebuild active reminders with compensation scheduling: - `now < remindAt`: schedule at remindAt - `remindAt <= now < endAt`: schedule immediate compensation reminder (e.g. +5s) - `now >= endAt`: archive path 4) enforce reminder dedupe key to avoid duplicate reminders after reinstall/restart **Step 4: Re-run tests (expect PASS)** Run: `flutter test test/core/startup/auth_session_bootstrapper_test.dart` **Step 5: Commit** ```bash git add apps/lib/core/startup/auth_session_bootstrapper.dart apps/lib/main.dart apps/test/core/startup/auth_session_bootstrapper_test.dart git commit -m "feat: replay reminder outbox on startup" ``` ### Task 6: Implement overlap strategy (aggregate popup) **Files:** - Create: `apps/lib/features/calendar/reminders/reminder_overlap_policy.dart` - Modify: `apps/lib/core/notifications/local_notification_service.dart` - Test: `apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart` **Step 1: Write failing tests** ```dart test('groups reminders whose fire time falls into same minute bucket', () {}); test('creates aggregate payload with top-3 preview and ids', () {}); ``` **Step 2: Run tests (expect FAIL)** Run: `flutter test test/features/calendar/reminders/reminder_overlap_policy_test.dart` **Step 3: Write minimal implementation** Policy: - same minute bucket => one aggregate popup - actions apply to all members by default - payload includes aggregateIds **Step 4: Re-run tests (expect PASS)** Run: `flutter test test/features/calendar/reminders/reminder_overlap_policy_test.dart` **Step 5: Commit** ```bash git add apps/lib/features/calendar/reminders/reminder_overlap_policy.dart apps/lib/core/notifications/local_notification_service.dart apps/test/features/calendar/reminders/reminder_overlap_policy_test.dart git commit -m "feat: add overlap aggregation policy for reminders" ``` ### Task 7: Render archived events as gray in calendar UI **Files:** - Modify: `apps/lib/features/calendar/ui/**` (event color resolution points) - Test: `apps/test/features/calendar/ui/*archived*test.dart` (new if missing) **Step 1: Write failing tests** ```dart testWidgets('archived events use gray token color', (tester) async {}); ``` **Step 2: Run tests (expect FAIL)** Run: `flutter test test/features/calendar/ui` **Step 3: Write minimal implementation** Rule: - if `status == archived`, force token-based gray (do not mutate persisted `metadata.color`) **Step 4: Re-run tests (expect PASS)** Run: `flutter test test/features/calendar/ui` **Step 5: Commit** ```bash git add apps/lib/features/calendar/ui apps/test/features/calendar/ui git commit -m "feat: render archived calendar events in gray" ``` ### Task 8: Backend reuse route + add expired-event auto-archive safety job **Files:** - Modify: `backend/src/v1/schedule_items/service.py` (if needed for stricter status transition) - Modify: `backend/src/v1/schedule_items/repository.py` (range query to overlap query) - Create: `backend/src/jobs/schedule_item_archive_job.py` (or existing worker module path) - Modify: worker scheduler registration file under `backend/src/core/celery/` (actual existing path) - Test: `backend/tests/unit/v1/schedule_items/test_service.py` - Test: `backend/tests/unit/v1/schedule_items/test_repository.py` - Test: `backend/tests/unit/jobs/test_schedule_item_archive_job.py` **Step 1: Write failing tests** ```python def test_patch_status_archived_allowed_for_owner() -> None: ... def test_list_by_overlap_includes_started_but_not_ended_items() -> None: ... def test_archive_job_marks_expired_active_items_archived() -> None: ... ``` **Step 2: Run tests (expect FAIL)** Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py backend/tests/unit/jobs/test_schedule_item_archive_job.py -q` **Step 3: Write minimal implementation** Implement/verify: - route reuse: PATCH status archived works as-is for authorized user - overlap query for bootstrap: `start_at <= window_end AND (end_at IS NULL OR end_at >= window_start)` and `status=active` - periodic archive job: `end_at < now and status=active -> archived` **Step 4: Re-run tests (expect PASS)** Run: `uv run pytest backend/tests/unit/v1/schedule_items/test_service.py backend/tests/unit/jobs/test_schedule_item_archive_job.py -q` **Step 5: Commit** ```bash git add backend/src backend/tests git commit -m "feat: add expired schedule auto-archive safety job" ``` ### Task 9: End-to-end verification and release notes **Files:** - Modify: `docs/runtime/runtime-runbook.md` - Modify: `docs/protocols/` reminder doc from Task 1 **Step 1: Run frontend verification** Run: - `flutter analyze` - `flutter test` Expected: PASS **Step 2: Run backend verification** Run: - `uv run pytest backend/tests/unit/v1/schedule_items -q` - `uv run pytest backend/tests/unit/jobs/test_schedule_item_archive_job.py -q` Expected: PASS **Step 3: Manual device matrix** Android: - app foreground/background/terminated for cancel/snooze/timeout - overlap popup behavior - endAt stop reminder + archive iOS: - action button behavior in foreground/background/terminated - timeout -> snooze behavior after relaunch sync - archive sync after offline period **Step 4: Document operational caveats** Include: - Android full-screen may degrade to heads-up by OEM policy - iOS does not guarantee Android-style full-screen alarm behavior - eventual consistency via outbox + startup replay + backend safety job **Step 5: Commit** ```bash git add docs/runtime/runtime-runbook.md docs/protocols git commit -m "docs: add reminder action runbook and platform caveats" ``` ## Notes on iOS parity - iOS supports actionable local notifications via Darwin categories; implement `cancel` and `snooze_10m` action identifiers with same payload model. - iOS cannot guarantee Android-like forced full-screen alarm takeover; use lock-screen alert + sound + action buttons as equivalent UX. - App-terminated network callback reliability is lower on iOS; therefore outbox + startup replay is mandatory for parity. ## Data contracts and constraints (added) ### Reminder payload contract ```json { "eventId": "uuid", "title": "string", "startAt": "iso8601-with-offset", "endAt": "iso8601-with-offset|null", "timezone": "IANA", "location": "string|null", "notes": "string|null", "color": "#RRGGBB|null", "mode": "single|aggregate", "aggregateIds": ["uuid"] } ``` Constraints: - `eventId` required and valid UUID. - `startAt` must be timezone-aware datetime. - `mode=aggregate` requires `aggregateIds.length >= 2`. - Payload versioning should be explicit if schema evolves. ### Reminder outbox contract ```json { "opId": "uuid", "eventId": "uuid", "action": "cancel|snooze_10m|timeout_30s|auto_archive", "targetStatus": "archived|null", "occurredAt": "iso8601-with-offset", "retryCount": 0, "nextRetryAt": "iso8601-with-offset|null", "state": "pending|done|dead", "lastError": "string|null" } ``` Constraints: - Idempotency key: `(eventId, action, occurredAtBucket)`. - Exponential backoff retries with capped max attempts. - `cancel` and `auto_archive` both map to backend `status=archived` PATCH. ### Uniqueness and dedupe rules - Notification identity uses deterministic key per event+cycle: `hash(eventId + cycleStartEpochMinutes + mode)`. - Before scheduling any reminder, cancel existing pending reminders for same dedupe key. - On bootstrap/reinstall, dedupe against local pending requests and outbox state before creating new schedules. - Compensation reminder (`remindAt <= now < endAt`) must generate exactly one immediate reminder per cycle window. ## Rollback plan 1) Disable action handling by feature flag while keeping plain reminders. 2) Keep backend PATCH status route unchanged (safe rollback path). 3) Pause auto-archive job if unexpected archival spikes occur.