diff --git a/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js b/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js index 44a2807a1a..87c126c6c1 100644 --- a/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js +++ b/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js @@ -56,7 +56,10 @@ function probeColor(expr) { * shared with `luci-app-*`, and an app's unlayered `span { color: … !important }` outranks * a layer and a plain inline style alike. A probe that loses its own colour reports the * app's, which then becomes the admin's saved axis on the next confirm. */ - _probe = E('span', { 'aria-hidden': 'true' }); + /* the id is this probe's escape from fs-router.js's strayBodyNode(): it is + * appended once and never removed, so without an fs-* id it would read as body litter and + * force every navigation away from this page into a full load. */ + _probe = E('span', { 'id': 'fs-appearance-probe', 'aria-hidden': 'true' }); _probe.style.cssText = 'position:fixed!important;left:-9999px!important;top:0!important;' + 'width:0!important;height:0!important;overflow:hidden!important;' + 'pointer-events:none!important;'; diff --git a/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js index 871aa38eae..08476ee2c9 100644 --- a/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js +++ b/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js @@ -110,6 +110,12 @@ let _probe = null; function resolveLen(token, dflt) { if (!_probe) { _probe = document.createElement('div'); + /* the id is fs-router.js's strayBodyNode() reading an fs-* id as "ours", which is + * a different question from the chrome mark above: this stays unmarked (no data-fs-chrome, + * so the fence still does not spare it from a hostile !important) but still needs SOME fs-* + * name, or an element appended once and never removed would read as body litter and force + * every navigation off this page into a full load. */ + _probe.id = 'fs-chrome-geom-probe'; _probe.setAttribute('aria-hidden', 'true'); /* out of flow, no box, no ink: it must never affect layout, scroll extent or hit-testing */ _probe.style.cssText = 'position:absolute!important;visibility:hidden!important;' + diff --git a/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js index 2726e7a665..c4b3793f23 100644 --- a/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js +++ b/themes/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js @@ -43,9 +43,9 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals = * keyed by that first id and the entry carries `live`, the id armed right now (null while * paused), so a view holding its handle can still stop its own poller after a trip through a * hidden tab. The arguments are kept because a `setInterval` id carries none of them back. */ - window.setInterval = function (fn, ms) { + window.setInterval = function (fn, ms, ...rest) { const id = _si.apply(window, arguments); - _viewIntervals.set(id, { fn, ms, rest: Array.prototype.slice.call(arguments, 2), live: id }); + _viewIntervals.set(id, { fn, ms, rest, live: id }); return id; }; window.clearInterval = function (id) { @@ -102,28 +102,19 @@ const _viewIntervals = (window.__fsViewIntervals || (window.__fsViewIntervals = * * `L.Poll.timer` is that id, and it is private state: `add`/`remove`/`start`/`stop`/`active` are the * documented surface, and the whole `L.Poll` alias is already deprecated (`'require poll'` replaces - * it, but no supported release ships poll.js yet). Read blind, a renamed field would make LuCI's - * tick look like a view's — cleared on the next navigation, every poll on every later page silently - * dead. So a missing field is a reason to do nothing, once, loudly. - * - * Asked through the documented half first: `active()` says whether the tick is running, and `timer` - * is deleted by `stop()`, so an absent field is the ordinary "nothing to protect" case. The anomaly - * worth reporting is the pair disagreeing — a tick running while the id it runs on has no name we - * know. - * - * The alias itself is guarded for the same reason: the sweep runs inside the staged render, and a - * TypeError there would leave every click showing the previous page's content under the new page's - * title. */ + * it, but no supported release ships poll.js yet). `active()` below is called unguarded: this + * caller runs from the visibilitychange listener at module eval (:69-81), before CONTRACT_FNS ever + * checks anything, so what backs it is the floor, not the boot contract — `L.Poll.active` is + * unconditional in every 24.10+ luci.js. Read blind, a renamed `timer` field would make LuCI's tick + * look like a view's — cleared on the next navigation, every poll on every later page silently + * dead. So a missing field is a reason to do nothing, once, loudly: `active()` says whether the + * tick is running, and `timer` is deleted by `stop()`, so an absent field is the ordinary "nothing + * to protect" case. The anomaly worth reporting is the pair disagreeing — a tick running while the + * id it runs on has no name we know. */ /* -> the tick's id; null when LuCI is not polling; false when the two cannot be told apart, which * every caller reads as "leave every interval alone" */ function pollTickId() { - if (!L.Poll) { - warnPollUnreadable('footstrap: L.Poll is gone from this luci-base, so LuCI\'s own tick cannot be ' - + 'told apart from a view\'s timers — leaving view intervals alone. fs-router.js needs ' - + 'updating for this luci-base.'); - return false; - } - const running = (typeof L.Poll.active === 'function') ? L.Poll.active() : (L.Poll.timer != null); + const running = L.Poll.active(); if (running && L.Poll.timer == null) { warnPollUnreadable('footstrap: LuCI is polling but L.Poll.timer is not readable — leaving view ' + 'intervals alone rather than risking its tick. fs-router.js needs updating for this ' @@ -138,6 +129,13 @@ function clearViewIntervals() { /* Map, not Set: the key is the timer id and the value is what it takes to re-arm it */ _viewIntervals.forEach((spec, id) => { if (id !== keep) window.clearInterval(id); }); } +/* The "Refreshing"/"Paused" pill must not outlive the poll it reports on — see the `poll-stop` + * listener below, which is its only caller. */ +function hidePollIndicatorIfEmpty() { + if (!(L.Poll && L.Poll.queue && L.Poll.queue.length === 0)) return; + try { ui.hideIndicator('poll-status'); } + catch (e) { console.error('footstrap: hideIndicator threw on the poll-status teardown', e); } +} /* one line per document: this runs on every navigation, and a router that cannot read L.Poll * cannot read it on the next click either */ let _pollWarned = false; @@ -327,17 +325,69 @@ function watchSession() { * which also takes it out of the live tree. Public API only; no reaching into `dom.registry`. */ function discard(el) { try { - const dom = window.L ? window.L.dom : null; - if (!dom || typeof dom.content !== 'function') { el.remove(); return; } const bin = document.createElement('div'); bin.appendChild(el); - dom.content(bin, null); + window.L.dom.content(bin, null); } catch (e) { el.remove(); } } +/* ---- a page that parks nodes on makes the next navigation a full load ---- + * + * Some apps' render() appends tooltip/modal nodes to document.body on every visit (one leaves 7) + * and nothing here sweeps them, so the document is spent, as with an invasive foreign sheet. The + * recorder is header.ut's first {# The wallpaper pattern's paint surface: 15-wallpaper.css masks this element so an uploaded SVG takes the theme's ink. A mask applies to an element's whole rendering,