Fix fresh-install pull skipping due to empty-profile timestamp pollution

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: <time>" and "Pulled: <time>"
   in the Gist status area on page load, giving both browsers
   meaningful feedback.

4. service-worker.js: after a successful auto-sync pull, broadcast
   vault:unlocked to all open content-script tabs so substitution
   works immediately without a page reload.

https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
This commit is contained in:
Claude
2026-04-01 04:53:12 +00:00
parent 8b6f779a07
commit 7028f60b56
4 changed files with 37 additions and 4 deletions
+14
View File
@@ -472,6 +472,20 @@ api.alarms.onAlarm.addListener(async (alarm) => {
const result = await SilentSendSync.performAutoSync(); const result = await SilentSendSync.performAutoSync();
if (result.pulled) { if (result.pulled) {
console.log('[Silent Send] Auto-sync pulled new data'); console.log('[Silent Send] Auto-sync pulled new data');
// Broadcast decrypted data to all open content scripts so
// substitution works immediately without a page reload.
const mappings = await Storage.getMappings();
const identity = await Storage.getIdentity();
const settings = await Storage.getSettings();
const allPatterns = [...BUILTIN_URL_PATTERNS];
const customDomains = settings.customDomains || [];
for (const domain of customDomains) allPatterns.push(domain + '/*');
for (const urlPattern of allPatterns) {
const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []);
for (const tab of tabs) {
api.tabs.sendMessage(tab.id, { type: 'vault:unlocked', mappings, identity, settings }).catch(() => {});
}
}
} }
if (result.error) { if (result.error) {
console.warn('[Silent Send] Auto-sync error:', result.error); console.warn('[Silent Send] Auto-sync error:', result.error);
+13 -1
View File
@@ -226,7 +226,19 @@ const Storage = {
}, },
async saveProfiles(profiles) { async saveProfiles(profiles) {
await this._writeSecure(KEYS.IDENTITY, { profiles }, { ss_lastModified: Date.now() }); // Only advance ss_lastModified if at least one profile contains real PII.
// Creating the default empty profile on a fresh install should not count
// as "this browser has data" — that would make the pull timestamp check
// think remote Gist data is stale and skip the import.
const hasRealData = (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()
);
const extras = hasRealData ? { ss_lastModified: Date.now() } : {};
await this._writeSecure(KEYS.IDENTITY, { profiles }, extras);
}, },
async addProfile(name) { async addProfile(name) {
+2 -1
View File
@@ -751,7 +751,8 @@ const SilentSendSync = {
} }
const json = await resp.json(); const json = await resp.json();
await api.storage.local.set({ ss_gist_id: json.id }); const now = Date.now();
await api.storage.local.set({ ss_gist_id: json.id, ss_last_push_time: now, ss_last_push_source: 'gist' });
return { success: true, gistId: json.id }; return { success: true, gistId: json.id };
} catch (e) { } catch (e) {
return { success: false, reason: e.message }; return { success: false, reason: e.message };
+8 -2
View File
@@ -161,11 +161,17 @@ document.addEventListener('DOMContentLoaded', async () => {
// --- GitHub Gist sync --- // --- GitHub Gist sync ---
// Restore saved token (session only — never persisted to storage) // Restore saved token (session only — never persisted to storage)
{ {
const stored = await api.storage.local.get(['ss_gist_id', 'ss_last_pull_time', 'ss_last_pull_source']); const stored = await api.storage.local.get([
'ss_gist_id', 'ss_last_pull_time', 'ss_last_pull_source',
'ss_last_push_time', 'ss_last_push_source',
]);
if (stored.ss_gist_id) { if (stored.ss_gist_id) {
let status = `Gist ID: ${stored.ss_gist_id.slice(0, 12)}`; let status = `Gist ID: ${stored.ss_gist_id.slice(0, 12)}`;
if (stored.ss_last_push_time && stored.ss_last_push_source === 'gist') {
status += ` · Pushed: ${new Date(stored.ss_last_push_time).toLocaleString()}`;
}
if (stored.ss_last_pull_time && stored.ss_last_pull_source === 'gist') { if (stored.ss_last_pull_time && stored.ss_last_pull_source === 'gist') {
status += ` · Last pulled: ${new Date(stored.ss_last_pull_time).toLocaleString()}`; status += ` · Pulled: ${new Date(stored.ss_last_pull_time).toLocaleString()}`;
} }
setGistSyncStatus(status, 'ok'); setGistSyncStatus(status, 'ok');
} }