From 7d93c373d11226f80221282fd1044b9e06e59732 Mon Sep 17 00:00:00 2001 From: adam Date: Tue, 13 Oct 2026 19:16:10 +0100 Subject: [PATCH] Explore: refresh with its own filters; the Statistics refresh stays on its page The Statistics page's once-a-minute refresh also ran on #/stats/explore and drew the Statistics page over it, losing the filters. It now runs on the Statistics page only, and the explore page refreshes itself every minute with the same filters (not while the filter bar is being edited). Co-Authored-By: Claude Opus 5.5 --- internal/web/static/assets/pages/explore.js | 24 ++++++++++++++++++++- internal/web/static/assets/pages/stats.js | 4 +++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/internal/web/static/assets/pages/explore.js b/internal/web/static/assets/pages/explore.js index 3098cb6..bffe552 100644 --- a/internal/web/static/assets/pages/explore.js +++ b/internal/web/static/assets/pages/explore.js @@ -49,7 +49,11 @@ function copy(f) { const g = {}; for (const k of kinds) g[k] = [...(f[k] || [])] const includes = (f, k) => (f[k] || []).filter((v) => !isNot(v)); const anyFilter = (f) => kinds.some((k) => (f[k] || []).length); +let generation = 0; + export async function render(el, params, query) { + const gen = ++generation; + const hash = location.hash; const f = parse(query); const since = query.get("since") || "24h"; const go = (g) => exploreHref(g, since); @@ -105,7 +109,25 @@ export async function render(el, params, query) { if (lists.length) parts.push(h("div", { class: "chart-grid" }, lists)); parts.push(recentPanel(ex, (k, v) => go(addTo(f, k, v)))); + if (gen !== generation) return; // the user moved on + el.classList.remove("refreshing"); mount(el, parts); + scheduleRefresh(el, params, query, gen, hash); +} + +// scheduleRefresh redraws the page a minute later with the same filters, +// so the graphs move along; not while a filter is being typed in. +function scheduleRefresh(el, params, query, gen, hash) { + setTimeout(() => { + if (gen !== generation || location.hash !== hash || !el.isConnected) return; + const form = el.querySelector("form.explore-filters"); + if (form && (form.contains(document.activeElement) || form.dataset.dirty)) { + scheduleRefresh(el, params, query, gen, hash); // try again later + return; + } + el.classList.add("refreshing"); + render(el, params, query).catch(() => el.classList.remove("refreshing")); + }, 60_000); } // filterBar types filters in. Each field takes several values, comma @@ -126,7 +148,7 @@ function filterBar(f, since, ex, views) { type: split(type.value).map((v) => v.toUpperCase()), rcode: split(rcode.value).map((v) => v.toUpperCase()), blocked: split(blocked.value) }; location.hash = exploreHref(g, since); }; - const form = h("form", { class: "explore-filters", onsubmit: (e) => { e.preventDefault(); apply(); } }, + const form = h("form", { class: "explore-filters", onsubmit: (e) => { e.preventDefault(); apply(); }, oninput: () => { form.dataset.dirty = "1"; } }, h("label", null, "Client ", client), h("label", null, "Name ", name), h("label", null, "View ", view), h("label", null, "Type ", type), h("label", null, "Result ", rcode), h("label", null, "Blocked ", blocked), h("button", { type: "submit", class: "primary small" }, "Show"), diff --git a/internal/web/static/assets/pages/stats.js b/internal/web/static/assets/pages/stats.js index 6893078..4e148ba 100644 --- a/internal/web/static/assets/pages/stats.js +++ b/internal/web/static/assets/pages/stats.js @@ -183,7 +183,9 @@ async function loadAll(since, heatSince, queryLog, vf, filtered) { // keeping the current render (dimmed) until the new one is ready. function scheduleRefresh(el, params, query, gen) { setTimeout(() => { - if (gen !== generation || !location.hash.startsWith("#/stats") || !el.isConnected) return; + // Only on the Statistics page itself: not #/stats/explore. + const h = location.hash; + if (gen !== generation || !(h === "#/stats" || h.startsWith("#/stats?")) || !el.isConnected) return; el.classList.add("refreshing"); render(el, params, query).catch(() => el.classList.remove("refreshing")); }, 60_000);