Hub stage 2: read-only Technitium-compatible server over several servers
node:http server that serves the console build and a /api/* the console accepts: its own login and session (clusterInitialized, the servers as nodes, hub flag, Dashboard and Logs only), node=<id> forwarded with that server's token, no node or cluster merged (stats, top lists, query logs, apps), every write refused with 403, a short response cache, and an unreachable server reported as hubWarnings. The dev mock takes --port and --scale so two of them differ. Vitest skips .claude/ worktrees. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9df2c2b908
commit
5f29a438c9
8 files changed
+980
-7
No files matched your search
+14
-6
@@ -4,6 +4,7 @@ A stand-in for the Technitium API, with made-up data, to look at and click throu
|
||||
whole console without a server and without Docker.
|
||||
|
||||
node dev/mock-api.mjs # listens on 127.0.0.1:5380
|
||||
node dev/mock-api.mjs --port 5381 --scale 2 # another port; counters and hits times 2
|
||||
npm run dev # the console, proxying /api to it
|
||||
|
||||
Any user and password log in. It answers the endpoints the screens call, keeps what you
|
||||
@@ -14,7 +15,10 @@ behaviour: it exists so a design can be seen with data in it. The real harness i
|
||||
*/
|
||||
import http from 'node:http'
|
||||
|
||||
const PORT = Number(process.env.PORT ?? 5380)
|
||||
// `--port <n>` and `--scale <x>`: two instances with different numbers 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)
|
||||
const now = Date.now()
|
||||
const iso = (ms) => new Date(ms).toISOString()
|
||||
const ago = (min) => iso(now - min * 60_000)
|
||||
@@ -46,12 +50,12 @@ const SERIES = [
|
||||
function dashboard(type) {
|
||||
const r = RANGES[type] ?? RANGES.LastDay
|
||||
const instants = Array.from({ length: r.n }, (_, i) => iso(now - (r.n - 1 - i) * r.step))
|
||||
const wave = (base, i) => Math.max(0, Math.round(base * r.scale * (1 + 0.35 * Math.sin(i / 3.2) + 0.15 * Math.cos(i / 1.7))))
|
||||
const wave = (base, i) => Math.max(0, Math.round(SCALE * base * r.scale * (1 + 0.35 * Math.sin(i / 3.2) + 0.15 * Math.cos(i / 1.7))))
|
||||
const totals = Object.fromEntries(SERIES.map(([name, base]) => [name, instants.reduce((a, _, i) => a + wave(base, i), 0)]))
|
||||
const stats = {
|
||||
totalQueries: totals.Total, totalNoError: totals['No Error'], totalServerFailure: totals['Server Failure'], totalNxDomain: totals['NX Domain'],
|
||||
totalRefused: totals.Refused, totalAuthoritative: totals.Authoritative, totalRecursive: totals.Recursive, totalCached: totals.Cached,
|
||||
totalBlocked: totals.Blocked, totalDropped: totals.Dropped, totalClients: 128, zones: zones.length, cachedEntries: 48210,
|
||||
totalBlocked: totals.Blocked, totalDropped: totals.Dropped, totalClients: Math.round(128 * SCALE), zones: zones.length, cachedEntries: 48210,
|
||||
allowedZones: allowed.size, blockedZones: blocked.size, allowListZones: 1, blockListZones: 1482903,
|
||||
}
|
||||
return {
|
||||
@@ -67,9 +71,9 @@ const CLIENTS = [['192.168.10.14', 'laptop-anna.lan'], ['192.168.10.31', 'tv-liv
|
||||
const DOMAINS = ['connectivitycheck.gstatic.com', 'time.cloudflare.com', 'api.github.com', 'updates.example.net', 'www.example.net', 'mail.example.net', 'cdn.jsdelivr.net', 'fonts.googleapis.com', 'api.telegram.org', 'ocsp.digicert.com']
|
||||
const BLOCKED = ['ads.tracker-example.com', 'telemetry.vendor-example.io', 'pixel.metrics-example.net', 'cdn.adserver-example.org', 'beacon.analytics-example.com', 'stats.adnetwork-example.net', 'track.shop-example.org', 'ads2.tracker-example.com', 'collect.data-example.io', 'sdk.ads-example.com']
|
||||
function top(kind) {
|
||||
if (kind === 'clients') return CLIENTS.map(([name, domain], i) => ({ name, domain: i === 3 ? DOMAINS[i] : DOMAINS[(i * 3) % 10], hits: Math.round(84210 / (1 + i * 0.55)), ...(i === 7 ? { rateLimited: true } : {}) }))
|
||||
if (kind === 'clients') return CLIENTS.map(([name, domain], i) => ({ name, domain: i === 3 ? DOMAINS[i] : DOMAINS[(i * 3) % 10], hits: Math.round((SCALE * 84210) / (1 + i * 0.55)), ...(i === 7 ? { rateLimited: true } : {}) }))
|
||||
const list = kind === 'domains' ? DOMAINS : BLOCKED
|
||||
return list.map((name, i) => ({ name, hits: Math.round((kind === 'domains' ? 22480 : 9804) / (1 + i * 0.5)) }))
|
||||
return list.map((name, i) => ({ name, hits: Math.round((SCALE * (kind === 'domains' ? 22480 : 9804)) / (1 + i * 0.5)) }))
|
||||
}
|
||||
|
||||
// ── Zones ──────────────────────────────────────────────────────────────────
|
||||
@@ -228,6 +232,10 @@ function route(path, q) {
|
||||
case 'user/login': case 'user/session/get': return session
|
||||
case 'user/profile/get': return ok({ displayName: 'Administrator', username: 'admin', totpEnabled: true, sessionTimeoutSeconds: 1800, memberOfGroups: ['Administrators'], sessions })
|
||||
case 'user/checkForUpdate': return ok({ updateAvailable: false })
|
||||
case 'dashboard/metrics/json': {
|
||||
const total = Math.round(SCALE * 1_250_000)
|
||||
return ok({ uptimestamp: ago(60 * 24 * 9), lifetimeCounters: { totalQueries: total, totalNoError: Math.round(total * 0.96), totalServerFailure: 40, totalNxDomain: Math.round(total * 0.03), totalRefused: 180, totalAuthoritative: Math.round(total * 0.08), totalRecursive: Math.round(total * 0.33), totalCached: Math.round(total * 0.48), totalBlocked: Math.round(total * 0.15), totalDropped: 5, totalClients: 128 } })
|
||||
}
|
||||
case 'dashboard/stats/get': return ok(dashboard(q.get('type') ?? 'LastDay'))
|
||||
case 'dashboard/stats/getTop': return ok({ [{ TopClients: 'topClients', TopDomains: 'topDomains', TopBlockedDomains: 'topBlockedDomains' }[q.get('statsType')] ?? 'topDomains']: top({ TopClients: 'clients', TopDomains: 'domains', TopBlockedDomains: 'blocked' }[q.get('statsType')] ?? 'domains') })
|
||||
case 'zones/list': {
|
||||
@@ -297,4 +305,4 @@ http.createServer((req, res) => {
|
||||
res.setHeader('content-type', 'application/json')
|
||||
res.end(JSON.stringify(route(path, q)))
|
||||
})
|
||||
}).listen(PORT, '127.0.0.1', () => console.log(`Mock Technitium API on http://127.0.0.1:${PORT} (any user and password)`))
|
||||
}).listen(PORT, '127.0.0.1', () => console.log(`Mock Technitium API on http://127.0.0.1:${PORT} (any user and password, scale ${SCALE})`))
|
||||
Reference in new issue
Block a user