diff --git a/frontend/src/lib/layerPreferences.ts b/frontend/src/lib/layerPreferences.ts index 953fc51..71a3a61 100644 --- a/frontend/src/lib/layerPreferences.ts +++ b/frontend/src/lib/layerPreferences.ts @@ -94,10 +94,8 @@ export function getDefaultActiveLayers(): ActiveLayers { const ACTIVE_LAYER_KEYS = Object.keys(getDefaultActiveLayers()) as (keyof ActiveLayers)[]; const LAYER_BACKEND_SYNC_INTERVAL_MS = 5000; -let layerBackendSyncTimer: ReturnType | null = null; +let layerBackendSyncTimer: number | null = null; let layerBackendSyncInFlight = false; -let layerBackendSeenSuccess = false; -let layerBackendSawFailure = false; let repairedDefaultBackendState = false; function isBooleanRecord(value: unknown): value is Record { @@ -199,10 +197,9 @@ function emitLayerPreferencesChanged(layers: ActiveLayers): void { * The dashboard already saves the operator's layer choices locally. The backend, * however, resets its fetcher gates on process restart. This reconciliation loop * observes the existing GET /api/layers endpoint and replays the saved operator - * state only when a connection is first established, after an observed outage, - * or when the backend has unmistakably fallen back to its shipped defaults. - * It does not continuously overwrite arbitrary non-default server state, so two - * independent clients do not get into a five-second preference tug-of-war. + * state only when the backend is unmistakably back at its shipped defaults. + * It deliberately leaves other non-default server state alone, so independent + * clients do not get into a five-second preference tug-of-war. */ export async function reconcileActiveLayersWithBackend(): Promise<'synced' | 'noop' | 'offline'> { if (typeof window === 'undefined' || typeof fetch !== 'function') return 'offline'; @@ -228,11 +225,7 @@ export async function reconcileActiveLayersWithBackend(): Promise<'synced' | 'no const backend = mergeActiveLayers(defaults, rawLayers); const mismatch = !layerMapsEqual(desired, backend); const backendAtDefaults = layerMapsEqual(backend, defaults); - const shouldRepair = - mismatch && - (!layerBackendSeenSuccess || - layerBackendSawFailure || - (backendAtDefaults && !repairedDefaultBackendState)); + const shouldRepair = mismatch && backendAtDefaults && !repairedDefaultBackendState; if (shouldRepair) { const syncResponse = await fetch(`${API_BASE}/api/layers`, { @@ -245,18 +238,15 @@ export async function reconcileActiveLayersWithBackend(): Promise<'synced' | 'no } // useDataPolling already listens for this event and refreshes gated data immediately. window.dispatchEvent(new Event('sb:layer-toggle')); - repairedDefaultBackendState = backendAtDefaults; - layerBackendSeenSuccess = true; - layerBackendSawFailure = false; + repairedDefaultBackendState = true; return 'synced'; } + // Once the repaired non-default state is observable, arm the watchdog for a + // future backend restart that returns the process-local gate to defaults. if (!backendAtDefaults) repairedDefaultBackendState = false; - layerBackendSeenSuccess = true; - layerBackendSawFailure = false; return 'noop'; } catch { - layerBackendSawFailure = true; return 'offline'; } finally { layerBackendSyncInFlight = false; @@ -352,3 +342,7 @@ export function loadLayerSectionExpanded(): Record { export function saveLayerSectionExpanded(expanded: Record): void { writeDashboardPrefs({ layerSectionsExpanded: expanded }); } + +// Start the passive restart watchdog as soon as the preferences module is loaded +// in a real browser. SSR and Vitest are guarded inside ensureLayerBackendSync(). +ensureLayerBackendSync();