Reverse zones: name hosts from other views' records; search and bulk dismiss for long lists
A reverse zone's fill (and fill-ptr, the missing-PTR counts) names an address no record of its own view uses from another view's record, so an internal reverse zone can give the PTR of a host only external has an AAAA for; New reverse zone with a view chosen lists those addresses, marked. Automatic suggestions stay per view. For hundreds of addresses: the New reverse zone list has a search; on the Zones page the public addresses fold away, have a search, show 25 at a time and Dismiss these N dismisses everything a search found in one change (reverse.dismiss takes prefixes; --dismiss takes several). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
304c89dba4
commit
bfab83da6e
11 files changed
+239
-41
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList, checkbox, plural } from "../ui.js";
|
||||
import { h, mount, table, modal, field, input, select, textarea, lines, toast, errorBox, display, badge, viewLink, lastList, checkbox, plural, confirmDialog } from "../ui.js";
|
||||
import { get, change, changeMany, describeResult, can, draftQuery } from "../api.js";
|
||||
import { rerender } from "../app.js";
|
||||
import { checkDomain, reverseZoneFor, maskNetwork } from "../names.js";
|
||||
@@ -66,7 +66,20 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
// For a reverse zone: sizes to pick from, and the networks the records
|
||||
// use without a reverse zone, in every view (docs/reverse.md).
|
||||
const helper = h("div", { class: "reverse-helper" });
|
||||
// The networks per view picked ("" for all views): a chosen view's list
|
||||
// also has the addresses only other views' records use.
|
||||
const candsBy = {};
|
||||
let cands = null;
|
||||
const candQ = input({ type: "search", placeholder: "Search: an address, a name or a view", class: "small", style: { marginBottom: "6px" } });
|
||||
candQ.addEventListener("input", () => drawHelper());
|
||||
const loadCands = () => {
|
||||
const k = view.value || "";
|
||||
if (candsBy[k]) { cands = candsBy[k]; return drawHelper(); }
|
||||
get("/reverse-zones/suggestions", draftQuery({ all: 1, view: k || undefined })).catch(() => []).then((c) => {
|
||||
candsBy[k] = c;
|
||||
if ((view.value || "") === k) { cands = c; drawHelper(); }
|
||||
});
|
||||
};
|
||||
// sizeFrom is the address the size buttons work from: a record's, once a
|
||||
// network was picked, so a /48 made smaller becomes that record's /64,
|
||||
// not the /48's first one.
|
||||
@@ -159,7 +172,7 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
name.placeholder = k === "reverse" ? "10.10.0.0/24 or 2001:db8:1::/48" : "adbraun.lol";
|
||||
for (const b of kindButtons.children) b.classList.toggle("active", b.dataset.kind === k);
|
||||
syncFill();
|
||||
if (k === "reverse" && !cands) get("/reverse-zones/suggestions", draftQuery({ all: 1 })).catch(() => []).then((c) => { cands = c; drawHelper(); });
|
||||
if (k === "reverse") loadCands();
|
||||
drawHelper();
|
||||
if (views.length) view.options[0].textContent = k === "reverse" ? "– the private view that uses these addresses –" : "– choose a view –";
|
||||
check();
|
||||
@@ -180,9 +193,13 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
(v6 ? [[48, "a whole site (all its /64s)"], [56, "a smaller delegation"], [64, "one subnet"], [128, "one address"]] : [[16, "a /16"], [24, "one network"], [32, "one address"]])
|
||||
.map(([b, title]) => h("button", { type: "button", class: "small" + (bits === b ? " active" : ""), title, onclick: () => { name.value = maskNetwork(sizeFrom || name.value, b); changed(); } }, "/" + b)),
|
||||
sizeFrom ? h("span", { class: "muted small" }, " around " + sizeFrom) : null) : null;
|
||||
const list = (cands || []).filter((c) => !view.value || c.view === view.value);
|
||||
const shown = list.slice(0, 12);
|
||||
const q = candQ.value.trim().toLowerCase();
|
||||
// Yours first, then the networks with the most records.
|
||||
const list = (cands || []).filter((c) => !q || [c.prefix, c.zone, c.view, c.hidden, ...c.samples].join(" ").toLowerCase().includes(q))
|
||||
.sort((a, b) => (a.group === "yours" ? 0 : 1) - (b.group === "yours" ? 0 : 1) || b.fill - a.fill);
|
||||
const shown = list.slice(0, 15);
|
||||
mount(helper, sizes,
|
||||
(cands || []).length > 8 ? candQ : null,
|
||||
shown.length ? h("div", { class: "cands" },
|
||||
h("div", { class: "muted small" }, "Networks your records use without a reverse zone" + (view.value ? " in view " + view.value : "") + ": pick one, then change its size if you need"),
|
||||
shown.map((c) => h("button", { type: "button", class: "cand", title: c.why, onclick: () => {
|
||||
@@ -195,11 +212,12 @@ export async function newZoneDialog(views, zones, settings, preset = {}) {
|
||||
views.length && !view.value ? h("span", { class: "muted" }, " " + c.view) : null,
|
||||
h("span", { class: "muted small" }, " " + c.fill + " PTR" + (c.samples.length ? " · " + c.samples.slice(0, 2).join(", ") : "")),
|
||||
c.hidden ? [" ", badge(c.hidden, "off")] : null)),
|
||||
list.length > shown.length ? h("div", { class: "muted small" }, "… and " + (list.length - shown.length) + " more (pick a view to narrow the list)") : null)
|
||||
list.length > shown.length ? h("div", { class: "muted small" }, "… and " + (list.length - shown.length) + " more: search to find one") : null)
|
||||
: q && cands && cands.length ? h("div", { class: "muted small" }, "No network matches " + JSON.stringify(candQ.value.trim()) + ".")
|
||||
: cands && !list.length ? h("div", { class: "muted small" }, "Every address your records use" + (view.value ? " in view " + view.value : "") + " has a reverse zone.") : null);
|
||||
};
|
||||
name.addEventListener("input", typed);
|
||||
view.addEventListener("change", () => { check(); drawHelper(); });
|
||||
view.addEventListener("change", () => { check(); if (kind === "reverse") loadCands(); else drawHelper(); });
|
||||
const glueRow = field("Name server addresses", nsAddr, "Optional, for name servers inside this zone: 'ns1 192.0.2.1' per line (glue).");
|
||||
const cloneRow = field("Copy records from", cloneWrap, "For split-horizon setups: start with the records of this zone in another view. Linked, Jarl keeps the shared records the same in both (docs/views.md#linked-zones).");
|
||||
const rows = {
|
||||
@@ -364,7 +382,48 @@ function suggestPanel(sugg, views) {
|
||||
yours.length > 1 ? h("div", { class: "actions" }, h("button", { onclick: () => create(yours) }, "Create all " + yours.length)) : null),
|
||||
h("p", { class: "muted small" }, "Your records use these addresses, but no reverse zone answers \"which host has this address?\" for them" + (views.length ? " in their view" : "") + ". Jarl creates each with a PTR record for every address there. Only your own clients ask this server: the internet asks whoever owns the addresses."),
|
||||
yours.length ? table(cols(false), yours) : null,
|
||||
pub.length ? [h("h3", null, "Public addresses: only if they're yours"),
|
||||
h("p", { class: "muted small" }, "Jarl can't tell your own servers from someone else's (a web host's). Create a zone for an address that is yours; dismiss the others. Each gets a zone of its own: the rest of its network belongs to others."),
|
||||
table(cols(true), pub)] : null);
|
||||
pub.length ? publicGroup(pub, cols(true), create) : null);
|
||||
}
|
||||
|
||||
// publicGroup lists the public addresses, collapsed: there can be hundreds
|
||||
// (a web host's), so it has a search, shows 25 at a time, and dismisses
|
||||
// what the search found at once.
|
||||
function publicGroup(pub, cols, create) {
|
||||
const PAGE = 25;
|
||||
let limit = PAGE;
|
||||
const q = input({ type: "search", placeholder: "Search: an address, a name or a view", class: "small", style: { width: "300px" } });
|
||||
const body = h("div");
|
||||
const match = () => {
|
||||
const t = q.value.trim().toLowerCase();
|
||||
return pub.filter((s) => !t || [s.prefix, s.zone, s.view, ...s.samples].join(" ").toLowerCase().includes(t));
|
||||
};
|
||||
const dismissAll = async (list) => {
|
||||
const byView = new Map();
|
||||
for (const s of list) {
|
||||
const k = s.view_id || 0;
|
||||
if (!byView.has(k)) byView.set(k, []);
|
||||
byView.get(k).push(s.prefix);
|
||||
}
|
||||
if (!await confirmDialog("Dismiss " + plural(list.length, "address", "addresses"), "Jarl stops suggesting reverse zones for " + (list.length === 1 ? list[0].prefix : list.length + " public addresses" + (q.value.trim() ? " matching " + JSON.stringify(q.value.trim()) : "")) + ". Bring one back with: jarl zone suggest-reverse --undismiss NETWORK --view VIEW.", "Dismiss")) return;
|
||||
try {
|
||||
const ops = [...byView].map(([vid, prefixes]) => ({ op: "reverse.dismiss", data: { view_id: vid || undefined, prefixes } }));
|
||||
toast(describeResult(await changeMany(ops, "dismiss " + plural(list.length, "reverse zone suggestion"))));
|
||||
rerender();
|
||||
} catch (e) { toast(e.message, "err", 8000); }
|
||||
};
|
||||
const draw = () => {
|
||||
const list = match();
|
||||
const shown = list.slice(0, limit);
|
||||
mount(body,
|
||||
h("div", { class: "compare-controls" }, q,
|
||||
list.length ? h("button", { class: "small", title: "Not yours: stop suggesting all of these", onclick: () => dismissAll(list) }, list.length === pub.length ? "Dismiss all " + list.length : "Dismiss these " + list.length) : null),
|
||||
shown.length ? table(cols, shown) : h("div", { class: "muted small" }, "No address matches."),
|
||||
list.length > shown.length ? h("button", { class: "small", onclick: () => { limit += PAGE; draw(); } }, "Show " + Math.min(PAGE, list.length - shown.length) + " more (of " + (list.length - shown.length) + ")") : null);
|
||||
};
|
||||
q.addEventListener("input", () => { limit = PAGE; draw(); });
|
||||
draw();
|
||||
return h("details", { class: "compare-only", open: pub.length <= 10 },
|
||||
h("summary", null, h("strong", null, "Public addresses: " + pub.length), h("span", { class: "muted small" }, " · create a zone only for an address that is yours; dismiss the others (a web host's)")),
|
||||
h("p", { class: "muted small" }, "Jarl can't tell your own servers from someone else's. Each gets a zone of its own: the rest of its network belongs to others."),
|
||||
body);
|
||||
}
|
||||
Reference in new issue
Block a user