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

@@ -239,6 +239,52 @@ describe('DELETE /api/instances/:vmid', () => {
})
})
// ── GET /api/export ───────────────────────────────────────────────────────────
describe('GET /api/export', () => {
it('returns 200 with instances array and attachment header', async () => {
await request(app).post('/api/instances').send(base)
const res = await request(app).get('/api/export')
expect(res.status).toBe(200)
expect(res.headers['content-disposition']).toMatch(/attachment/)
expect(res.body.instances).toHaveLength(1)
expect(res.body.instances[0].name).toBe('traefik')
})
it('returns empty instances array when no data', async () => {
const res = await request(app).get('/api/export')
expect(res.body.instances).toEqual([])
})
})
// ── POST /api/import ──────────────────────────────────────────────────────────
describe('POST /api/import', () => {
it('replaces all instances and returns imported count', async () => {
await request(app).post('/api/instances').send(base)
const res = await request(app).post('/api/import')
.send({ instances: [{ ...base, vmid: 999, name: 'imported' }] })
expect(res.status).toBe(200)
expect(res.body.imported).toBe(1)
expect((await request(app).get('/api/instances')).body[0].name).toBe('imported')
})
it('returns 400 if instances is not an array', async () => {
expect((await request(app).post('/api/import').send({ instances: 'bad' })).status).toBe(400)
})
it('returns 400 with per-row errors for invalid rows', async () => {
const res = await request(app).post('/api/import')
.send({ instances: [{ ...base, name: '', vmid: 1 }] })
expect(res.status).toBe(400)
expect(res.body.errors[0].index).toBe(0)
})
it('returns 400 if body has no instances key', async () => {
expect((await request(app).post('/api/import').send({})).status).toBe(400)
})
})
// ── Static assets & SPA routing ───────────────────────────────────────────────
describe('static assets and SPA routing', () => {