From e93e27012e9701f8385a36b10c61d835b4bc7e5a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Apr 2026 23:45:10 +0000 Subject: [PATCH] Fix fresh-install pull blocked by saveSettings timestamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a browser restart with no extension data, enabling encryption calls saveSettings() which writes ss_lastModified = Date.now(). All pull paths then compare that fresh timestamp against the remote's older timestamp, see local >= remote, and skip the pull with "already on latest version" — even though local is completely empty. Fix: add _hasRealLocalData() which returns true only if local storage contains actual identity profiles or mappings with real values. The timestamp skip condition now requires BOTH a newer local timestamp AND real local data. An empty/fresh install always falls through to pull. Affects pullFromGist, pullFromUrl, pullFromSyncStorage, and importSyncCode. https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo --- src/lib/sync.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/lib/sync.js b/src/lib/sync.js index 2a62e64..4a6c388 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -613,7 +613,7 @@ const SilentSendSync = { if (!force) { const local = await this._getAllData(); - if (local.lastModified && local.lastModified >= data.lastModified) { + if (local.lastModified && local.lastModified >= data.lastModified && this._hasRealLocalData(local)) { return { success: false, skipped: true, @@ -676,7 +676,7 @@ const SilentSendSync = { // Check if there's new data before requiring auth const local = await this._getAllData(); - if (local.lastModified && local.lastModified >= syncMeta.lastModified) return null; + if (local.lastModified && local.lastModified >= syncMeta.lastModified && this._hasRealLocalData(local)) return null; // New data exists — reassemble const chunkKeys = Array.from({ length: syncMeta.chunks }, (_, i) => SYNC_KEY_PREFIX + i); @@ -819,7 +819,7 @@ const SilentSendSync = { 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)) { + if (remoteMod <= (local.lastModified || 0) && this._hasRealLocalData(local)) { return { success: true, imported: false }; } @@ -882,7 +882,7 @@ const SilentSendSync = { const local = await this._getAllData(); const remoteMod = data._ssEncrypted ? data.lastModified : data.lastModified; - if (remoteMod <= (local.lastModified || 0)) { + if (remoteMod <= (local.lastModified || 0) && this._hasRealLocalData(local)) { return { success: true, imported: false }; } @@ -908,6 +908,25 @@ const SilentSendSync = { // Internal helpers // ---------------------------------------------------------------- + /** + * Returns true if local storage contains real user data (identity or mappings). + * Used to bypass the lastModified timestamp check on a fresh install where + * saveSettings() has written a recent timestamp even though there is no PII + * configured yet. Without this, a fresh browser after restart would always + * skip pulls because its saveSettings timestamp > the remote's older timestamp. + */ + _hasRealLocalData(local) { + if ((local.mappings || []).length > 0) return true; + const profiles = local.identity?.profiles || []; + return profiles.some(p => + (p.names || []).some(n => n.real?.trim()) || + (p.emails || []).some(e => e.real?.trim()) || + (p.usernames || []).some(u => u.real?.trim()) || + (p.phones || []).some(ph => ph.real?.trim()) || + p.catchAllEmail?.trim() + ); + }, + async _getAllData() { // Use dynamic import to avoid circular dependency const StorageModule = (await import('./storage.js')).default;