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 @@
`);
- 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 += `
Schedule:
-
- Disabled
- Hourly
- Daily
- Weekly
- Monthly
-
`;
- html += `
Keep last:
-
- 3 backups
- 5 backups
- 10 backups
- 30 backups
-
`;
- html += `
`;
- html += `
-
-
- Encrypt backups
-
`;
- html += `
- Save Schedule
- ▶️ Run Backup Now
-
`;
- html += `
`;
- html += `
`;
+ var html = '
';
+ html += '
⏰ Backup Schedule ';
+ html += '
';
+ html += '
Schedule: ';
+ html += ' ';
+ html += ' Disabled ';
+ html += ' Hourly ';
+ html += ' Daily ';
+ html += ' Weekly ';
+ html += ' Monthly ';
+ html += '
';
+ html += '
Keep last: ';
+ html += ' ';
+ html += ' 3 backups ';
+ html += ' 5 backups ';
+ html += ' 10 backups ';
+ html += ' 30 backups ';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += ' ';
+ html += ' ';
+ html += ' Encrypt backups';
+ html += '
';
+ html += '
';
+ html += ' Save Schedule ';
+ html += ' ▶️ Run Backup Now ';
+ 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' ? `Restore ` : ''}
-
-
-
- ${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 += ' Restore ';
+ 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(() => {