From d0ee2c884457cf33c1db2600c3ea8171e289e5b2 Mon Sep 17 00:00:00 2001 From: abrown38 Date: Wed, 14 Oct 2026 13:52:36 +0100 Subject: [PATCH] UI: activity client links, online badges, ended-leases toggle, All activity - Activity: a client's MAC address opens its leases; "Only this" filters the list to that client. - Reservations: Online is a green online / grey offline badge. - A subnet's leases: "Include ended leases" is a checkbox. - Dashboard: "All activity" is a button beside the heading. Co-Authored-By: Claude Opus 5.5 --- internal/web/static/assets/pages/activity.js | 6 ++++-- internal/web/static/assets/pages/dashboard.js | 2 +- internal/web/static/assets/pages/reservations.js | 6 ++++-- internal/web/static/assets/pages/subnet.js | 10 +++++----- internal/web/static/assets/style.css | 2 ++ 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/internal/web/static/assets/pages/activity.js b/internal/web/static/assets/pages/activity.js index 2a35fea..d8d223e 100644 --- a/internal/web/static/assets/pages/activity.js +++ b/internal/web/static/assets/pages/activity.js @@ -20,7 +20,9 @@ export async function render(el, params, query) { h("td", { class: "mono small" }, new Date(e.time).toLocaleTimeString(undefined, { hourCycle: "h23" })), h("td", { class: "mono" }, e.interface), h("td", null, h("strong", { class: "type-" + t.split(" ")[0] }, t)), - h("td", { class: "mono" }, e.mac ? h("a", { href: "#", onclick: (ev) => { ev.preventDefault(); mac.value = e.mac; draw(); } }, e.mac) : ""), + h("td", { class: "mono nowrap" }, e.mac ? [ + h("a", { href: "#/leases?q=" + encodeURIComponent(e.mac), title: "This client's leases" }, e.mac), " ", + mac.value.trim() ? null : h("button", { class: "small reveal", title: "Show only this client's messages", onclick: () => { mac.value = e.mac; draw(); } }, "Only this")] : ""), h("td", { class: "mono" }, e.ip || ""), h("td", null, e.hostname || ""), h("td", { class: "detail" }, e.detail || "")); @@ -30,7 +32,7 @@ export async function render(el, params, query) { iface.addEventListener("change", draw); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Activity"), - h("div", { class: "muted" }, "The latest DHCP messages Steward received and sent, newest first. Click a MAC address to follow one client."))), + h("div", { class: "muted" }, "The latest DHCP messages Steward received and sent, newest first. A MAC address leads to that client's leases; Only this follows just that client."))), h("div", { class: "filter-row" }, mac, iface, pause), h("div", { class: "panel" }, out)); await draw(); diff --git a/internal/web/static/assets/pages/dashboard.js b/internal/web/static/assets/pages/dashboard.js index 8cb8817..ce2064b 100644 --- a/internal/web/static/assets/pages/dashboard.js +++ b/internal/web/static/assets/pages/dashboard.js @@ -50,7 +50,7 @@ export async function render(el) { problemsList(st.problems), st.ping_problem ? h("div", { class: "msg warn" }, st.ping_problem) : null) : null, events.length ? h("div", { class: "panel" }, - h("h2", null, "Latest DHCP messages ", h("a", { href: "#/activity", class: "small" }, "all")), + h("div", { class: "panel-head" }, h("h2", null, "Latest DHCP messages"), h("a", { class: "button small", href: "#/activity" }, "All activity")), table([ { label: "When", render: (e) => time(e.time) }, { label: "Interface", render: (e) => e.interface }, diff --git a/internal/web/static/assets/pages/reservations.js b/internal/web/static/assets/pages/reservations.js index 9dbd746..77e8243 100644 --- a/internal/web/static/assets/pages/reservations.js +++ b/internal/web/static/assets/pages/reservations.js @@ -1,5 +1,5 @@ // Reservations: fixed addresses for clients. -import { h, mount, table, input, modal, errorBox, field, confirmDialog, select, debounce } from "../ui.js"; +import { h, mount, table, input, modal, errorBox, field, confirmDialog, select, debounce, badge } from "../ui.js"; import { get, change, can } from "../api.js"; import { optionsEditor, subnetLabel } from "../widgets.js"; @@ -11,7 +11,9 @@ export function reservationsTable(rs, done, opts = {}) { { 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) : "–" }, - { label: "Online", render: (r) => r.lease ? h("span", { title: "lease until " + new Date(r.lease.end).toLocaleString() }, "yes, on " + r.lease.interface) : h("span", { class: "muted" }, "no") }, + { label: "Online", render: (r) => r.lease + ? h("span", { title: "lease until " + new Date(r.lease.end).toLocaleString() }, badge("online", "ok"), h("span", { class: "muted small" }, " on " + r.lease.interface)) + : badge("offline") }, { label: "Options", render: (r) => r.options.length ? h("span", { class: "mono small" }, r.options.map((o) => o.name + "=" + o.value).join("; ")) : "" }, { label: "Comment", render: (r) => r.comment }, write ? { label: "", tdClass: "actions-cell", render: (r) => [ diff --git a/internal/web/static/assets/pages/subnet.js b/internal/web/static/assets/pages/subnet.js index a77554f..f84bc61 100644 --- a/internal/web/static/assets/pages/subnet.js +++ b/internal/web/static/assets/pages/subnet.js @@ -1,5 +1,5 @@ // A subnet's page: its leases, its reservations and its settings. -import { h, mount, badge, errorBox, confirmDialog, modal, input, debounce, readOnly } from "../ui.js"; +import { h, mount, badge, errorBox, confirmDialog, modal, input, debounce, readOnly, checkbox } from "../ui.js"; import { get, change, can } from "../api.js"; import { usageBar, subnetForm, fmtSeconds, subnetLabel } from "../widgets.js"; import { leasesTable } from "./leases.js"; @@ -32,12 +32,12 @@ export async function render(el, params) { async function leasesTab(el, s) { const search = input({ type: "search", placeholder: "address, MAC or host name" }); const out = h("div"); - let all = false; - const toggle = h("a", { href: "#", class: "small", onclick: (e) => { e.preventDefault(); all = !all; toggle.textContent = all ? "Only current leases" : "Also expired and released"; draw(); } }, "Also expired and released"); + const toggle = checkbox("Include ended leases (expired and released)", false); + toggle.input.addEventListener("change", () => draw()); const draw = async () => { let ls = await get("/leases", { subnet: s.id, q: search.value.trim() }); - if (!all) ls = ls.filter((l) => l.state === "active" || l.state === "declined"); - mount(out, leasesTable(ls, rerender, { subnet: true, empty: all ? "No leases." : "No current leases." })); + if (!toggle.input.checked) ls = ls.filter((l) => l.state === "active" || l.state === "declined"); + mount(out, leasesTable(ls, rerender, { subnet: true, empty: toggle.input.checked ? "No leases." : "No current leases." })); }; search.addEventListener("input", debounce(draw, 300)); mount(el, h("div", { class: "filter-row" }, search, toggle), h("div", { class: "panel" }, out)); diff --git a/internal/web/static/assets/style.css b/internal/web/static/assets/style.css index 834521c..ccc6f62 100644 --- a/internal/web/static/assets/style.css +++ b/internal/web/static/assets/style.css @@ -353,3 +353,5 @@ table.activity td.detail { white-space: normal; color: var(--muted); } td.mono, span.mono { font-family: var(--mono); font-size: 13px; } .mt { margin-top: 10px; } input:disabled, select:disabled, textarea:disabled { background: var(--info-soft); color: var(--muted); cursor: not-allowed; opacity: 1; } +.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; } +.panel-head h2 { margin: 0; }