From 8b6f779a07331abf652a2ac3f852b7d877ea8a61 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 04:43:52 +0000 Subject: [PATCH 1/3] Prevent empty-browser auto-sync from clobbering Gist data On a fresh browser install with auto-sync enabled, performAutoSync would push because config.lastPush was null (!config.lastPush = true). With _getAllData now returning lastModified: 0 for browsers with no saved data, this pushed a payload with lastModified: 0 to the Gist, overwriting the real data from the source browser. Subsequent pulls then saw remoteMod (0) <= local (0) and returned "Already up to date" without ever prompting for a password or importing anything. Three fixes: 1. performAutoSync: add local.lastModified > 0 guard to the push condition so a browser with no saved data never pushes in the background (both Gist and URL paths). 2. pushToGist / pushToUrl: return an explicit error if lastModified is 0, preventing a manual Push click on a fresh browser from clobbering the Gist too. 3. pullFromGist: if the Gist's lastModified is 0 (already clobbered), return a clear error message telling the user to push from the source browser first, rather than silently returning "up to date". https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53 --- src/lib/sync.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/lib/sync.js b/src/lib/sync.js index c5f8792..d01dbb0 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -709,6 +709,12 @@ const SilentSendSync = { try { const data = await this._getAllData(); + // Don't push if this browser has no saved data — it would overwrite + // real data on the Gist with an empty payload. + if (data.lastModified === 0) { + return { success: false, reason: 'Nothing to push — no data has been saved on this browser yet. Pull first.' }; + } + // Encrypt if enabled const encResult = await this._encryptForSync(data); if (encResult.needsAuth) { @@ -804,9 +810,14 @@ const SilentSendSync = { const rawResp = await fetch(file.raw_url); let data = JSON.parse(await rawResp.text()); - // Check if new data exists before requiring auth + // Check if new data exists before requiring auth. + // remoteMod === 0 means the Gist was clobbered by an empty push — treat + // as stale rather than skipping so the user sees a useful error. const local = await this._getAllData(); - const remoteMod = data._ssEncrypted ? data.lastModified : data.lastModified; + const remoteMod = data.lastModified || 0; + if (remoteMod === 0) { + return { success: false, reason: 'Gist contains no data (timestamp is 0). Push from the source browser first.' }; + } if (remoteMod <= (local.lastModified || 0)) { return { success: true, imported: false }; } @@ -839,6 +850,10 @@ const SilentSendSync = { try { const data = await this._getAllData(); + if (data.lastModified === 0) { + return { success: false, reason: 'Nothing to push — no data has been saved on this browser yet. Pull first.' }; + } + const encResult = await this._encryptForSync(data); if (encResult.needsAuth) { return { success: false, needsAuth: true, reason: 'Authentication required.' }; @@ -1032,9 +1047,9 @@ const SilentSendSync = { const pullResult = await this.pullFromGist(config.gistToken); if (pullResult.success && pullResult.imported) pulled = true; - // Push if local data changed since last push + // Push if local data changed since last push (skip if no real data yet) const local = await this._getAllData(); - if (!config.lastPush || local.lastModified > config.lastPush) { + if (local.lastModified > 0 && (!config.lastPush || local.lastModified > config.lastPush)) { const pushResult = await this.pushToGist(config.gistToken); if (pushResult.success) { pushed = true; @@ -1047,9 +1062,9 @@ const SilentSendSync = { const pullResult = await this.pullFromUrl({ url: config.url, headers }); if (pullResult.success && pullResult.imported) pulled = true; - // Push if local data changed since last push + // Push if local data changed since last push (skip if no real data yet) const local = await this._getAllData(); - if (!config.lastPush || local.lastModified > config.lastPush) { + if (local.lastModified > 0 && (!config.lastPush || local.lastModified > config.lastPush)) { const pushResult = await this.pushToUrl({ url: config.url, method: config.httpMethod || 'PUT', From 7028f60b561718ef786136db9a919d29e7e29ae0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 04:53:12 +0000 Subject: [PATCH 2/3] Fix fresh-install pull skipping due to empty-profile timestamp pollution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: when the popup opens for the first time it calls addProfile('Personal') + updateProfile(...) to create a default empty profile. Both call saveProfiles, which was unconditionally setting ss_lastModified: Date.now(). This made the new browser's local timestamp look like right now — newer than any Gist data pushed by the source browser — so every pull returned "Already up to date" without ever prompting for a password or importing anything. Fixes: 1. storage.js saveProfiles: only advance ss_lastModified when at least one profile contains real PII (non-empty real value in names, emails, usernames, phones, or catchAllEmail). Creating the default empty profile structure on first install leaves ss_lastModified at 0 so pulls correctly see remote data as newer. 2. sync.js pushToGist: persist ss_last_push_time and ss_last_push_source alongside ss_gist_id so the source browser (which only pushes) can also show its last activity time. 3. options.js: display both "Pushed: