// ========== 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 schedules...
💾 Loading backup files...
Loading...
📋 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-schedules-container'); var historyContainer = document.getElementById('backup-history-container'); var diskContainer = document.getElementById('backup-disk-container'); var pointintimeContainer = document.getElementById('pointintime-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'; }); // === Schedules Tab (Premium) === async function loadSchedulesTab() { if (!scheduleContainer) return; scheduleContainer.innerHTML = '
Loading...
'; try { var res = await fetch('/api/v1/backups/schedule'); var data = await res.json(); if (data.premiumRequired) { scheduleContainer.innerHTML = '
' + '
' + '
Premium Feature
' + '
Auto-backup scheduling requires a DashCaddy Premium subscription.
' + '' + '
'; return; } if (!data.success) throw new Error(data.error || 'Failed to load schedules'); var schedules = data.schedules || []; if (schedules.length === 0) { scheduleContainer.innerHTML = '
' + '' + '
No backup schedules configured
' + '
Select apps below to enable auto-backup
' + '
'; return; } var html = '
'; for (var i = 0; i < schedules.length; i++) { var sch = schedules[i]; var nextRunStr = sch.nextRun ? new Date(sch.nextRun).toLocaleString() : 'Not scheduled'; var lastRunStr = sch.lastRun ? new Date(sch.lastRun).toLocaleString() : 'Never'; html += '
' + '
' + '
' + escapeHtml(sch.appId) + '
' + ' ' + '
' + '
' + '
Schedule:
' + '
Keep last:
' + '
' + '
' + '
Next run: ' + escapeHtml(nextRunStr) + '
' + '
Last run: ' + escapeHtml(lastRunStr) + '
' + '
' + '
' + ' ' + ' ' + '
' + '
'; } html += '
'; // Add "Add Schedule" section at bottom html += '
' + '

➕ Add New Schedule

' + '
' + ' ' + ' ' + ' ' + ' ' + '
' + '
'; scheduleContainer.innerHTML = html; // Wire up event listeners scheduleContainer.querySelectorAll('.schedule-toggle').forEach(function(toggle) { toggle.addEventListener('change', function() { updateSchedule(toggle.dataset.appid, { enabled: toggle.checked }); }); }); scheduleContainer.querySelectorAll('.schedule-select').forEach(function(sel) { sel.addEventListener('change', function() { updateSchedule(sel.dataset.appid, { schedule: sel.value }); }); }); scheduleContainer.querySelectorAll('.retention-input').forEach(function(inp) { inp.addEventListener('change', function() { updateSchedule(inp.dataset.appid, { retention: { keep: parseInt(inp.value) || 7 } }); }); }); scheduleContainer.querySelectorAll('.schedule-run-now').forEach(function(btn) { btn.addEventListener('click', function() { runBackupNow(btn.dataset.appid); }); }); scheduleContainer.querySelectorAll('.schedule-delete').forEach(function(btn) { btn.addEventListener('click', function() { deleteSchedule(btn.dataset.appid); }); }); document.getElementById('add-schedule-btn')?.addEventListener('click', addNewSchedule); } catch (e) { scheduleContainer.innerHTML = '
Failed to load: ' + escapeHtml(e.message) + '
'; } } async function updateSchedule(appId, updates) { try { var res = await secureFetch('/api/v1/backups/schedule', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appId, ...updates }) }); var data = await res.json(); if (data.success) { showNotification('Schedule updated for ' + appId, 'success'); } else { showNotification('Update failed: ' + (data.error || 'Unknown'), 'error'); loadSchedulesTab(); // Refresh on failure } } catch (e) { showNotification('Error: ' + e.message, 'error'); } } async function runBackupNow(appId) { try { var res = await secureFetch('/api/v1/backups/backup/' + encodeURIComponent(appId), { method: 'POST', headers: { 'Content-Type': 'application/json' } }); var data = await res.json(); if (data.success) { showNotification('Backup started for ' + appId + '!', 'success'); } else { showNotification('Backup failed: ' + (data.error || 'Unknown'), 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); } } async function deleteSchedule(appId) { if (!confirm('Remove backup schedule for ' + appId + '?')) return; try { var res = await secureFetch('/api/v1/backups/schedule/' + encodeURIComponent(appId), { method: 'DELETE' }); var data = await res.json(); if (data.success) { showNotification('Schedule removed for ' + appId, 'success'); loadSchedulesTab(); } else { showNotification('Delete failed: ' + (data.error || 'Unknown'), 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); } } async function addNewSchedule() { var appId = document.getElementById('new-schedule-appid')?.value?.trim(); var interval = document.getElementById('new-schedule-interval')?.value || 'daily'; var retention = parseInt(document.getElementById('new-schedule-retention')?.value) || 7; if (!appId) { showNotification('Please enter an App ID', 'warning'); return; } try { var res = await secureFetch('/api/v1/backups/schedule', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appId, schedule: interval, retention: { keep: retention }, enabled: true }) }); var data = await res.json(); if (data.success) { showNotification('Schedule created for ' + appId, 'success'); loadSchedulesTab(); // Clear inputs var appIdInput = document.getElementById('new-schedule-appid'); if (appIdInput) appIdInput.value = ''; } else { showNotification('Failed: ' + (data.error || 'Unknown'), 'error'); } } catch (e) { showNotification('Error: ' + e.message, 'error'); } } // === Backups on Disk Tab === async function loadDiskBackups() { if (!diskContainer) return; diskContainer.innerHTML = '
Loading...
'; try { var res = await fetch('/api/v1/backups/files'); var data = await res.json(); if (!data.success) throw new Error(data.error || 'Failed to load'); var files = data.files || []; if (files.length === 0) { diskContainer.innerHTML = '
' + '💾' + '
No backup files on disk
' + '
Run a backup to create backup files
' + '
'; return; } // Group by appId var byApp = {}; for (var i = 0; i < files.length; i++) { var f = files[i]; var appId = f.appId || 'unknown'; if (!byApp[appId]) byApp[appId] = []; byApp[appId].push(f); } var html = '
' + files.length + ' backup file(s) across ' + Object.keys(byApp).length + ' app(s)
'; html += '
'; var appIds = Object.keys(byApp).sort(); for (var a = 0; a < appIds.length; a++) { var appId = appIds[a]; var appFiles = byApp[appId]; html += '
' + '
' + escapeHtml(appId) + ' (' + appFiles.length + ' backup(s))' + '
'; for (var j = 0; j < appFiles.length; j++) { var f = appFiles[j]; var dateStr = new Date(f.timestamp).toLocaleString(); html += '
' + '
' + '
' + escapeHtml(f.name) + '
' + '
' + f.sizeFormatted + '
' + '
' + '
' + dateStr + '
' + '
' + ' ' + ' ' + '
' + '
'; } html += '
'; } html += '
'; diskContainer.innerHTML = html; // Wire up buttons diskContainer.querySelectorAll('.disk-compare-btn').forEach(function(btn) { btn.addEventListener('click', function() { compareBackupFile(btn.dataset.appid, btn.dataset.filename); }); }); diskContainer.querySelectorAll('.disk-restore-btn').forEach(function(btn) { btn.addEventListener('click', function() { restoreBackupFile(btn.dataset.appid, btn.dataset.filename); }); }); } catch (e) { diskContainer.innerHTML = '
Failed: ' + escapeHtml(e.message) + '
'; } } // === 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-schedules-tab"]')?.addEventListener('click', loadSchedulesTab); document.querySelector('[data-panel="backup-disk-tab"]')?.addEventListener('click', loadDiskBackups); document.querySelector('[data-panel="backup-pointintime-tab"]')?.addEventListener('click', loadPointInTimeTab); document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener('click', loadBackupHistory); // ===== Point-in-Time Restore Tab ===== async function loadPointInTimeTab() { if (!pointintimeContainer) return; // Check premium try { var licenseRes = await fetch('/api/v1/license/status'); var licenseData = await licenseRes.json(); if (licenseData.tier !== 'premium') { pointintimeContainer.innerHTML = '
' + '
' + '
Premium Feature
' + '
Point-in-time restore requires DashCaddy Premium with auto-backup enabled.
' + '' + '
'; return; } } catch (e) { /* ignore */ } pointintimeContainer.innerHTML = '
Loading...
'; try { // Fetch services for dropdown var servicesRes = await fetch('/api/v1/services'); var servicesData = await servicesRes.json(); var services = servicesData.services || []; if (services.length === 0) { pointintimeContainer.innerHTML = '
📦 No apps deployed yet
'; return; } var html = '
' + '
' + ' ' + ' ' + ' ' + '
' + '
' + '
'; pointintimeContainer.innerHTML = html; document.getElementById('pit-load-btn')?.addEventListener('click', function() { var appId = document.getElementById('pit-app-select')?.value; if (appId) loadPointInTimeBackups(appId); }); } catch (e) { pointintimeContainer.innerHTML = '
Failed: ' + escapeHtml(e.message) + '
'; } } async function loadPointInTimeBackups(appId) { var listEl = document.getElementById('pit-backups-list'); if (!listEl) return; listEl.innerHTML = '
Loading backups...
'; try { var res = await fetch('/api/v1/backups/files/' + encodeURIComponent(appId)); var data = await res.json(); if (!data.success || !data.files || data.files.length === 0) { listEl.innerHTML = '
💾 No backup files for ' + escapeHtml(appId) + '
'; return; } var html = '
' + data.files.length + ' backup(s)
' + '
'; for (var i = 0; i < data.files.length; i++) { var f = data.files[i]; var dateStr = new Date(f.timestamp).toLocaleString(); html += '
' + '
' + '
' + escapeHtml(f.name) + '
' + '
' + f.sizeFormatted + '
' + '
' + '
' + dateStr + '
' + '
' + ' ' + ' ' + '
' + '
'; } html += '
'; listEl.innerHTML = html; // Wire up buttons listEl.querySelectorAll('.pit-compare-btn').forEach(function(btn) { btn.addEventListener('click', function() { compareBackupFile(btn.dataset.appid, btn.dataset.filename); }); }); listEl.querySelectorAll('.pit-restore-btn').forEach(function(btn) { btn.addEventListener('click', function() { restoreBackupFile(btn.dataset.appid, btn.dataset.filename); }); }); } catch (e) { listEl.innerHTML = '
Failed: ' + escapeHtml(e.message) + '
'; } } async function compareBackupFile(appId, filename) { try { var res = await secureFetch('/api/v1/backups/compare/' + encodeURIComponent(filename), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); var data = await res.json(); if (!data.success) { showNotification('Compare failed: ' + (data.error || 'Unknown'), 'error'); return; } var diff = data.diff; var html = '
' + '
' + '

📊 Compare: ' + escapeHtml(filename) + '

' + '
Size: ' + (diff.sizeFormatted || '?') + ' | Created: ' + new Date(diff.timestamp).toLocaleString() + '
'; if (diff.services) { var svcChanged = diff.services.hasChanges ? '🔴' : '🟢'; html += '
' + '
' + svcChanged + ' Services (backup vs current)
' + '
Backup: ' + diff.services.backupCount + ' services | Current: ' + diff.services.currentCount + ' services
'; if (diff.services.hasChanges) { html += '
Services differ — restoring will replace current configuration
'; } html += '
'; } if (diff.config) { var cfgChanged = diff.config.hasChanges ? '🔴' : '🟢'; html += '
' + '
' + cfgChanged + ' Configuration
'; if (diff.config.hasChanges) { html += '
Configuration differs — restoring will replace current settings
'; } else { html += '
No changes
'; } html += '
'; } html += '' + '
'; document.body.insertAdjacentHTML('beforeend', html); document.getElementById('compare-close-btn')?.addEventListener('click', function() { document.getElementById('compare-overlay')?.remove(); }); document.getElementById('compare-overlay')?.addEventListener('click', function(e) { if (e.target === this) this.remove(); }); } catch (e) { showNotification('Compare error: ' + e.message, 'error'); } } async function restoreBackupFile(appId, filename) { if (!confirm('Restore ' + filename + ' for ' + appId + '?\n\nThis will replace current configuration, credentials, and data. Containers will be restarted.')) return; try { var res = await secureFetch('/api/v1/apps/' + encodeURIComponent(appId) + '/revert/' + encodeURIComponent(filename), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ restartContainers: true }) }); var data = await res.json(); if (data.success) { showNotification(appId + ' restored to ' + filename, 'success'); setTimeout(function() { location.reload(); }, 1500); } else { showNotification('Restore failed: ' + (data.error || 'Unknown'), 'error'); } } catch (e) { showNotification('Restore error: ' + e.message, 'error'); } } })();