const KIOSK_QUERY_VALUE = '1'; const REVISION_KEY = 'analyzer_kiosk_backup_revision_v1'; const SYNC_INTERVAL_MS = 2000; function isKioskBrowser() { try { return new URLSearchParams(globalThis.location?.search || '').get('kiosk') === KIOSK_QUERY_VALUE; } catch (_) { return false; } } function isPhoenixStorageKey(key) { return key !== REVISION_KEY && ( key.startsWith('analyzer_') || key.startsWith('MIGRATE_') || [ 'ppm_din_mode', 'calibration_notice_seen_v1', 'recorder_warning_ack_v1', 'ppm_din_loudness_warn_ack_v1', 'migrate_rta_peak_fall_default_v1', 'migrate_vu_offset_default_zero_v2', 'migrate_lr_delay_zero_to_0_05_v1', ].includes(key) ); } function collectStorage() { const storage = {}; try { for (let index = 0; index < localStorage.length; index += 1) { const key = localStorage.key(index); if (!key || !isPhoenixStorageKey(key)) continue; const value = localStorage.getItem(key); if (value !== null) storage[key] = value; } } catch (_) {} return storage; } function storageFingerprint(storage) { return JSON.stringify(Object.entries(storage).sort(([left], [right]) => left.localeCompare(right))); } function localRevision() { try { const value = Number(localStorage.getItem(REVISION_KEY)); return Number.isSafeInteger(value) && value >= 0 ? value : 0; } catch (_) { return 0; } } function setLocalRevision(revision) { try { localStorage.setItem(REVISION_KEY, String(revision)); } catch (_) {} } function applyStorage(storage, revision) { try { const remove = []; for (let index = 0; index < localStorage.length; index += 1) { const key = localStorage.key(index); if (key && isPhoenixStorageKey(key)) remove.push(key); } remove.forEach((key) => localStorage.removeItem(key)); Object.entries(storage || {}).forEach(([key, value]) => { if (isPhoenixStorageKey(key) && typeof value === 'string') localStorage.setItem(key, value); }); setLocalRevision(revision); return true; } catch (error) { console.warn('Kiosk browser restore failed:', error); return false; } } function apiUrl() { const hostname = String(globalThis.location?.hostname || '127.0.0.1'); return `http://${hostname}:8789/api/v1/kiosk-browser-config`; } async function readRemote() { const response = await fetch(`${apiUrl()}?_=${Date.now()}`, { cache: 'no-store' }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function writeRemote(baseRevision, storage) { const response = await fetch(apiUrl(), { method: 'POST', headers: { 'content-type': 'application/json' }, cache: 'no-store', body: JSON.stringify({ baseRevision, storage }), }); const payload = await response.json().catch(() => null); if (!response.ok) { const error = new Error(payload?.error || `HTTP ${response.status}`); error.status = response.status; throw error; } return payload; } export async function setupKioskBrowserBackup() { if (!isKioskBrowser()) return true; let remote; try { remote = await readRemote(); } catch (error) { console.warn('Kiosk browser backup unavailable:', error); return true; } let revision = Number(remote?.revision) || 0; const storedRevision = localRevision(); if (revision > storedRevision) { if (applyStorage(remote?.storage || {}, revision)) { globalThis.location?.reload?.(); return false; } } else if (revision === 0 && Object.keys(remote?.storage || {}).length === 0) { try { const result = await writeRemote(0, collectStorage()); revision = Number(result?.revision) || 1; setLocalRevision(revision); } catch (error) { console.warn('Initial kiosk browser backup failed:', error); } } else { setLocalRevision(revision); } let fingerprint = storageFingerprint(collectStorage()); let running = false; globalThis.setInterval(async () => { if (running) return; running = true; try { const latest = await readRemote(); const latestRevision = Number(latest?.revision) || 0; if (latestRevision > revision) { if (applyStorage(latest?.storage || {}, latestRevision)) globalThis.location?.reload?.(); return; } const storage = collectStorage(); const nextFingerprint = storageFingerprint(storage); if (nextFingerprint !== fingerprint) { const result = await writeRemote(revision, storage); revision = Number(result?.revision) || (revision + 1); setLocalRevision(revision); fingerprint = nextFingerprint; } } catch (error) { if (error?.status !== 409) console.warn('Kiosk browser backup sync failed:', error); } finally { running = false; } }, SYNC_INTERVAL_MS); return true; }