From ef7a021d89a512760427bb4080271ed930b160be Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:28:29 -0400 Subject: [PATCH] Count import changes against the server's export The import dialog compared the file with the editor's in-memory items, which keep row key order and secret values the export does not, so an unchanged export reported updated items. The export also left out the Settings system item, so a fresh install's export reported one deletion. The dialog now compares the file with a fresh export using a key-order independent comparison, and the export includes the system items. --- api/src/routes/config.js | 2 +- api/test/http-integration.test.js | 15 ++++++++++++++- ui/admin/index.html | 2 +- ui/js/admin-drag.js | 2 +- ui/js/admin-list.js | 4 ++-- ui/js/admin-save-logic.js | 19 +++++++++++++++++++ ui/js/admin-settings.js | 2 +- ui/js/admin.js | 22 ++++++---------------- ui/js/import-foreign.js | 2 +- ui/test/admin-save-logic.test.mjs | 25 +++++++++++++++++++++++++ 10 files changed, 71 insertions(+), 24 deletions(-) diff --git a/api/src/routes/config.js b/api/src/routes/config.js index 3707c37d..52d76809 100644 --- a/api/src/routes/config.js +++ b/api/src/routes/config.js @@ -230,7 +230,7 @@ on('POST', '/api/config', async (req, res) => { }); on('GET', '/api/config/export', (_, res) => { - const safe = scrubSecrets(loadConfig()); + const safe = ensureSystemItems(scrubSecrets(loadConfig())); delete safe._rev; const d = JSON.stringify(safe, null, 2); res.writeHead(200, { diff --git a/api/test/http-integration.test.js b/api/test/http-integration.test.js index 4e7a4fe2..019add3e 100644 --- a/api/test/http-integration.test.js +++ b/api/test/http-integration.test.js @@ -35,7 +35,7 @@ const { unfinishedUpload } = require('../test-support/unfinished-upload'); require('../src/routes'); // registers auth/config/health/badges/... + OPTIONS require('../src/widget-data'); // registers /api/widget-data/:id (pulls in widgets) const { dispatch, on } = require('../src/router'); -const { saveConfig, loadConfig } = require('../src/config'); +const { saveConfig, loadConfig, loadConfigForUpdate } = require('../src/config'); const { hashPassword, makeToken, verifyToken, clearAttempts, RENEW_AFTER_MS } = require('../src/auth'); /* Routes that fail on purpose, to prove the router turns a handler error into a @@ -407,6 +407,19 @@ test('GET /api/config/export is downloadable and free of secrets', async () => { assert.equal(r.body.settings?.auth?.passwordHash, undefined); }); +test('GET /api/config/export lists the same items as GET /api/config', async () => { + const cfg = loadConfigForUpdate(); + cfg.items = []; + saveConfig(cfg, { keepRev: true }); + const exp = await req('GET', '/api/config/export', { cookie: validCookie }); + const live = await req('GET', '/api/config', { cookie: validCookie }); + assert.deepEqual( + exp.body.items.map(i => i.id), + live.body.items.map(i => i.id), + ); + assert.ok(exp.body.items.some(i => i.system === 'settings')); +}); + test('POST /api/config drops unknown settings and preserves the stored auth secret', async () => { const r = await req('POST', '/api/config', { cookie: validCookie, diff --git a/ui/admin/index.html b/ui/admin/index.html index 6228512d..3497c55b 100644 --- a/ui/admin/index.html +++ b/ui/admin/index.html @@ -301,6 +301,6 @@