Show blocked counts on the Blocking page, and reveal + CNAME on hover
The Blocking page shows how many queries each blocklist, and the rules together, blocked in the last 24 hours (with statistics on). The + CNAME button on A/AAAA rows shows when the row is hovered or focused, and always on touch screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f0a0cc233
commit
5b8f681e2a
5 files changed
+32
-7
No files matched your search
+5
-1
@@ -92,7 +92,11 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
reorder them; view links everywhere open the view's page.
|
reorder them; view links everywhere open the view's page.
|
||||||
- Web UI: A and AAAA records have a **+ CNAME** button that fills in the
|
- Web UI: A and AAAA records have a **+ CNAME** button that fills in the
|
||||||
add form with a CNAME pointing to the record's name, leaving only the
|
add form with a CNAME pointing to the record's name, leaving only the
|
||||||
alias's name to type.
|
alias's name to type. It shows when the row is hovered or focused
|
||||||
|
(always on touch screens).
|
||||||
|
- Blocking page: how many queries each blocklist blocked in the last 24
|
||||||
|
hours, and your rules together, with statistics on (linking to the
|
||||||
|
Statistics page).
|
||||||
- Options are kept in one fixed order, in named.conf, the web UI and the
|
- Options are kept in one fixed order, in named.conf, the web UI and the
|
||||||
CLI: grouped by topic (files, listening, access control, recursion and
|
CLI: grouped by topic (files, listening, access control, recursion and
|
||||||
forwarding, DNSSEC, transfers and notify, then the rest), alphabetical
|
forwarding, DNSSEC, transfers and notify, then the rest), alphabetical
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ names, and the clients with the most blocked queries (`jarl stats summary`,
|
|||||||
breakdown blocklist`). Blocked answers are left out of the NXDOMAIN figures, which
|
breakdown blocklist`). Blocked answers are left out of the NXDOMAIN figures, which
|
||||||
are about names that really don't exist.
|
are about names that really don't exist.
|
||||||
|
|
||||||
|
The Blocking page shows the last 24 hours too: a **Blocked (24h)** column
|
||||||
|
for each list, and one count for your rules together (named logs which
|
||||||
|
policy zone matched, not which rule in it).
|
||||||
|
|
||||||
Without statistics named doesn't log what it blocks at all.
|
Without statistics named doesn't log what it blocks at all.
|
||||||
|
|
||||||
## Memory and performance
|
## Memory and performance
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ let refreshTimer = null;
|
|||||||
|
|
||||||
export async function render(el, params, query) {
|
export async function render(el, params, query) {
|
||||||
clearTimeout(refreshTimer);
|
clearTimeout(refreshTimer);
|
||||||
const [lists, rules, views, status] = await Promise.all([get("/blocking/lists", draftQuery()), get("/blocking/rules", draftQuery()), get("/views"), get("/blocking/status")]);
|
// Queries blocked in the last day, per list and for the rules together
|
||||||
|
// (null without query statistics, or without permission to see them).
|
||||||
|
const [lists, rules, views, status, hitList] = await Promise.all([get("/blocking/lists", draftQuery()), get("/blocking/rules", draftQuery()), get("/views"), get("/blocking/status"),
|
||||||
|
can("stats") ? get("/stats/breakdown", { group: "blocklist", since: "24h" }).catch(() => null) : null]);
|
||||||
|
const hits = hitList ? Object.fromEntries(hitList.map((e) => [e.key, e.count])) : null;
|
||||||
const writable = can("blocking", true);
|
const writable = can("blocking", true);
|
||||||
const recursive = lists.concat(rules).find((x) => x.auto_views);
|
const recursive = lists.concat(rules).find((x) => x.auto_views);
|
||||||
const autoViews = recursive ? recursive.views : null;
|
const autoViews = recursive ? recursive.views : null;
|
||||||
@@ -53,8 +57,8 @@ export async function render(el, params, query) {
|
|||||||
h("form", { class: "actions", onsubmit: runCheck },
|
h("form", { class: "actions", onsubmit: runCheck },
|
||||||
h("label", { class: "muted" }, "Check a name"), checkName, h("button", { type: "submit" }, "Check")),
|
h("label", { class: "muted" }, "Check a name"), checkName, h("button", { type: "submit" }, "Check")),
|
||||||
checkOut),
|
checkOut),
|
||||||
rulesPanel(rules, views, autoViews, writable),
|
rulesPanel(rules, views, autoViews, writable, hits),
|
||||||
listsPanel(lists, views, autoViews, writable));
|
listsPanel(lists, views, autoViews, writable, hits));
|
||||||
if (checkName.value) runCheck();
|
if (checkName.value) runCheck();
|
||||||
|
|
||||||
// Lists are downloaded in the background, and a pause ends by itself:
|
// Lists are downloaded in the background, and a pause ends by itself:
|
||||||
@@ -207,7 +211,12 @@ function viewsCell(x) {
|
|||||||
return x.auto_views ? [x.views.join(", "), h("span", { class: "muted small" }, " (auto)")] : x.views.join(", ");
|
return x.auto_views ? [x.views.join(", "), h("span", { class: "muted small" }, " (auto)")] : x.views.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function rulesPanel(rules, views, autoViews, writable) {
|
// hitsLink shows a count of blocked queries, linking to the statistics.
|
||||||
|
function hitsLink(n) {
|
||||||
|
return h("a", { href: "#/stats?since=24h", title: "queries blocked in the last 24 hours; more on the Statistics page" }, (n || 0).toLocaleString());
|
||||||
|
}
|
||||||
|
|
||||||
|
function rulesPanel(rules, views, autoViews, writable, hits) {
|
||||||
const name = input({ placeholder: "ads.example.com", class: "mono" });
|
const name = input({ placeholder: "ads.example.com", class: "mono" });
|
||||||
const action = select([["block", "Block"], ["allow", "Allow"]], "block");
|
const action = select([["block", "Block"], ["allow", "Allow"]], "block");
|
||||||
const exact = checkbox("only this exact name", false);
|
const exact = checkbox("only this exact name", false);
|
||||||
@@ -230,6 +239,7 @@ function rulesPanel(rules, views, autoViews, writable) {
|
|||||||
h("div", { class: "f" }, h("label", null, " "), h("button", { class: "primary", type: "submit" }, "Add rule")));
|
h("div", { class: "f" }, h("label", null, " "), h("button", { class: "primary", type: "submit" }, "Add rule")));
|
||||||
return h("div", { class: "panel" },
|
return h("div", { class: "panel" },
|
||||||
h("h2", null, "Your rules"),
|
h("h2", null, "Your rules"),
|
||||||
|
hits && rules.some((r) => r.action === "block") ? h("p", { class: "small" }, "Blocked ", hitsLink(hits.rules), " queries in the last 24 hours (all rules together: named doesn't say which rule matched).") : null,
|
||||||
h("p", { class: "muted small" }, "A rule covers the domain and every name below it, unless it's for the exact name only. Rules beat every blocklist, and the most specific rule wins: allowing cdn.example.com overrides blocking example.com for cdn.example.com and the names below it."),
|
h("p", { class: "muted small" }, "A rule covers the domain and every name below it, unless it's for the exact name only. Rules beat every blocklist, and the most specific rule wins: allowing cdn.example.com overrides blocking example.com for cdn.example.com and the names below it."),
|
||||||
writable ? [form, err] : null,
|
writable ? [form, err] : null,
|
||||||
table([
|
table([
|
||||||
@@ -243,7 +253,7 @@ function rulesPanel(rules, views, autoViews, writable) {
|
|||||||
].filter(Boolean), rules, { empty: "No rules yet.", rowClass: (r) => r.pending ? "pending-" + r.pending : "" }));
|
].filter(Boolean), rules, { empty: "No rules yet.", rowClass: (r) => r.pending ? "pending-" + r.pending : "" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function listsPanel(lists, views, autoViews, writable) {
|
function listsPanel(lists, views, autoViews, writable, hits) {
|
||||||
const name = input({ placeholder: "hagezi-multi", class: "mono", style: { width: "11em" } });
|
const name = input({ placeholder: "hagezi-multi", class: "mono", style: { width: "11em" } });
|
||||||
const url = input({ placeholder: "https://…", class: "mono" });
|
const url = input({ placeholder: "https://…", class: "mono" });
|
||||||
const vp = viewPicker(views, autoViews);
|
const vp = viewPicker(views, autoViews);
|
||||||
@@ -273,6 +283,7 @@ function listsPanel(lists, views, autoViews, writable) {
|
|||||||
{ label: "Name", render: (l) => [h("strong", { class: "mono" }, l.name), l.pending ? [" ", pendingBadge(l.pending)] : null] },
|
{ label: "Name", render: (l) => [h("strong", { class: "mono" }, l.name), l.pending ? [" ", pendingBadge(l.pending)] : null] },
|
||||||
{ label: "State", render: (l) => { const [t, k] = stateBadge[l.state] || [l.state, ""]; return [badge(t, k), l.status && l.status.error ? h("div", { class: "small err-text" }, l.status.error) : null]; } },
|
{ label: "State", render: (l) => { const [t, k] = stateBadge[l.state] || [l.state, ""]; return [badge(t, k), l.status && l.status.error ? h("div", { class: "small err-text" }, l.status.error) : null]; } },
|
||||||
{ label: "Domains", render: (l) => l.status && l.status.updated_at ? l.status.entries.toLocaleString() : "–", tdClass: "right" },
|
{ label: "Domains", render: (l) => l.status && l.status.updated_at ? l.status.entries.toLocaleString() : "–", tdClass: "right" },
|
||||||
|
hits ? { label: "Blocked (24h)", render: (l) => hitsLink(hits[l.name]), class: "right", tdClass: "right" } : null,
|
||||||
views.length ? { label: "Views", render: viewsCell } : null,
|
views.length ? { label: "Views", render: viewsCell } : null,
|
||||||
{ label: "Updated", render: (l) => l.status && l.status.updated_at ? h("span", { title: "checked " + time(l.status.checked_at) }, relTime(l.status.updated_at)) : "–", tdClass: "small" },
|
{ label: "Updated", render: (l) => l.status && l.status.updated_at ? h("span", { title: "checked " + time(l.status.checked_at) }, relTime(l.status.updated_at)) : "–", tdClass: "small" },
|
||||||
{ label: "Memory", render: (l) => l.memory_mb ? h("span", { title: "named's memory for this list, estimated (all its views)" }, "~" + l.memory_mb + " MB") : "–", tdClass: "small right" },
|
{ label: "Memory", render: (l) => l.memory_mb ? h("span", { title: "named's memory for this list, estimated (all its views)" }, "~" + l.memory_mb + " MB") : "–", tdClass: "small right" },
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ async function recordsTab(el, zone, query) {
|
|||||||
{ label: "Comment", render: (r) => [r.pending ? [pendingBadge(r.pending), " "] : null, h("span", { class: "muted" }, r.comment || "")] },
|
{ label: "Comment", render: (r) => [r.pending ? [pendingBadge(r.pending), " "] : null, h("span", { class: "muted" }, r.comment || "")] },
|
||||||
{ label: "", render: (r) => r.pending === "delete" ? "" : h("span", null,
|
{ label: "", render: (r) => r.pending === "delete" ? "" : h("span", null,
|
||||||
!reverse && (r.type === "A" || r.type === "AAAA") && !r.disabled
|
!reverse && (r.type === "A" || r.type === "AAAA") && !r.disabled
|
||||||
? [h("button", { class: "small", title: "Add a CNAME record that points to " + r.name.replace(/\.$/, ""), onclick: () => aliasTo(r) }, "+ CNAME"), " "] : null,
|
? [h("button", { class: "small reveal", title: "Add a CNAME record that points to " + r.name.replace(/\.$/, ""), onclick: () => aliasTo(r) }, "+ CNAME"), " "] : null,
|
||||||
h("button", { class: "small", title: "Edit", onclick: () => edit(r) }, "Edit"), " ",
|
h("button", { class: "small", title: "Edit", onclick: () => edit(r) }, "Edit"), " ",
|
||||||
r.disabled
|
r.disabled
|
||||||
? h("button", { class: "small toggle", title: "Put the record back in the zone", onclick: () => toggle(r) }, "Enable")
|
? h("button", { class: "small toggle", title: "Put the record back in the zone", onclick: () => toggle(r) }, "Enable")
|
||||||
|
|||||||
@@ -320,6 +320,12 @@ td.break { word-break: break-all; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refreshing { opacity: 0.6; transition: opacity 0.2s; }
|
.refreshing { opacity: 0.6; transition: opacity 0.2s; }
|
||||||
|
/* Secondary row actions show when the row is hovered or focused; touch
|
||||||
|
screens, which can't hover, always show them. Opacity keeps them in the
|
||||||
|
layout (no shifting) and reachable with Tab. */
|
||||||
|
tr .reveal { opacity: 0; transition: opacity 0.1s; }
|
||||||
|
tr:hover .reveal, tr:focus-within .reveal { opacity: 1; }
|
||||||
|
@media (hover: none) { tr .reveal { opacity: 1; } }
|
||||||
h2.section { margin: 24px 0 10px; font-size: 18px; }
|
h2.section { margin: 24px 0 10px; font-size: 18px; }
|
||||||
.filter-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
.filter-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
||||||
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--panel); }
|
.segmented { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: var(--panel); }
|
||||||
|
|||||||
Reference in new issue
Block a user