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:
@@ -244,8 +244,12 @@ def test_boot_order_header_then_public_read() -> None:
|
||||
assert "showInvalid()" in branch
|
||||
assert "renderSharedChat" not in branch, "no render on a null read"
|
||||
# The relative import of the shared header module (no absolute
|
||||
# /assets/ import — the esbuild bundle contract).
|
||||
assert 'import { initSharedHeader } from "./header.js";' in js
|
||||
# /assets/ import — the esbuild bundle contract). The explicit
|
||||
# binding init rides the same import (2026-09-08 double-binding fix).
|
||||
assert (
|
||||
'import { bindSharedHeaderControls, initSharedHeader } from "./header.js";'
|
||||
in js
|
||||
)
|
||||
assert '"/assets/header.js"' not in js
|
||||
# No cross-page import (the per-page duplication house style).
|
||||
import_lines = [
|
||||
|
||||
Reference in New Issue
Block a user