feat: settings modal with database export and import
All checks were successful
CI / test (pull_request) Successful in 12s
CI / build-dev (pull_request) Has been skipped

Adds a gear button to the nav that opens a settings modal with:
- Export: GET /api/export returns all instances as a JSON backup file
  with a Content-Disposition attachment header
- Import: POST /api/import validates and bulk-replaces all instances;
  client uses FileReader to POST the parsed JSON, with a confirm dialog
  before destructive replace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 14:10:59 -04:00
parent 20d8a13375
commit af207339a4
7 changed files with 230 additions and 4 deletions

View File

@@ -258,12 +258,62 @@ function showToast(msg, type = 'success') {
toastTimer = setTimeout(() => t.classList.remove('show'), 3000);
}
// ── Settings Modal ────────────────────────────────────────────────────────────
function openSettingsModal() {
document.getElementById('settings-modal').classList.add('open');
}
function closeSettingsModal() {
document.getElementById('settings-modal').classList.remove('open');
document.getElementById('import-file').value = '';
}
async function exportDB() {
const res = await fetch('/api/export');
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `catalyst-backup-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
}
async function importDB() {
const file = document.getElementById('import-file').files[0];
if (!file) { showToast('Select a backup file first', 'error'); return; }
document.getElementById('confirm-title').textContent = 'Replace all instances?';
document.getElementById('confirm-msg').textContent =
`This will delete all current instances and replace them with the contents of "${file.name}". This cannot be undone.`;
document.getElementById('confirm-overlay').classList.add('open');
document.getElementById('confirm-ok').onclick = async () => {
closeConfirm();
try {
const { instances } = JSON.parse(await file.text());
const res = await fetch('/api/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instances }),
});
const data = await res.json();
if (!res.ok) { showToast(data.error ?? 'Import failed', 'error'); return; }
showToast(`Imported ${data.imported} instance${data.imported !== 1 ? 's' : ''}`, 'success');
closeSettingsModal();
renderDashboard();
} catch {
showToast('Invalid backup file', 'error');
}
};
}
// ── Keyboard / backdrop ───────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
if (e.key !== 'Escape') return;
if (document.getElementById('instance-modal').classList.contains('open')) { closeModal(); return; }
if (document.getElementById('confirm-overlay').classList.contains('open')) { closeConfirm(); return; }
if (document.getElementById('instance-modal').classList.contains('open')) { closeModal(); return; }
if (document.getElementById('confirm-overlay').classList.contains('open')) { closeConfirm(); return; }
if (document.getElementById('settings-modal').classList.contains('open')) { closeSettingsModal(); return; }
});
document.getElementById('instance-modal').addEventListener('click', e => {
@@ -272,3 +322,6 @@ document.getElementById('instance-modal').addEventListener('click', e => {
document.getElementById('confirm-overlay').addEventListener('click', e => {
if (e.target === document.getElementById('confirm-overlay')) closeConfirm();
});
document.getElementById('settings-modal').addEventListener('click', e => {
if (e.target === document.getElementById('settings-modal')) closeSettingsModal();
});