Hub stage 2: install, update and uninstall apps on several servers
Writes use only <ID>_ADMIN_TOKEN and need the apps.write capability. Store URLs are resolved from each server's own listing. The config is saved to history before an uninstall. Zip uploads are fanned out to the servers. Every target's result is audited. The console gets a row menu on the matrix, a target picker, a results dialog with Retry failed, the store and install from file in All servers mode, and single-server actions for writers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d8df08fa0
commit
6acd2d656d
27 files changed
+1991
-71
No files matched your search
+30
-3
@@ -202,11 +202,18 @@ const appConfigs = {
|
||||
'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 = [
|
||||
const catalogRows = [
|
||||
['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'],
|
||||
['Drop Requests', 'Drops requests from the listed networks', '7.0', '19 KB'], ['NX Domain', 'Answers NXDOMAIN for the listed domains', '6.1', '17 KB'],
|
||||
].map(([name, description, version, size]) => ({ name, description, version, size, url: `https://download.technitium.com/dns/apps/${name.replace(/ /g, '')}App-v${version}.zip`, installed: false }))
|
||||
['Advanced Blocking', 'Group based blocking with regex', '10.2', '240 KB'], ['Query Logs (Sqlite)', 'Logs DNS queries into a SQLite database', '7.2', '310 KB'],
|
||||
['Split Horizon', 'Different answers for different networks', '9.0', '120 KB'],
|
||||
].map(([name, description, version, size]) => ({ name, description, version, size, url: `https://download.technitium.com/dns/apps/${name.replace(/[ ()]/g, '')}App-v${version}.zip` }))
|
||||
const catalog = () => catalogRows
|
||||
const storeListing = () => catalogRows.map((c) => {
|
||||
const have = apps.find((a) => a.name === c.name)
|
||||
return have ? { ...c, installed: true, installedVersion: have.version, updateAvailable: have.version !== c.version } : { ...c, installed: false }
|
||||
})
|
||||
|
||||
// ── Query logs ─────────────────────────────────────────────────────────────
|
||||
const RTYPES = ['Recursive', 'Cached', 'Authoritative', 'Blocked', 'Cached', 'Recursive']
|
||||
@@ -279,7 +286,27 @@ function route(path, q) {
|
||||
case 'dhcp/scopes/enable': case 'dhcp/scopes/disable': scopes = scopes.map((s) => (s.name === q.get('name') ? { ...s, enabled: path === 'dhcp/scopes/enable' } : s)); return ok()
|
||||
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/listStoreApps': return ok({ storeApps: storeListing() })
|
||||
case 'apps/downloadAndInstall': case 'apps/downloadAndUpdate': {
|
||||
const entry = catalog().find((c) => c.name === q.get('name') && c.url === q.get('url'))
|
||||
if (!entry) return { status: 'error', errorMessage: 'The app was not found in the store.' }
|
||||
const update = path === 'apps/downloadAndUpdate'
|
||||
const have = apps.find((a) => a.name === entry.name)
|
||||
if (update !== Boolean(have)) return { status: 'error', errorMessage: update ? `DNS application '${entry.name}' was not found.` : `DNS application '${entry.name}' already exists.` }
|
||||
const installed = { ...(have ?? { name: entry.name, description: entry.description, dnsApps: [app(false)] }), version: entry.version }
|
||||
delete installed.updateAvailable; delete installed.updateVersion; delete installed.updateUrl
|
||||
apps = update ? apps.map((a) => (a.name === entry.name ? installed : a)) : [...apps, installed]
|
||||
return ok(update ? { updatedApp: installed } : { installedApp: installed })
|
||||
}
|
||||
case 'apps/install': case 'apps/update': {
|
||||
const name = q.get('name') ?? ''
|
||||
const have = apps.find((a) => a.name === name)
|
||||
const update = path === 'apps/update'
|
||||
if (update !== Boolean(have)) return { status: 'error', errorMessage: update ? `DNS application '${name}' was not found.` : `DNS application '${name}' already exists.` }
|
||||
const installed = { ...(have ?? { name, description: 'Installed from a zip file', dnsApps: [app(false)] }), version: update ? `${have.version}.1` : '1.0' }
|
||||
apps = update ? apps.map((a) => (a.name === name ? installed : a)) : [...apps, installed]
|
||||
return ok(update ? { updatedApp: installed } : { installedApp: installed })
|
||||
}
|
||||
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' }] })
|
||||
|
||||
Reference in new issue
Block a user