feat(offline): tier 2 phase 2 — extend cache to tasks/projects/milestones/events/conversations

Phase 1 cached only notes. Phase 2 brings the read-through pattern to every
domain the app reads in bulk:

- Drift schema v2: 5 new cached_* tables + onUpgrade migration. Calendar
  events use range-scoped read/write (replaceEventsInRange) so disjoint
  month fetches don't clobber each other; milestones are per-project.
- Wrap TasksRepository, ProjectsRepository, MilestonesRepository, and
  ChatRepository (conversation list only) with NetworkException fallback.
  Writes hit the API then sync the cache.
- New EventsRepository wrapping EventsApi; calendar_provider and
  event_form_sheet repointed at the repository.
- OfflineBanner now surfaces getLatestSync() — most-recent timestamp
  across all domains — so the hint reads sensibly regardless of screen.

flutter analyze clean; existing tests pass. Phase 3 (offline write queue)
and Phase 4 (read-only UI indicators) still to come.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 21:22:06 -04:00
parent 6ef658558a
commit fe10067761
12 changed files with 5376 additions and 104 deletions
+3 -3
View File
@@ -47,7 +47,7 @@ class CalendarNotifier extends AsyncNotifier<CalendarState> {
// Fetch current month ± 1 month as the initial window.
final from = DateTime(now.year, now.month - 1, 1);
final to = DateTime(now.year, now.month + 2, 0, 23, 59, 59);
final events = await ref.watch(eventsApiProvider).getEvents(from, to);
final events = await ref.watch(eventsRepositoryProvider).getEvents(from, to);
return CalendarState(
eventsByDay: _groupByDay(events),
selectedDay: today,
@@ -60,7 +60,7 @@ class CalendarNotifier extends AsyncNotifier<CalendarState> {
Future<void> refresh() async {
final current = state.value;
if (current == null) return;
final events = await ref.read(eventsApiProvider).getEvents(
final events = await ref.read(eventsRepositoryProvider).getEvents(
current.loadedRange.start,
current.loadedRange.end,
);
@@ -93,7 +93,7 @@ class CalendarNotifier extends AsyncNotifier<CalendarState> {
try {
final from = DateTime(month.year, month.month, 1);
final to = DateTime(month.year, month.month + 1, 0, 23, 59, 59);
final events = await ref.read(eventsApiProvider).getEvents(from, to);
final events = await ref.read(eventsRepositoryProvider).getEvents(from, to);
final s = state.value!;
final merged = Map<DateTime, List<CalendarEvent>>.from(s.eventsByDay);
for (final e in events) {