diff options
| author | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-12 17:56:17 +0100 |
|---|---|---|
| committer | Hermes SysOp <sysop@webx11.duckdns.org> | 2026-11-12 17:56:17 +0100 |
| commit | 924779e5fd49daabc2056f17df91fcafbe6e956d (patch) | |
| tree | 24ca047fef962a17ce976d7efc9636e13d7e0bb3 /modules/core | |
| parent | 8013a6146533e2f043d386ad774b8c12f57add53 (diff) | |
fix(newos4): a mode the user picks now survives a reload (v4.6.164)
Found by the operator actually running /ui-selftest.html on his phone. The report said 6/6 PASS,
and the interesting part was what happened next: pick "easy" from the picker, reload, and you are
back in classic. Two defects stacked on top of each other, neither of which reported anything:
1. `apply()` on an unregistered mode starts a lazy load and returns false. When the mode registers,
`registerMode()` re-applied it with a hardcoded `persist:false` — so the picker's choice was
never written to `webx11_ui_mode` at all. The intent ("the user asked for this") was dropped the
moment the mode file had to be fetched, which is the normal case for all six modes.
2. Even when the mode did persist, `webx11_ui_experimental` was never set — and that key is what the
in-bundle gate reads before UIShellModule is registered. A stored mode behind a closed gate is a
preference the product silently ignores.
Fix:
* `_pendingPersist` carries the caller's intent through the lazy path; `registerMode()` honours it
instead of hardcoding false, and the load-completion apply already forwarded `o.persist`;
* `_persistMode(want)` writes the mode AND, for a real (non-classic) mode, sets
`webx11_ui_experimental='1'`, so the gate opens on the next boot.
Both directions verified live on v4.6.164:
* user pick (lazy path) -> webx11_ui_mode=easy + webx11_ui_experimental=1, then
main.html with NO query boots easy (#easy-actions present, body wx11-ui-easy, picker suppressed);
* shared link ?ui=chat -> mode applied for the session, _fromQuery=true, localStorage STILL null
(a link must not become the recipient's preference — that is the shell's own rule and it survives).
Also: /ui-selftest.html no longer prints a meaningless `heapMB: 9.5`. Chromium pins
performance.memory.usedJSHeapSize at 10,000,000 without --enable-precise-memory-info, which a page
cannot request, so the report now says the heap is not measurable in-page instead of inventing a
number — the same fake-green that was fixed in probe-ui-crash.py.
guard 7/7 rc=0, maintain OK, served core-bundle byte-identical to disk.
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/core-bundle.js | 33 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-uishell.js | 33 |
3 files changed, 61 insertions, 7 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 349c02b..a9a8de5 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -23468,6 +23468,7 @@ class UIShellModule { this._pending = null; // mode requested before its ext module loaded this._picker = null; this._fromQuery = false; + this._pendingPersist = null; // was the pending apply a USER choice or a link? this.key = 'webx11_ui_mode'; this.seenKey = 'webx11_ui_seen'; } @@ -23527,6 +23528,23 @@ class UIShellModule { localStorage.setItem(this.key, this.current); } + /** Store a user's mode choice AND the opt-in it depends on. + * + * The gate lives in terminal.js and reads `webx11_ui_experimental` before UIShellModule is + * registered. Storing only `webx11_ui_mode` therefore saves a preference the gate cannot see: + * pick "easy" in the picker, reload, and you are back in classic with no error anywhere + * (measured on the operator's phone via /ui-selftest.html, 2026-11-12). Only a real user action + * reaches this method — links and pending restores pass persist:false. + */ + _persistMode(want) { + try { + localStorage.setItem(this.key, want); + if (want && want !== 'classic' && this.lazyIds().indexOf(want) !== -1) { + localStorage.setItem('webx11_ui_experimental', '1'); + } + } catch (e) { /* private mode / quota: a preference that cannot be stored is not fatal */ } + } + /* ---------- registry ---------- */ registerMode(spec) { if (!spec || !spec.id || typeof spec.mount !== 'function') { @@ -23536,7 +23554,13 @@ class UIShellModule { this.modes[spec.id] = spec; if (this._pending === spec.id) { this._pending = null; - this.apply(spec.id, { silent: true, persist: false }); + // Honour the ORIGINAL intent: a picker click persists, a shared link does not. This path + // used to hardcode persist:false, so a mode chosen from the picker lived for the session + // and vanished on the next reload — the opt-in flag stayed off and nothing reported it + // (measured 2026-11-12 on the operator's phone). + const persist = this._pendingPersist === true; + this._pendingPersist = null; + this.apply(spec.id, { silent: true, persist: persist }); } return true; } @@ -23568,6 +23592,9 @@ class UIShellModule { return false; } this._pending = want; + // Carry the intent into registerMode(): without this, a picker click that has to wait for + // the mode file is applied with persist:false and the choice dies at the next reload. + this._pendingPersist = o.persist !== false; if (this.terminal) this.toast('Loading ' + want + '…', 1500); this._loadMode(want).then((ok) => { // registerMode() applies the mode the moment it registers — which already bumped this @@ -23586,7 +23613,7 @@ class UIShellModule { // _syncChrome all key off data-ui-mode, so it must be set even when nothing changes. document.body.dataset.uiMode = want; document.body.classList.add('wx11-ui-' + want); - if (o.persist !== false) localStorage.setItem(this.key, want); + if (o.persist !== false) this._persistMode(want); return true; } @@ -23599,7 +23626,7 @@ class UIShellModule { .filter((c) => c.indexOf('wx11-ui-') === 0 && c !== 'wx11-ui-' + want) .forEach((c) => document.body.classList.remove(c)); document.body.classList.add('wx11-ui-' + want); - if (o.persist !== false) localStorage.setItem(this.key, want); + if (o.persist !== false) this._persistMode(want); // _syncChrome on EVERY switch: it hides #quick-actions, which otherwise stays in the tab order // and the accessibility tree behind the opaque mode root (found by review). diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 754b532..d87b11f 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.163'; + const LATEST_VER = 'v4.6.164'; const CACHE = '?v=' + LATEST_VER; // Exposed so ext modules can register their own URL with the same cache-busting version // instead of an unversioned path the browser may already have cached (shell.js did). diff --git a/modules/core/module-uishell.js b/modules/core/module-uishell.js index 94e9bd1..2817790 100644 --- a/modules/core/module-uishell.js +++ b/modules/core/module-uishell.js @@ -25,6 +25,7 @@ class UIShellModule { this._pending = null; // mode requested before its ext module loaded this._picker = null; this._fromQuery = false; + this._pendingPersist = null; // was the pending apply a USER choice or a link? this.key = 'webx11_ui_mode'; this.seenKey = 'webx11_ui_seen'; } @@ -84,6 +85,23 @@ class UIShellModule { localStorage.setItem(this.key, this.current); } + /** Store a user's mode choice AND the opt-in it depends on. + * + * The gate lives in terminal.js and reads `webx11_ui_experimental` before UIShellModule is + * registered. Storing only `webx11_ui_mode` therefore saves a preference the gate cannot see: + * pick "easy" in the picker, reload, and you are back in classic with no error anywhere + * (measured on the operator's phone via /ui-selftest.html, 2026-11-12). Only a real user action + * reaches this method — links and pending restores pass persist:false. + */ + _persistMode(want) { + try { + localStorage.setItem(this.key, want); + if (want && want !== 'classic' && this.lazyIds().indexOf(want) !== -1) { + localStorage.setItem('webx11_ui_experimental', '1'); + } + } catch (e) { /* private mode / quota: a preference that cannot be stored is not fatal */ } + } + /* ---------- registry ---------- */ registerMode(spec) { if (!spec || !spec.id || typeof spec.mount !== 'function') { @@ -93,7 +111,13 @@ class UIShellModule { this.modes[spec.id] = spec; if (this._pending === spec.id) { this._pending = null; - this.apply(spec.id, { silent: true, persist: false }); + // Honour the ORIGINAL intent: a picker click persists, a shared link does not. This path + // used to hardcode persist:false, so a mode chosen from the picker lived for the session + // and vanished on the next reload — the opt-in flag stayed off and nothing reported it + // (measured 2026-11-12 on the operator's phone). + const persist = this._pendingPersist === true; + this._pendingPersist = null; + this.apply(spec.id, { silent: true, persist: persist }); } return true; } @@ -125,6 +149,9 @@ class UIShellModule { return false; } this._pending = want; + // Carry the intent into registerMode(): without this, a picker click that has to wait for + // the mode file is applied with persist:false and the choice dies at the next reload. + this._pendingPersist = o.persist !== false; if (this.terminal) this.toast('Loading ' + want + '…', 1500); this._loadMode(want).then((ok) => { // registerMode() applies the mode the moment it registers — which already bumped this @@ -143,7 +170,7 @@ class UIShellModule { // _syncChrome all key off data-ui-mode, so it must be set even when nothing changes. document.body.dataset.uiMode = want; document.body.classList.add('wx11-ui-' + want); - if (o.persist !== false) localStorage.setItem(this.key, want); + if (o.persist !== false) this._persistMode(want); return true; } @@ -156,7 +183,7 @@ class UIShellModule { .filter((c) => c.indexOf('wx11-ui-') === 0 && c !== 'wx11-ui-' + want) .forEach((c) => document.body.classList.remove(c)); document.body.classList.add('wx11-ui-' + want); - if (o.persist !== false) localStorage.setItem(this.key, want); + if (o.persist !== false) this._persistMode(want); // _syncChrome on EVERY switch: it hides #quick-actions, which otherwise stays in the tab order // and the accessibility tree behind the opaque mode root (found by review). |