fix(ui): clear session caches at the identity boundary and give the wallet view a loader - #251
Merged
Merged
Conversation
…allet view a loader
`Live` is a per-session cache, but nothing ever cleared it when the identity
changed, and `renderView`'s wallet branch was the only one without a loader. So
after Alice used the dashboard and logged out, Bob opening the wallet saw Alice's
`Live.wallet.balance` in `#wallet-forever` ("permanent points") — and it never
self-healed, because that view never fetched anything itself.
Two halves, both required:
- `resetSessionCaches()` — derived from the `Live` object literal, so a newly
added slot is covered automatically — is called at both identity boundaries:
session end (`exitGuest`: logout / 401) and session establishment
(`loadSession`: boot / login).
- `renderView("wallet")` gets its own loader `loadWallet()`, mirroring the tail
of `loadDashboard`. This also fixes a single-session bug: logging in and going
straight to the wallet left `Live.wallet` null forever, so the cell fell back
to `D.USER.balance` (`available` = permanent + today's gift) as if it were the
permanent balance.
Static gate (`src/state_gate.rs`, test-only) pins both shapes:
- every `Live` slot is dropped at both identity boundaries, by a derived reset
(a hand-written roster is rejected);
- every `renderView` branch both renders and loads.
A/B (jsdom, real index.html + 4 scripts, only fetch stubbed): pre-change tree red
`{B1,B2,B3}`; "loader only, caches never cleared" red `{B3}` (the dashboard still
renders the previous account's numbers on its first frame); "caches cleared,
no loader" red `{B1,B2}` (the wallet cell falls back to available). The two
half-fix red sets are disjoint and their union is the pre-change red set; both
half-fixes also fail the new gate tests.
argszero
added a commit
that referenced
this pull request
Sep 15, 2026
Ships the 18 PRs merged since v0.7.24 (#242-#259). Schema 14 -> 15 (two covering indexes, applied at startup). No config change, so no deployment-side config.toml edit is needed. Two themes: Perf on the NFS dev database - #259: stop mapping the db (PRAGMA mmap_size 64MB -> 0) and stop a real write per request (dao::touch_api_key gains a 60s guard). Measured on the live dev db: mmap=64MB 1.7-3.1s per COUNT / 250 MiB read vs mmap=0 ~10.5ms / 80 KiB; mmap=0 alone still leaves ~1.2s behind any write, so the pair is required. - #242: codify the two emergency indexes in a v15 migration and gate the conditional joins at the plan level. - #243: read the sharing page's earn total from one batched aggregate. Frontend: display must equal what it filters on, and one fact, one source - #250 one writer for the transaction cache; #251 clear every session slot at the identity boundary and give the wallet view a loader; #253 one shared writer for the wallet/dashboard month-changes; #254 boot loads only the destination view; #255 a model row's identity is the model, not its index; #256 the marketplace source follows the session, not whether data arrived; #257 the sidebar advertises only digits that work; #258 the admin total-balance card sums the gift amount its caption names. i18n - #249 every backend error reaches the wordlist, and the comment stripper stops mangling UTF-8; #252 the backend stops inventing Chinese display labels in response data fields. Forms and robustness - #244 a non-auth boot failure no longer looks like being logged out; #245 a credential 401 is no longer read as a session expiry; #246 wire timestamps reach the renderer unsliced; #247 inline cards submit from every field; #248 a market row's availability label comes from that row. - Cargo.toml / Cargo.lock: 0.7.24 -> 0.7.25. - CHANGELOG.md: v0.7.25 entry. - ui/index.html: cache-bust left as-is; the UI PRs in this release already advanced it past the value deployed with v0.7.24 (app.js 20260915-13, i18n.js 20260915-3). cargo test 288 passed; cargo fmt --check clean; clippy unchanged.
7 tasks
argszero
added a commit
that referenced
this pull request
Sep 21, 2026
`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.
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Liveinui/js/app.jsis a per-session cache, but nothing ever cleared it when the identity changed — andrenderView'swalletbranch was the only one without a loader. So after Alice used the dashboard and logged out, Bob opening the wallet saw Alice'sLive.wallet.balancerendered into#wallet-forever("永久点数" / permanent points) — and it never self-healed, because that view never fetched anything itself.This PR fixes both halves at once.
Related Issue
None (no matching issue; found by inspection + a jsdom instrument).
Changes
Two halves, both required (removing either leaves a red face):
resetSessionCaches()derives the slot names from theLiveobject literal itself (Object.keys(Live)), so a newly added slot is covered automatically; a hand-written roster is rejected by the gate. It is called at both identity boundaries:exitGuest()(logout button +__atpLogouton 401);loadSession()(boot and login both go throughrestoreSession()).renderView("wallet")becomesrenderWallet(); if (loggedIn()) loadWallet();, withloadWallet()mirroring the tail ofloadDashboard()(refreshWallet()→renderWallet()).Half 2 also fixes a single-session display bug: logging in and going straight to the wallet left
Live.walletnull forever, so the cell fell back toD.USER.balance— which isavailable(balance + gift_balance,src/routes/wallet.rs), i.e. permanent points plus today's gift shown as if it were the permanent balance.No new i18n keys;
ui/index.htmlcache-bust bumped toapp.js?v=20260915-5.Tests
cargo test全部通过 — 271 passed (was 269; +2 new gate tests)cargo fmt --check通过Static gate —
src/state_gate.rs(test-only, zero new deps)Two derived invariants added:
the_identity_boundaries_drop_every_session_cache— the reset must be derived (containsObject.keys(Live)), must not contain a per-slot assignment (a second roster would rot), and must be called from bothloadSessionandexitGuest.the_view_router_renders_and_loads_in_every_branch— everyrenderViewbranch line must contain both arenderand aload.Both carry an extractor self-proof and synthetic input (a hand-written roster / a branch without a loader must go red).
A/B (both instruments, four legs)
DOM probe: jsdom booting the real
ui/index.html+ the four real scripts, onlyfetchstubbed; drives the real nav buttons, the real logout button and the real login form; two accounts whose wallets differ and whereavailable != balance.a81839b:ui/js/app.js){B1, B2, B3}{identity, view-router}{B3}{identity}{B1, B2}{view-router}{}(13/13){}(6/6)The three red sets are mutually disjoint and their union is the pre-change red set — i.e. each half-fix fixes exactly one face and leaves the other. Both half-fixes are also rejected by the static gate, which is the evidence that the gate (not the DOM probe) pins the direction.
Probe faces:
B1—#wallet-forevershows the signed-in account's permanent balance (was Alice's4,242.42424instead of Bob's7.5);B2— the wallet view issues a/api/walletrequest of its own (was none);B3— landing on the dashboard, the synchronous first frame never shows another account's numbers (was Alice'smonth_use = 12,345.6789).Note on
B3: it only has teeth when Bob lands on the dashboard. If he lands on the wallet, the wallet's new loader refreshes the sharedLive.walletslot during login and the leak becomes invisible — which is exactly why the "loader only" half-fix can pass a naively-built probe.Checklist
fix/)