# 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.
