fix: date false-positive, partial-word highlights, cross-browser sync
Bug fixes: - Date (possible DOB) pattern now requires context words (born, birthday, dob, etc.) before firing — prevents spurious warnings on page-load API calls that happen to contain ISO dates in conversation history. - Highlight regex now uses word boundaries (\b) so short substitute values (e.g. "aud") no longer match inside unrelated words like "Claude". - Both TreeWalkers in content.js now skip the extension's own UI elements (.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge) to prevent the highlight API from marking text in the extension's banners. Settings sync: - New src/lib/sync.js: exportSyncCode / importSyncCode (base64 JSON) for manual copy-paste across any browser combination. Newest lastModified timestamp wins; force flag available to override. - browser.storage.sync support: when "Browser account sync" is enabled the extension automatically pushes/pulls via Firefox Sync or Chrome account, chunked to stay within per-item quota limits. - storage.js now writes ss_lastModified on every save so conflict resolution has an accurate timestamp. - service-worker.js listens for both local and sync storage changes to keep all copies in sync. - New "Sync Between Browsers" section in options.html with Generate/Copy/ Import Sync Code UI and the browser sync toggle. https://claude.ai/code/session_01TKpSR9M8JgHLXCp5CeDsQP
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
|
import SilentSendSync from '../lib/sync.js';
|
||||||
import api from '../lib/browser-polyfill.js';
|
import api from '../lib/browser-polyfill.js';
|
||||||
|
|
||||||
// Track substitution counts per tab
|
// Track substitution counts per tab
|
||||||
@@ -296,11 +297,24 @@ async function updateIcon(settings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update icon when settings, identity, or mappings change
|
// Update icon when settings, identity, or mappings change; push to sync storage if enabled
|
||||||
api.storage.onChanged.addListener(async (changes) => {
|
api.storage.onChanged.addListener(async (changes, areaName) => {
|
||||||
if (changes.ss_settings || changes.ss_identity || changes.ss_mappings) {
|
if (changes.ss_settings || changes.ss_identity || changes.ss_mappings) {
|
||||||
const settings = await Storage.getSettings();
|
const settings = await Storage.getSettings();
|
||||||
await updateIcon(settings);
|
await updateIcon(settings);
|
||||||
|
|
||||||
|
// Push to browser.storage.sync when local data changes (same-browser cross-device)
|
||||||
|
if (areaName === 'local' && settings.browserSync) {
|
||||||
|
await SilentSendSync.pushToSyncStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When sync storage changes (another device pushed new data), pull it into local
|
||||||
|
if (areaName === 'sync' && (changes.ss_sync_meta || Object.keys(changes).some(k => k.startsWith('ss_sync_chunk_')))) {
|
||||||
|
const settings = await Storage.getSettings();
|
||||||
|
if (settings.browserSync) {
|
||||||
|
await SilentSendSync.pullFromSyncStorage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -311,8 +325,11 @@ api.runtime.onInstalled.addListener(async () => {
|
|||||||
await updateIcon(settings);
|
await updateIcon(settings);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also set icon on startup (service worker wake)
|
// Also set icon on startup (service worker wake) + pull any newer sync data
|
||||||
(async () => {
|
(async () => {
|
||||||
const settings = await Storage.getSettings();
|
const settings = await Storage.getSettings();
|
||||||
await updateIcon(settings);
|
await updateIcon(settings);
|
||||||
|
if (settings.browserSync) {
|
||||||
|
await SilentSendSync.pullFromSyncStorage();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
+13
-5
@@ -327,7 +327,7 @@
|
|||||||
hint: 'GPS coordinates — pinpoints a location', cat: 'address' },
|
hint: 'GPS coordinates — pinpoints a location', cat: 'address' },
|
||||||
// Personal
|
// Personal
|
||||||
{ name: 'Date (possible DOB)', re: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g,
|
{ name: 'Date (possible DOB)', re: /\b(?:(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01])[-/](?:19|20)\d{2}|(?:19|20)\d{2}[-/](?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12]\d|3[01]))\b/g,
|
||||||
hint: 'Date — could be a birthday', cat: 'personal' },
|
hint: 'Date — could be a birthday', cat: 'personal', contextRequired: true },
|
||||||
{ name: 'EIN / Tax ID', re: /\b\d{2}-\d{7}\b/g,
|
{ name: 'EIN / Tax ID', re: /\b\d{2}-\d{7}\b/g,
|
||||||
hint: 'Could be a tax ID', cat: 'document' },
|
hint: 'Could be a tax ID', cat: 'document' },
|
||||||
// Paths not caught by smart patterns
|
// Paths not caught by smart patterns
|
||||||
@@ -344,8 +344,11 @@
|
|||||||
hint: 'Env variable with personal data', cat: 'env' },
|
hint: 'Env variable with personal data', cat: 'env' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CONTEXT_WORDS_RE = /\b(?:born|birthday|dob|birth|passport|license|driver|ssn|social\s*security|address|zip|postal|date\s+of\s+birth)\b/i;
|
||||||
|
|
||||||
function autoDetectPPI(text, ident) {
|
function autoDetectPPI(text, ident) {
|
||||||
if (!text || text.length < 5) return [];
|
if (!text || text.length < 5) return [];
|
||||||
|
const hasContext = CONTEXT_WORDS_RE.test(text);
|
||||||
|
|
||||||
// Build skip set from configured values
|
// Build skip set from configured values
|
||||||
const configured = new Set();
|
const configured = new Set();
|
||||||
@@ -360,6 +363,7 @@
|
|||||||
|
|
||||||
const findings = [];
|
const findings = [];
|
||||||
for (const pat of PPI_PATTERNS) {
|
for (const pat of PPI_PATTERNS) {
|
||||||
|
if (pat.contextRequired && !hasContext) continue;
|
||||||
pat.re.lastIndex = 0;
|
pat.re.lastIndex = 0;
|
||||||
let m;
|
let m;
|
||||||
while ((m = pat.re.exec(text)) !== null) {
|
while ((m = pat.re.exec(text)) !== null) {
|
||||||
@@ -796,7 +800,7 @@
|
|||||||
acceptNode(node) {
|
acceptNode(node) {
|
||||||
const parent = node.parentElement;
|
const parent = node.parentElement;
|
||||||
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
if (parent?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -846,7 +850,7 @@
|
|||||||
acceptNode(node) {
|
acceptNode(node) {
|
||||||
const parent = node.parentElement;
|
const parent = node.parentElement;
|
||||||
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
if (parent && SKIP_REVEAL_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
if (parent?.classList?.contains('ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
if (parent?.closest?.('.ss-autodetect-warning, .ss-presend-warning, .ss-reveal-badge')) return NodeFilter.FILTER_REJECT;
|
||||||
return NodeFilter.FILTER_ACCEPT;
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -857,9 +861,13 @@
|
|||||||
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
if (!text || text.length < MIN_STRING_LENGTH) continue;
|
||||||
|
|
||||||
for (const p of pairs) {
|
for (const p of pairs) {
|
||||||
const escaped = esc(settings.revealMode ? p.to : p.from);
|
|
||||||
const searchTerm = settings.revealMode ? p.to : p.from;
|
const searchTerm = settings.revealMode ? p.to : p.from;
|
||||||
const regex = new RegExp(escaped, p.caseSensitive ? 'g' : 'gi');
|
const escaped = esc(searchTerm);
|
||||||
|
// Add word boundaries when the term starts/ends with word chars to
|
||||||
|
// prevent partial-word matches (e.g. "aud" inside "Claude")
|
||||||
|
const bStart = /^\w/.test(searchTerm) ? '\\b' : '';
|
||||||
|
const bEnd = /\w$/.test(searchTerm) ? '\\b' : '';
|
||||||
|
const regex = new RegExp(bStart + escaped + bEnd, p.caseSensitive ? 'g' : 'gi');
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
|||||||
+4
-2
@@ -25,6 +25,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
maxLogEntries: 200,
|
maxLogEntries: 200,
|
||||||
customDomains: [],
|
customDomains: [],
|
||||||
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'general'],
|
||||||
|
browserSync: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Storage = {
|
const Storage = {
|
||||||
@@ -36,7 +37,7 @@ const Storage = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveMappings(mappings) {
|
async saveMappings(mappings) {
|
||||||
await api.storage.local.set({ [KEYS.MAPPINGS]: mappings });
|
await api.storage.local.set({ [KEYS.MAPPINGS]: mappings, ss_lastModified: Date.now() });
|
||||||
},
|
},
|
||||||
|
|
||||||
async addMapping(mapping) {
|
async addMapping(mapping) {
|
||||||
@@ -96,7 +97,7 @@ const Storage = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveProfiles(profiles) {
|
async saveProfiles(profiles) {
|
||||||
await api.storage.local.set({ [KEYS.IDENTITY]: { profiles } });
|
await api.storage.local.set({ [KEYS.IDENTITY]: { profiles }, ss_lastModified: Date.now() });
|
||||||
},
|
},
|
||||||
|
|
||||||
async addProfile(name) {
|
async addProfile(name) {
|
||||||
@@ -210,6 +211,7 @@ const Storage = {
|
|||||||
const current = await this.getSettings();
|
const current = await this.getSettings();
|
||||||
await api.storage.local.set({
|
await api.storage.local.set({
|
||||||
[KEYS.SETTINGS]: { ...current, ...settings },
|
[KEYS.SETTINGS]: { ...current, ...settings },
|
||||||
|
ss_lastModified: Date.now(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Silent Send - Cross-Browser Settings Sync
|
||||||
|
*
|
||||||
|
* Two sync mechanisms:
|
||||||
|
*
|
||||||
|
* 1. Sync Code — base64-encoded JSON snapshot for manual copy-paste between
|
||||||
|
* browsers (works across any browser/device combination).
|
||||||
|
*
|
||||||
|
* 2. browser.storage.sync — automatic sync within the same browser family
|
||||||
|
* (Firefox ↔ Firefox via Firefox Sync, Chrome ↔ Chrome via Google account).
|
||||||
|
* Data is chunked to stay within per-item size limits.
|
||||||
|
*
|
||||||
|
* Conflict resolution: newest `lastModified` timestamp wins.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import api from './browser-polyfill.js';
|
||||||
|
|
||||||
|
// browser.storage.sync per-item limit (leave generous headroom)
|
||||||
|
const SYNC_CHUNK_SIZE = 5000;
|
||||||
|
const SYNC_KEY_PREFIX = 'ss_sync_chunk_';
|
||||||
|
const SYNC_META_KEY = 'ss_sync_meta';
|
||||||
|
|
||||||
|
const SilentSendSync = {
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Sync Code — manual cross-browser copy-paste
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export all settings/mappings/identity as a compact base64 sync code.
|
||||||
|
* Share this code with another browser to import.
|
||||||
|
*/
|
||||||
|
async exportSyncCode() {
|
||||||
|
const data = await this._getAllData();
|
||||||
|
const json = JSON.stringify(data);
|
||||||
|
// btoa requires ASCII; use encodeURIComponent to handle Unicode
|
||||||
|
return btoa(unescape(encodeURIComponent(json)));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import from a sync code string.
|
||||||
|
* Returns { success, importTime } or { success: false, skipped?, reason, localTime?, importTime? }
|
||||||
|
*
|
||||||
|
* Conflict resolution: newest timestamp wins.
|
||||||
|
* Pass force=true to override even if local is newer.
|
||||||
|
*/
|
||||||
|
async importSyncCode(code, { force = false } = {}) {
|
||||||
|
try {
|
||||||
|
const json = decodeURIComponent(escape(atob(code.trim())));
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
|
||||||
|
if (!data.version || !data.lastModified) {
|
||||||
|
return { success: false, reason: 'Invalid sync code — missing version or timestamp.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
const local = await this._getAllData();
|
||||||
|
if (local.lastModified && local.lastModified >= data.lastModified) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
skipped: true,
|
||||||
|
reason: 'Local data is the same age or newer.',
|
||||||
|
localTime: new Date(local.lastModified).toLocaleString(),
|
||||||
|
importTime: new Date(data.lastModified).toLocaleString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._applyData(data);
|
||||||
|
return { success: true, importTime: new Date(data.lastModified).toLocaleString() };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, reason: e.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// browser.storage.sync — automatic within-browser-family sync
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push current local data to browser.storage.sync (chunked).
|
||||||
|
* Called automatically whenever settings, mappings, or identity change
|
||||||
|
* and browserSync setting is enabled.
|
||||||
|
*/
|
||||||
|
async pushToSyncStorage() {
|
||||||
|
if (!api.storage?.sync) return;
|
||||||
|
try {
|
||||||
|
const data = await this._getAllData();
|
||||||
|
const json = JSON.stringify(data);
|
||||||
|
|
||||||
|
// Split into chunks to stay under per-item quota
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < json.length; i += SYNC_CHUNK_SIZE) {
|
||||||
|
chunks.push(json.slice(i, i + SYNC_CHUNK_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stale chunks
|
||||||
|
const staleKeys = await this._getSyncChunkKeys();
|
||||||
|
if (staleKeys.length > 0) {
|
||||||
|
await api.storage.sync.remove(staleKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write new chunks + metadata in one shot
|
||||||
|
const toWrite = {
|
||||||
|
[SYNC_META_KEY]: { chunks: chunks.length, lastModified: data.lastModified },
|
||||||
|
};
|
||||||
|
chunks.forEach((chunk, i) => { toWrite[SYNC_KEY_PREFIX + i] = chunk; });
|
||||||
|
await api.storage.sync.set(toWrite);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Silent Send] pushToSyncStorage failed:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull from browser.storage.sync and apply if newer than local.
|
||||||
|
* Returns { imported: true, time } if data was applied, null otherwise.
|
||||||
|
*/
|
||||||
|
async pullFromSyncStorage() {
|
||||||
|
if (!api.storage?.sync) return null;
|
||||||
|
try {
|
||||||
|
const metaResult = await api.storage.sync.get(SYNC_META_KEY);
|
||||||
|
const syncMeta = metaResult[SYNC_META_KEY];
|
||||||
|
if (!syncMeta?.chunks) return null;
|
||||||
|
|
||||||
|
// Skip if local is already up to date
|
||||||
|
const local = await this._getAllData();
|
||||||
|
if (local.lastModified && local.lastModified >= syncMeta.lastModified) return null;
|
||||||
|
|
||||||
|
// Reassemble chunks
|
||||||
|
const chunkKeys = Array.from({ length: syncMeta.chunks }, (_, i) => SYNC_KEY_PREFIX + i);
|
||||||
|
const chunkResult = await api.storage.sync.get(chunkKeys);
|
||||||
|
const json = chunkKeys.map(k => chunkResult[k] || '').join('');
|
||||||
|
const data = JSON.parse(json);
|
||||||
|
|
||||||
|
await this._applyData(data);
|
||||||
|
return { imported: true, time: new Date(data.lastModified).toLocaleString() };
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[Silent Send] pullFromSyncStorage failed:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
async _getAllData() {
|
||||||
|
const result = await api.storage.local.get(null);
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
lastModified: result.ss_lastModified || Date.now(),
|
||||||
|
identity: result.ss_identity || {},
|
||||||
|
mappings: result.ss_mappings || [],
|
||||||
|
settings: result.ss_settings || {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async _applyData(data) {
|
||||||
|
const toSet = { ss_lastModified: data.lastModified };
|
||||||
|
if (data.identity !== undefined) toSet.ss_identity = data.identity;
|
||||||
|
if (data.mappings !== undefined) toSet.ss_mappings = data.mappings;
|
||||||
|
if (data.settings !== undefined) toSet.ss_settings = data.settings;
|
||||||
|
await api.storage.local.set(toSet);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _getSyncChunkKeys() {
|
||||||
|
if (!api.storage?.sync) return [];
|
||||||
|
try {
|
||||||
|
const all = await api.storage.sync.get(null);
|
||||||
|
return Object.keys(all).filter(k => k.startsWith(SYNC_KEY_PREFIX));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SilentSendSync;
|
||||||
@@ -96,13 +96,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sync Between Browsers</h2>
|
||||||
|
<p class="section-desc">Keep your identities, mappings, and settings in sync across browsers.</p>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<div>
|
||||||
|
<label>Browser account sync</label>
|
||||||
|
<p class="setting-desc">Automatically sync via your browser account (Firefox Sync / Chrome account). Works across devices using the same browser.</p>
|
||||||
|
</div>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="browserSync">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:16px">
|
||||||
|
<h3 style="font-size:13px;font-weight:600;margin:0 0 4px">Sync Code — cross-browser copy-paste</h3>
|
||||||
|
<p class="section-desc" style="margin-bottom:8px">Generate a code in one browser, paste it in another. Newest data wins automatically.</p>
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<button class="btn btn-primary" id="btnGenerateSyncCode">Generate Sync Code</button>
|
||||||
|
<button class="btn" id="btnImportSyncCode">Import from Code</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="syncCodeDisplay" style="display:none;margin-top:10px">
|
||||||
|
<textarea id="syncCodeText" rows="3" style="width:100%;box-sizing:border-box;font-family:monospace;font-size:11px;resize:vertical;padding:6px;border:1px solid #d1d5db;border-radius:6px" readonly></textarea>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;margin-top:6px">
|
||||||
|
<button class="btn btn-primary" id="btnCopySyncCode">Copy to Clipboard</button>
|
||||||
|
<span id="syncCodeTime" style="font-size:11px;color:#6b7280"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="syncImportSection" style="display:none;margin-top:10px">
|
||||||
|
<textarea id="syncImportText" rows="3" placeholder="Paste sync code here…" style="width:100%;box-sizing:border-box;font-family:monospace;font-size:11px;resize:vertical;padding:6px;border:1px solid #d1d5db;border-radius:6px"></textarea>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;margin-top:6px;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-primary" id="btnApplySyncCode">Apply</button>
|
||||||
|
<button class="btn" id="btnCancelSyncImport">Cancel</button>
|
||||||
|
<label style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer">
|
||||||
|
<input type="checkbox" id="syncForce"> Force (override if local is newer)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="syncStatus" style="margin-top:8px;font-size:12px;min-height:16px"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2>Transfer Data</h2>
|
<h2>Transfer Data</h2>
|
||||||
<p class="section-desc">Export all your identities, mappings, and settings to move between browsers. Encrypted exports require a password to decrypt.</p>
|
<p class="section-desc">Export all your identities, mappings, and settings to a file. Encrypted exports require a password to decrypt.</p>
|
||||||
<div class="bulk-actions">
|
<div class="bulk-actions">
|
||||||
<button class="btn" id="btnExportAll">Export All (plain)</button>
|
<button class="btn" id="btnExportAll">Export All (plain)</button>
|
||||||
<button class="btn" id="btnExportEncrypted">Export Encrypted</button>
|
<button class="btn" id="btnExportEncrypted">Export Encrypted</button>
|
||||||
<button class="btn" id="btnImportAll">Import</button>
|
<button class="btn" id="btnImportAll">Import from File</button>
|
||||||
<input type="file" id="fileImportAll" accept=".json,.ssbackup" hidden>
|
<input type="file" id="fileImportAll" accept=".json,.ssbackup" hidden>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Storage from '../lib/storage.js';
|
import Storage from '../lib/storage.js';
|
||||||
import SilentSendCrypto from '../lib/crypto.js';
|
import SilentSendCrypto from '../lib/crypto.js';
|
||||||
|
import SilentSendSync from '../lib/sync.js';
|
||||||
import api from '../lib/browser-polyfill.js';
|
import api from '../lib/browser-polyfill.js';
|
||||||
|
|
||||||
let mappings = [];
|
let mappings = [];
|
||||||
@@ -18,11 +19,82 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
|
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
|
||||||
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
$('#autoAddDetected').checked = settings.autoAddDetected !== false;
|
||||||
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
$('#maxLogEntries').value = settings.maxLogEntries || 200;
|
||||||
|
$('#browserSync').checked = settings.browserSync === true;
|
||||||
|
|
||||||
renderMappings();
|
renderMappings();
|
||||||
renderDomains();
|
renderDomains();
|
||||||
renderLog();
|
renderLog();
|
||||||
|
|
||||||
|
// --- Sync section ---
|
||||||
|
$('#browserSync').addEventListener('change', async (e) => {
|
||||||
|
await Storage.saveSettings({ browserSync: e.target.checked });
|
||||||
|
if (e.target.checked) {
|
||||||
|
await SilentSendSync.pushToSyncStorage();
|
||||||
|
setSyncStatus('Browser account sync enabled. Your settings will sync automatically.', 'ok');
|
||||||
|
} else {
|
||||||
|
setSyncStatus('Browser account sync disabled.', 'neutral');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnGenerateSyncCode').addEventListener('click', async () => {
|
||||||
|
const code = await SilentSendSync.exportSyncCode();
|
||||||
|
const data = await SilentSendSync._getAllData();
|
||||||
|
$('#syncCodeText').value = code;
|
||||||
|
$('#syncCodeDisplay').style.display = 'block';
|
||||||
|
$('#syncImportSection').style.display = 'none';
|
||||||
|
$('#syncCodeTime').textContent = 'Generated: ' + new Date(data.lastModified).toLocaleString();
|
||||||
|
setSyncStatus('', 'neutral');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnCopySyncCode').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText($('#syncCodeText').value);
|
||||||
|
$('#btnCopySyncCode').textContent = 'Copied!';
|
||||||
|
setTimeout(() => { $('#btnCopySyncCode').textContent = 'Copy to Clipboard'; }, 2000);
|
||||||
|
} catch {
|
||||||
|
$('#syncCodeText').select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnImportSyncCode').addEventListener('click', () => {
|
||||||
|
$('#syncImportSection').style.display = 'block';
|
||||||
|
$('#syncCodeDisplay').style.display = 'none';
|
||||||
|
$('#syncImportText').focus();
|
||||||
|
setSyncStatus('', 'neutral');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnApplySyncCode').addEventListener('click', async () => {
|
||||||
|
const code = $('#syncImportText').value.trim();
|
||||||
|
if (!code) return;
|
||||||
|
const force = $('#syncForce').checked;
|
||||||
|
const result = await SilentSendSync.importSyncCode(code, { force });
|
||||||
|
if (result.success) {
|
||||||
|
setSyncStatus(`Imported successfully (data from ${result.importTime}).`, 'ok');
|
||||||
|
$('#syncImportSection').style.display = 'none';
|
||||||
|
$('#syncImportText').value = '';
|
||||||
|
mappings = await Storage.getMappings();
|
||||||
|
settings = await Storage.getSettings();
|
||||||
|
$('#browserSync').checked = settings.browserSync === true;
|
||||||
|
renderMappings();
|
||||||
|
renderDomains();
|
||||||
|
renderLog();
|
||||||
|
} else if (result.skipped) {
|
||||||
|
setSyncStatus(
|
||||||
|
`Skipped: local data is newer (local: ${result.localTime} vs import: ${result.importTime}). Check "Force" to override.`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setSyncStatus(`Failed: ${result.reason}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btnCancelSyncImport').addEventListener('click', () => {
|
||||||
|
$('#syncImportSection').style.display = 'none';
|
||||||
|
$('#syncImportText').value = '';
|
||||||
|
setSyncStatus('', 'neutral');
|
||||||
|
});
|
||||||
|
|
||||||
// Transfer data
|
// Transfer data
|
||||||
$('#btnExportAll').addEventListener('click', exportAllPlain);
|
$('#btnExportAll').addEventListener('click', exportAllPlain);
|
||||||
$('#btnExportEncrypted').addEventListener('click', exportAllEncrypted);
|
$('#btnExportEncrypted').addEventListener('click', exportAllEncrypted);
|
||||||
@@ -393,6 +465,13 @@ async function importAll(e) {
|
|||||||
e.target.value = '';
|
e.target.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSyncStatus(msg, type) {
|
||||||
|
const el = $('#syncStatus');
|
||||||
|
if (!el) return;
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.color = type === 'ok' ? '#10b981' : type === 'warn' ? '#f59e0b' : type === 'error' ? '#dc2626' : '#6b7280';
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = str;
|
div.textContent = str;
|
||||||
|
|||||||
Reference in New Issue
Block a user