fix(state): persist to localStorage + explicit Disconnect (survive reload)

The Zustand store persisted to sessionStorage, which is tab-volatile — a
hard reload / new tab / closing the tab dropped the whole session,
including the board binding (device.connected, nodeUrl, teamId). The
workshop runs on the team's own laptop, so switch the store (and the
offline outbox) to localStorage: the binding + progress now survive a
refresh, hard reload, navigation, and tab close.

Clearing is now explicit only: a new `disconnect()` action drops the board
binding (keeping team/domain/phases/ADD so they can re-bind and resume),
surfaced as a "Disconnect" button on the claimed-board card. `reset()`
still wipes everything.

Tests: add an in-memory localStorage to setupTests (Node 22's disabled
experimental localStorage shadows jsdom's) + clear it per test.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-22 10:34:55 -07:00
co-authored by Claude Opus 4.8
parent 6a9fad9f10
commit c12f40837b
6 changed files with 65 additions and 14 deletions
+3 -3
View File
@@ -24,13 +24,13 @@ type OutboxItem = { kind: 'team'; payload: TeamSnapshot } | { kind: 'submission'
function readOutbox(): OutboxItem[] {
try {
return JSON.parse(sessionStorage.getItem(OUTBOX_KEY) ?? '[]') as OutboxItem[]
return JSON.parse(localStorage.getItem(OUTBOX_KEY) ?? '[]') as OutboxItem[]
} catch {
return []
}
}
function writeOutbox(items: OutboxItem[]): void {
sessionStorage.setItem(OUTBOX_KEY, JSON.stringify(items))
localStorage.setItem(OUTBOX_KEY, JSON.stringify(items))
}
function enqueue(item: OutboxItem): void {
// collapse duplicate team pushes — only the latest snapshot matters
@@ -83,7 +83,7 @@ export interface UseCollectiveSyncOptions {
* Best-effort, offline-first sync of this team's state to the collective.
* Mounted once near the router root. Pushes on phase changes and submission,
* throttles stat-driven churn, and never blocks the participant flow — failed
* pushes go to a sessionStorage outbox and retry on interval / `online`.
* pushes go to a localStorage outbox and retry on interval / `online`.
*/
export function useCollectiveSync(opts: UseCollectiveSyncOptions = {}): void {
const throttleMs = opts.throttleMs ?? 8000