// ========== UNIFIED BACKUP/RESTORE v2.0 ========== // Single file captures everything: server config + browser state + themes + encryption key (function() { // All DashCaddy localStorage keys to include in backup var BROWSER_STATE_KEYS = [ 'dashcaddy_site_config', 'dashcaddy_onboarding', 'dashcaddy-encryption-key', 'dashcaddy-setup', 'dashcaddy-config', 'theme', 'user-themes', 'custom-theme', 'custom-apps', 'custom-services', 'toolbar-sections', 'weather-location', 'weather-zip', 'weather-geo', 'weather-unit', 'clock-style', 'clock-chimes', 'clock-chime-volume' ]; // Collect all DashCaddy browser state from localStorage function collectBrowserState() { var state = {}; // Grab all whitelisted keys for (var i = 0; i < BROWSER_STATE_KEYS.length; i++) { var key = BROWSER_STATE_KEYS[i]; var val = safeGet(key); if (val !== null && val !== undefined) state[key] = val; } // Grab dynamic widget-*-enabled keys try { for (var j = 0; j < localStorage.length; j++) { var k = localStorage.key(j); if (/^widget-.+-enabled$/.test(k)) { state[k] = localStorage.getItem(k); } } } catch (e) { /* private browsing */ } return state; } // Restore browser state from backup into localStorage function restoreBrowserState(browserState) { if (!browserState || typeof browserState !== 'object') return 0; var count = 0; for (var key in browserState) { if (!browserState.hasOwnProperty(key)) continue; safeSet(key, browserState[key]); count++; } return count; } // Handle legacy v1.0.0 import-export format (pre-backup modal) function isLegacyFormat(data) { return data.version && !data.files && data.services; } function restoreLegacyFormat(data) { // Map the old flat keys into browserState for localStorage restore var browserState = {}; if (data.customServices) browserState['custom-services'] = JSON.stringify(data.customServices); if (data.customApps) browserState['custom-apps'] = JSON.stringify(data.customApps); if (data.weatherZip) browserState['weather-zip'] = data.weatherZip; if (data.theme) browserState['theme'] = data.theme; if (data.userThemes && Object.keys(data.userThemes).length) browserState['user-themes'] = JSON.stringify(data.userThemes); restoreBrowserState(browserState); // Push services to server if available if (data.services && Array.isArray(data.services)) { secureFetch('/api/v1/services', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data.services) }).catch(function() {}); } // Push themes to server if available if (data.userThemes) { Object.keys(data.userThemes).forEach(function(slug) { var t = data.userThemes[slug]; var colors = {}; (window.THEME_PROPS || []).forEach(function(p) { if (t[p]) colors[p] = t[p]; }); secureFetch('/api/v1/themes/' + slug, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: t.name || slug, colors: colors }) }).catch(function() {}); }); } } // Inject modal HTML injectModal('backup-modal', `

💾 Backup & Restore

📤 Export Backup

Downloads everything — services, Caddyfile, credentials, encryption key, themes, and all browser preferences.

📥 Restore Backup

Upload a backup file to restore your entire configuration — drag and drop ready.

Loading backup schedule...
📋 Loading backup history...
`); var modal = document.getElementById('backup-modal'); var openBtn = document.getElementById('backup-restore-btn'); var cancelBtn = document.getElementById('backup-cancel'); var exportBtn = document.getElementById('backup-export-btn'); var selectFileBtn = document.getElementById('backup-select-file'); var fileInput = document.getElementById('backup-file-input'); var fileNameDiv = document.getElementById('backup-file-name'); var previewDiv = document.getElementById('backup-preview'); var previewContent = document.getElementById('backup-preview-content'); var restoreBtn = document.getElementById('backup-do-restore-btn'); var resultDiv = document.getElementById('backup-result'); var scheduleContainer = document.getElementById('backup-schedule-container'); var historyContainer = document.getElementById('backup-history-container'); var selectedBackup = null; // Open modal openBtn?.addEventListener('click', function() { modal.classList.add('show'); if (resultDiv) resultDiv.style.display = 'none'; if (previewDiv) previewDiv.style.display = 'none'; if (fileNameDiv) fileNameDiv.style.display = 'none'; selectedBackup = null; }); wireModal(modal, cancelBtn); // === EXPORT: Server backup + browser state in one file === exportBtn?.addEventListener('click', async function() { exportBtn.disabled = true; exportBtn.innerHTML = ' Exporting...'; try { // Fetch server-side backup (config, services, caddyfile, credentials, encryption key, themes, etc.) var response = await fetch('/api/v1/backup/export'); var data = await response.json(); // Add all browser localStorage state data.browserState = collectBrowserState(); // Download unified backup var blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'dashcaddy-backup-' + new Date().toISOString().split('T')[0] + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); var stateCount = Object.keys(data.browserState).length; var themeCount = data.themes ? Object.keys(data.themes).length : 0; resultDiv.innerHTML = '✅ Full backup downloaded — server config + ' + stateCount + ' browser settings' + (themeCount ? ' + ' + themeCount + ' themes' : ''); resultDiv.style.display = 'block'; resultDiv.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)'; resultDiv.style.border = '1px solid var(--ok-fg)'; } catch (e) { resultDiv.innerHTML = '❌ Export failed: ' + escapeHtml(e.message); resultDiv.style.display = 'block'; resultDiv.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultDiv.style.border = '1px solid var(--bad-fg)'; } exportBtn.disabled = false; exportBtn.innerHTML = '⬇️ Download Full Backup'; }); // Select file button selectFileBtn?.addEventListener('click', function() { fileInput.click(); }); // === FILE SELECTED: Preview contents === fileInput?.addEventListener('change', async function(e) { var file = e.target.files[0]; if (!file) return; fileNameDiv.textContent = '📄 ' + file.name; fileNameDiv.style.display = 'block'; resultDiv.style.display = 'none'; try { var text = await file.text(); var backup = JSON.parse(text); // Handle legacy v1.0.0 format (from old import-export.js) if (isLegacyFormat(backup)) { selectedBackup = backup; var html = '
Legacy format (v' + escapeHtml(backup.version) + ')
'; html += '
'; if (backup.services?.length) html += '📋 ' + backup.services.length + ' services'; if (backup.customApps?.length) html += '📦 ' + backup.customApps.length + ' custom apps'; if (backup.theme) html += '🎨 Theme: ' + escapeHtml(backup.theme) + ''; if (backup.userThemes) html += '🎨 ' + Object.keys(backup.userThemes).length + ' custom themes'; html += '
'; previewContent.innerHTML = html; previewDiv.style.display = 'block'; return; } // v1.1+ / v2.0 format — send to server for preview var response = await secureFetch('/api/v1/backup/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(backup) }); var preview = await response.json(); if (preview.success) { selectedBackup = backup; var html = '
Exported: ' + new Date(backup.exportedAt).toLocaleString() + ' (v' + escapeHtml(backup.version) + ')
'; // Server files html += '
Server Config
'; html += '
'; for (var key in preview.preview.files) { var info = preview.preview.files[key]; var icon = info.action === 'create' ? '🆕' : '📝'; html += '' + icon + ' ' + escapeHtml(info.description) + ''; } html += '
'; // Services count if (preview.preview.serviceCount) { html += '
' + preview.preview.serviceCount + ' services
'; } // Themes if (preview.preview.themeCount) { html += '
🎨 ' + preview.preview.themeCount + ' custom themes
'; } // Browser state if (preview.preview.browserStateCount) { html += '
Browser Preferences
'; html += '
🖥️ ' + preview.preview.browserStateCount + ' saved settings (theme, weather, clock, widgets, etc.)
'; } previewContent.innerHTML = html; previewDiv.style.display = 'block'; } else { resultDiv.innerHTML = '⚠️ Invalid backup file: ' + escapeHtml(preview.error); resultDiv.style.display = 'block'; resultDiv.style.background = 'color-mix(in srgb, #f39c12 15%, transparent)'; resultDiv.style.border = '1px solid #f39c12'; previewDiv.style.display = 'none'; } } catch (e) { resultDiv.innerHTML = '❌ Could not read file: ' + escapeHtml(e.message); resultDiv.style.display = 'block'; resultDiv.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultDiv.style.border = '1px solid var(--bad-fg)'; previewDiv.style.display = 'none'; } }); // === RESTORE: Server + browser state === restoreBtn?.addEventListener('click', async function() { if (!selectedBackup) return; if (!confirm('This will overwrite your current configuration and browser preferences. Continue?')) return; restoreBtn.disabled = true; restoreBtn.innerHTML = ' Restoring...'; try { // Handle legacy format if (isLegacyFormat(selectedBackup)) { restoreLegacyFormat(selectedBackup); resultDiv.innerHTML = '✅ Legacy backup restored — browser settings and services imported.'; resultDiv.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)'; resultDiv.style.border = '1px solid var(--ok-fg)'; resultDiv.style.display = 'block'; setTimeout(function() { location.reload(); }, 2000); restoreBtn.disabled = false; restoreBtn.innerHTML = '⚡ Restore Everything'; return; } // v1.1+ / v2.0 — restore server-side first var reloadCaddy = document.getElementById('backup-reload-caddy')?.checked ?? true; var response = await secureFetch('/api/v1/backup/restore', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backup: selectedBackup, options: { reloadCaddy: reloadCaddy } }) }); var data = await response.json(); // Then restore browser state var browserCount = 0; if (selectedBackup.browserState) { browserCount = restoreBrowserState(selectedBackup.browserState); } if (data.success) { var msg = '✅ ' + data.message; if (browserCount > 0) msg += '
' + browserCount + ' browser settings restored'; if (data.results.caddyReloaded) msg += '
Caddy configuration reloaded'; resultDiv.innerHTML = msg; resultDiv.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)'; resultDiv.style.border = '1px solid var(--ok-fg)'; setTimeout(function() { location.reload(); }, 2000); } else { resultDiv.innerHTML = '⚠️ ' + escapeHtml(data.message); if (browserCount > 0) resultDiv.innerHTML += '
' + browserCount + ' browser settings were restored'; if (data.results?.errors?.length > 0) { resultDiv.innerHTML += '
' + data.results.errors.map(function(e) { return escapeHtml(e.file) + ': ' + escapeHtml(e.error); }).join(', ') + ''; } resultDiv.style.background = 'color-mix(in srgb, #f39c12 15%, transparent)'; resultDiv.style.border = '1px solid #f39c12'; } resultDiv.style.display = 'block'; } catch (e) { resultDiv.innerHTML = '❌ Restore failed: ' + escapeHtml(e.message); resultDiv.style.display = 'block'; resultDiv.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultDiv.style.border = '1px solid var(--bad-fg)'; } restoreBtn.disabled = false; restoreBtn.innerHTML = '⚡ Restore Everything'; }); // === Automated Backups Tab === async function loadBackupSchedule() { if (!scheduleContainer) return; try { var res = await fetch('/api/v1/backups/config'); var data = await res.json(); if (!data.success) throw new Error(data.error || 'Failed to load config'); var cfg = data.config?.backups || {}; var autoKey = Object.keys(cfg)[0]; var auto = autoKey ? cfg[autoKey] : null; var html = '
'; html += '

⏰ Backup Schedule

'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; html += ' '; html += ' '; html += '
'; html += '
'; html += ''; scheduleContainer.innerHTML = html; document.getElementById('backup-save-schedule')?.addEventListener('click', saveSchedule); document.getElementById('backup-run-now')?.addEventListener('click', runBackupNow); } catch (e) { scheduleContainer.innerHTML = '
Failed to load schedule: ' + escapeHtml(e.message) + '
'; } } async function saveSchedule() { var schedule = document.getElementById('backup-schedule-select')?.value; var retention = parseInt(document.getElementById('backup-retention-select')?.value) || 5; var encrypt = document.getElementById('backup-encrypt-toggle')?.checked ?? true; var resultEl = document.getElementById('backup-schedule-result'); try { var res = await secureFetch('/api/v1/backups/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backups: { auto: { enabled: schedule !== 'disabled', schedule: schedule === 'disabled' ? 'daily' : schedule, include: ['all'], encrypt: encrypt, verify: true, retention: { keep: retention }, destinations: [{ type: 'local' }] } } }) }); var data = await res.json(); if (resultEl) { resultEl.innerHTML = data.success ? '✅ Schedule saved' : '⚠️ ' + escapeHtml(data.error); resultEl.style.display = 'block'; resultEl.style.background = data.success ? 'color-mix(in srgb, var(--ok-fg) 15%, transparent)' : 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = data.success ? '1px solid var(--ok-fg)' : '1px solid var(--bad-fg)'; setTimeout(function() { if (resultEl) resultEl.style.display = 'none'; }, 3000); } } catch (e) { if (resultEl) { resultEl.innerHTML = '❌ ' + escapeHtml(e.message); resultEl.style.display = 'block'; resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } } } async function runBackupNow() { var btn = document.getElementById('backup-run-now'); var resultEl = document.getElementById('backup-schedule-result'); if (btn) { btn.disabled = true; btn.innerHTML = ' Running...'; } try { var res = await secureFetch('/api/v1/backups/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ include: ['all'], destinations: [{ type: 'local' }] }) }); var data = await res.json(); if (resultEl) { if (data.success) { var sizeMB = data.backup?.size ? (data.backup.size / 1024 / 1024).toFixed(2) : '?'; resultEl.innerHTML = '✅ Backup complete (' + sizeMB + ' MB)'; resultEl.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--ok-fg)'; } else { resultEl.innerHTML = '⚠️ ' + escapeHtml(data.error); resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } resultEl.style.display = 'block'; } loadBackupHistory(); } catch (e) { if (resultEl) { resultEl.innerHTML = '❌ ' + escapeHtml(e.message); resultEl.style.display = 'block'; resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } } if (btn) { btn.disabled = false; btn.innerHTML = '▶️ Run Backup Now'; } } // === Backup History Tab === async function loadBackupHistory() { if (!historyContainer) return; historyContainer.innerHTML = '
Loading...
'; try { var res = await fetch('/api/v1/backups/history?limit=50'); var data = await res.json(); if (!data.success || !data.history?.length) { historyContainer.innerHTML = '
📋 No backup history yet
'; return; } var html = '
'; for (var i = 0; i < data.history.length; i++) { var bk = data.history[i]; var sizeMB = bk.size ? (bk.size / 1024 / 1024).toFixed(2) : '?'; html += '
'; html += '
'; html += ' ' + escapeHtml(bk.name || 'backup') + ''; html += '
'; html += ' ' + escapeHtml(bk.status) + ''; if (bk.status === 'success') html += ' '; html += '
'; html += '
'; html += '
'; html += ' ' + new Date(bk.timestamp).toLocaleString() + ' | ' + sizeMB + ' MB | ' + (bk.duration ? (bk.duration / 1000).toFixed(1) + 's' : '--'); if (bk.encrypted) html += ' | 🔒'; html += '
'; html += '
'; } html += '
'; historyContainer.innerHTML = html; historyContainer.querySelectorAll('.backup-restore-btn').forEach(function(btn) { btn.addEventListener('click', function() { window.__restoreServerBackup(btn.dataset.backupId); }); }); } catch (e) { historyContainer.innerHTML = '
Failed: ' + escapeHtml(e.message) + '
'; } } window.__restoreServerBackup = async function(backupId) { if (!confirm('Restore from this server backup? This will overwrite current configuration.')) return; try { var res = await secureFetch('/api/v1/backups/restore/' + backupId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ restoreServices: true, restoreConfig: true }) }); var data = await res.json(); if (data.success) { showNotification('Restore completed successfully!', 'success'); location.reload(); } else { showNotification('Restore failed: ' + (data.error || 'Unknown error'), 'error'); } } catch (e) { showNotification('Restore error: ' + e.message, 'error'); } }; // Lazy-load tabs document.querySelector('[data-panel="backup-automated"]')?.addEventListener('click', loadBackupSchedule); document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener('click', loadBackupHistory); })();