Blocking: count Advanced Blocking, Dashboard layout and KPI cards
CI / check (push) Successful in 3m7s
Hub / check (push) Successful in 53s
CI / image (push) Successful in 1m29s
Hub / image (push) Successful in 1m19s

- Status, sidebar mark and header treat a server that blocks with the
  Advanced Blocking app as blocking, per server behind the hub.
- Overview panel summarising the app's groups; Lists and Rules show and
  edit the app's list URLs and rules (read, change one field, write back).
- Block List Domains adds the app's list domains, counted by the hub
  (hub/blocking/app-lists/count, public hosts only unless
  HUB_LISTS_ALLOW_PRIVATE=1) or by the browser where CORS allows.
- Header and KPI cards follow the Dashboard: selector in the chrome slot,
  Compare servers in the header actions, period under the title, StatCard
  tiles without sparklines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-11-01 18:34:17 +07:00
1 parent b396bfd317
commit 172e06f1ce
40 files changed
+3126 -208

No files matched your search

+44 -3
View File
@@ -176,7 +176,7 @@ const settings = {
cacheMaximumEntries: 10000, cacheMinimumRecordTtl: 10, cacheMaximumRecordTtl: 86400, cacheNegativeRecordTtl: 300, cacheFailureRecordTtl: 60,
enableCachePrefetch: true, cachePrefetchEligibility: 2, cachePrefetchTrigger: 9,
enableBlocking: true, allowTxtBlockingReport: true, temporaryDisableBlockingTill: null, blockingBypassList: [], blockingType: 'NxDomain', blockingAnswerTtl: 30,
customBlockingAddresses: [], blockListUrls: ['https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts', '# Privacy', 'https://big.oisd.nl/domainswild', 'https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt'],
customBlockingAddresses: [], blockListUrls: ['https://lists.invalid/hosts', '# Privacy', 'https://big.oisd.nl/domainswild', 'https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt'],
blockListUpdateIntervalHours: 24, blockListNextUpdatedOn: iso(now + 3 * 3_600_000),
proxy: null, forwarders: ['1.1.1.1', '9.9.9.9'], forwarderProtocol: 'Udp', concurrentForwarding: true, forwarderRetries: 3, forwarderTimeout: 2000, forwarderConcurrency: 2,
loggingType: 'File, Console', ignoreResolverLogs: false, logQueries: false, useLocalTime: false, logFolder: 'logs', maxLogFileDays: 365, enableInMemoryStats: false, maxStatFileDays: 365,
@@ -220,10 +220,42 @@ 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))
}
// A small fake list the mock serves itself at /lists/<name>.txt (no internet), so the Advanced Blocking
// "Block List Domains" count has something to download. Open to any origin, as a public list host is.
const FAKE_LISTS = {
'hosts.txt': ['# fake hosts list', '127.0.0.1 localhost', ...Array.from({ length: 120 }, (_, i) => `0.0.0.0 ads${i}.tracker.example`)].join('\n'),
'easylist.txt': ['[Adblock Plus 2.0]', '! fake adblock list', ...Array.from({ length: 45 }, (_, i) => `||banner${i}.adnet.example^`)].join('\n'),
}
const listUrl = (name) => `http://127.0.0.1:${PORT}/lists/${name}`
const group = (over) => ({
allowTxtBlockingReport: true, blockAsNxDomain: true, blockingAddresses: ['0.0.0.0', '::'],
allowed: [], blocked: [], allowListUrls: [], blockListUrls: [], allowedRegex: [], blockedRegex: [],
regexAllowListUrls: [], regexBlockListUrls: [], adblockListUrls: [], ...over,
})
const appConfigs = {
'Advanced Blocking': VARIANT === 'b'
? { 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 }] },
? {
enableBlocking: true, blockingAnswerTtl: 60, blockListUrlUpdateIntervalHours: 24, localEndPointGroupMap: { '127.0.0.1': 'bypass' },
networkGroupMap: { '0.0.0.0/0': 'default', '::/0': 'default' },
groups: [
group({ name: 'default', enableBlocking: true, blocked: ['tracker.example'], blockedRegex: ['^ads\\.'], blockListUrls: [listUrl('hosts.txt')], adblockListUrls: [listUrl('easylist.txt')] }),
group({ name: 'bypass', enableBlocking: false }),
],
}
: {
enableBlocking: true, blockingAnswerTtl: 30, blockListUrlUpdateIntervalHours: 24, localEndPointGroupMap: { '127.0.0.1': 'bypass' },
networkGroupMap: { '192.168.10.0/24': 'family', '0.0.0.0/0': 'everyone', '::/0': 'everyone' },
groups: [
group({
name: 'everyone', enableBlocking: true, blocked: ['ads.example.com', 'telemetry.example.net'], allowed: ['good.example.org'],
blockListUrls: [listUrl('hosts.txt'), 'https://lists.invalid/hosts'],
allowListUrls: ['https://example.org/allow.txt'], blockedRegex: ['^ads\\.', '^track\\d+\\.'], allowedRegex: ['^cdn\\.good\\.'],
regexBlockListUrls: ['https://example.org/regex.txt'], adblockListUrls: [listUrl('easylist.txt')],
}),
group({ name: 'family', enableBlocking: true, blocked: ['games.example.com'], blockListUrls: [listUrl('hosts.txt')], regexAllowListUrls: ['https://example.org/regex-allow.txt'] }),
group({ name: 'bypass', enableBlocking: 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' } },
}
@@ -438,6 +470,15 @@ http.createServer((req, res) => {
const q = new URLSearchParams(url.search)
if ((req.headers['content-type'] ?? '').includes('application/x-www-form-urlencoded')) for (const [k, v] of new URLSearchParams(body)) q.set(k, v)
if ((req.headers['content-type'] ?? '').includes('text/plain') && body !== '') q.set('__text', body)
const fakeList = url.pathname.match(/^\/lists\/([\w.-]+)$/)
if (fakeList !== null) {
const text = FAKE_LISTS[fakeList[1]]
res.statusCode = text === undefined ? 404 : 200
res.setHeader('access-control-allow-origin', '*')
res.setHeader('content-type', 'text/plain')
res.end(text ?? 'not found')
return
}
const file = fileFor(path, q)
if (file !== undefined) {
res.setHeader('content-type', 'text/plain')