// ========== BACKUP/RESTORE (Enhanced) ========== (function() { // Inject modal HTML injectModal('backup-modal', `

💾 Backup & Restore

📤 Export Backup

Download all your settings: services, Caddyfile, DNS credentials, notifications.

📥 Restore Backup

Upload a backup file to restore your configuration.

Loading backup schedule...
📋 Loading backup history...
`); 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'); let selectedBackup = null; // Open modal openBtn?.addEventListener('click', () => { 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 backup exportBtn?.addEventListener('click', async () => { 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'); 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); resultDiv.innerHTML = '✅ Backup downloaded successfully!'; 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 Backup'; }); // Select file button selectFileBtn?.addEventListener('click', () => fileInput.click()); // File selected fileInput?.addEventListener('change', async (e) => { const file = e.target.files[0]; if (!file) return; 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', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(backup) }); const 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}`; } html += '
'; if (preview.preview.serviceCount) { html += `
${preview.preview.serviceCount} services in backup
`; } 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 from file backup restoreBtn?.addEventListener('click', async () => { if (!selectedBackup) return; if (!confirm('This will overwrite your current configuration. 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', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backup: selectedBackup, options: { reloadCaddy } }) }); const data = await response.json(); if (data.success) { let msg = `✅ ${data.message}`; 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); } else { resultDiv.innerHTML = `⚠️ ${escapeHtml(data.message)}`; if (data.results?.errors?.length > 0) { resultDiv.innerHTML += '
' + data.results.errors.map(e => `${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 Configuration'; }); // === Automated Backups Tab === async function loadBackupSchedule() { if (!scheduleContainer) return; try { const res = await fetch('/api/v1/backups/config'); const 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; let html = `
`; html += `

⏰ Backup Schedule

`; html += `
`; html += `
`; html += `
`; html += `
`; html += `
`; html += `
`; html += `
`; html += ``; scheduleContainer.innerHTML = html; document.getElementById('backup-save-schedule')?.addEventListener('click', saveSchedule); document.getElementById('backup-run-now')?.addEventListener('click', runBackupNow); } catch (e) { scheduleContainer.innerHTML = `
Failed to load schedule: ${escapeHtml(e.message)}
`; } } async function saveSchedule() { 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'); try { const res = await secureFetch('/api/v1/backups/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backups: { auto: { enabled: schedule !== 'disabled', schedule: schedule === 'disabled' ? 'daily' : schedule, include: ['all'], encrypt, verify: true, retention: { keep: retention }, destinations: [{ type: 'local' }] } } }) }); const data = await res.json(); if (resultEl) { resultEl.innerHTML = data.success ? '✅ Schedule saved' : `⚠️ ${escapeHtml(data.error)}`; resultEl.style.display = 'block'; resultEl.style.background = data.success ? 'color-mix(in srgb, var(--ok-fg) 15%, transparent)' : 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = data.success ? '1px solid var(--ok-fg)' : '1px solid var(--bad-fg)'; setTimeout(() => { if (resultEl) resultEl.style.display = 'none'; }, 3000); } } catch (e) { if (resultEl) { resultEl.innerHTML = `❌ ${escapeHtml(e.message)}`; resultEl.style.display = 'block'; resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } } } async function runBackupNow() { const btn = document.getElementById('backup-run-now'); const resultEl = document.getElementById('backup-schedule-result'); if (btn) { btn.disabled = true; btn.innerHTML = ' Running...'; } try { const 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(); if (resultEl) { if (data.success) { const sizeMB = data.backup?.size ? (data.backup.size / 1024 / 1024).toFixed(2) : '?'; resultEl.innerHTML = `✅ Backup complete (${sizeMB} MB)`; resultEl.style.background = 'color-mix(in srgb, var(--ok-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--ok-fg)'; } else { resultEl.innerHTML = `⚠️ ${escapeHtml(data.error)}`; resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } resultEl.style.display = 'block'; } loadBackupHistory(); } catch (e) { if (resultEl) { resultEl.innerHTML = `❌ ${escapeHtml(e.message)}`; resultEl.style.display = 'block'; resultEl.style.background = 'color-mix(in srgb, var(--bad-fg) 15%, transparent)'; resultEl.style.border = '1px solid var(--bad-fg)'; } } if (btn) { btn.disabled = false; btn.innerHTML = '▶️ Run Backup Now'; } } // === Backup History Tab === async function loadBackupHistory() { if (!historyContainer) return; historyContainer.innerHTML = '
Loading...
'; try { const res = await fetch('/api/v1/backups/history?limit=50'); const 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 ? ' | 🔒' : ''}
`; } 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)); }); } 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 { const 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(); if (data.success) { showNotification('Restore completed successfully!', 'success'); location.reload(); } else { showNotification('Restore failed: ' + (data.error || 'Unknown error'), 'error'); } } catch (e) { showNotification('Restore error: ' + e.message, 'error'); } }; // Lazy-load tabs document.querySelector('[data-panel="backup-automated"]')?.addEventListener('click', loadBackupSchedule); document.querySelector('[data-panel="backup-history-tab"]')?.addEventListener('click', loadBackupHistory); })();