Files
steward/internal/web/static/assets/pages/dashboard.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

72 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The dashboard: interfaces, subnets and how full they are, problems,
// recent activity.
import { h, mount, problemsList, table, time, plural } from "../ui.js";
import { get, can } from "../api.js";
import { usageBar, subnetLabel } from "../widgets.js";
import { onLeave } from "../app.js";
export async function render(el) {
const draw = async () => {
const st = await get("/status");
const events = can("leases") ? await get("/events", { limit: 8 }) : [];
const leased = st.leases.active || 0;
const reservations = st.subnets.reduce((n, s) => n + s.usage.reservations, 0);
const poolSize = st.subnets.reduce((n, s) => n + (s.disabled ? 0 : s.usage.pool_size), 0);
const notServing = st.interfaces.filter((i) => !i.listening);
const errors = st.problems.filter((p) => p.severity === "error");
mount(el,
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Dashboard"))),
h("div", { class: "cards" },
card("Subnets", st.subnets.length, plural(st.interfaces.length, "interface"), "#/subnets"),
card("Active leases", leased, poolSize ? Math.round(leased * 100 / poolSize) + "% of " + poolSize + " pool addresses" : "no pools yet", "#/leases"),
card("Reservations", reservations, "fixed addresses", "#/reservations"),
card("Problems", st.problems.length, errors.length ? plural(errors.length, "error") : notServing.length ? plural(notServing.length, "interface") + " not serving" : "none", null)),
st.subnets.length === 0 ? h("div", { class: "panel" },
h("h2", null, "Getting started"),
h("p", null, "Steward serves no subnets yet. Add one for each network, on the interface its clients are on: ", h("a", { href: "#/subnets?new=1" }, "New subnet"), "."),
h("p", null, "Moving from Kea? On the server, ", h("code", null, "sudo steward import kea /etc/kea/kea-dhcp4.conf --leases /var/lib/kea/kea-leases4.csv"),
" brings over the subnets, reservations, options and current leases.")) : null,
h("div", { class: "panel" },
h("h2", null, "Interfaces"),
st.interfaces.length ? table([
{ label: "Interface", tdClass: "nowrap", render: (i) => [h("span", { class: "dot " + (i.listening ? "ok" : "err") }), h("span", { class: "mono" }, h("strong", null, i.name))] },
{ label: "Addresses", render: (i) => h("span", { class: "mono small" }, i.addresses.join(", ") || "none") },
{ label: "DHCP", render: (i) => i.listening ? h("span", { class: "muted" }, "serving") : h("span", { class: "err-text small" }, "not serving: " + i.error) },
{ label: "Subnets", render: (i) => i.subnets.map((id, n) => {
const s = st.subnets.find((x) => x.id === id);
return [n ? ", " : "", s ? h("a", { href: "#/subnets/" + id }, subnetLabel(s)) : "#" + id];
}) },
], st.interfaces) : h("div", { class: "muted" }, "DHCP is served on the interfaces that have subnets.")),
st.subnets.length ? h("div", { class: "panel" },
h("h2", null, "Subnets"),
table([
{ label: "Subnet", render: (s) => h("a", { href: "#/subnets/" + s.id }, subnetLabel(s)) },
{ label: "Interface", render: (s) => h("span", { class: "mono" }, s.interface) },
{ label: "Pools in use", render: (s) => s.disabled ? h("span", { class: "badge off" }, "disabled") : usageBar(s.usage) },
{ label: "Reservations", render: (s) => s.usage.reservations ? s.usage.reservations_leased + " of " + s.usage.reservations + " online" : "–" },
], st.subnets)) : null,
st.problems.length || st.ping_problem ? h("div", { class: "panel" },
h("h2", null, "Problems"),
problemsList(st.problems),
st.ping_problem ? h("div", { class: "msg warn" }, st.ping_problem) : null) : null,
events.length ? h("div", { class: "panel" },
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 },
{ label: "Message", render: (e) => h("span", { class: "type-" + e.type.replace(/^DHCP/, "").split(" ")[0] }, e.type.replace(/^DHCP/, "")) },
{ label: "Client", render: (e) => h("span", { class: "mono" }, e.mac || "") },
{ label: "Address", render: (e) => h("span", { class: "mono" }, e.ip || "") },
{ label: "Host name", render: (e) => e.hostname || "" },
], events, { })) : null);
};
await draw();
const t = setInterval(() => { draw().catch(() => {}); }, 15000);
onLeave(() => clearInterval(t));
}
function card(label, value, sub, href) {
const body = [h("div", { class: "label" }, label), h("div", { class: "value" }, String(value)), h("div", { class: "sub" }, sub)];
return href ? h("a", { class: "card link", href }, body) : h("div", { class: "card" }, body);
}