Files
steward/internal/web/static/assets/pages/activity.js
T
abrown38andClaude Opus 5.5 d0ee2c8844 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 <noreply@anthropic.com>
2026-10-14 13:52:36 +01:00

42 lines
2.6 KiB
JavaScript

// Activity: the latest DHCP messages, refreshed every few seconds.
import { h, mount, input, select, debounce } from "../ui.js";
import { get } from "../api.js";
import { onLeave } from "../app.js";
export async function render(el, params, query) {
const ifaces = await get("/interfaces");
const mac = input({ placeholder: "MAC address", value: query.get("mac") || "", class: "mono" });
const iface = select([["", "all interfaces"], ...ifaces.filter((i) => i.subnets.length).map((i) => [i.name, i.name])], query.get("interface") || "");
const out = h("div");
let live = true;
const pause = h("button", { onclick: () => { live = !live; pause.textContent = live ? "Pause" : "Resume"; } }, "Pause");
const draw = async () => {
const evs = await get("/events", { mac: mac.value.trim(), interface: iface.value, limit: 300 });
mount(out, evs.length ? h("div", { class: "table-wrap" }, h("table", { class: "activity" },
h("thead", null, h("tr", null, ["Time", "Interface", "Message", "Client", "Address", "Host name", ""].map((t) => h("th", null, t)))),
h("tbody", null, evs.map((e) => {
const t = e.type.replace(/^DHCP/, "");
return h("tr", null,
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 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 || ""));
})))) : h("div", { class: "empty" }, "No DHCP messages yet" + (mac.value || iface.value ? " that match." : ".")));
};
mac.addEventListener("input", debounce(draw, 300));
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. 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();
const t = setInterval(() => { if (live) draw().catch(() => {}); }, 3000);
onLeave(() => clearInterval(t));
}