feat: offline-first client

All reads now come directly from Dexie; all mutations write to Dexie +
outbox immediately without waiting for the server. The background sync
loop (every 30s) pushes the outbox and pulls server changes.

Day/week close and reopen remain server-only (require server-side
computation). triggerSync() is called after them to update Dexie
promptly. The optimistic closedDaysMap update in the week page is kept
separate from the Dexie reload to avoid a race that was causing the
reopen button and day actions to disappear until a page reload.

- client.ts: remove online-first fetch paths; all reads from Dexie
- sync.ts: add triggerSync() and waitForSync() exports
- DayDetail: pass ClosedDay | null to oninvalidate after close/reopen
- week/+page.svelte: update closedDaysMap optimistically on close/reopen;
  only reload from Dexie on entry mutations
- settings/+page.svelte: read history() directly (never throws 503);
  derive current locally
- layout: remove offline banner and online.ts (behaviour is now
  identical online and offline)
This commit is contained in:
2026-05-01 16:35:02 +02:00
parent f4836a6fa2
commit 57697ec2aa
8 changed files with 205 additions and 266 deletions

24
PLAN.md
View File

@@ -215,11 +215,13 @@ Three regions:
### Offline Strategy
Fully offline-first: the UI always reads from and writes to Dexie (IndexedDB) without touching the network. A background sync loop (every 30 s) reconciles with the server.
1. App shell precached by Workbox.
2. All reads come from Dexie (IndexedDB); a sync worker reconciles with the server in the background.
3. All mutations create a local Dexie record with a client-generated UUIDv7 and an entry in a local `outbox` table.
4. When online, the outbox is flushed via `/api/sync/push`; responses update the local cache.
5. `/api/sync/pull` fetches any server-side changes since `last_pulled_version`.
2. **All reads** come directly from Dexie — no network latency in the hot path.
3. **All mutations** write to Dexie + outbox immediately and return. The background sync loop pushes the outbox via `POST /api/sync/push` and pulls server changes via `GET /api/sync/pull`.
4. **Exceptions**: day/week close and reopen require server-side computation and remain server-only. `triggerSync()` is called after them to update Dexie promptly without waiting 30 s.
5. On `410 Gone` (server pruned data the client hasn't seen), `coldStart()` wipes Dexie and re-pulls everything from version 0.
## 4. Sync Protocol
@@ -285,7 +287,17 @@ Full offline support with online-first, offline-fallback mutation strategy.
- `db.ts`: Dexie v3 — `settings_history` key path changed to `id` (string); upgrade handler clears the table for repopulation via pull.
- Settings page: `editingId` and ID params updated from `number` to `string`.
### M10 — Future
### M10 — Offline-first client ✅
Switched from online-first/offline-fallback to fully offline-first.
- `client.ts`: all reads now come directly from Dexie (no server fetch). All mutations write to Dexie + outbox immediately and return without waiting for the server.
- `sync.ts`: added `triggerSync()` for imperative sync after server-only mutations. Updated comment header.
- `DayDetail.svelte`, `week/+page.svelte`: call `triggerSync()` after day/week close and reopen so Dexie reflects server-computed state promptly.
- `+layout.svelte`: removed offline banner (no longer meaningful; behaviour is identical online or offline).
- `online.ts`: deleted (unused).
### M11 — Future
CSV/JSON export, monthly summary view.
## 7. Decisions & Rationale
@@ -308,4 +320,4 @@ CSV/JSON export, monthly summary view.
| Settings history PK | TEXT UUID (migration 003) | Consistent with other entities; enables offline create; `updated_at` enables last-write-wins sync |
| Sync prune strategy | Prune marker row at boundary version | No extra table; client detects stale state from the log itself; 410 triggers full re-sync |
| Sync conflict resolution | Last `updated_at` wins | Server is authoritative; simple to implement and reason about for single-user |
| Offline mutation flow | Online-first, offline-fallback | Server is primary; client writes to Dexie+outbox only on network failure; simpler than full local-first |
| Offline mutation flow | Offline-first | All reads/writes go through Dexie; background sync loop reconciles with server. Day/week close remains server-only (requires server computation); `triggerSync()` called after. |