Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
421
status/js/backup-restore.js
Normal file
421
status/js/backup-restore.js
Normal file
@@ -0,0 +1,421 @@
|
||||
// ========== BACKUP/RESTORE (Enhanced) ==========
|
||||
(function() {
|
||||
// Inject modal HTML
|
||||
injectModal('backup-modal', `<div id="backup-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 550px; max-width: 650px;">
|
||||
<h3>💾 Backup & Restore</h3>
|
||||
<p class="modal-subtitle">
|
||||
Manual and automated backups for your DashCaddy configuration.
|
||||
</p>
|
||||
|
||||
<!-- Tab bar -->
|
||||
<div class="panel-tabs">
|
||||
<button class="panel-tab active" data-panel="backup-manual">Manual</button>
|
||||
<button class="panel-tab" data-panel="backup-automated">Automated</button>
|
||||
<button class="panel-tab" data-panel="backup-history-tab">History</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Manual (existing export/import) -->
|
||||
<div id="backup-manual" class="panel-section active">
|
||||
<!-- Export Section -->
|
||||
<div style="margin-bottom: 20px; padding: 16px; background: linear-gradient(135deg, color-mix(in srgb, #27ae60 15%, transparent), color-mix(in srgb, #2ecc71 10%, transparent)); border-radius: 10px; border: 1px solid #27ae60;">
|
||||
<h4 style="margin: 0 0 8px; color: #2ecc71; font-size: 0.95rem;">📤 Export Backup</h4>
|
||||
<p style="font-size: 0.8rem; color: var(--muted); margin: 0 0 12px;">
|
||||
Download all your settings: services, Caddyfile, DNS credentials, notifications.
|
||||
</p>
|
||||
<button id="backup-export-btn" style="width: 100%; padding: 10px; background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); border: none; color: white; font-weight: 600; border-radius: 8px; cursor: pointer;">
|
||||
⬇️ Download Backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Import Section -->
|
||||
<div style="padding: 16px; background: linear-gradient(135deg, color-mix(in srgb, #3498db 15%, transparent), color-mix(in srgb, #2980b9 10%, transparent)); border-radius: 10px; border: 1px solid #3498db;">
|
||||
<h4 style="margin: 0 0 8px; color: #3498db; font-size: 0.95rem;">📥 Restore Backup</h4>
|
||||
<p style="font-size: 0.8rem; color: var(--muted); margin: 0 0 12px;">
|
||||
Upload a backup file to restore your configuration.
|
||||
</p>
|
||||
<input type="file" id="backup-file-input" accept=".json" style="display: none;" />
|
||||
<button id="backup-select-file" style="width: 100%; padding: 10px; background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); border: none; color: white; font-weight: 600; border-radius: 8px; cursor: pointer;">
|
||||
📁 Select Backup File
|
||||
</button>
|
||||
<div id="backup-file-name" style="display: none; margin-top: 8px; padding: 8px; background: var(--card-base); border-radius: 6px; font-size: 0.85rem;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section (shown after file selected) -->
|
||||
<div id="backup-preview" style="display: none; margin-top: 16px; padding: 16px; background: var(--card-base); border-radius: 10px; border: 1px solid var(--border);">
|
||||
<h4 style="margin: 0 0 12px; font-size: 0.9rem;">📋 Backup Contents</h4>
|
||||
<div id="backup-preview-content" style="font-size: 0.85rem;"></div>
|
||||
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
|
||||
<label class="checkbox-label" style="font-size: 0.85rem;">
|
||||
<input type="checkbox" id="backup-reload-caddy" checked />
|
||||
Reload Caddy after restore
|
||||
</label>
|
||||
</div>
|
||||
<button id="backup-do-restore-btn" style="width: 100%; margin-top: 12px; padding: 10px; background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%); border: none; color: white; font-weight: 600; border-radius: 8px; cursor: pointer;">
|
||||
⚡ Restore Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Result Message -->
|
||||
<div id="backup-result" style="display: none; margin-top: 16px; padding: 12px; border-radius: 8px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Automated Backups -->
|
||||
<div id="backup-automated" class="panel-section">
|
||||
<div id="backup-schedule-container">
|
||||
<div class="panel-empty">
|
||||
<span class="empty-icon">⏰</span>
|
||||
<span class="brand-spinner"></span> Loading backup schedule...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Backup History -->
|
||||
<div id="backup-history-tab" class="panel-section">
|
||||
<div id="backup-history-container" style="max-height: 400px; overflow-y: auto;">
|
||||
<div class="panel-empty">
|
||||
<span class="empty-icon">📋</span>
|
||||
<span class="brand-spinner"></span> Loading backup history...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="weather-modal-buttons modal-footer-bar">
|
||||
<button id="backup-cancel">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
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 = '<span class="brand-spinner"></span> 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: ${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 = `<div style="margin-bottom: 8px; color: var(--muted); font-size: 0.8rem;">
|
||||
Exported: ${new Date(backup.exportedAt).toLocaleString()}</div>`;
|
||||
html += '<div style="display: flex; flex-wrap: wrap; gap: 6px;">';
|
||||
for (const [key, info] of Object.entries(preview.preview.files)) {
|
||||
const icon = info.action === 'create' ? '🆕' : '📝';
|
||||
html += `<span style="padding: 4px 8px; background: var(--base); border-radius: 4px; font-size: 0.8rem;">${icon} ${info.description}</span>`;
|
||||
}
|
||||
html += '</div>';
|
||||
if (preview.preview.serviceCount) {
|
||||
html += `<div style="margin-top: 8px; font-size: 0.8rem; color: var(--accent);">${preview.preview.serviceCount} services in backup</div>`;
|
||||
}
|
||||
previewContent.innerHTML = html;
|
||||
previewDiv.style.display = 'block';
|
||||
} else {
|
||||
resultDiv.innerHTML = `⚠️ Invalid backup file: ${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: ${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 = '<span class="brand-spinner"></span> 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 += '<br><small style="color: var(--muted);">Caddy configuration reloaded</small>';
|
||||
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 = `⚠️ ${data.message}`;
|
||||
if (data.results?.errors?.length > 0) {
|
||||
resultDiv.innerHTML += '<br><small>' + data.results.errors.map(e => `${e.file}: ${e.error}`).join(', ') + '</small>';
|
||||
}
|
||||
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: ${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 = `<div style="padding: 16px; background: color-mix(in srgb, var(--accent) 8%, transparent); border-radius: 10px; border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent); margin-bottom: 16px;">`;
|
||||
html += `<h4 style="margin: 0 0 12px; color: var(--accent); font-size: 0.9rem;">⏰ Backup Schedule</h4>`;
|
||||
html += `<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">`;
|
||||
html += `<div><label style="font-size: 0.8rem; color: var(--muted);">Schedule:</label>
|
||||
<select id="backup-schedule-select" style="width: 100%;">
|
||||
<option value="disabled" ${!auto?.enabled ? 'selected' : ''}>Disabled</option>
|
||||
<option value="hourly" ${auto?.schedule === 'hourly' ? 'selected' : ''}>Hourly</option>
|
||||
<option value="daily" ${auto?.schedule === 'daily' ? 'selected' : ''}>Daily</option>
|
||||
<option value="weekly" ${auto?.schedule === 'weekly' ? 'selected' : ''}>Weekly</option>
|
||||
<option value="monthly" ${auto?.schedule === 'monthly' ? 'selected' : ''}>Monthly</option>
|
||||
</select></div>`;
|
||||
html += `<div><label style="font-size: 0.8rem; color: var(--muted);">Keep last:</label>
|
||||
<select id="backup-retention-select" style="width: 100%;">
|
||||
<option value="3" ${auto?.retention?.keep === 3 ? 'selected' : ''}>3 backups</option>
|
||||
<option value="5" ${!auto?.retention || auto?.retention?.keep === 5 ? 'selected' : ''}>5 backups</option>
|
||||
<option value="10" ${auto?.retention?.keep === 10 ? 'selected' : ''}>10 backups</option>
|
||||
<option value="30" ${auto?.retention?.keep === 30 ? 'selected' : ''}>30 backups</option>
|
||||
</select></div>`;
|
||||
html += `</div>`;
|
||||
html += `<div style="margin-top: 12px;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; font-size: 0.85rem; cursor: pointer;">
|
||||
<input type="checkbox" id="backup-encrypt-toggle" ${auto?.encrypt !== false ? 'checked' : ''} />
|
||||
Encrypt backups
|
||||
</label></div>`;
|
||||
html += `<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||
<button id="backup-save-schedule" style="padding: 8px 16px; background: color-mix(in srgb, var(--accent) 20%, transparent); border: 1px solid var(--accent); color: var(--accent); border-radius: 6px; cursor: pointer; font-weight: 500;">Save Schedule</button>
|
||||
<button id="backup-run-now" style="padding: 8px 16px; border-radius: 6px; cursor: pointer;">▶️ Run Backup Now</button>
|
||||
</div>`;
|
||||
html += `</div>`;
|
||||
html += `<div id="backup-schedule-result" style="display: none; margin-top: 12px; padding: 10px; border-radius: 8px; font-size: 0.85rem;"></div>`;
|
||||
scheduleContainer.innerHTML = html;
|
||||
|
||||
document.getElementById('backup-save-schedule')?.addEventListener('click', saveSchedule);
|
||||
document.getElementById('backup-run-now')?.addEventListener('click', runBackupNow);
|
||||
} catch (e) {
|
||||
scheduleContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed to load schedule: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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' : `⚠️ ${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 = `❌ ${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 = '<span class="brand-spinner"></span> 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 = `⚠️ ${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 = `❌ ${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 = '<div class="panel-empty"><span class="brand-spinner"></span> Loading...</div>';
|
||||
try {
|
||||
const res = await fetch('/api/v1/backups/history?limit=50');
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.history?.length) {
|
||||
historyContainer.innerHTML = '<div class="panel-empty"><span class="empty-icon">📋</span> No backup history yet</div>';
|
||||
return;
|
||||
}
|
||||
let html = '<div style="display: flex; flex-direction: column; gap: 6px;">';
|
||||
for (const bk of data.history) {
|
||||
const statusColor = bk.status === 'success' ? '#2ecc71' : '#e74c3c';
|
||||
const sizeMB = bk.size ? (bk.size / 1024 / 1024).toFixed(2) : '?';
|
||||
html += `<div style="padding: 10px 12px; background: var(--card-base); border-radius: 6px; border: 1px solid var(--border); font-size: 0.85rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||
<span style="font-weight: 500;">${bk.name || 'backup'}</span>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="status-badge ${bk.status === 'success' ? 'success' : 'down'}">${bk.status}</span>
|
||||
${bk.status === 'success' ? `<button onclick="window.__restoreServerBackup('${bk.id}')" style="padding: 3px 8px; font-size: 0.75rem;">Restore</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 0.75rem; color: var(--muted);">
|
||||
${new Date(bk.timestamp).toLocaleString()} | ${sizeMB} MB | ${bk.duration ? (bk.duration / 1000).toFixed(1) + 's' : '--'}
|
||||
${bk.encrypted ? ' | 🔒' : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
historyContainer.innerHTML = html;
|
||||
} catch (e) {
|
||||
historyContainer.innerHTML = `<div class="panel-empty" style="color: var(--bad-fg);">Failed: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
})();
|
||||
Reference in New Issue
Block a user