Hub stage 3: compare and sync app configs across servers

The hub keeps a baseline per app in /data/apps/<app>/baseline.json. Local
fields (overrides.json) are left out of the sync and out of the matrix
comparison. Each server gets the baseline plus its own values.

Applying goes target picker, preview with per-server diffs and warnings,
then apply. Apply refuses a server whose config changed since the
preview, saves the old config to history, then reads the new one back
to check it.

Large config bodies are read only for signed-in callers. The App config
dialog has Compare, Baseline and Local fields tabs and an empty state
that offers an import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-31 16:46:15 +07:00
1 parent 6acd2d656d
commit 0e05039202
22 files changed
+2434 -58

No files matched your search

+8 -2
View File
@@ -197,8 +197,8 @@ if (VARIANT === 'b') {
}
const appConfigs = {
'Advanced Blocking': VARIANT === 'b'
? { enableBlocking: true, blockingAnswerTtl: 60, networkGroupMap: { '0.0.0.0/0': 'default' }, groups: [{ name: 'default', enableBlocking: false, blockAsNxDomain: false }] }
: { enableBlocking: true, blockingAnswerTtl: 30, networkGroupMap: { '0.0.0.0/0': 'default' }, groups: [{ name: 'default', enableBlocking: true, blockAsNxDomain: false }] },
? { enableBlocking: true, blockingAnswerTtl: 60, localGroup: 'office', networkGroupMap: { '0.0.0.0/0': 'default' }, groups: [{ name: 'default', enableBlocking: false, blockAsNxDomain: false }] }
: { enableBlocking: true, blockingAnswerTtl: 30, localGroup: 'home', networkGroupMap: { '0.0.0.0/0': 'default' }, groups: [{ name: 'default', enableBlocking: true, blockAsNxDomain: false }] },
'Query Logs (Sqlite)': { enableLogging: true, maxLogDays: 365, sqliteDbPath: VARIANT === 'b' ? '/var/lib/dns/ndns/querylogs.db' : '/var/lib/dns/mdns/querylogs.db' },
'Split Horizon': { domainGroupMap: { 'lan.example.net': 'internal' }, networkGroupMap: { '10.0.0.0/8': 'internal' } },
}
@@ -307,6 +307,12 @@ function route(path, q) {
apps = update ? apps.map((a) => (a.name === name ? installed : a)) : [...apps, installed]
return ok(update ? { updatedApp: installed } : { installedApp: installed })
}
case 'apps/config/set': {
const name = q.get('name') ?? ''
if (!apps.some((a) => a.name === name)) return { status: 'error', errorMessage: `DNS application '${name}' was not found.` }
try { appConfigs[name] = JSON.parse(q.get('config') ?? '') } catch (e) { return { status: 'error', errorMessage: `The config is not valid JSON: ${e.message}` } }
return ok()
}
case 'apps/config/get': return ok({ config: appConfigs[q.get('name')] ? JSON.stringify(appConfigs[q.get('name')], null, 2) : JSON.stringify({ enabled: true }, null, 2) })
case 'apps/uninstall': apps = apps.filter((a) => a.name !== q.get('name')); return ok()
case 'logs/list': return ok({ logFiles: [{ fileName: '2026-10-26', size: '412.8 KB' }, { fileName: '2026-10-25', size: '1.2 MB' }, { fileName: '2026-10-24', size: '980.4 KB' }] })