Offline writes now queue + apply optimistically instead of throwing.
On reconnect, the queue drains automatically; conflicts and rejections
surface as one-time SnackBars so the user knows their edit didn't land.
- Drift schema v3: `pending_writes` table (verb + payload + baseline +
tries + last_error). Negative ids serve as cache placeholders for
offline-created rows until replay assigns the server id.
- Each write repository (notes, tasks, projects, milestones, events)
now catches NetworkException, applies the change to cache (with a
fresh `nextTempId()` for creates), and enqueues the API call.
- Edits to a still-queued offline-created row coalesce into the
original create payload — only one server call per row, in order.
- Deletes of still-queued offline-created rows drop the queue entry
and the cache row; no server call ever happens.
- New WriteQueue service drains the queue oldest-first.
Server-wins on `updated_at` baseline check (notes/tasks/projects);
last-writer-wins for milestones/events (no getOne available).
4xx → drop + surface as `rejected`; conflict → drop + `overwritten`;
404 on update → drop + `missing`; 5xx/network → keep + retry next
online cycle.
- Replay fires on AuthStatus → authenticated transitions
(cold-start, came-back-online, login).
- OfflineBanner shows "Retry (N)" with the pending-writes count.
- Distinct ServerException added so 4xx no longer masquerade as
NetworkException — the queue can drop them instead of looping.
flutter analyze clean; 21 tests pass (3 new for QueueFailure messaging).
Phase 4 (read-only UI indicators on cached/temp rows) still ahead.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
- ProjectLibraryCard.onTap navigates to /projects/:id/tasks
- New ProjectTasksScreen: milestone sections with coloured dot,
done/total counter and thin progress bar; unassigned tasks at bottom
- Status cycle icon updates optimistically (pendingStatus map) so
the list doesn't flash on every tap; syncs global tasksProvider
- New route /projects/:id/tasks registered in app.dart
- TasksApi.getByProject + TasksRepository + projectTasksProvider (family)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TasksApi: add getSubTasks(parentId) + parentId param to create()
- TasksRepository: expose new API methods
- app.dart: invalidate projectsProvider + projectMilestonesProvider
after quick capture creates a task; taskNew route reads ?projectId=
query param and passes as initialProjectId to TaskEditScreen
- project_list_screen.dart: _AddTaskRow passes projectId query param
so the task editor pre-fills the project on navigation
- task_edit_screen.dart: fully rewritten with initialProjectId support,
sub-task fetching/creation, and _SubTasksSection widget
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>