From 91e4962a43912bead2b7e68063180e37a2b1ea24 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 7 May 2026 14:54:47 -0400 Subject: [PATCH 1/2] fix: prevent false-positive conflict toast on first write to unbaselined keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verifyWrite was treating any stale readback as a device conflict when the key had no prior baseline entry (first-ever write, or fresh session after a firmware update rotates the git commit and clears the cache). valuesEqual returns false when compared against undefined, so any value from the poll that didn't match desiredValue immediately triggered "was changed on the device" and rolled back the UI — even though the write had succeeded. Two fixes: - batchPush: skip conflict detection in verifyWrite when baselineAtPushTime has no entry for the key; fall through to optimistic confirm and let drift detection surface genuine mismatches on the next poll cycle. - +layout.svelte: after the background prefetch, backfill the driftStore baseline with any freshly-fetched key that isn't already there. Closes the root cause so the guard rarely triggers in practice. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/lib/stores/batchPush.svelte.ts | 4 ++++ src/routes/dashboard/settings/+layout.svelte | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/lib/stores/batchPush.svelte.ts b/src/lib/stores/batchPush.svelte.ts index 35754f6a..aa0d828f 100644 --- a/src/lib/stores/batchPush.svelte.ts +++ b/src/lib/stores/batchPush.svelte.ts @@ -346,6 +346,10 @@ class BatchPushStore { // means the device changed it independently → conflict. // Equal to baseline → write pending, not a conflict. const baselineVal = baselineAtPushTime[item.key]; + if (!Object.prototype.hasOwnProperty.call(baselineAtPushTime, item.key)) { + allMatch = false; + continue; + } if (!this.valuesEqual(deviceValue, baselineVal)) { conflicts.push({ key: item.key, deviceValue }); } diff --git a/src/routes/dashboard/settings/+layout.svelte b/src/routes/dashboard/settings/+layout.svelte index 6eb8a1c4..0cefe2ad 100644 --- a/src/routes/dashboard/settings/+layout.svelte +++ b/src/routes/dashboard/settings/+layout.svelte @@ -542,6 +542,19 @@ const resolvedKeys = Object.keys(freshValues).filter((k) => !driftedKeys.has(k)); if (resolvedKeys.length > 0) driftStore.resolveKeys(did, resolvedKeys); } + + { + const currentBaseline = driftStore.getBaseline(did); + const additions: Record = {}; + for (const [key, val] of Object.entries(freshValues)) { + if (!Object.prototype.hasOwnProperty.call(currentBaseline, key)) { + additions[key] = val; + } + } + if (Object.keys(additions).length > 0) { + driftStore.updateBaseline(did, { ...currentBaseline, ...additions }); + } + } } catch { // Errors are non-fatal — flags still cleared in finally so the UI // recovers from spinner-stuck state even on partial failure. From fe3df8d1902edfd7f9a1a12f862b629707c8b3c3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 13 May 2026 00:42:36 -0400 Subject: [PATCH 2/2] fix: widen verify settle window to cover slow POST round-trips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 500ms was too aggressive for uncommon keys (Ford etc.) where POST takes 1.2–3.5s. Readback fired before the write applied, producing false-positive "changed on device" toasts. 2.5s covers observed round-trips with no UX cost — confirmKeys already fires optimistically. --- src/lib/stores/batchPush.svelte.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/stores/batchPush.svelte.ts b/src/lib/stores/batchPush.svelte.ts index aa0d828f..6e96cab4 100644 --- a/src/lib/stores/batchPush.svelte.ts +++ b/src/lib/stores/batchPush.svelte.ts @@ -12,7 +12,7 @@ import { toast } from 'svelte-sonner'; const DEBOUNCE_MS = 4_000; const CONFIRMED_CLEAR_MS = 3_000; -const VERIFY_SETTLE_MS = 500; // give the device a beat before the readback +const VERIFY_SETTLE_MS = 2_500; // POST round-trip for uncommon keys (Ford etc.) can exceed 3s const VERIFY_POLL_INTERVAL_MS = 0; // unused: single-shot verify now const VERIFY_MAX_ATTEMPTS = 1; // one quick readback; fall through to optimistic confirm on miss (was 9s — caused 20s spinner regression) @@ -303,10 +303,9 @@ class BatchPushStore { * causes false-positive conflicts when compared against the boolean * desiredValue. * - * Timing budget is tight (user perceives anything past ~4s as a regression): - * one short settle, single read. If we can't confirm, fall back to - * optimistic confirm — periodic drift detection will surface any mismatch - * later. Previous 6-attempt loop added ~9s to every push. */ + * Settle (2.5s) accounts for slow POST round-trips (Ford/uncommon keys + * observed at 1.2–3.5s). confirmKeys fires optimistically before this + * runs, so the settle window has no UX cost. */ private async verifyWrite( deviceId: string, keys: string[],