From 75e2d7853e348e673b71d94a7b25fab21fed8e2c Mon Sep 17 00:00:00 2001 From: Sami Date: Tue, 17 Mar 2026 21:06:56 -0700 Subject: [PATCH] Unify backup/restore into single v2.0 file with full state capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server export now includes encryption key, themes, and all config files. Client export bundles all DashCaddy localStorage keys (19 named + dynamic widget keys) as browserState. Restore handles both server and browser state in one operation. Legacy v1.0 import format still supported. Removed redundant Export/Import toolbar buttons — Backup modal is now the single entry point. Co-Authored-By: Claude Opus 4.6 --- dashcaddy-api/crypto-utils.js | 11 +- dashcaddy-api/routes/config/backup.js | 75 ++++- status/index.html | 2 - status/js/backup-restore.js | 434 +++++++++++++++++--------- status/js/import-export.js | 140 +-------- 5 files changed, 375 insertions(+), 287 deletions(-) diff --git a/dashcaddy-api/crypto-utils.js b/dashcaddy-api/crypto-utils.js index 3ff6030..f534f10 100644 --- a/dashcaddy-api/crypto-utils.js +++ b/dashcaddy-api/crypto-utils.js @@ -315,6 +315,14 @@ function decryptWithKey(encryptedData, key) { // Initialize key on module load loadOrCreateKey(); +/** + * Clear the cached encryption key so it reloads from file on next use. + * Called after restoring an encryption key from backup. + */ +function clearCachedKey() { + encryptionKey = null; +} + module.exports = { encrypt, decrypt, @@ -327,5 +335,6 @@ module.exports = { loadOrCreateKey, deriveKey, rotateKey, - decryptWithKey + decryptWithKey, + clearCachedKey }; diff --git a/dashcaddy-api/routes/config/backup.js b/dashcaddy-api/routes/config/backup.js index 7dfc519..b742e3e 100644 --- a/dashcaddy-api/routes/config/backup.js +++ b/dashcaddy-api/routes/config/backup.js @@ -1,4 +1,5 @@ const fsp = require('fs').promises; +const fs = require('fs'); const path = require('path'); const { CADDY } = require('../../constants'); const { exists } = require('../../fs-helpers'); @@ -7,20 +8,36 @@ module.exports = function(ctx) { const express = require('express'); const router = express.Router(); + const THEMES_DIR = process.env.THEMES_DIR || path.join(path.dirname(ctx.SERVICES_FILE), 'themes'); + + function readAllThemes() { + const themes = {}; + try { + if (!fs.existsSync(THEMES_DIR)) return themes; + const files = fs.readdirSync(THEMES_DIR).filter(f => f.endsWith('.json')); + for (const file of files) { + const slug = path.basename(file, '.json'); + themes[slug] = JSON.parse(fs.readFileSync(path.join(THEMES_DIR, file), 'utf8')); + } + } catch (e) { /* ignore */ } + return themes; + } + // ===== BACKUP/RESTORE ENDPOINTS ===== - // Export and import DashCaddy configuration + // Unified v2.0 backup — server config + encryption key + themes (browser state added client-side) // Export all configuration as a downloadable JSON bundle router.get('/backup/export', ctx.asyncHandler(async (req, res) => { const backup = { - version: '1.1', + version: '2.0', exportedAt: new Date().toISOString(), dashcaddyVersion: '1.0.0', files: {}, + themes: {}, assets: {} }; - // Collect all configuration files + // Collect all configuration files (encryption key now included for self-contained restore) const ENCRYPTION_KEY_FILE = process.env.ENCRYPTION_KEY_FILE || path.join(path.dirname(ctx.SERVICES_FILE), '.encryption-key'); const filesToBackup = [ { key: 'services', path: ctx.SERVICES_FILE, required: true }, @@ -28,7 +45,7 @@ module.exports = function(ctx) { { key: 'config', path: ctx.CONFIG_FILE, required: false }, { key: 'dnsCredentials', path: ctx.dns.credentialsFile, required: false }, { key: 'credentials', path: process.env.CREDENTIALS_FILE || path.join(__dirname, '..', '..', 'credentials.json'), required: false }, - // NOTE: encryptionKey deliberately excluded — bundling it with encrypted data defeats the encryption + { key: 'encryptionKey', path: ENCRYPTION_KEY_FILE, required: false }, { key: 'totpConfig', path: ctx.TOTP_CONFIG_FILE, required: false }, { key: 'tailscaleConfig', path: ctx.TAILSCALE_CONFIG_FILE, required: false }, { key: 'notifications', path: ctx.NOTIFICATIONS_FILE, required: false } @@ -95,6 +112,13 @@ module.exports = function(ctx) { ctx.log.warn('backup', 'Could not include assets in backup', { error: e.message }); } + // Include user-created themes + try { + backup.themes = readAllThemes(); + } catch (e) { + ctx.log.warn('backup', 'Could not include themes in backup', { error: e.message }); + } + // Set headers for file download const backupFilename = `dashcaddy-backup-${new Date().toISOString().split('T')[0]}.json`; res.setHeader('Content-Type', 'application/json'); @@ -156,6 +180,16 @@ module.exports = function(ctx) { preview.serviceCount = services.length; } + // Count themes if present + if (backup.themes && typeof backup.themes === 'object') { + preview.themeCount = Object.keys(backup.themes).length; + } + + // Count browser state items if present + if (backup.browserState && typeof backup.browserState === 'object') { + preview.browserStateCount = Object.keys(backup.browserState).length; + } + res.json({ success: true, preview }); }, 'backup-preview')); @@ -192,13 +226,14 @@ module.exports = function(ctx) { errors: [] }; - // File mapping (encryptionKey excluded — must never be overwritten from backup) + const ENCRYPTION_KEY_FILE = process.env.ENCRYPTION_KEY_FILE || path.join(path.dirname(ctx.SERVICES_FILE), '.encryption-key'); const fileMapping = { services: ctx.SERVICES_FILE, caddyfile: ctx.caddy.filePath, config: ctx.CONFIG_FILE, dnsCredentials: ctx.dns.credentialsFile, credentials: process.env.CREDENTIALS_FILE || path.join(__dirname, '..', '..', 'credentials.json'), + encryptionKey: ENCRYPTION_KEY_FILE, totpConfig: ctx.TOTP_CONFIG_FILE, tailscaleConfig: ctx.TAILSCALE_CONFIG_FILE, notifications: ctx.NOTIFICATIONS_FILE @@ -304,6 +339,36 @@ module.exports = function(ctx) { } } + // Restore themes + if (backup.themes && typeof backup.themes === 'object' && Object.keys(backup.themes).length) { + try { + if (!fs.existsSync(THEMES_DIR)) fs.mkdirSync(THEMES_DIR, { recursive: true }); + for (const [slug, data] of Object.entries(backup.themes)) { + if (/^[a-z0-9-]+$/.test(slug)) { + fs.writeFileSync(path.join(THEMES_DIR, slug + '.json'), JSON.stringify(data, null, 2), 'utf8'); + } + } + results.restored.push(`themes:${Object.keys(backup.themes).length}`); + ctx.log.info('backup', `Restored ${Object.keys(backup.themes).length} themes`); + } catch (e) { + results.errors.push({ file: 'themes', error: e.message }); + } + } + + // Reload encryption key in memory if it was restored + if (results.restored.includes('encryptionKey')) { + try { + // Clear the cached key so crypto-utils reloads from the new file on next use + const cryptoUtils = require('../../crypto-utils'); + if (typeof cryptoUtils.clearCachedKey === 'function') { + cryptoUtils.clearCachedKey(); + } + results.encryptionKeyReloaded = true; + } catch (e) { + ctx.log.warn('backup', 'Could not reload encryption key', { error: e.message }); + } + } + const success = results.restored.length > 0 && results.errors.length === 0; res.json({ diff --git a/status/index.html b/status/index.html index d91c55d..91090b7 100644 --- a/status/index.html +++ b/status/index.html @@ -140,8 +140,6 @@
- - diff --git a/status/js/backup-restore.js b/status/js/backup-restore.js index c328de6..f462aec 100644 --- a/status/js/backup-restore.js +++ b/status/js/backup-restore.js @@ -1,11 +1,94 @@ -// ========== BACKUP/RESTORE (Enhanced) ========== +// ========== 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

@@ -15,16 +98,16 @@
- +

📤 Export Backup

- Download all your settings: services, Caddyfile, DNS credentials, notifications. + Downloads everything — services, Caddyfile, credentials, encryption key, themes, and all browser preferences.

@@ -32,7 +115,7 @@

📥 Restore Backup

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

@@ -87,24 +170,24 @@
`); - const modal = document.getElementById('backup-modal'); - const openBtn = document.getElementById('backup-restore-btn'); - const cancelBtn = document.getElementById('backup-cancel'); - const exportBtn = document.getElementById('backup-export-btn'); - const selectFileBtn = document.getElementById('backup-select-file'); - const fileInput = document.getElementById('backup-file-input'); - const fileNameDiv = document.getElementById('backup-file-name'); - const previewDiv = document.getElementById('backup-preview'); - const previewContent = document.getElementById('backup-preview-content'); - const restoreBtn = document.getElementById('backup-do-restore-btn'); - const resultDiv = document.getElementById('backup-result'); - const scheduleContainer = document.getElementById('backup-schedule-container'); - const historyContainer = document.getElementById('backup-history-container'); + 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'); - let selectedBackup = null; + var selectedBackup = null; // Open modal - openBtn?.addEventListener('click', () => { + openBtn?.addEventListener('click', function() { modal.classList.add('show'); if (resultDiv) resultDiv.style.display = 'none'; if (previewDiv) previewDiv.style.display = 'none'; @@ -114,79 +197,122 @@ wireModal(modal, cancelBtn); - // Export backup - exportBtn?.addEventListener('click', async () => { + // === EXPORT: Server backup + browser state in one file === + exportBtn?.addEventListener('click', async function() { exportBtn.disabled = true; exportBtn.innerHTML = ' Exporting...'; try { - const response = await fetch('/api/v1/backup/export'); - const data = await response.json(); - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); + // 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`; + a.download = 'dashcaddy-backup-' + new Date().toISOString().split('T')[0] + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - resultDiv.innerHTML = '✅ Backup downloaded successfully!'; + + 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.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 Backup'; + exportBtn.innerHTML = '⬇️ Download Full Backup'; }); // Select file button - selectFileBtn?.addEventListener('click', () => fileInput.click()); + selectFileBtn?.addEventListener('click', function() { fileInput.click(); }); - // File selected - fileInput?.addEventListener('change', async (e) => { - const file = e.target.files[0]; + // === FILE SELECTED: Preview contents === + fileInput?.addEventListener('change', async function(e) { + var file = e.target.files[0]; if (!file) return; - fileNameDiv.textContent = `📄 ${file.name}`; + fileNameDiv.textContent = '📄 ' + file.name; fileNameDiv.style.display = 'block'; resultDiv.style.display = 'none'; try { - const text = await file.text(); - const backup = JSON.parse(text); - const response = await secureFetch('/api/v1/backup/preview', { + 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) }); - const preview = await response.json(); + var preview = await response.json(); if (preview.success) { selectedBackup = backup; - let html = `
- Exported: ${new Date(backup.exportedAt).toLocaleString()}
`; - html += '
'; - for (const [key, info] of Object.entries(preview.preview.files)) { - const icon = info.action === 'create' ? '🆕' : '📝'; - html += `${icon} ${info.description}`; + 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 in backup
`; + 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.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.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)'; @@ -194,103 +320,126 @@ } }); - // Restore from file backup - restoreBtn?.addEventListener('click', async () => { + // === RESTORE: Server + browser state === + restoreBtn?.addEventListener('click', async function() { if (!selectedBackup) return; - if (!confirm('This will overwrite your current configuration. Continue?')) return; + if (!confirm('This will overwrite your current configuration and browser preferences. Continue?')) return; restoreBtn.disabled = true; restoreBtn.innerHTML = ' Restoring...'; try { - const reloadCaddy = document.getElementById('backup-reload-caddy')?.checked ?? true; - const response = await secureFetch('/api/v1/backup/restore', { + // 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 } }) + body: JSON.stringify({ backup: selectedBackup, options: { reloadCaddy: reloadCaddy } }) }); - const data = await response.json(); + var data = await response.json(); + + // Then restore browser state + var browserCount = 0; + if (selectedBackup.browserState) { + browserCount = restoreBrowserState(selectedBackup.browserState); + } + if (data.success) { - let msg = `✅ ${data.message}`; + 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(() => location.reload(), 2000); + setTimeout(function() { location.reload(); }, 2000); } else { - resultDiv.innerHTML = `⚠️ ${escapeHtml(data.message)}`; + 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(e => `${escapeHtml(e.file)}: ${escapeHtml(e.error)}`).join(', ') + ''; + 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.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 Configuration'; + restoreBtn.innerHTML = '⚡ Restore Everything'; }); // === Automated Backups Tab === async function loadBackupSchedule() { if (!scheduleContainer) return; try { - const res = await fetch('/api/v1/backups/config'); - const data = await res.json(); + 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'); - const cfg = data.config?.backups || {}; - const autoKey = Object.keys(cfg)[0]; - const auto = autoKey ? cfg[autoKey] : null; + var cfg = data.config?.backups || {}; + var autoKey = Object.keys(cfg)[0]; + var auto = autoKey ? cfg[autoKey] : null; - let html = `
`; - html += `

⏰ Backup Schedule

`; - html += `
`; - html += `
-
`; - html += `
-
`; - html += `
`; - html += `
-
`; - html += `
- - -
`; - html += `
`; - html += ``; + 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)}
`; + scheduleContainer.innerHTML = '
Failed to load schedule: ' + escapeHtml(e.message) + '
'; } } async function saveSchedule() { - const schedule = document.getElementById('backup-schedule-select')?.value; - const retention = parseInt(document.getElementById('backup-retention-select')?.value) || 5; - const encrypt = document.getElementById('backup-encrypt-toggle')?.checked ?? true; - const resultEl = document.getElementById('backup-schedule-result'); + 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 { - const res = await secureFetch('/api/v1/backups/config', { + var res = await secureFetch('/api/v1/backups/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -299,7 +448,7 @@ enabled: schedule !== 'disabled', schedule: schedule === 'disabled' ? 'daily' : schedule, include: ['all'], - encrypt, + encrypt: encrypt, verify: true, retention: { keep: retention }, destinations: [{ type: 'local' }] @@ -307,17 +456,17 @@ } }) }); - const data = await res.json(); + var data = await res.json(); if (resultEl) { - resultEl.innerHTML = data.success ? '✅ Schedule saved' : `⚠️ ${escapeHtml(data.error)}`; + 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(() => { if (resultEl) resultEl.style.display = 'none'; }, 3000); + setTimeout(function() { if (resultEl) resultEl.style.display = 'none'; }, 3000); } } catch (e) { if (resultEl) { - resultEl.innerHTML = `❌ ${escapeHtml(e.message)}`; + 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)'; @@ -326,24 +475,24 @@ } async function runBackupNow() { - const btn = document.getElementById('backup-run-now'); - const resultEl = document.getElementById('backup-schedule-result'); + var btn = document.getElementById('backup-run-now'); + var resultEl = document.getElementById('backup-schedule-result'); if (btn) { btn.disabled = true; btn.innerHTML = ' Running...'; } try { - const res = await secureFetch('/api/v1/backups/execute', { + var res = await secureFetch('/api/v1/backups/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ include: ['all'], destinations: [{ type: 'local' }] }) }); - const data = await res.json(); + var data = await res.json(); if (resultEl) { if (data.success) { - const sizeMB = data.backup?.size ? (data.backup.size / 1024 / 1024).toFixed(2) : '?'; - resultEl.innerHTML = `✅ Backup complete (${sizeMB} MB)`; + 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.innerHTML = '⚠️ ' + escapeHtml(data.error); resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } @@ -352,7 +501,7 @@ loadBackupHistory(); } catch (e) { if (resultEl) { - resultEl.innerHTML = `❌ ${escapeHtml(e.message)}`; + 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)'; @@ -366,50 +515,49 @@ if (!historyContainer) return; historyContainer.innerHTML = '
Loading...
'; try { - const res = await fetch('/api/v1/backups/history?limit=50'); - const data = await res.json(); + 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; } - let html = '
'; - for (const bk of data.history) { - const statusColor = bk.status === 'success' ? '#2ecc71' : '#e74c3c'; - const sizeMB = bk.size ? (bk.size / 1024 / 1024).toFixed(2) : '?'; - html += `
-
- ${escapeHtml(bk.name || 'backup')} -
- ${escapeHtml(bk.status)} - ${bk.status === 'success' ? `` : ''} -
-
-
- ${new Date(bk.timestamp).toLocaleString()} | ${sizeMB} MB | ${bk.duration ? (bk.duration / 1000).toFixed(1) + 's' : '--'} - ${bk.encrypted ? ' | 🔒' : ''} -
-
`; + 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; - // Wire restore buttons with addEventListener (not inline onclick — HTML entity decode bypass) - historyContainer.querySelectorAll('.backup-restore-btn').forEach(btn => { - btn.addEventListener('click', () => window.__restoreServerBackup(btn.dataset.backupId)); + 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)}
`; + 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 { - const res = await secureFetch(`/api/v1/backups/restore/${backupId}`, { + var res = await secureFetch('/api/v1/backups/restore/' + backupId, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ restoreServices: true, restoreConfig: true }) }); - const data = await res.json(); + var data = await res.json(); if (data.success) { showNotification('Restore completed successfully!', 'success'); location.reload(); diff --git a/status/js/import-export.js b/status/js/import-export.js index ff4d9e8..898fdde 100644 --- a/status/js/import-export.js +++ b/status/js/import-export.js @@ -1,142 +1,10 @@ -// ========== IMPORT/EXPORT FUNCTIONALITY ========== +// ========== CADDY RELOAD BUTTON ========== (function() { - // Export dashboard configuration - async function exportDashboard() { - // Fetch themes from server (source of truth) with localStorage fallback - let userThemes = safeGetJSON('user-themes', {}); - try { - const res = await secureFetch('/api/v1/themes'); - const data = await res.json(); - if (data.success && data.themes) userThemes = data.themes; - } catch (e) {} - - const exportData = { - version: '1.0.0', - exportDate: new Date().toISOString(), - services: window.APPS || [], - customServices: safeGetJSON('custom-services', []), - customApps: safeGetJSON('custom-apps', []), - weatherZip: safeGet('weather-zip') || '', - theme: safeGet('theme') || 'dark', - userThemes: userThemes, - // Note: API tokens are intentionally NOT exported for security - }; - - const dataStr = JSON.stringify(exportData, null, 2); - const dataBlob = new Blob([dataStr], { type: 'application/json' }); - const url = URL.createObjectURL(dataBlob); - - const link = document.createElement('a'); - link.href = url; - link.download = `dashcaddy-backup-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - showNotification('Dashboard exported successfully! Note: API tokens are not included for security reasons.', 'success'); - } - - // Import dashboard configuration - function importDashboard() { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'application/json,.json'; - - input.onchange = async (e) => { - const file = e.target.files[0]; - if (!file) return; - - try { - const text = await file.text(); - const importData = JSON.parse(text); - - // Validate import data - if (!importData.version || !importData.services) { - throw new Error('Invalid dashboard backup file'); - } - - // Confirm import - const confirmed = confirm( - `Import dashboard configuration?\n\n` + - `Export Date: ${new Date(importData.exportDate).toLocaleString()}\n` + - `Services: ${importData.services.length}\n` + - `Custom Apps: ${(importData.customApps || []).length}\n\n` + - `⚠️ This will replace your current dashboard configuration.\n` + - `API tokens will need to be reconfigured.` - ); - - if (!confirmed) return; - - // Import services to API - try { - const response = await secureFetch('/api/v1/services', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(importData.services) - }); - - if (!response.ok) { - console.warn('Could not save services to API, saving locally only'); - } - } catch (err) { - console.warn('API not available, saving locally only:', err); - } - - // Import to localStorage - if (importData.customServices) { - safeSet('custom-services', JSON.stringify(importData.customServices)); - } - if (importData.customApps) { - safeSet('custom-apps', JSON.stringify(importData.customApps)); - } - if (importData.weatherZip) { - safeSet('weather-zip', importData.weatherZip); - } - if (importData.theme) { - safeSet('theme', importData.theme); - } - if (importData.userThemes && Object.keys(importData.userThemes).length) { - safeSet('user-themes', JSON.stringify(importData.userThemes)); - // Push imported themes to server - Object.keys(importData.userThemes).forEach(function (slug) { - var t = importData.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 () {}); - }); - } - - // Update APPS array - window.APPS = importData.services; - - showNotification('Dashboard imported successfully! The page will now reload.', 'success'); - - // Reload page to apply changes - window.location.reload(); - - } catch (err) { - showNotification(`Import failed: ${err.message}. Please check the file and try again.`, 'error'); - console.error('Import error:', err); - } - }; - - input.click(); - } - - // Add event listeners for import/export buttons - document.getElementById('export-dashboard')?.addEventListener('click', exportDashboard); - document.getElementById('import-dashboard')?.addEventListener('click', importDashboard); - // Reload Caddy button handler document.getElementById('reload-caddy-top')?.addEventListener('click', async () => { const button = document.getElementById('reload-caddy-top'); const originalText = button.textContent; - + try { button.textContent = '⏳ Reloading...'; button.disabled = true; @@ -145,9 +13,9 @@ method: 'POST', headers: { 'Content-Type': 'application/json' } }); - + const result = await response.json(); - + if (response.ok && result.success) { button.textContent = '✅ Reloaded!'; setTimeout(() => {