diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5e21d..31c295b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.9.5] + +- Added Backup & Restore to the options page — download all extension data (model snapshots, export history, preferences) to a JSON file and restore it later +- Survives uninstall/reinstall, and lets you move data between browsers, devices, or extension builds (e.g. store version ↔ GitHub build) + ## [1.9.4] - Browse table's Model column now shows each chat's original (first-seen) model from the snapshot data, falling back to the current/inferred model when no snapshot exists diff --git a/TODO.md b/TODO.md index 2e49f4e..fbcc3d3 100644 --- a/TODO.md +++ b/TODO.md @@ -128,6 +128,12 @@ - Moved `formatModelName`, `getModelBadgeClass`, `DEFAULT_MODEL_TIMELINE` out of `content.js`/`browse.js` into shared `utils.js` - Doc-linked the Anthropic model-ID schema in code comments +- **Backup & Restore for extension data** (v1.9.5) + - Options page can download all `chrome.storage.local` + `chrome.storage.sync` data to a JSON file and restore it + - Solves uninstall/reinstall data loss, and migration between separate extension builds (store vs. GitHub) which have separate storage + - Backup file is structured `{ _meta, local, sync }`; restore validates `_meta.app` and confirms before overwriting + - Future enhancement: smart per-key merge on restore (e.g. union `modelSnapshots`, keep earliest `firstSeen`) instead of overwrite + ## Pending 🔄 ### Critical Priority 🔴 diff --git a/chrome/manifest.json b/chrome/manifest.json index 7ba69d2..bb515a7 100644 --- a/chrome/manifest.json +++ b/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Claude Exporter Beta", - "version": "1.9.4", + "version": "1.9.5", "description": "Export conversations and artifacts from Claude.ai", "permissions": [ "activeTab", diff --git a/chrome/options.html b/chrome/options.html index b2b1e2a..31444c2 100644 --- a/chrome/options.html +++ b/chrome/options.html @@ -175,6 +175,15 @@
+
+

Backup & Restore

+

Save the extension's data — model snapshots, export history, and preferences — to a file you can restore later. Useful before uninstalling, or to move your data to another browser, device, or extension build.

+ + + +
+
+ diff --git a/chrome/options.js b/chrome/options.js index 71d4759..6845b2e 100644 --- a/chrome/options.js +++ b/chrome/options.js @@ -64,6 +64,86 @@ document.getElementById('testBtn').addEventListener('click', async () => { } }); +// Backup all extension data to a file +document.getElementById('backupBtn').addEventListener('click', () => { + chrome.storage.local.get(null, (local) => { + chrome.storage.sync.get(null, (sync) => { + const backup = { + _meta: { + app: 'claude-exporter', + backupVersion: 1, + extensionVersion: chrome.runtime.getManifest().version, + createdAt: new Date().toISOString() + }, + local: local || {}, + sync: sync || {} + }; + const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + const snapCount = Object.keys(backup.local.modelSnapshots || {}).length; + const exportCount = Object.keys(backup.local.exportTimestamps || {}).length; + showStatus('backupStatus', `Backup downloaded — ${snapCount} model snapshot(s), ${exportCount} export record(s).`, 'success'); + }); + }); +}); + +// Restore extension data from a backup file +document.getElementById('restoreBtn').addEventListener('click', () => { + document.getElementById('restoreFile').click(); +}); + +document.getElementById('restoreFile').addEventListener('change', (event) => { + const file = event.target.files[0]; + event.target.value = ''; // allow re-selecting the same file later + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + let backup; + try { + backup = JSON.parse(e.target.result); + } catch (err) { + showStatus('backupStatus', 'Restore failed: the file is not valid JSON.', 'error'); + return; + } + + // Make sure this is actually one of our backup files + if (!backup || typeof backup !== 'object' || !backup._meta || + backup._meta.app !== 'claude-exporter' || typeof backup.local !== 'object') { + showStatus('backupStatus', 'Restore failed: this does not look like a Claude Exporter backup file.', 'error'); + return; + } + + const snapCount = Object.keys(backup.local.modelSnapshots || {}).length; + const exportCount = Object.keys(backup.local.exportTimestamps || {}).length; + const proceed = confirm( + `Restore this backup?\n\n` + + `It contains ${snapCount} model snapshot(s) and ${exportCount} export record(s), ` + + `created ${backup._meta.createdAt || 'an unknown date'}.\n\n` + + `This overwrites the extension's current data with the backup's contents.` + ); + if (!proceed) { + showStatus('backupStatus', 'Restore cancelled.', 'error'); + return; + } + + chrome.storage.local.set(backup.local, () => { + const syncData = (backup.sync && typeof backup.sync === 'object') ? backup.sync : {}; + chrome.storage.sync.set(syncData, () => { + showStatus('backupStatus', `Restore complete — ${snapCount} model snapshot(s), ${exportCount} export record(s) restored. Reload any open Claude pages and the browse page to see the changes.`, 'success'); + }); + }); + }; + reader.readAsText(file); +}); + // Helper functions function showStatus(elementId, message, type) { const statusEl = document.getElementById(elementId); diff --git a/firefox/manifest.json b/firefox/manifest.json index 50ce678..77a800c 100644 --- a/firefox/manifest.json +++ b/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "Claude Exporter Beta", - "version": "1.9.4", + "version": "1.9.5", "description": "Export conversations and artifacts from Claude.ai", "permissions": [ "activeTab", diff --git a/firefox/options.html b/firefox/options.html index b2b1e2a..31444c2 100644 --- a/firefox/options.html +++ b/firefox/options.html @@ -175,6 +175,15 @@
+
+

Backup & Restore

+

Save the extension's data — model snapshots, export history, and preferences — to a file you can restore later. Useful before uninstalling, or to move your data to another browser, device, or extension build.

+ + + +
+
+ diff --git a/firefox/options.js b/firefox/options.js index 71d4759..6845b2e 100644 --- a/firefox/options.js +++ b/firefox/options.js @@ -64,6 +64,86 @@ document.getElementById('testBtn').addEventListener('click', async () => { } }); +// Backup all extension data to a file +document.getElementById('backupBtn').addEventListener('click', () => { + chrome.storage.local.get(null, (local) => { + chrome.storage.sync.get(null, (sync) => { + const backup = { + _meta: { + app: 'claude-exporter', + backupVersion: 1, + extensionVersion: chrome.runtime.getManifest().version, + createdAt: new Date().toISOString() + }, + local: local || {}, + sync: sync || {} + }; + const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `claude-exporter-backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + const snapCount = Object.keys(backup.local.modelSnapshots || {}).length; + const exportCount = Object.keys(backup.local.exportTimestamps || {}).length; + showStatus('backupStatus', `Backup downloaded — ${snapCount} model snapshot(s), ${exportCount} export record(s).`, 'success'); + }); + }); +}); + +// Restore extension data from a backup file +document.getElementById('restoreBtn').addEventListener('click', () => { + document.getElementById('restoreFile').click(); +}); + +document.getElementById('restoreFile').addEventListener('change', (event) => { + const file = event.target.files[0]; + event.target.value = ''; // allow re-selecting the same file later + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + let backup; + try { + backup = JSON.parse(e.target.result); + } catch (err) { + showStatus('backupStatus', 'Restore failed: the file is not valid JSON.', 'error'); + return; + } + + // Make sure this is actually one of our backup files + if (!backup || typeof backup !== 'object' || !backup._meta || + backup._meta.app !== 'claude-exporter' || typeof backup.local !== 'object') { + showStatus('backupStatus', 'Restore failed: this does not look like a Claude Exporter backup file.', 'error'); + return; + } + + const snapCount = Object.keys(backup.local.modelSnapshots || {}).length; + const exportCount = Object.keys(backup.local.exportTimestamps || {}).length; + const proceed = confirm( + `Restore this backup?\n\n` + + `It contains ${snapCount} model snapshot(s) and ${exportCount} export record(s), ` + + `created ${backup._meta.createdAt || 'an unknown date'}.\n\n` + + `This overwrites the extension's current data with the backup's contents.` + ); + if (!proceed) { + showStatus('backupStatus', 'Restore cancelled.', 'error'); + return; + } + + chrome.storage.local.set(backup.local, () => { + const syncData = (backup.sync && typeof backup.sync === 'object') ? backup.sync : {}; + chrome.storage.sync.set(syncData, () => { + showStatus('backupStatus', `Restore complete — ${snapCount} model snapshot(s), ${exportCount} export record(s) restored. Reload any open Claude pages and the browse page to see the changes.`, 'success'); + }); + }); + }; + reader.readAsText(file); +}); + // Helper functions function showStatus(elementId, message, type) { const statusEl = document.getElementById(elementId);