# Phase 20: Offline Mode Hardening - Context

**Gathered:** 2026-04-10
**Status:** Ready for planning

<domain>
## Phase Boundary

The dashboard adopts an offline-first architecture where all user writes go to local SQLite first, update the UI immediately, and are independently queued for remote sync. A clear offline indicator shows connectivity state, and the flusher drains pending edits to GH Issues on its own schedule. Sync never overwrites a pending local edit.

</domain>

<decisions>
## Implementation Decisions

### Offline Indicator UX
- **D-01:** The existing status dot (red/"Disconnected" text) is sufficient for offline indication. No additional banner, overlay, or dimmed UI needed.
- **D-02:** Binary online/offline model — no per-service status breakdown (GitHub vs Shopify). The existing `connection-status == 4` ("Connected, no Shopify") state is unrelated and stays as-is.

### Core Architecture (Offline-First)
- **D-03:** All writes go to local SQLite first and update the UI immediately. Every change is also enqueued as a pending edit for remote sync. The app's local database is the UI's source of truth at all times.
- **D-04:** Every write action — notes, card body, card title, serial state transitions, item/product changes — uses the local-first + queue pattern uniformly. No exceptions.
- **D-05:** While pending unflushed edits exist, the green status indicator circle "breathes" (gradual lighter-to-darker-to-lighter loop animation). When the queue is empty, it stays solid green. This is the only visual feedback for pending edits — no per-card sync icons, no pending count in the status bar.

### Flush-then-Sync Ordering
- **D-06:** The flusher runs continuously on its own short interval, independent of the sync cycle timer. They are decoupled — flusher does not block sync, sync does not block flusher.
- **D-07:** When sync pulls remote data and a pending edit exists for the same field on the same card, the local pending edit wins. Sync writes remote data to SQLite but skips any field that has a pending edit. The local version is authoritative until the edit is flushed and confirmed.
- **D-08:** Failed flushes retry with the existing exponential backoff strategy. After a max retry count (e.g., 5 attempts), the edit is dropped and a warning is logged. The local SQLite state remains as-is. No user notification for dropped edits.

### Connectivity Detection
- **D-09:** Post-sync-failure detection is sufficient. The `connection-status` property updates after each sync cycle attempt. No additional health pings, lightweight probes, or OS-level network listeners. Since the app works identically online or offline, there is no urgency to detect disconnection quickly.

### Claude's Discretion
- Exact breathing animation timing (pulse frequency, easing curve)
- Max retry count before dropping a failed edit (suggested: 5)
- Flusher interval when running continuously (currently 60s — may adjust)
- Implementation approach for field-level pending edit conflict avoidance during sync

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Data Architecture
- `.planning/DATA-FLOW.md` — Authoritative reference for all data entities, field sources, sync directions, and agent rules (RULE-03: SQLite is the single read source)

### Existing Implementation
- `crates/app/src/dashboard/pending_edit_flusher.rs` — Current PendingEditFlusher with exponential backoff, SQLite-backed queue
- `crates/app/src/dashboard/edit_queue.rs` — DEPRECATED file documenting migration from JSON queue to SQLite pending_edits table
- `crates/app/src/live_client.rs` — LiveClient sync thread, connection_status setting, 5-minute poll interval
- `crates/app/ui/dashboard.slint` — `connection-status` property (line 103-104), status dot rendering (lines 450-467)
- `crates/service/src/db/sqlite.rs` — SqliteStore including PendingEditRow and pending_edits table

### Requirements
- `.planning/REQUIREMENTS.md` — OFFLINE-01, OFFLINE-02, OFFLINE-03

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- **PendingEditFlusher** (`pending_edit_flusher.rs`): Already implements background flush loop with exponential backoff and retry counting. Needs to be extended to handle all write action types, not just SaveNote/UpdateCardBody/UpdateCardTitle.
- **SqliteStore::insert_pending_edit()**: Existing method to enqueue edits to the `pending_edits` SQLite table.
- **connection-status property**: Already wired in `dashboard.slint` with 5 states (0-4). The breathing animation will attach to the existing green dot (state 2).

### Established Patterns
- **Sync thread architecture**: LiveClient spawns a background thread that runs sync cycles every 5 minutes with early-wake on manual refresh. The flusher runs as a separate background thread.
- **SyncUpdateCallback**: After each sync, a callback updates the Slint UI on the event-loop thread. This is the mechanism for updating connection-status and card data.
- **PendingEditRow**: Struct with `id`, `card_id`, `edit_type`, `payload`, `retry_count`, `last_attempt` fields. The `edit_type` discriminator determines what the flusher does with each row.

### Integration Points
- **Sync pipeline conflict avoidance**: The sync cycle writes card data to SQLite via SqliteStore. The conflict avoidance logic (D-07) needs to be inserted here — before writing a synced field, check if a pending edit exists for that card+field.
- **Write action callsites**: Every place in the Rust code that currently writes to a remote API (GH Issues, etc.) needs to be redirected to write-local-then-queue instead.
- **Slint animation**: The breathing dot animation will be implemented in `dashboard.slint` using Slint's animation system, driven by a boolean property indicating whether pending edits exist.

</code_context>

<specifics>
## Specific Ideas

- The user explicitly described this as "all app actions should happen offline" — the offline-first model is the core architectural principle, not a fallback for disconnections.
- Breathing green dot animation was specifically requested as the pending-edit indicator. No other UI signals for queue state.
- The user wants local state updates to be "snappy and persistent in the UI" — immediate SQLite writes and UI updates, no loading spinners or confirmation waits for remote sync.

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope.

</deferred>

---

*Phase: 20-offline-mode-hardening*
*Context gathered: 2026-04-10*
