Stream the delegation check's queries too
release / release (push) Successful in 1m42s

Every DNS query the name server checks send is now reported from
exchangeHostPort as a dig command line with a short answer (addresses,
referral, DS, NXDOMAIN) and its round-trip time; the serials check's SOA
queries keep their own events with the server's result. The delegation
check streams with ?stream=1 like /serials, and the Name servers tab
shows its queries as they happen, keeps its result and follows a running
check when you come back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-09 16:00:39 +00:00
1 parent 1d6c28ba69
commit bb6b4ab64d
6 files changed
+334 -128

No files matched your search

+87 -57
View File
@@ -92,7 +92,7 @@ function nsTable(servers, withSource) {
// Name server checks ask other servers over the network, which can take
// seconds. Results are kept for a while (per zone, for this page load),
// so coming back to the tab shows them at once; "Check again" asks anew.
const nsCache = new Map(); // zone id -> {serials: {at, resp, log} | {job}, delegation: {at, resolver, resp}}
const nsCache = new Map(); // zone id -> {serials, delegation}: {at, resp, log, resolver} once done, {job, resolver} while running
const nsFresh = 10 * 60 * 1000;
function nsEntry(zone) {
@@ -100,32 +100,59 @@ function nsEntry(zone) {
return nsCache.get(zone.id);
}
// runSerials starts the serials check, streamed: the log fills with the
// queries as they are sent and answered. Views subscribe to it.
function runSerials(zone) {
const job = { log: [], subs: new Set(), done: null, error: null, started: Date.now() };
// runCheck starts a check ("serials" or "delegation"), streamed: its log
// fills with the queries as they are sent and answered. Views subscribe
// to the job; its result is kept in nsCache.
function runCheck(zone, kind, query) {
const job = { log: [], subs: new Set(), done: null, error: null };
const entry = nsEntry(zone);
entry.serials = { job };
const resolver = query.resolver || "";
entry[kind] = { job, resolver };
const notify = () => job.subs.forEach((f) => f());
stream("/zones/" + zone.id + "/serials", { stream: 1 }, (ev) => {
stream("/zones/" + zone.id + "/" + kind, { ...query, stream: 1 }, (ev) => {
if (ev.kind === "start") job.log.push({ id: ev.id, query: ev.query, started: Date.now() });
else if (ev.kind === "end") {
const q = job.log.find((q) => q.id === ev.id && !q.ended);
if (q) Object.assign(q, { ended: Date.now(), result: ev.result, addresses: ev.addresses, error: ev.error });
if (q) Object.assign(q, { ended: Date.now(), result: ev.result, answer: ev.answer, error: ev.error, rtt: ev.rtt_ms });
} else if (ev.kind === "done") job.done = ev.response;
notify();
}).then(() => {
if (job.done) entry.serials = { at: Date.now(), resp: job.done, log: job.log };
if (job.done) entry[kind] = { at: Date.now(), resp: job.done, log: job.log, resolver };
else job.error = new Error("the check ended without a result");
notify();
}).catch((e) => {
job.error = e;
delete entry.serials;
if (entry[kind] && entry[kind].job === job) delete entry[kind];
notify();
});
return job;
}
// followCheck shows a running check in el, drawing progress(job) until
// it's done and then result(response, log). It stops following once el
// has been shown and is gone (the check goes on, and its result is kept).
function followCheck(el, job, progress, result) {
let seen = false, timer = null;
const stop = () => { job.subs.delete(draw); clearInterval(timer); timer = null; };
function draw() {
if (el.isConnected) seen = true;
else if (seen) { stop(); return; }
if (job.error) mount(el, errorBox(job.error));
else if (job.done) mount(el, result(job.done, job.log));
else mount(el, progress(job));
if (job.done || job.error) stop();
}
job.subs.add(draw);
draw();
if (!job.done && !job.error) {
// Tick the "waiting n s" counters.
timer = setInterval(() => {
if (seen && !el.isConnected) { stop(); return; }
for (const s of el.querySelectorAll(".elapsed")) s.textContent = "waiting " + ((Date.now() - Number(s.dataset.started)) / 1000).toFixed(1) + " s";
}, 200);
}
}
// queryLog lists the queries of a check: what was sent, and the answer or
// that it's still waiting.
function queryLog(log) {
@@ -133,50 +160,64 @@ function queryLog(log) {
return h("ul", { class: "query-log" }, log.map((q) => {
let outcome;
if (!q.ended) outcome = [h("span", { class: "spinner", "aria-hidden": "true" }), h("span", { class: "muted elapsed", dataset: { started: q.started } }, "waiting " + secs(Date.now() - q.started))];
else if (q.result) outcome = [nsBadge(q.result), q.result.rtt_ms ? h("span", { class: "muted" }, " " + q.result.rtt_ms + " ms") : h("span", { class: "muted" }, " " + secs(q.ended - q.started))];
else if (q.addresses) outcome = h("span", { class: "muted mono" }, "→ " + q.addresses.join(", "));
else if (q.error) outcome = h("span", { class: "err-text" }, q.error);
else outcome = h("span", { class: "muted" }, "done, " + secs(q.ended - q.started));
else {
// The server's round-trip time when it has one; else how long the
// answer took to arrive here (at least the timeouts, for errors).
const ms = q.rtt || q.ended - q.started;
const took = h("span", { class: "muted" }, ms < 1 ? "<1 ms" : ms < 1000 ? ms + " ms" : secs(ms));
if (q.result) outcome = [nsBadge(q.result), took];
else if (q.error) outcome = [h("span", { class: "err-text" }, q.error), took];
else outcome = [h("span", { class: "mono" }, "→ " + (q.answer || "done")), took];
}
return h("li", { class: q.ended ? "" : "pending" }, h("code", null, q.query), " ", outcome);
}));
}
// serialsView fills el with the serials check: the cached result, or the
// serialsView fills el with the serials check: the kept result, or the
// running check's progress until its result is in.
function serialsView(el, zone, again) {
const entry = nsEntry(zone);
const c = entry.serials;
const c = nsEntry(zone).serials;
if (!again && c && c.resp && Date.now() - c.at < nsFresh) {
mount(el, serialsResult(c.resp, c.log, true));
return;
}
const job = !again && c && c.job ? c.job : runSerials(zone);
let seen = false, timer = null;
const draw = () => {
// Stop following the check once the tab is gone (the check goes on
// and its result is kept).
if (el.isConnected) seen = true;
else if (seen) { job.subs.delete(draw); clearInterval(timer); return; }
if (job.error) mount(el, errorBox(job.error));
else if (job.done) mount(el, serialsResult(job.done, job.log, false));
else {
const local = job.log.find((q) => q.id === 0);
mount(el,
h("p", null, "This server: serial ", local && !local.ended ? [h("span", { class: "spinner", "aria-hidden": "true" }), h("span", { class: "muted" }, "asking named")] : h("span", { class: "muted" }, "…")),
h("p", { class: "muted small" }, job.log.length ? "Waiting for the servers to answer:" : "Finding the servers to ask…"),
queryLog(job.log));
}
if ((job.done || job.error) && timer) { clearInterval(timer); timer = null; }
};
job.subs.add(draw);
draw();
// Tick the "waiting n s" counters.
if (!job.done && !job.error) {
timer = setInterval(() => {
if (seen && !el.isConnected) { clearInterval(timer); job.subs.delete(draw); return; }
for (const s of el.querySelectorAll(".elapsed")) s.textContent = "waiting " + ((Date.now() - Number(s.dataset.started)) / 1000).toFixed(1) + " s";
}, 200);
const job = !again && c && c.job ? c.job : runCheck(zone, "serials", {});
followCheck(el, job, (job) => {
const local = job.log.find((q) => q.id === 0);
return [
h("p", null, "This server: serial ", local && !local.ended ? [h("span", { class: "spinner", "aria-hidden": "true" }), h("span", { class: "muted" }, "asking named")] : h("span", { class: "muted" }, "…")),
h("p", { class: "muted small" }, job.log.length ? "Waiting for the servers to answer:" : "Finding the servers to ask…"),
queryLog(job.log)];
}, (resp, log) => serialsResult(resp, log, false));
}
// delegationView fills el with the delegation check, like serialsView.
// It starts a check only when asked (run), or follows one that's running.
function delegationView(el, zone, resolver, run) {
const c = nsEntry(zone).delegation;
if (!run) {
if (c && c.job) followCheck(el, c.job, delegationProgress, (resp, log) => delegationResult(resp, log, false));
else if (c && c.resp && Date.now() - c.at < nsFresh) mount(el, delegationResult(c.resp, c.log, true, c.at));
return;
}
const job = runCheck(zone, "delegation", resolver ? { resolver } : {});
followCheck(el, job, delegationProgress, (resp, log) => delegationResult(resp, log, false));
}
function delegationProgress(job) {
return [
h("p", { class: "muted small" }, job.log.length
? "Finding the parent zone, asking its servers for the delegation and the DS records, then each delegated server for the zone:"
: [h("span", { class: "spinner", "aria-hidden": "true" }), "Starting…"]),
queryLog(job.log)];
}
function delegationResult(r, log, cached, at) {
return [
delegationReport(r),
log && log.length ? h("details", { class: "query-details" }, h("summary", { class: "small muted" }, "Queries sent (" + log.length + ")"), queryLog(log)) : null,
h("p", { class: "muted small" }, "Checked ", time(r.checked_at || new Date(at).toISOString()), cached ? " (kept from earlier; Check delegation to ask anew)." : "."),
];
}
function serialsResult(r, log, cached) {
@@ -205,22 +246,11 @@ async function serversTab(el, zone) {
const out = h("div");
const entry = nsEntry(zone);
const resolver = input({ placeholder: "the server's own", value: entry.delegation ? entry.delegation.resolver : "" });
const show = (d, cached) => mount(out, delegationReport(d.resp),
h("p", { class: "muted small" }, "Checked ", time(new Date(d.at).toISOString()), cached ? " (kept from earlier; Check delegation to ask anew)." : "."));
const run = async (e) => {
const run = (e) => {
e.preventDefault();
const res = resolver.value.trim();
mount(out, h("p", { class: "muted" }, h("span", { class: "spinner", "aria-hidden": "true" }),
"Finding the parent zone through " + (res || "the server's resolver") + ", then asking its servers for the delegation and each delegated server for the zone…"));
try {
const d = { at: Date.now(), resolver: res, resp: await get("/zones/" + zone.id + "/delegation", res ? { resolver: res } : {}) };
entry.delegation = d;
if (out.isConnected) show(d, false);
} catch (err) {
mount(out, errorBox(err));
}
delegationView(out, zone, resolver.value.trim(), true);
};
if (entry.delegation && Date.now() - entry.delegation.at < nsFresh) show(entry.delegation, true);
delegationView(out, zone, "", false);
parts.push(h("div", { class: "panel" },
h("h2", null, "Delegation from the parent zone"),
h("p", { class: "muted small" }, "For zones on the internet: checks that the parent zone (for adbraun.lol, \"lol\") points to this zone's name servers with the right glue addresses, and that each of them serves the zone. A zone used only on your own network isn't delegated, and doesn't need this."),