Web UI: zone naming and reverse zones, ACL suggestions, record form

- New zone dialog: choose Domain or Reverse zone. Reverse zones are
  created from a network and their name is generated; both kinds show the
  exact zone name and flag likely mistakes (addresses or URLs as names,
  host-like names, duplicates in the view, parent/child zones, networks
  off the octet/nibble boundaries) before anything is submitted.
- Record form: clears every field but the type after an add; shows the
  record exactly as it will be stored and warns about unqualified names.
  In reverse zones it takes an address, defaults to PTR, suggests host
  names of A/AAAA records with that address and warns about a second PTR;
  the record list shows addresses.
- Address match lists (options, view match lists, ACL elements) offer the
  existing ACLs, TSIG keys and built-ins as one-click suggestions and
  flag unknown names.
- Server: in reverse zones a record can be named by its address, and a
  PTR target like nas.example.com without the final dot is read as the
  full host name (a bare label is refused). Warnings noticed while parsing
  record data reach the result again.
- Fixes: brand link hover underline, the dashboard's "New zone" link
  opening the dialog twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-07 20:49:43 +00:00
1 parent 3b7bee67f8
commit bf7f99874b
19 files changed
+577 -35

No files matched your search

+4 -2
View File
@@ -1,4 +1,5 @@
import { h, mount, table, field, input, select, textarea, toast, errorBox, confirmDialog, modal, lines, checkbox } from "../ui.js";
import { amlContext, suggestions } from "../aml.js";
import { get, change, describeResult } from "../api.js";
import { rerender } from "../app.js";
import { optionsEditor } from "./options.js";
@@ -44,6 +45,7 @@ export async function render(el, params, query) {
async function viewDialog(v, viewless) {
const err = h("div");
const ctx = await amlContext();
const name = input({ value: v ? v.name : "", placeholder: "internal" });
const mc = textarea(v ? (v.match_clients || []).join("\n") : "", { class: "mono", rows: 4, placeholder: "192.168.1.0/24\nlocalhost\n(empty = any)" });
const md = textarea(v ? (v.match_destinations || []).join("\n") : "", { class: "mono", rows: 2, placeholder: "(empty = any)" });
@@ -52,8 +54,8 @@ async function viewDialog(v, viewless) {
const adopt = checkbox("Move the " + viewless.length + " zone(s) that are not in a view into this view", true);
const body = h("div", null, h("div", { class: "form-grid" },
field("Name", name),
field("Match clients", mc, "One element per line: addresses, prefixes, ACL names, key NAME, any/none/localhost/localnets; !negates. First match wins."),
field("Match destinations", md, "Server addresses this view answers on (optional)."),
field("Match clients", h("div", null, mc, suggestions(mc, ctx)), "One element per line: addresses, prefixes, ACL names, key NAME, any/none/localhost/localnets; !negates. First match wins."),
field("Match destinations", h("div", null, md, suggestions(md, ctx)), "Server addresses this view answers on (optional)."),
field("Recursive only", mro, "If yes, only recursive queries match this view."),
field("Comment", comment)),
!v && viewless.length ? h("div", { class: "msg warn" }, "Once views exist, named requires every zone to be inside a view. ", adopt) : null,