Reservations page refreshes itself every 15 seconds
Online and last-seen times stay current. A newer draw (typing, a change) wins over a slower older one; no refresh while the tab is hidden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e6c61cc8e
commit
29fc991ce1
2 files changed
+10
-1
No files matched your search
@@ -113,6 +113,8 @@ and a CLI, built the way Jarl is.
|
|||||||
(Leases page, reservations' online badge, `steward lease list|show`,
|
(Leases page, reservations' online badge, `steward lease list|show`,
|
||||||
`steward reservation list|show`, `last_seen` in the API). A client that
|
`steward reservation list|show`, `last_seen` in the API). A client that
|
||||||
blocks pings stays online for as long as its lease lasts, without one.
|
blocks pings stays online for as long as its lease lasts, without one.
|
||||||
|
- **The Reservations page refreshes itself** every 15 seconds, so online
|
||||||
|
and last seen stay current.
|
||||||
- **Activity by subnet:** `steward activity --subnet`, a subnet filter on
|
- **Activity by subnet:** `steward activity --subnet`, a subnet filter on
|
||||||
the Activity page, `GET /events?subnet=` (ID, name or prefix).
|
the Activity page, `GET /events?subnet=` (ID, name or prefix).
|
||||||
- **Token rotation** (`steward token rotate`, `POST /tokens/{id}/rotate`,
|
- **Token rotation** (`steward token rotate`, `POST /tokens/{id}/rotate`,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { h, mount, table, time, input, modal, errorBox, field, confirmDialog, select, debounce, badge, checkbox } from "../ui.js";
|
import { h, mount, table, time, input, modal, errorBox, field, confirmDialog, select, debounce, badge, checkbox } from "../ui.js";
|
||||||
import { get, change, can } from "../api.js";
|
import { get, change, can } from "../api.js";
|
||||||
import { optionsEditor, subnetLabel } from "../widgets.js";
|
import { optionsEditor, subnetLabel } from "../widgets.js";
|
||||||
|
import { onLeave } from "../app.js";
|
||||||
|
|
||||||
// reservationsTable lists reservations with Edit and Delete.
|
// reservationsTable lists reservations with Edit and Delete.
|
||||||
export function reservationsTable(rs, done, opts = {}) {
|
export function reservationsTable(rs, done, opts = {}) {
|
||||||
@@ -87,19 +88,25 @@ export async function render(el, params, query) {
|
|||||||
const subnetSel = select([["", "all subnets"], ...subnets.map((s) => [String(s.id), subnetLabel(s)])], query.get("subnet") || "");
|
const subnetSel = select([["", "all subnets"], ...subnets.map((s) => [String(s.id), subnetLabel(s)])], query.get("subnet") || "");
|
||||||
const search = input({ type: "search", placeholder: "address, MAC or host name" });
|
const search = input({ type: "search", placeholder: "address, MAC or host name" });
|
||||||
const out = h("div");
|
const out = h("div");
|
||||||
|
let gen = 0;
|
||||||
const draw = async () => {
|
const draw = async () => {
|
||||||
|
const my = ++gen;
|
||||||
const q = search.value.trim().toLowerCase();
|
const q = search.value.trim().toLowerCase();
|
||||||
const rs = (await get("/reservations", { subnet: subnetSel.value })).filter((r) =>
|
const rs = (await get("/reservations", { subnet: subnetSel.value })).filter((r) =>
|
||||||
!q || r.ip.includes(q) || (r.mac || "").includes(q) || (r.hostname || "").toLowerCase().includes(q) || (r.client_id || "").includes(q));
|
!q || r.ip.includes(q) || (r.mac || "").includes(q) || (r.hostname || "").toLowerCase().includes(q) || (r.client_id || "").includes(q));
|
||||||
|
if (my !== gen) return; // a newer draw (typing, a change) won
|
||||||
mount(out, reservationsTable(rs, draw, { empty: q || subnetSel.value ? "None match." : "No reservations yet." }));
|
mount(out, reservationsTable(rs, draw, { empty: q || subnetSel.value ? "None match." : "No reservations yet." }));
|
||||||
};
|
};
|
||||||
subnetSel.addEventListener("change", draw);
|
subnetSel.addEventListener("change", draw);
|
||||||
search.addEventListener("input", debounce(draw, 200));
|
search.addEventListener("input", debounce(draw, 200));
|
||||||
mount(el,
|
mount(el,
|
||||||
h("div", { class: "page-head" },
|
h("div", { class: "page-head" },
|
||||||
h("div", { class: "grow" }, h("h1", null, "Reservations"), h("div", { class: "muted" }, "Fixed addresses for clients, by MAC address or client identifier.")),
|
h("div", { class: "grow" }, h("h1", null, "Reservations"), h("div", { class: "muted" }, "Fixed addresses for clients, by MAC address or client identifier. Refreshed every 15 seconds.")),
|
||||||
can("config", true) ? h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => reservationDialog(null, draw) }, "New reservation")) : null),
|
can("config", true) ? h("div", { class: "actions" }, h("button", { class: "primary", onclick: () => reservationDialog(null, draw) }, "New reservation")) : null),
|
||||||
h("div", { class: "filter-row" }, subnetSel, search),
|
h("div", { class: "filter-row" }, subnetSel, search),
|
||||||
h("div", { class: "panel" }, out));
|
h("div", { class: "panel" }, out));
|
||||||
await draw();
|
await draw();
|
||||||
|
// Online and last seen change as clients come and go.
|
||||||
|
const t = setInterval(() => { if (!document.hidden) draw().catch(() => {}); }, 15000);
|
||||||
|
onLeave(() => clearInterval(t));
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user