Hub stage 1: apps across servers, read-only
hub/apps/matrix reads apps/list and each app's config on every server (the read token first, the admin token once if a server refuses) and returns per app and server the version, available update and a config hash, plus whether versions and configs match and how many JSON leaf paths differ. Config texts never leave the hub. In hub mode the Apps section has the server selector: All servers shows the matrix with filters for differing versions, differing configs and updates; one server shows its list without write actions. The dev mock takes --variant a|b with differing apps. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0ef2d53a65
commit
3d8df08fa0
14 files changed
+1082
-37
No files matched your search
+15
-2
@@ -15,10 +15,12 @@ behaviour: it exists so a design can be seen with data in it. The real harness i
|
||||
*/
|
||||
import http from 'node:http'
|
||||
|
||||
// `--port <n>` and `--scale <x>`: two instances with different numbers stand in for two servers.
|
||||
// `--port <n>`, `--scale <x>` and `--variant a|b`: two instances with different numbers (and apps) stand in for two servers.
|
||||
const arg = (name) => { const i = process.argv.indexOf(`--${name}`); return i >= 0 ? process.argv[i + 1] : undefined }
|
||||
const PORT = Number(arg('port') ?? process.env.PORT ?? 5380)
|
||||
const SCALE = Number(arg('scale') ?? 1)
|
||||
// `--variant b`: the second server of the Apps matrix (a missing app, another version, other config fields).
|
||||
const VARIANT = arg('variant') === 'b' ? 'b' : 'a'
|
||||
const now = Date.now()
|
||||
const iso = (ms) => new Date(ms).toISOString()
|
||||
const ago = (min) => iso(now - min * 60_000)
|
||||
@@ -189,6 +191,17 @@ let apps = [
|
||||
{ name: 'Advanced Blocking', description: 'Group based blocking with regex', version: '10.1', updateAvailable: true, updateVersion: '10.2', updateUrl: 'https://download.technitium.com/dns/apps/AdvancedBlockingApp-v10.2.zip', dnsApps: [{ ...app(false), classPath: 'AdvancedBlocking.App' }] },
|
||||
{ name: 'Split Horizon', description: 'Different answers for different networks', version: '9.0', dnsApps: [{ ...app(false), classPath: 'SplitHorizon.SimpleAddress' }, { ...app(false), classPath: 'SplitHorizon.SimpleCNAME' }] },
|
||||
]
|
||||
if (VARIANT === 'b') {
|
||||
apps = apps.filter((a) => a.name !== 'Split Horizon')
|
||||
apps = apps.map((a) => (a.name === 'Query Logs (Sqlite)' ? { ...a, version: '7.1', updateAvailable: true, updateVersion: '7.2', updateUrl: 'https://download.technitium.com/dns/apps/QueryLogsSqliteApp-v7.2.zip' } : a))
|
||||
}
|
||||
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 }] },
|
||||
'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' } },
|
||||
}
|
||||
const store = [
|
||||
['Failover', 'Fails over to a backup address when the primary is down', '8.0', '58 KB'], ['Geo Country', 'Answers by the country of the client', '7.2', '1.2 MB'],
|
||||
['Wild IP', 'Answers with the IP address in the name', '5.1', '22 KB'], ['Auto PTR', 'Answers PTR queries from the forward zones', '6.0', '31 KB'],
|
||||
@@ -267,7 +280,7 @@ function route(path, q) {
|
||||
case 'dhcp/scopes/delete': scopes = scopes.filter((s) => s.name !== q.get('name')); return ok()
|
||||
case 'apps/list': return ok({ apps })
|
||||
case 'apps/listStoreApps': return ok({ storeApps: store })
|
||||
case 'apps/config/get': return ok({ config: '{\n "enabled": true\n}' })
|
||||
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' }] })
|
||||
case 'logs/query': {
|
||||
|
||||
Reference in new issue
Block a user