- 新增 ReminderActionExecutor 处理取消/稍后提醒操作 - 新增 ReminderOutboxStore 本地存储待处理操作 - 重构 LocalNotificationService 支持聚合提醒和交互操作 - 新增 event_color_resolver 工具类统一颜色解析 - 新增 CalendarService.archiveEvent 归档方法 - 增强 ModelTracking 支持缓存命中、推理token和成本追踪 - 添加 qwen3.5-35b-a3b 模型配置 - 更新 AndroidManifest 全屏intent权限 - 补充相关单元测试和文档
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 reminderssnooze_10m: reschedule +10m, stop whennow >= endAttimeout_30s: same assnooze_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, ifnext >= endAtthen 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:
- replay outbox
- fetch events with overlap semantics (active and not ended)
- rebuild active reminders with compensation scheduling:
now < remindAt: schedule at remindAtremindAt <= now < endAt: schedule immediate compensation reminder (e.g. +5s)now >= endAt: archive path
- 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 persistedmetadata.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)andstatus=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 analyzeflutter test
Expected: PASS
Step 2: Run backend verification
Run:
uv run pytest backend/tests/unit/v1/schedule_items -quv 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
cancelandsnooze_10maction 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:
eventIdrequired and valid UUID.startAtmust be timezone-aware datetime.mode=aggregaterequiresaggregateIds.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.
cancelandauto_archiveboth map to backendstatus=archivedPATCH.
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
- Disable action handling by feature flag while keeping plain reminders.
- Keep backend PATCH status route unchanged (safe rollback path).
- Pause auto-archive job if unexpected archival spikes occur.