Disable zones instead of deleting them

A disabled zone stays in Jarl with its records, options and history,
still editable, but named doesn't load it; enabling it serves it as it is
then. Signed zones, and zones another view shares, can't be disabled.
jarl zone disable|enable, the zone page's Disable/Enable button.
Database schema 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-10 13:45:55 +00:00
1 parent 59baccd636
commit 89663e366b
24 files changed
+264 -26

No files matched your search

+23 -1
View File
@@ -36,7 +36,13 @@ export async function render(el, params, query) {
zone.dnssec_policy ? [h("a", { href: "#/zones/" + id + "/dnssec" }, badge(zone.dnssec_policy === "insecure" ? "DNSSEC: turning off" : "DNSSEC", zone.dnssec_policy === "insecure" ? "warn" : "ok")), " "] : null,
zone.zone_file ? h("span", { class: "mono small" }, zone.zone_file) : null)),
h("div", { class: "actions" },
zone.disabled
? h("button", { onclick: () => toggleZone(zone) }, "Enable zone")
: h("button", { disabled: !!zone.dnssec_policy, title: zone.dnssec_policy ? "A signed zone can't be disabled: turn signing off first" : "Stop serving the zone, keeping it in Jarl", onclick: () => toggleZone(zone) }, "Disable zone"),
" ",
h("button", { class: "danger", onclick: () => deleteZone(zone) }, "Delete zone")));
const banner = zone.disabled ? h("div", { class: "msg warn" },
h("strong", null, "This zone is disabled: "), "named doesn't load it. It is kept here, with its records, and can still be edited; enabling it serves it as it is then.") : null;
const tabBar = h("div", { class: "tabs" }, tabs.map(([k, label]) => h("a", { href: "#/zones/" + id + "/" + k, class: k === tab ? "active" : "" }, label)));
// The header and tabs show at once. Below them, switching tabs keeps the
// previous tab's content (faded) until the new one is loaded, rather
@@ -47,7 +53,7 @@ export async function render(el, params, query) {
holder.classList.add("stale");
body.className = "zone-body";
body.dataset.zone = id;
mount(el, head, tabBar, holder);
mount(el, head, banner, tabBar, holder);
if (tab === "records") await recordsTab(body, zone, query);
else if (tab === "settings") await settingsTab(body, zone);
else if (tab === "options") await optionsEditor(body, { scope: "zone", scope_id: zone.id, catalogScope: "zone:" + zone.type, title: "Zone options" });
@@ -653,3 +659,19 @@ function diffViewOf(r) {
if (!res) return h("div", { class: "muted" }, (r.warnings || []).join("; ") || "Nothing to import.");
return h("div", null, (r.warnings || []).length ? h("div", { class: "msg warn" }, r.warnings.join("; ")) : null, diffView(res.diff));
}
// toggleZone disables or enables a zone (asking first when disabling).
async function toggleZone(zone) {
if (!zone.disabled) {
const where = zone.view ? "view " + zone.view : "this server";
const ok = await confirmDialog("Disable zone " + zone.display_name,
"named stops serving " + zone.display_name + " in " + where + ". Its names are then answered by recursion where the view offers it (the internet's version of the zone, if any), and REFUSED elsewhere. The zone, its records and options stay in Jarl.", "Disable zone", "danger");
if (!ok) return;
}
try {
const r = await change("PATCH", "/zones/" + zone.id, { disabled: !zone.disabled });
toast(describeResult(r));
(r.warnings || []).forEach((w) => toast(w, "warn", 10000));
rerender();
} catch (e) { toast(e.message, "err", 8000); }
}