feat(fase-14): sync_conflicts review route + chrome banner (14.2.3)

New page `/settings/sync-conflicts` that lists the user's last 100
sync_conflicts rows side-by-side (local vs remote payloads) with two
dismiss actions: "Discard local" and "Discard remote". Both actions
write the row id into a `syncConflictsDismissed` localStorage set —
migration 016 made sync_conflicts append-only at the RLS layer, and
Fase 14 ships no DB migration, so dismissal is per-device and the
underlying log is preserved for operator audits.

`(app)/+layout.svelte` now probes sync_conflicts once per session
(deferred out of the auth callback for the same lock-deadlock reason
the collectives query is) and feeds the count into a new
`unresolvedConflictsCount` store. When non-zero AND the user isn't
already on the review route, an amber pill banner offers a one-tap
"Review" link to the page.

OF-02 e2e plants a sync_conflicts row through the dev-only `__sb`
window client, reloads, asserts the banner, clicks through to the
route, discards the row, and asserts the empty state. Existing rows
are pre-dismissed via localStorage so the test is deterministic
regardless of accumulated history (we can't DELETE from the table).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 07:06:24 +02:00
parent c8c7f9f8c9
commit 45cca5072b
4 changed files with 271 additions and 0 deletions

View File

@@ -30,6 +30,13 @@ export const isOnline = readable<boolean>(browser ? navigator.onLine : true, (se
/** Number of pending ops in the offline queue (set by sync/queue integration). */
export const pendingOpsCount = writable<number>(0);
/**
* Fase 14.2.3 — number of `sync_conflicts` rows currently awaiting the
* user's attention (i.e. on the server AND not dismissed locally). Set
* by the `(app)/+layout.svelte` conflict probe; the banner reads it.
*/
export const unresolvedConflictsCount = writable<number>(0);
/** High-level status used by the UI banner. */
export const syncStatus: Readable<SyncState> = derived(
[isOnline, pendingOpsCount],