Skip to content

fix(ui): render and load a view only when it is the destination - #254

Merged
argszero merged 1 commit into
mainfrom
fix/boot-loads-before-session
Sep 15, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/boot-loads-before-session

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

ui/js/app.jsDOMContentLoaded 处理器无条件调用 renderView("dashboard"),而 renderView 是「渲染 + 装载」(登录时还会异步拉取),且它跑在 restoreSession() 之前。带 token 时 loggedIn() 此刻已为 true ⇒ 会话还不存在,仪表盘那一整套查询就已经发了出去。

Related Issue

(无关联 issue:本改动由一支 jsdom 仪器实测驱动,证据见下)

Changes

  • ui/js/app.js:boot 只搭外壳(renderNav() / bindEvents() / 余额占位),不渲染也不装载任何视图 —— 视图一律由 switchView当前目的地渲染装载(登录后 enterApp()、游客 enterGuest() 已各自触发)。
  • src/state_gate.rs:新增静态门禁 the_boot_handler_touches_no_view(+提取器自证 the_boot_body_extractor_stops_at_the_right_place)。
  • 同步更正一条已被实测证伪的旧注释:C2135 曾断言「Live.dashboardTrend 不在 Live 字面量里 ⇒ resetSessionCaches() 的派生名册清不到它」。实测(写后登出)该槽确实为 null —— Live.x = v新建 own enumerable 属性,调用时的 Object.keys(Live) 就包含它。src/state_gate.rs 与本文件相关段落一并更正。
  • ui/README.md 新增约定段落;ui/index.html cache-bust app.js?v=20260915-9

实测(jsdom 启真 ui/index.html + 四个真脚本,只 stub 并记账 fetch

改前f83df40 的客户端):

场景 读数
A 带 token 刷新在 / 14 个请求;仪表盘那套各 2 次/api/dashboard、趋势 type=all&bucket=daypage=1&page_size=1/api/sharings),/api/wallet 3 次
A2 同上 log[0] = GET /api/wallet —— 视图数据排在会话请求 /api/me(第 2 位)之前
B 带 token 刷新在 #/transactions 目的地不是仪表盘,仍白拉仪表盘那套 4 次(+交易页自己那条)
C 无 token 刷新(阴性对照) 0 请求
E 过期 token 刷新 6 个请求 ⇒ __atpLogout() 被调用 6 次TOAST_MAX = 3 ⇒ 用户看到 3 条一模一样的「登录已过期,请重新登录」);对照腿 E2:仍正确落在登录页

改后:A 各 1 次、log[0] = GET /api/me、B 目的地之外的请求 0、E 通知 1 次、C 仍 0;控制腿(目的地照常装载、登出/401 行为)全绿。

为什么这条要由静态门禁钉

三种「最小改法」在仪器上都能全绿:① boot 里 renderDashboard()(只渲染不装载);② if (!api.getToken()) renderView("dashboard");③(改前)原样。它们只是症状消失 —— 屏幕上看不出来,请求日志也干净。门禁钉的是形状

  1. boot 处理器体内不得调用视图层的「渲染器 / 装载器」(视图层名册派生自 renderView 自己的分支,不写第二份名册);
  2. renderView(...) 只许以当前目的地为实参(全仓唯一合法的一处是语言切换监听器里的 renderView(activeView));
  3. switchView 必须仍调用 renderView —— 防止矫枉过正:删掉 boot 那句之后顺手清空 renderView 的调用点,就得到一个什么都不渲染的空壳。

Tests

  • cargo test 全部通过(277 → 279 passed
  • cargo fmt --check 通过
  • cargo clippy 无新增告警(仅既有 src/protocol.rs:662 假阳性)
  • 新增/更新了单元测试:门禁 2 条(规则 + 提取器自证,含合成输入对照)
  • A/B(原地变异 + 逐字节还原核对 md5):改前树红;两条竞争修法腿(renderonly / notoken由仪器接受、由门禁拒绝;另两条反例(switchView 硬编码视图名 / 删掉 renderView 调用)分别让规则 ②/③ 变红 —— 三条规则各有各的牙。

Checklist

  • 分支命名符合约定(fix/…
  • Commit message 使用 Conventional Commits 格式
  • 单一职责,改动最小化(4 文件,+268/−6)

The DOMContentLoaded handler called renderView("dashboard") unconditionally,
before the session was restored. renderView is "render + load", and with a
token stored loggedIn() is already true at that moment, so the dashboard's
whole query set was fired while the session did not exist yet: the responses
were then discarded by loadSession()'s resetSessionCaches() and requested
again by enterApp() -> switchView(destination).

A jsdom instrument (real index.html + the four real scripts, only fetch
stubbed and logged) measured the current tree: one token boot issues 14
requests with the dashboard set fetched twice each (/api/dashboard, the
type=all&bucket=day trend, page=1&page_size=1, /api/sharings) and
/api/wallet three times, and log[0] is GET /api/wallet while the session
request /api/me is only second. When the destination is another view
(#/transactions) the dashboard set is still fetched five times. With an
expired token every one of those six pre-session requests answers 401, so
__atpLogout() runs six times and the user sees three identical
"Session expired, please sign in again" toasts.

Boot now only builds the shell (nav, events, balance placeholder); every
view is rendered and loaded by switchView for the current destination,
which enterApp() and enterGuest() already drive.

The static gate src/state_gate.rs::the_boot_handler_touches_no_view pins
the shape: the boot body may not call any view renderer/loader (the view
layer is derived from renderView's own branches), renderView(...) may only
take the current destination, and switchView must still call renderView so
that removing the boot line cannot silently empty the app.
@argszero
argszero merged commit 6be548a into main Sep 15, 2026
1 check passed
@argszero
argszero deleted the fix/boot-loads-before-session branch September 15, 2026 00:33
@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