Pause blocking, blocked clients and a blocked line in the statistics, compact view picker

jarl block pause|resume (and the Blocking page) lets every name through
for a while; jarld resumes on time. The lists stay loaded: the rules zone
just passes everything. Statistics gain the clients with the most blocked
queries and blocked queries on the queries chart. The view choice in the
blocking forms is a dropdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-10 11:10:44 +00:00
1 parent 9fc23310bd
commit 27d1f9e538
31 files changed
+417 -45

No files matched your search

+9 -4
View File
@@ -60,10 +60,14 @@ export async function render(el, params, query) {
s.blocked_ratio != null ? tile("Blocked", pct(s.blocked_ratio), compact(s.blocked) + " queries, by your rules and blocklists") : null));
// Volume over time: one series, so no legend box; the title names it.
// With blocking, a second line shows how many of them were blocked.
const q = timeline.series.find((x) => x.key === "queries") || { values: timeline.times.map(() => 0) };
const qSeries = [{ label: "Queries", values: q.values, slot: 1 }];
parts.push(chartCard("Queries", "per " + stepName(timeline.step) + ", " + rangeLabel,
lineChart({ series: qSeries, times: timeline.times, step, area: true }), timeTable(qSeries, timeline.times, step), { wide: true }));
const bl = timeline.series.find((x) => x.key === "blocked");
if (bl && bl.values.some((v) => v > 0)) qSeries.push({ label: "Blocked", values: bl.values, slot: 2 });
parts.push(chartCard(qSeries.length > 1 ? "Queries and blocked queries" : "Queries", "per " + stepName(timeline.step) + ", " + rangeLabel,
lineChart({ series: qSeries, times: timeline.times, step, area: true }), timeTable(qSeries, timeline.times, step),
{ wide: true, legend: qSeries.length > 1 ? legend(qSeries) : null }));
// Failed answers: three fixed series, legend plus end labels.
const fSeries = failures.map((f) => ({ ...f, values: (errors.series.find((x) => x.key === f.key) || { values: errors.times.map(() => 0) }).values }));
@@ -86,6 +90,7 @@ export async function render(el, params, query) {
listCard("Names that don't exist", "NXDOMAIN answers: typos, removed hosts, misconfigured clients, malware", barList(t.nxdomain, { empty: "None " + rangeLabel + "." })),
listCard("Refused clients", "clients asking for something they may not have (often internet scanners)", barList(t.refused, { empty: "None " + rangeLabel + "." })),
t.servfail.length ? listCard("Names that failed to resolve", "SERVFAIL answers", barList(t.servfail)) : null,
t.blocked_client.length ? listCard("Clients with the most blocked queries", "often a smart TV, phone or app phoning home", barList(t.blocked_client)) : null,
t.blocked.length ? listCard("Most blocked names", "answered \"doesn't exist\" by blocking; click a name to see why, or to allow it", barList(t.blocked, { href: (k) => "#/blocking?check=" + encodeURIComponent(k) })) : null));
} else {
parts.push(h("div", { class: "msg info" }, "Top names and clients need the response log: set stats.query_log: true in /etc/jarl/jarl.yaml and restart jarl."));
@@ -110,14 +115,14 @@ export async function render(el, params, query) {
async function loadAll(since, heatSince, queryLog) {
const p = {
summary: get("/stats/summary", { since, top: 0 }),
timeline: get("/stats/timeline", { since, key: "queries" }),
timeline: get("/stats/timeline", { since, key: "queries,blocked" }),
errors: get("/stats/timeline", { since, key: failures.map((f) => f.key).join(",") }),
heat: get("/stats/heatmap", { since: heatSince }),
};
const groups = ["qtype", "rcode", "view", "zone", "answer", "rtt", "blocklist"];
groups.forEach((g) => { p["b_" + g] = get("/stats/breakdown", { group: g, since }); });
if (queryLog) {
for (const k of ["name", "client", "nxdomain", "refused", "servfail", "blocked"]) p["t_" + k] = get("/stats/top", { kind: k, since, limit: 15 });
for (const k of ["name", "client", "nxdomain", "refused", "servfail", "blocked", "blocked_client"]) p["t_" + k] = get("/stats/top", { kind: k, since, limit: 15 });
}
const keys = Object.keys(p);
const vals = await Promise.all(Object.values(p));