fix(ui): reset the session overlays when the session ends - #278
Conversation
`exitGuest()` is the identity boundary, but it only hid `#app`. The panels that exist only while a session is live — `#help-panel`, `#chat-modal` and the first-login tour — are *siblings* of `#app` in `ui/index.html`, so hiding `#app` left them on screen over the login form, and the next session inherited them (with `#help-context` still naming the previous user's view, and nothing ever repainting it). The boundary already owns the closers: the same file closes these panels on Escape (`app.js:4260`/`:4262`). This adds one named function, `resetSessionOverlays()`, that hands each `#app`-external panel back to its own closer, and calls it from `exitGuest()` right after `resetSessionCaches()` — the same direction #251 took one PR ago. The new invariant `the_identity_boundary_closes_the_panels_outside_the_app` derives the required element set from `ui/index.html` (top-level elements after `#app` whose `class` carries the standalone token `hidden`) and compares it against the transitive call closure of that one function. Zero exemption list: adding a sixth panel turns CI red.
|
Self-review (Committer, Re-ran everything on the tree that is actually being merged, not on the drafting notes: Product change — one named function plus one call site ( DOM probe (
Observed on the unfixed tree: Gate A/B ( Pre-flight anchor verifier ( Scope, stated so nobody reads more into it than is there (pitfall #341): the gate is lexical. It proves "every element derived from Honest severity: not a data leak. The One thing worth flagging for the record: |
Summary
exitGuest()— the identity boundary — only did three things: clear the session caches, addhiddento
#app, and reveal#login-view. But the panels that only exist while a session is live —the shortcut help panel (
#help-panel), the consume simulator (#chat-modal) and the first-logintour (
#tour-overlay/#tour-ring/#tour-pop) — are siblings of#app, not children(
ui/index.html:#appat :137, the panels at :812 / :822 / :840-842). Hiding#appthereforedoes not hide them: they stay on screen over the login form, and are inherited by the next
session.
The strongest face is cross-session. Open the help panel on the Settings view, log out, and let
someone else log in: the panel is still there, and
#help-contextstill readsView: Settings(
renderHelp()only runs when the panel is opened), so the new user sees a panel captioned with theprevious user's view — and nothing ever repaints it.
The boundary already knows how to close these panels: the very same file closes them on Escape
(
ui/js/app.js:4260if (e.key === "Escape" && tourOpen) { closeTour(); return; },:4262help).This change is therefore the missing half of the boundary, not new product behaviour — it hands
each
#app-external panel back to the closer that already exists (toggleHelp(false)/closeChat()/closeTour()), in one named function.Severity, stated honestly — this is not a data leak. The
#chat-modalface appears on the 401hook (
window.__atpLogout), i.e. the same person's token expiring, so the balance shown there istheir own. The cross-session face discloses which view the previous user was on (
#help-context's{view}) — not balances and not content. The defect is UI state surviving the identity boundary(wrong container still mounted + stale context), which is why it is fixed at the boundary.
Root cause and provenance — drift, not a deliberate trade-off
The boundary's shape is older than the panels it fails to close, and each panel brought its own
closer when it was added:
ef258efexitGuest()itself (guest browse mode)9a3ff3e#chat-modal+closeChat()8c49025#help-panel+toggleHelp(false)91fb61d#tour-*+closeTour()(+ the Escape handler at:4211)0e4acc2resetSessionCaches()for the very same reason — "one session's state must not be inherited by the next" — and still only handled#appSo the author's intent is unambiguous and it is the one this PR implements: #251 extended the
boundary in exactly this direction one PR ago and simply did not consider the sibling panels. There
is no reading of the history under which "the panels are supposed to survive logout" was ever
decided.
Changes
ui/js/app.js(identity boundary)resetSessionOverlays()(one named roster:toggleHelp(false),closeChat(),closeTour()) and call it fromexitGuest()right afterresetSessionCaches()ui/index.html:858?v=by procedure — read the live token and write a strictly greater one (do not copy a literal from any sheet)ui/README.md#app, plus the gate's stated scopesrc/state_gate.rsthe_identity_boundary_closes_the_panels_outside_the_app(4 rules) + its ruler self-checkthe_c2171_overlay_extractors_have_teeth(synthetic inputs)The roster is deliberately named and singular: the gate derives the required element set from
ui/index.htmland compares it against the closure of this one function, so adding a sixth panelwithout teaching the boundary about it turns CI red.
The gate (E4) — four rules, each with its own tooth
login-viewor
toast-wrap, and must equal the known overlay set.#toast-wrapis a permanent container withno
hiddentoken, so the derivation excludes it rather than the author remembering to;#login-viewsits before
#app, so the "after#app" cut excludes it.closure_hidden_elements(app.js) == overlays_outside_app(index.html):every derived element must have, inside the reset's transitive call closure, both a literal
"#<id>"and an operation that addshidden.resetSessionOverlays().resetSessionCaches(), still hide#app, still reveal#login-view— this blocks the "delete theboundary so rule 2 passes" shortcut.
Stated scope of the gate (recorded here on purpose, pitfall #341)
The gate is lexical. It proves "every element derived from
ui/index.htmlhas a closer thatclaims to hide it, and that closer is inside the boundary's call closure" — it does not prove
that anything is actually hidden on screen at runtime. That half is the DOM probe's job
(
c2171-probe.js); this repo's CI has no JS runner, so the two instruments are deliberatelydisjoint and neither pretends to cover the other.
Known blind spots, listed rather than hidden: ① the derivation reads only column-0 (top-level)
elements of
ui/index.html— an indented panel would be invisible to it; ② "addshidden" isliteral matching, so
classList.toggle("hidden", false)(a two-argument hide) would not count(nobody writes it that way today); ③ only literal
"#<id>"selectors are recognised, not dynamicones (
`#${id}`). Exemption list: zero.Evidence — A/B
DOM probe (
c2171-probe.js, jsdom, realindex.html+ the four real scripts)Stubs and accounts for
fetchonly; drives the real login form, the real Log outbutton, the real
?shortcut, real nav clicks and the real 401 hook. Every leg declares itsexpectation up front, and the harness prints that declaration next to the PASS/FAIL verdict (#339).
c6da6b8app.js68faa626…)--base(axis legs declared "defect present"){H3,H4,C3,C4,T2}ui/js/app.jscd0aa602…)c2171-app-m1-only2.js(c7d4b154…) — closes only the two reachable panelsT2redc2171-app-m2-noclose-tour.js(7af478ee…) — reset exists but skips the tourT2redObserved on the unfixed tree:
H3 help-panel visible over the login view = true,H4 panel visible in the NEW session = true ctx="View: Settings · Theme: Dark",C3 modal visible after the boundary = true,T2 tour-overlay visible after the boundary = true. The legs' declarations come from theinvariant ("the panel is closed after the boundary"), the observed values are printed as a second,
independent fact; the
N1control (a fresh boot has all panels hidden) shows the staleness is theboundary's doing.
Gate A/B and isolating mutation legs (
c2171_gate_mut_ab.py, 12 legs, run against the shippedui/js/app.js)Both take the real
src/state_gate.rs, make theinclude_str!paths absolute, appendc2171-gate-test.rsand compile withrustc --test --edition 2021— i.e. the gate is compiled andrun before it is ever pasted into the repo (the C2168 precedent: hand-written, never-compiled Rust
usually has errors, and finding them at fix time costs a whole round).
fixedm1-only2left {}m2-noclose-tourleft {chat-modal, help-panel}(non-empty)m_rule2— boundary stops calling the resetm_rule3a— boundary stops clearing the session cachesm_rule3b— boundary stops hiding#appm_rule3c— boundary stops revealing#login-viewm_rule4a— nothing outside#appcarrieshidden(empty derivation)m_rule4b— a new overlay the reset does not closem_rule1— reset closes only two of the fiven_loginview—#login-view(before#app) gainshiddenTwo independent instruments each reject both competing half-fixes: the probe red on
T2, the gatered on rule 1. The two ways of "passing" without fixing anything — hiding the panels by deleting the
data-*/hiddenmarkers fromindex.html, or deleting the boundary — are covered by rule 4b andrule 3 respectively.
Tests
cargo testpasses: 318 passed / 0 failed on the shipped tree (316 onmainc6da6b8, so+2 — exactly the invariant and its self-check added here).
cargo fmt --checkpasses.// ====box comment is a column-0 block thatrustfmt re-indents into
mod tests(rustfmtalso gives up on the twoassert_eq!calls whosemessage string is longer than
max_width, so those were hand-normalised). Runcargo fmtfirst,then
cargo fmt --check, then re-runcargo test.cargo clippy --all-targets -- -D warningsclean. Note CI runs 1.97.1 while the local toolchainis 1.95.0 (pitfall #344), so clippy is run on the spliced tree, not only via
rustc."mentions the id but only removes
hidden" (renderer) shape and the "addshiddenbut nevermentions the id" shape.
What this PR deliberately does NOT do
location.reload(), and it does not inline the three closers intoexitGuest()— an inline version has no closure for the gate to derive, and it would drift fromthe shape of the neighbouring
resetSessionCaches()call.#toast-wrap(a permanent container, not a panel — closing it would swallowtoasts) and it does not touch
#login-view(it sits before#appand the boundary is supposedto reveal it).
resetSessionCaches's definition, which gainsresetTxView()): the two anchors are disjoint (the boundary's call body vs. the function'sdefinition), so either landing order works.
ui/index.htmlcache-bust token is the one line shared with C2167/E2 and C2170/E4⇒ it must be re-read and incremented at landing time, never copied from a sheet (R98 made this a
procedure; there is no literal token anywhere in the queue).
Related Issue
None — reported directly from reconnaissance of the identity-boundary family (C2132 → C2142 →
C2145 → C2170 → C2171). No issue to close.
Checklist
fix/) — checked againstCONTRIBUTING.md's whitelist(
feat/fix/docs/refactor), not against habitfix(ui): …)