Files
social-app/docs/plans/2026-03-18-reminder-alert-archival-plan.md
T
qzl 00f37d7e19 feat: 实现日历提醒完整功能(操作执行、通知服务重构、归档)
- 新增 ReminderActionExecutor 处理取消/稍后提醒操作
- 新增 ReminderOutboxStore 本地存储待处理操作
- 重构 LocalNotificationService 支持聚合提醒和交互操作
- 新增 event_color_resolver 工具类统一颜色解析
- 新增 CalendarService.archiveEvent 归档方法
- 增强 ModelTracking 支持缓存命中、推理token和成本追踪
- 添加 qwen3.5-35b-a3b 模型配置
- 更新 AndroidManifest 全屏intent权限
- 补充相关单元测试和文档
2026-03-18 19:12:47 +08:00

15 KiB

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)

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:

{
  "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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{
  "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

{
  "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.