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:
15
server/db.js
15
server/db.js
@@ -125,6 +125,21 @@ export function deleteInstance(vmid) {
|
||||
return db.prepare('DELETE FROM instances WHERE vmid = ?').run(vmid);
|
||||
}
|
||||
|
||||
export function importInstances(rows) {
|
||||
db.exec('BEGIN');
|
||||
db.exec('DELETE FROM instances');
|
||||
const insert = db.prepare(`
|
||||
INSERT INTO instances
|
||||
(name, state, stack, vmid, atlas, argus, semaphore, patchmon,
|
||||
tailscale, andromeda, tailscale_ip, hardware_acceleration)
|
||||
VALUES
|
||||
(@name, @state, @stack, @vmid, @atlas, @argus, @semaphore, @patchmon,
|
||||
@tailscale, @andromeda, @tailscale_ip, @hardware_acceleration)
|
||||
`);
|
||||
for (const row of rows) insert.run(row);
|
||||
db.exec('COMMIT');
|
||||
}
|
||||
|
||||
// ── Test helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function _resetForTest() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import {
|
||||
getInstances, getInstance, getDistinctStacks,
|
||||
createInstance, updateInstance, deleteInstance,
|
||||
createInstance, updateInstance, deleteInstance, importInstances,
|
||||
} from './db.js';
|
||||
|
||||
export const router = Router();
|
||||
@@ -104,6 +104,35 @@ router.put('/instances/:vmid', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/export
|
||||
router.get('/export', (_req, res) => {
|
||||
const instances = getInstances();
|
||||
const date = new Date().toISOString().slice(0, 10);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="catalyst-backup-${date}.json"`);
|
||||
res.json({ version: 1, exported_at: new Date().toISOString(), instances });
|
||||
});
|
||||
|
||||
// POST /api/import
|
||||
router.post('/import', (req, res) => {
|
||||
const { instances } = req.body ?? {};
|
||||
if (!Array.isArray(instances)) {
|
||||
return res.status(400).json({ error: 'body must contain an instances array' });
|
||||
}
|
||||
const errors = [];
|
||||
for (const [i, row] of instances.entries()) {
|
||||
const errs = validate(normalise(row));
|
||||
if (errs.length) errors.push({ index: i, errors: errs });
|
||||
}
|
||||
if (errors.length) return res.status(400).json({ errors });
|
||||
try {
|
||||
importInstances(instances.map(normalise));
|
||||
res.json({ imported: instances.length });
|
||||
} catch (e) {
|
||||
console.error('POST /api/import', e);
|
||||
res.status(500).json({ error: 'internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/instances/:vmid
|
||||
router.delete('/instances/:vmid', (req, res) => {
|
||||
const vmid = parseInt(req.params.vmid, 10);
|
||||
|
||||
Reference in New Issue
Block a user