The dashboard counted every open changeset, including empty ones and other users', so "Pending changes (1 open)" led to an empty Changes page. It now counts the user's own changesets that contain changes, the status API's open_changesets skips empty ones, and in review mode a draft created for an edit the server rejects is discarded instead of left open. docs/concepts.md: zone types, a new zone's name servers and glue, reverse zones for any network, and dynamic updates versus API tokens and TSIG keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
56 lines
3.8 KiB
JavaScript
56 lines
3.8 KiB
JavaScript
import { h, mount, time, problemsList, badge, plural } from "../ui.js";
|
||
import { get, state } from "../api.js";
|
||
|
||
export async function render(el) {
|
||
const [st, mine] = await Promise.all([get("/status"), get("/changesets", { status: "open", mine: 1, limit: 50 }).catch(() => [])]);
|
||
const s = st.engine || {};
|
||
// The user's own changesets with something in them: what the Changes page shows.
|
||
const pending = mine.filter((c) => (c.ops || []).length).length;
|
||
const n = s.named;
|
||
const cards = h("div", { class: "cards" },
|
||
card("named", n ? (n.running ? badge("running", "ok") : badge("unknown", "warn")) : badge(s.named_error ? "unreachable" : "not checked", s.named_error ? "err" : ""),
|
||
n ? [n.version, h("br"), "last configured " + (n.last_configured || "?")] : (s.named_error || "")),
|
||
card("Jarl", s.adopted ? badge("active", "ok") : badge("not adopted", "warn"),
|
||
s.adopted ? "named loads Jarl's configuration" : "named does not load Jarl's configuration yet — run 'jarld adopt' on the server"),
|
||
card("Zones", (s.counts || {}).zones ?? "–", plural((s.counts || {}).records || 0, "record") + " · " + plural((s.counts || {}).views || 0, "view")),
|
||
card("Access", plural((s.counts || {}).acls || 0, "ACL"), plural((s.counts || {}).tsig_keys || 0, "TSIG key") + " · " + plural((s.counts || {}).options || 0, "option")),
|
||
);
|
||
const last = s.last_applied;
|
||
const parts = [h("div", { class: "page-head" },
|
||
h("div", { class: "grow" }, h("h1", null, "Dashboard"), h("div", { class: "muted" }, "Signed in as " + state.me.user.username + " · " + (state.me.permissions || []).join(", "))),
|
||
h("div", { class: "actions" },
|
||
h("a", { class: "button primary", href: "#/zones?new=1" }, "New zone"),
|
||
h("a", { class: "button", href: "#/changes" }, "Pending changes" + (pending ? " (" + pending + " open)" : "")))),
|
||
cards];
|
||
if (s.drift && s.drift.length) {
|
||
parts.push(h("div", { class: "msg warn" },
|
||
h("strong", null, "Files changed outside Jarl: "),
|
||
s.drift.map((d) => d.path + " (" + d.reason + ")").join(", "),
|
||
h("div", { class: "small" }, "The next apply refuses to overwrite them unless forced. An administrator can rewrite them with 'jarld resync' (or System → Resync).")));
|
||
}
|
||
parts.push(h("div", { class: "panel" },
|
||
h("h2", null, "Last change"),
|
||
last ? h("div", null,
|
||
h("a", { href: "#/history/" + last.id }, "#" + last.id + " " + (last.description || "")), " ",
|
||
h("span", { class: "muted" }, "by " + last.username + " via " + last.via + ", "), time(last.applied_at))
|
||
: h("div", { class: "muted" }, "Nothing applied yet.")));
|
||
const problems = s.problems || [];
|
||
const real = problems.filter((p) => p.severity !== "info");
|
||
parts.push(h("div", { class: "panel" },
|
||
h("h2", null, "Configuration checks"),
|
||
real.length ? problemsList(real) : h("div", { class: "msg ok" }, "No problems found in the current configuration."),
|
||
problems.length > real.length ? h("details", null, h("summary", { class: "muted" }, plural(problems.length - real.length, "note")), problemsList(problems.filter((p) => p.severity === "info"))) : null));
|
||
if (s.paths) {
|
||
parts.push(h("div", { class: "panel" }, h("h2", null, "Files"), h("dl", { class: "kv" },
|
||
h("dt", null, "named's main config"), h("dd", { class: "mono" }, s.main_config || "–"),
|
||
h("dt", null, "Jarl's named.conf"), h("dd", { class: "mono" }, s.paths.config_dir + "/named.conf"),
|
||
h("dt", null, "Zone files"), h("dd", { class: "mono" }, s.paths.zone_dir),
|
||
h("dt", null, "BIND tools"), h("dd", null, s.bind_version || "–"))));
|
||
}
|
||
mount(el, parts);
|
||
}
|
||
|
||
function card(label, value, sub) {
|
||
return h("div", { class: "card" }, h("div", { class: "label" }, label), h("div", { class: "value" }, value), sub ? h("div", { class: "sub" }, sub) : null);
|
||
}
|