fix(ui): bind the shared header controls on explicit init, not at module import
header.js's control bindings (sign-out, the mobile hamburger, the SINGLE New chat button) ran at module import. The Containerfile stage-1 build inlines header.js into every bundle that imports it (the shell's app.js, token-gate.js and the router's lazy views), so the shell page registered the #nav-toggle click handler twice, and two toggle handlers cancel each other — one tap = open + close = the mobile menu dead in the deployed image only. The dev tree's single ESM instance (and every test that runs against it) never showed it; a lazy view load adding a THIRD copy made the menu work again, which is why the failure looked state-dependent (chat cold boot dead, /sources.html alive). - header.js: the three bindings move into an exported bindSharedHeaderControls(), guarded by a marker on <body> (NOT module state — every bundle copy has its own function instance), so later bundle copies and repeated inits (the token gate's mid-page header re-boot) are no-ops; header.js is now side-effect-free at top level, which also lets esbuild tree-shake the dead copies out of the bundles that do not need them (the token-gate bundle no longer carries the binding code at all) - app.js / login.js / shared.js / document.js: call bindSharedHeaderControls() once at module top — import-time parity, unconditional (no async boot path to miss); doc-edit.js ships no header controls and calls nothing - unit: tests/unit/test_header_bindings_once_per_document.py pins the contract — the init export, the document-level idempotency marker, all three bindings inside the init, NO top-level addEventListener remaining, and exactly one module-top call in each header-carrying page script; stale import-time docstrings in the legacy header pins updated to the new contract Verified: full unit + integration suite (1746 passed), the hamburger / pinned-composer / smoke E2E stories green in isolation, ruff + pyright clean. Containerfile-equivalent esbuild 0.25.5 rebuild probed in Chromium: exactly ONE #nav-toggle click listener on chat cold boot, /sources.html and login.html, and a touch tap opens the menu in all three states (pre-fix production: two listeners on cold boot = dead, three on sources = alive).
This commit is contained in:
@@ -68,9 +68,14 @@
|
||||
* network call) until the admin gate resolves true.
|
||||
*/
|
||||
|
||||
import { fetchIsAdmin, initSharedHeader } from "./header.js";
|
||||
import { bindSharedHeaderControls, fetchIsAdmin, initSharedHeader } from "./header.js";
|
||||
import { mountGate } from "./token-gate.js"; // phase 79 (task 05): the inline token gate
|
||||
|
||||
// The shared header's control bindings (sign-out / mobile hamburger) —
|
||||
// EXPLICIT init, once per document (header.js is bundle-inlined per
|
||||
// entry; import-time side effects would double-bind — 2026-09-08 fix).
|
||||
bindSharedHeaderControls();
|
||||
|
||||
/* Phase 39: the page title's display name — window.BOR_BRAND (set at
|
||||
* parse time by the classic assets/brand.js, refreshed from
|
||||
* /api/config). This is a module, so the global is set by the time this
|
||||
|
||||
Reference in New Issue
Block a user