// ========== 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
Full backup of your entire DashCaddy setup — server config, credentials, themes, and browser preferences in one file.
Manual
Schedules
Backups on Disk
Point-in-Time
History
📤 Export Backup
Downloads everything — services, Caddyfile, credentials, encryption key, themes, and all browser preferences.
⬇️ Download Full Backup
💾
Loading backup files...
📋
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.
' +
'
Upgrade to Premium ' +
'
';
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) + '
' +
'
' +
' ' +
' ' + (sch.enabled ? 'ON' : 'OFF') + ' ' +
' ' +
'
' +
'
' +
'
Schedule: ' +
' Hourly ' +
' Daily ' +
' Weekly ' +
' Monthly ' +
'
' +
'
Keep last:
' +
'
' +
'
' +
'
Next run: ' + escapeHtml(nextRunStr) + '
' +
'
Last run: ' + escapeHtml(lastRunStr) + '
' +
'
' +
'
' +
' ▶️ Run Now ' +
' 🗑️ Remove ' +
'
' +
'
';
}
html += '
';
// Add "Add Schedule" section at bottom
html += '';
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 + '
' +
'
' +
' Compare ' +
' Restore ' +
'
' +
'
';
}
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 += ' 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;
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.
' +
'
Upgrade to Premium ' +
'
';
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 = '' +
'
' +
' App: ' +
' ';
for (var i = 0; i < services.length; i++) {
html += '' +
escapeHtml(services[i].name || services[i].id || '') + ' ';
}
html += ' ' +
' Load Backups ' +
'
' +
'
' +
'
';
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 + '
' +
'
' +
' Compare ' +
' Restore ' +
'
' +
'
';
}
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 += '
Close ' +
'
';
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');
}
}
})();