🎁 Sync 2026-08-28 19:05:48

This commit is contained in:
github-actions[bot] committed 2026-08-28 19:05:48 +08:00
1 parent 23a6a22cf3
commit 045d307bfe
15 files changed
+79 -52

No files matched your search

@@ -774,14 +774,14 @@ const MARK = 'fs-ap'; /* the built form's class, and how mount() knows it is alr
const TAB_DEADLINE = 5000;
const TAB = 'fs-appearance'; /* the pane's data-tab, which ui.tabs' click handler matches on */
let _routeObserver = null, _viewObserver = null, _observedView = null, _building = false;
let _routeObserver = null, _viewObserver = null, _observedRoot = null, _building = false;
function onPage() { return (document.body.getAttribute('data-page') || '') === PAGE; }
function stopWatch() {
if (_viewObserver) _viewObserver.disconnect();
_viewObserver = null;
_observedView = null;
_observedRoot = null;
}
/* The stock tab GROUP: the element whose children are the panes, which ui.tabs marks
@@ -831,24 +831,20 @@ function tabGroup(view) {
*
* A map redraw (Save without Apply) rebuilds the group and `ui.tabs` stamps `data-initialized` as
* an attribute change that can land after the last childList change, so the observer watches that
* attribute too and a miss schedules retries on a widening delay — otherwise the tab is missing
* until the next navigation (openwrt/luci#8903). */
const RETRIES = [ 0, 60, 150, 300, 600, 1200 ];
let _retryTimer = 0, _retryAt = 0;
function retryMount() {
if (_retryTimer) return;
if (_retryAt >= RETRIES.length) return;
const delay = RETRIES[_retryAt++];
_retryTimer = window.setTimeout(() => { _retryTimer = 0; mount(); }, delay);
}
* attribute too — otherwise the tab is missing until the next navigation (openwrt/luci#8903).
*
* A ladder of retries on a widening delay shipped beside that watch and is gone: instrumented over
* ten sessions on both package managers — a full load, an SPA return, a Save and a Save & Apply —
* it mounted the tab zero times, only ever arming a timer that woke to find the work done. The
* deadline below could once be seen mounting it, but only because watch() was binding the observer
* to a node the router was about to replace; with that fixed the deadline is a diagnostic again. */
function mount() {
const view = document.getElementById('view');
if (!view || !onPage()) return;
if (view.querySelector('.' + MARK)) { _retryAt = 0; return; }
if (view.querySelector('.' + MARK)) return;
if (_building) return;
const tabs = tabGroup(view);
if (!tabs) { retryMount(); return; }
if (!tabs) return; /* not built yet; the observer calls again when it is */
_building = true;
render()
.then((form) => {
@@ -882,13 +878,23 @@ function mount() {
function watch() {
const view = document.getElementById('view');
if (_viewObserver && _observedView !== view) stopWatch();
/* The CONTAINER, by id — not `#view`, which is not the same element for long. A client
* navigation builds a fresh one before it is in the document and swaps it in afterwards, while
* watch() runs on the `data-page` stamp, which comes first: the node bound here then reported
* `isConnected: false` while `#view` and `#maincontent` were both alive, and a Save's redraw
* produced four mutation batches that reached no callback at all. `#maincontent` outlives every
* swap, so the swap is itself a childList record. Save put the tab back in 250 ms after this,
* against 1,750-2,250 ms of waiting for the deadline before it. */
const root = document.getElementById('maincontent') || view;
if (_viewObserver && _observedRoot !== root) stopWatch();
if (_viewObserver || !view || !onPage()) return;
_observedView = view;
_observedRoot = root;
_viewObserver = new MutationObserver(mount);
/* `data-initialized` is the moment the group becomes usable, and it does not always come with
* a childList change (see retryMount above) */
_viewObserver.observe(view, {
/* `data-initialized` is when the group becomes usable, and the filter is load-bearing — proven
* by staging openwrt/luci#8903 rather than waiting for it: tear the tab, the pane and the
* attribute down in one task, restore the attribute ALONE 1,200 ms later, and the tab is back
* 4 ms after it. Without the filter it never comes back. No ordinary Save separates the two. */
_viewObserver.observe(root, {
childList: true, subtree: true,
attributes: true, attributeFilter: [ 'data-initialized' ],
});
@@ -900,13 +906,11 @@ function watch() {
* early on every mutation: the stock page renders, nothing throws, and every Appearance axis
* is unreachable. */
window.setTimeout(() => {
/* It asks, it does not repair: with the observer on a node that survives a navigation the
* repair this used to attempt became unreachable, and the whole matrix passes without it.
* A group still found here means the observer is working, so there is nothing to report. */
const v = document.getElementById('view');
if (!onPage() || !v || v.querySelector('.' + MARK) || _building) return;
/* one last attempt: the complaint below claims ui.tabs changed shape, which is only true
* if a fresh look still finds no group */
_retryAt = 0;
mount();
if (v.querySelector('.' + MARK) || _building || tabGroup(v)) return;
if (!onPage() || !v || v.querySelector('.' + MARK) || _building || tabGroup(v)) return;
console.error('footstrap: the Appearance tab could not be attached — this page has tabs, but '
+ 'ui.tabs no longer marks them the way fs-appearance.js looks for. Every Appearance axis '
+ 'is unreachable until that is updated.');
@@ -104,16 +104,22 @@ function arrange() {
* coalesced and one observer per #view node. The SPA router may replace #view between visits, so
* re-attach when the observed node is no longer the current one — a singleton bound to the first
* #view would watch a detached tree and the grid would never apply again. */
let _observer = null, _observedView = null, _routeObserver = null;
let _observer = null, _observedRoot = null, _routeObserver = null;
function stopWatch() {
if (_observer) _observer.disconnect();
_observer = null;
_observedView = null;
_observedRoot = null;
_wrapEl = null; /* the grid belongs to the #view we are leaving */
}
function watch() {
const view = document.getElementById('view');
if (_observer && _observedView !== view)
/* `#maincontent`, not `#view`: a client navigation builds a fresh `#view` before it is in the
* document and swaps it in afterwards, and this runs on the `data-page` stamp, which comes
* first — so the observer bound here read `isConnected: false` after one round trip and the
* grid stopped being re-arranged on every poll tick, silently, until the next full load. The
* shell's column outlives every swap. Same fault, same fix as fs-appearance.js. */
const root = document.getElementById('maincontent') || view;
if (_observer && _observedRoot !== root)
stopWatch();
arrange();
/* a chrome module is alive on every page, so without the route check an observer would attach
@@ -121,11 +127,11 @@ function watch() {
if (_observer || !view ||
(document.body.getAttribute('data-page') || '') !== 'admin-status-overview')
return;
_observedView = view;
_observedRoot = root;
/* one arrange() per frame, however many mutations a poll tick delivers (fit.frame — the
* theme's shared coalescer, fs-fit.js) */
_observer = new MutationObserver(fit.frame(arrange));
_observer.observe(view, { childList: true, subtree: true });
_observer.observe(root, { childList: true, subtree: true });
}
/* A chrome module is instantiated once per page load, so it has to notice SPA navigation itself.