From f0340ed425ba204e0a9d35832faa4e569e37ed5e Mon Sep 17 00:00:00 2001 From: OmikronApex Date: Tue, 14 Jul 2026 03:02:18 +0200 Subject: [PATCH] =?UTF-8?q?fix(audio-engine):=20single=20persistence=20sto?= =?UTF-8?q?re=20for=20device=20settings=20(TLC=20Part=20II=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The device config was persisted in TWO stores — the main process's file-backed settings AND localStorage['slopsmith-audio-device'] — merged on load by newest-savedAt. A main-side migration/reset left stale localStorage that could win the timestamp race and resurrect wiped settings, and a device re-save from either path re-persisted mute flags captured at that moment, interleaving with the (now-arbitrated) runtime mute writers. The file store is now the only write target. localStorage is treated as a one-time migration source: a strictly-newer browser copy is imported into the file store, then the key is deleted either way — after the first load the file is the single source of truth. Co-Authored-By: Claude Fable 5 --- src/renderer/screen.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/renderer/screen.js b/src/renderer/screen.js index 8f449ee..c7e5dae 100644 --- a/src/renderer/screen.js +++ b/src/renderer/screen.js @@ -210,11 +210,14 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; } function saveDeviceSettings(settings = captureDeviceSettings()) { + // Single persistence store (TLC Part II §4): the file-backed settings + // are the only writer target. The old parallel localStorage copy meant + // a main-side migration/reset could lose the timestamp race against a + // stale browser copy and resurrect wiped settings. const snapshot = { ...cloneDeviceSettings(settings), savedAt: Date.now(), }; - try { localStorage.setItem('slopsmith-audio-device', JSON.stringify(snapshot)); } catch (_) {} pendingDeviceSave = pendingDeviceSave .catch(() => null) .then(() => { @@ -255,17 +258,29 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {}; } catch (e) { console.warn('[audio-engine] Failed to load file-backed device settings:', e); } + // Migration only (TLC Part II §4): 'slopsmith-audio-device' was a + // second store racing the file on savedAt. Import a strictly-newer + // browser copy into the file store ONCE, then delete the key either + // way — after this the file is the single source of truth. let browserSettings = null; try { const raw = localStorage.getItem('slopsmith-audio-device'); browserSettings = normalizeDeviceSettings(raw ? JSON.parse(raw) : null); } catch { browserSettings = null; } - if (fileSettings && browserSettings) { - return getDeviceSettingsSavedAt(browserSettings) > getDeviceSettingsSavedAt(fileSettings) - ? browserSettings - : fileSettings; + if (browserSettings !== null) { + const browserNewer = !fileSettings + || getDeviceSettingsSavedAt(browserSettings) > getDeviceSettingsSavedAt(fileSettings); + if (browserNewer) { + try { + if (typeof api.saveDeviceSettings === 'function') await api.saveDeviceSettings(browserSettings); + } catch (e) { + console.warn('[audio-engine] device-settings migration save failed:', e); + } + } + try { localStorage.removeItem('slopsmith-audio-device'); } catch (_) {} + if (browserNewer) return browserSettings; } - return fileSettings || browserSettings; + return fileSettings; } function hasSettingValue(value) {