feat: settings modal with database export and import
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:
57
js/ui.js
57
js/ui.js
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user