Files
steward/internal/web/static/assets/pages/activity.js
T
abrown38andClaude Opus 5.5 5e6c61cc8e Hidden interfaces, last-seen pings, activity by subnet
- interfaces.exclude in steward.yaml (names or shell patterns) leaves
  interfaces out of GET /interfaces, the web UI's picker and
  'steward interface list'. Subnets on them are still served.
- With the ping check on, active leases are pinged every minute; a
  client that answers gets a last-seen time (leases.last_seen,
  migration 4). Not answering never takes a client offline.
- The activity log filters by subnet (--subnet, the Activity page,
  GET /events?subnet=).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:50:21 +01:00

45 lines
2.9 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";
import { subnetLabel } from "../widgets.js";
export async function render(el, params, query) {
const [ifaces, subnets] = await Promise.all([get("/interfaces"), get("/subnets")]);
const subnetSel = select([["", "all subnets"], ...subnets.map((s) => [String(s.id), subnetLabel(s)])], query.get("subnet") || "");
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(), subnet: subnetSel.value, 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 || subnetSel.value || iface.value ? " that match." : ".")));
};
mac.addEventListener("input", debounce(draw, 300));
subnetSel.addEventListener("change", draw);
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, subnetSel, iface, pause),
h("div", { class: "panel" }, out));
await draw();
const t = setInterval(() => { if (live) draw().catch(() => {}); }, 3000);
onLeave(() => clearInterval(t));
}