/** * Settings — backup / restore configuration. */ import { apiKey } from '../core/state.js'; import { API_BASE, fetchWithAuth } from '../core/api.js'; import { Modal } from '../core/modal.js'; import { showToast, showConfirm } from '../core/ui.js'; import { t } from '../core/i18n.js'; import { ICON_UNDO, ICON_DOWNLOAD } from '../core/icons.js'; // Simple modal (no form / no dirty check needed) const settingsModal = new Modal('settings-modal'); export function openSettingsModal() { document.getElementById('settings-error').style.display = 'none'; settingsModal.open(); loadAutoBackupSettings(); loadBackupList(); } export function closeSettingsModal() { settingsModal.forceClose(); } // ─── Backup ──────────────────────────────────────────────── export async function downloadBackup() { try { const resp = await fetchWithAuth('/system/backup', { timeout: 30000 }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } const blob = await resp.blob(); const disposition = resp.headers.get('Content-Disposition') || ''; const match = disposition.match(/filename="(.+?)"/); const filename = match ? match[1] : 'ledgrab-backup.json'; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); showToast(t('settings.backup.success'), 'success'); } catch (err) { console.error('Backup download failed:', err); showToast(t('settings.backup.error') + ': ' + err.message, 'error'); } } // ─── Restore ─────────────────────────────────────────────── export async function handleRestoreFileSelected(input) { const file = input.files[0]; input.value = ''; if (!file) return; const confirmed = await showConfirm(t('settings.restore.confirm')); if (!confirmed) return; try { const formData = new FormData(); formData.append('file', file); const resp = await fetch(`${API_BASE}/system/restore`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, body: formData, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } const data = await resp.json(); showToast(data.message || t('settings.restore.success'), 'success'); settingsModal.forceClose(); if (data.restart_scheduled) { showRestartOverlay(); } } catch (err) { console.error('Restore failed:', err); showToast(t('settings.restore.error') + ': ' + err.message, 'error'); } } // ─── Restart overlay ─────────────────────────────────────── function showRestartOverlay() { const overlay = document.createElement('div'); overlay.id = 'restart-overlay'; overlay.style.cssText = 'position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;' + 'align-items:center;justify-content:center;background:rgba(0,0,0,0.85);color:#fff;font-size:1.2rem;'; overlay.innerHTML = '
' + `
${t('settings.restore.restarting')}
`; // Add spinner animation if not present if (!document.getElementById('restart-spinner-style')) { const style = document.createElement('style'); style.id = 'restart-spinner-style'; style.textContent = '@keyframes spin{to{transform:rotate(360deg)}}'; document.head.appendChild(style); } document.body.appendChild(overlay); pollHealth(); } function pollHealth() { const start = Date.now(); const maxWait = 30000; const interval = 1500; const check = async () => { if (Date.now() - start > maxWait) { const msg = document.getElementById('restart-msg'); if (msg) msg.textContent = t('settings.restore.restart_timeout'); return; } try { const resp = await fetch('/health', { signal: AbortSignal.timeout(3000) }); if (resp.ok) { window.location.reload(); return; } } catch { /* server still down */ } setTimeout(check, interval); }; // Wait a moment before first check to let the server shut down setTimeout(check, 2000); } // ─── Auto-Backup settings ───────────────────────────────── export async function loadAutoBackupSettings() { try { const resp = await fetchWithAuth('/system/auto-backup/settings'); if (!resp.ok) return; const data = await resp.json(); document.getElementById('auto-backup-enabled').checked = data.enabled; document.getElementById('auto-backup-interval').value = String(data.interval_hours); document.getElementById('auto-backup-max').value = data.max_backups; const statusEl = document.getElementById('auto-backup-status'); if (data.last_backup_time) { const d = new Date(data.last_backup_time); statusEl.textContent = t('settings.auto_backup.last_backup') + ': ' + d.toLocaleString(); } else { statusEl.textContent = t('settings.auto_backup.last_backup') + ': ' + t('settings.auto_backup.never'); } } catch (err) { console.error('Failed to load auto-backup settings:', err); } } export async function saveAutoBackupSettings() { const enabled = document.getElementById('auto-backup-enabled').checked; const interval_hours = parseFloat(document.getElementById('auto-backup-interval').value); const max_backups = parseInt(document.getElementById('auto-backup-max').value, 10); try { const resp = await fetchWithAuth('/system/auto-backup/settings', { method: 'PUT', body: JSON.stringify({ enabled, interval_hours, max_backups }), }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } showToast(t('settings.auto_backup.saved'), 'success'); loadAutoBackupSettings(); loadBackupList(); } catch (err) { console.error('Failed to save auto-backup settings:', err); showToast(t('settings.auto_backup.save_error') + ': ' + err.message, 'error'); } } // ─── Saved backup list ──────────────────────────────────── export async function loadBackupList() { const container = document.getElementById('saved-backups-list'); try { const resp = await fetchWithAuth('/system/backups'); if (!resp.ok) return; const data = await resp.json(); if (data.count === 0) { container.innerHTML = `
${t('settings.saved_backups.empty')}
`; return; } container.innerHTML = data.backups.map(b => { const sizeKB = (b.size_bytes / 1024).toFixed(1); const date = new Date(b.created_at).toLocaleString(); return `
${date} ${sizeKB} KB
`; }).join(''); } catch (err) { console.error('Failed to load backup list:', err); container.innerHTML = ''; } } export async function downloadSavedBackup(filename) { try { const resp = await fetchWithAuth(`/system/backups/${encodeURIComponent(filename)}`, { timeout: 30000 }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } const blob = await resp.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); } catch (err) { console.error('Backup download failed:', err); showToast(t('settings.backup.error') + ': ' + err.message, 'error'); } } export async function restoreSavedBackup(filename) { const confirmed = await showConfirm(t('settings.restore.confirm')); if (!confirmed) return; try { // Download the backup file from the server const dlResp = await fetchWithAuth(`/system/backups/${encodeURIComponent(filename)}`, { timeout: 30000 }); if (!dlResp.ok) { const err = await dlResp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${dlResp.status}`); } const blob = await dlResp.blob(); // POST it to the restore endpoint const formData = new FormData(); formData.append('file', blob, filename); const resp = await fetch(`${API_BASE}/system/restore`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` }, body: formData, }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } const data = await resp.json(); showToast(data.message || t('settings.restore.success'), 'success'); settingsModal.forceClose(); if (data.restart_scheduled) { showRestartOverlay(); } } catch (err) { console.error('Restore from saved backup failed:', err); showToast(t('settings.restore.error') + ': ' + err.message, 'error'); } } export async function deleteSavedBackup(filename) { const confirmed = await showConfirm(t('settings.saved_backups.delete_confirm')); if (!confirmed) return; try { const resp = await fetchWithAuth(`/system/backups/${encodeURIComponent(filename)}`, { method: 'DELETE', }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || `HTTP ${resp.status}`); } loadBackupList(); } catch (err) { console.error('Backup delete failed:', err); showToast(t('settings.saved_backups.delete_error') + ': ' + err.message, 'error'); } }