From 29fc991ce1755ac9c260c8cf8e849bd7d4cbdc86 Mon Sep 17 00:00:00 2001 From: abrown38 Date: Sun, 18 Oct 2026 18:03:06 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 2 ++ internal/web/static/assets/pages/reservations.js | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6132b0..c17c655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,6 +113,8 @@ and a CLI, built the way Jarl is. (Leases page, reservations' online badge, `steward lease list|show`, `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. +- **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 the Activity page, `GET /events?subnet=` (ID, name or prefix). - **Token rotation** (`steward token rotate`, `POST /tokens/{id}/rotate`, diff --git a/internal/web/static/assets/pages/reservations.js b/internal/web/static/assets/pages/reservations.js index 9106dd2..c030699 100644 --- a/internal/web/static/assets/pages/reservations.js +++ b/internal/web/static/assets/pages/reservations.js @@ -2,6 +2,7 @@ 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 { optionsEditor, subnetLabel } from "../widgets.js"; +import { onLeave } from "../app.js"; // reservationsTable lists reservations with Edit and Delete. 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 search = input({ type: "search", placeholder: "address, MAC or host name" }); const out = h("div"); + let gen = 0; const draw = async () => { + const my = ++gen; const q = search.value.trim().toLowerCase(); 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)); + 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." })); }; subnetSel.addEventListener("change", draw); search.addEventListener("input", debounce(draw, 200)); mount(el, 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), h("div", { class: "filter-row" }, subnetSel, search), h("div", { class: "panel" }, out)); await draw(); + // Online and last seen change as clients come and go. + const t = setInterval(() => { if (!document.hidden) draw().catch(() => {}); }, 15000); + onLeave(() => clearInterval(t)); }