Skip to content

fix(ui): the wallet view renders a slot only the dashboard loader filled - #253

Merged
argszero merged 1 commit into
mainfrom
fix/wallet-month-changes-slot
Sep 15, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/wallet-month-changes-slot

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

#month-changes (wallet view) and #dash-month-changes (dashboard) are painted by the same renderMonthChanges(), and both read Live.dashboard. But that slot had exactly one writer — the dashboard's loader (loadDashboard) — while the wallet branch of renderView called loadWallet(), which only refreshes Live.wallet.

So whenever the session is established on the wallet view, nobody ever filled the slot and the block rendered net 0 plus "No changes this month" for good, while the very same /api/dashboard payload rendered correctly on the dashboard:

  • hash #/wallet (left over from a 401 or a previous session), then log in;
  • or, the everyday path: log out on the wallet page, then log back in.

(Refreshing with a live token hides it: boot calls renderView("dashboard") unconditionally from DOMContentLoaded, which fills the slot as a side effect. That is why it stayed latent.)

Related Issue

None (internal defect found by recon; no issue was opened for it).

Changes

  • ui/js/app.js
    • refreshDashboard() is now the single writer of Live.dashboard (the C2131 discipline: a shared slot may have exactly one writer).
    • loadDashboard() and loadWallet() each await refreshDashboard(); — a view loads what it renders.
  • src/state_gate.rs — new invariant every_view_branch_loads_each_slot_its_renderer_reads: for every Live slot S and every renderView branch B, if anyone in B's render closure (transitive calls of render…) reads Live.S, then someone in B's loader closure (transitive calls of load… ∪ the session-level loadSession closure) must write Live.S. Crediting loadSession covers the session-level slots (models, publicUrl) with zero exemption list.
    • the_slot_closure_scanners_have_teeth proves the discriminators on synthetic input: transitive closure across functions, identifier boundaries (Live.dashboardTrend is not Live.dashboard), // and /* */ comments must not create phantom reads, a loader that only writes its own slot must be red, and an indirect write through a helper must be green.
  • ui/README.md — a new convention section ("渲染谁就装载谁"), including why a refresh does not reproduce it.
  • ui/index.htmlapp.js cache-bust bumped to 20260915-8.

A/B evidence (in-place mutation, md5-verified restore):

tree cargo test gate jsdom probe
pre-change (91a2da1) RED, exactly one failure: renderWallet() reads Live.dashboard … branch wallet red on both defect legs
competing fix "prettier empty state" RED red (X1/X3/X4; the "no empty-state copy" check passes by construction)
competing fix "reuse Live.wallet's month_use/month_earn" RED red (all four)
this branch green (8/8 gate tests) 53/53 expectations met, exit 0

The instrument (jsdom, real index.html + the four real scripts, only fetch stubbed, driving the real login form / real logout button / real navigation) declares each leg's expectation, so "the defect is visible as intended" and "the fix did not work" cannot be confused.

Tests

  • cargo test 全部通过 (277 passed; state_gate 8/8)
  • cargo fmt --check 通过
  • 新增/更新了单元测试 (two new tests in src/state_gate.rs)

Checklist

  • 分支命名符合约定 (fix/)
  • Commit message 使用 Conventional Commits 格式
  • 单一职责,改动最小化

…iter

`#month-changes` (wallet view) and `#dash-month-changes` (dashboard) are painted by the
same `renderMonthChanges()`, which reads `Live.dashboard`. Only the dashboard's loader
wrote that slot, while the wallet branch of `renderView` called `loadWallet()`
(refreshes `Live.wallet` only). So when the session is established *on* the wallet view
(hash `#/wallet` then login; or logout on the wallet page and log back in), nobody ever
filled the slot: the block rendered net `0` plus "No changes this month" for good, while
the very same `/api/dashboard` payload rendered correctly on the dashboard.

- `refreshDashboard()` is now the slot's single writer (C2131 discipline); `loadDashboard()`
  and `loadWallet()` each call it.
- New invariant in `src/state_gate.rs`: every `renderView` branch's loader closure
  (`load…` transitive calls ∪ the session-level `loadSession` closure) must write every
  slot its render closure (`render…` transitive calls) reads. The pre-change tree fails
  it on the wallet branch only, and a display-only "fix" keeps it red.
@argszero
argszero merged commit f83df40 into main Sep 15, 2026
1 check passed
@argszero
argszero deleted the fix/wallet-month-changes-slot branch September 15, 2026 00:13
@argszero argszero mentioned this pull request Sep 15, 2026
10 tasks
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant