Disable reservations; subnet enable/disable buttons; page-head CSS fix
release / release (push) Successful in 5m53s

- A disabled reservation is kept but ignored: the client gets a pool
  address like any other (steward reservation disable|enable, buttons and
  a checkbox in the web UI). Schema migration 3.
- Subnets get Disable/Enable buttons on their page and in the list.
- A rule left without its selector in style.css broke the page heading's
  layout, so the New ... buttons sat cramped under the description.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-14 16:02:15 +01:00
1 parent 7b3f0f764c
commit 6ad1bb9a0d
21 files changed
+165 -37

No files matched your search

@@ -1,5 +1,5 @@
// Reservations: fixed addresses for clients.
import { h, mount, table, input, modal, errorBox, field, confirmDialog, select, debounce, badge } from "../ui.js";
import { h, mount, table, input, modal, errorBox, field, confirmDialog, select, debounce, badge, checkbox } from "../ui.js";
import { get, change, can } from "../api.js";
import { optionsEditor, subnetLabel } from "../widgets.js";
@@ -7,7 +7,7 @@ import { optionsEditor, subnetLabel } from "../widgets.js";
export function reservationsTable(rs, done, opts = {}) {
const write = can("config", true);
return table([
{ label: "Address", render: (r) => h("span", { class: "mono" }, r.ip) },
{ label: "Address", render: (r) => [h("span", { class: "mono" }, r.ip), r.disabled ? [" ", badge("disabled", "off")] : null] },
{ label: "Client", render: (r) => h("span", { class: "mono" }, r.client_id ? "id " + r.client_id : r.mac) },
{ label: "Host name", render: (r) => r.hostname || h("span", { class: "muted" }, "–") },
opts.subnet ? null : { label: "Subnet", render: (r) => r.subnet_id ? h("a", { href: "#/subnets/" + r.subnet_id }, r.subnet) : "–" },
@@ -18,9 +18,18 @@ export function reservationsTable(rs, done, opts = {}) {
{ label: "Comment", render: (r) => r.comment },
write ? { label: "", tdClass: "actions-cell", render: (r) => [
h("button", { class: "small reveal", onclick: () => reservationDialog(r, done) }, "Edit"), " ",
h("button", { class: "small reveal", title: r.disabled ? "Use this reservation again" : "Keep it, but ignore it: the client gets a pool address like any other",
onclick: () => toggle(r, done) }, r.disabled ? "Enable" : "Disable"), " ",
h("button", { class: "small reveal", onclick: () => del(r, done) }, "Delete"),
] } : null,
].filter(Boolean), rs, { empty: opts.empty || "No reservations." });
].filter(Boolean), rs, { empty: opts.empty || "No reservations.", rowClass: (r) => r.disabled ? "disabled" : "" });
}
async function toggle(r, done) {
try {
await change("PATCH", "/reservations/" + r.id, { disabled: !r.disabled });
done();
} catch (e) { modal(r.disabled ? "Not enabled" : "Not disabled", errorBox(e)); }
}
async function del(r, done) {
@@ -44,6 +53,7 @@ export async function reservationDialog(r, done, preset = {}) {
const host = input({ value: r ? r.hostname : preset.hostname || "", placeholder: "nas" });
const opts = await optionsEditor(r ? r.options : [], inherited);
const comment = input({ value: r ? r.comment : "" });
const disabled = checkbox("Disabled: keep it, but ignore it (the client gets a pool address like any other)", r ? r.disabled : false);
const err = h("div");
const body = h("div", null, h("div", { class: "form-grid" },
field("Address", ip, "Its subnet is the one whose network holds it. Inside a pool is fine: nobody else gets it."),
@@ -51,12 +61,14 @@ export async function reservationDialog(r, done, preset = {}) {
field("Client identifier", cid, "Optional: match the client's identifier (option 61, hex) instead of its MAC address."),
field("Host name", host, "Sent to the client."),
field("Options", opts, "Only for this client; they win over the subnet's."),
field("Comment", comment)), err);
field("Comment", comment),
r ? [h("label"), disabled] : null), err);
await modal(r ? "Edit reservation " + r.ip : "New reservation", body, [
{ label: "Cancel", value: null },
{ label: r ? "Save" : "Add reservation", kind: "primary", onclick: async () => {
mount(err);
const v = { ip: ip.value.trim(), mac: mac.value.trim(), client_id: cid.value.trim(), hostname: host.value.trim(), options: opts.value(), comment: comment.value };
if (r) v.disabled = disabled.input.checked;
try {
await change(r ? "PATCH" : "POST", r ? "/reservations/" + r.id : "/reservations", v);
done();