Files
steward/internal/web/static/assets/pages/settings.js
T
abrown38andClaude Opus 5.5 8a69038ab5 Admin name at init, token rotation, read-only forms, token role presets
- stewardd init asks for the first administrator's name on a terminal.
- Tokens can be rotated (steward token rotate, POST /tokens/{id}/rotate,
  Rotate in the web UI), with a grace period for the old secret, as in
  Jarl. Schema migration 2 adds the token columns.
- Read-only users see the settings and a subnet's settings locked,
  instead of editable fields without a Save button.
- The New token dialog only offers roles within what the user may do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:42:37 +01:00

46 lines
3.0 KiB
JavaScript

// Settings: the default lease time, conflict checks, authority, and the
// options every subnet gets.
import { h, mount, input, field, checkbox, errorBox, readOnly } from "../ui.js";
import { get, change, can } from "../api.js";
import { optionsEditor, fmtSeconds, parseSeconds } from "../widgets.js";
import { rerender } from "../app.js";
export async function render(el) {
const s = await get("/settings");
const write = can("config", true);
const lease = input({ value: fmtSeconds(s.lease_time || 43200) });
const ping = checkbox("Ping an address before offering it, and skip it if something answers", s.ping_check);
const timeout = input({ type: "number", min: 50, max: 5000, value: s.ping_timeout_ms || 500 });
timeout.style.width = "7em";
const hold = input({ value: fmtSeconds(s.decline_hold || 86400) });
const auth = checkbox("Authoritative: refuse (NAK) requests for addresses that don't belong on the interface", !s.not_authoritative);
const opts = await optionsEditor(s.options);
const err = h("div");
const save = async (dry) => {
mount(err);
const lt = parseSeconds(lease.value), dh = parseSeconds(hold.value);
if (lt === null || dh === null) { mount(err, h("div", { class: "msg err" }, "Write times as 12h, 1d or 30m.")); return; }
try {
const r = await change("PATCH", "/settings", { lease_time: lt, ping_check: ping.input.checked, ping_timeout_ms: parseInt(timeout.value, 10) || 0,
decline_hold: dh, not_authoritative: !auth.input.checked, options: opts.value() }, { query: dry ? { dry_run: 1 } : undefined });
if (!dry && r.applied) rerender();
} catch (e) { mount(err, errorBox(e)); }
};
mount(el,
write ? null : h("div", { class: "msg info" }, "You can see the settings but not change them (that needs config:write)."),
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Settings and options"),
h("div", { class: "muted" }, "Server-wide settings, and the options every subnet sends. A subnet's or a reservation's own option of the same name wins."))),
h("div", { class: "panel" },
h("div", { class: "form-grid" },
field("Default lease time", lease, "How long clients keep an address unless a subnet says otherwise (12h, 1d). They renew halfway."),
field("Options for every subnet", opts, "Usually domain-name-servers and domain-name; routers belong to each subnet."),
field("Conflict check", h("div", null, ping, h("div", { class: "small" }, "wait ", timeout, " ms for an answer"))),
field("Hold back for", hold, "How long an address a client declined (or one that answered a ping) isn't handed out."),
field("Authority", auth, "Turn this off only if another DHCP server serves the same network.")),
err,
write ? h("div", { class: "actions", style: { marginTop: "14px" } },
h("button", { class: "primary", onclick: () => save(false) }, "Save"),
h("button", { onclick: () => save(true), title: "Check without saving" }, "Check")) : null));
if (!write) readOnly(el.querySelector(".panel"));
}