fix(ui): advertise only shortcut digits that work in the sidebar - #257
Merged
Merged
Conversation
The sidebar prints a keycap digit and a "Shortcut N" tooltip on every nav item, and one global number-key handler resolves the digit. Both sides must read the same array: const NAV_ORDER = NAV.flatMap((g) => g.items); const short = NAV_ORDER.indexOf(item) + 1; // the keycap const item = NAV_ORDER[Number(e.key) - 1]; // the handler renderNav()'s guest branch hand-built an equivalent-looking literal that is not a member of NAV_ORDER, so indexOf(item) was -1 and the guest sidebar advertised keycap 0 with the tooltip "Shortcut 0 . Marketplace" -- while 0 resolves to NAV_ORDER[-1], i.e. nothing at all. The digit that does open the marketplace is 2, which the guest is never told. The shape dates from #44 (v1.17 D keyboard accessibility), which added the keycap machinery without noticing that the guest item is not a registry member; it survived #86 (i18n) and the recent nav work. ui/README.md documents the opposite (the badge is NAV_ORDER's index + 1, and the digits are 1..N of that same array), so this is drift, not a trade-off. Fix: the guest group draws from the registry it shares with the handler -- NAV_ORDER.filter((it) => GUEST_VIEWS.includes(it.id)). Gate: state_gate::the_sidebar_advertises_only_digits_that_work -- four rules with independent teeth: (1) renderNav renders no hand-built nav-item literal; (2) the keycap is derived once and never branches on the session; (3) exactly one line indexes the registry and it selects with the pressed digit; (4) the registry is derived from NAV, not a second hand-written list. A companion test pins the scanners on synthetic input, comments included. Also recalibrates the i18n positive controls (T_LITERAL_COUNT 543 -> 542, T_LITERAL_DISTINCT 434 -> 433): removing that one T("nav.marketplace") call site is the only change to the T() literal population.
Owner
Author
|
Self-review (Committer, Verified on the branch tip
|
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.
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
The sidebar advertises a keyboard shortcut digit on every nav item (a
.nav-keybadge plus atitlereading "Shortcut N · "), and one globalkeydownhandler resolves that digit. Both sides are halves of a single contract and must read the same array:renderNav()'s guest branch hand-built an equivalent-looking literal instead of drawing from the registry. That object is not a member ofNAV_ORDER, soindexOf(item)was -1:0, andNAV_ORDER[-1]isundefined— a dead key;2— a number the guest is never told.Reproduction (no account needed): open the instance, click "browse as guest", and press
0/2while watching the single nav item.Fingerprint of drift rather than design:
ui/README.mddocuments the opposite — "数字 1-8 → 切换侧边栏视图(键位 =NAV_ORDER下标 +1…)", and the handler's own comment states the invariant ("角标是NAV_ORDER下标 +1,这里用同一数组取项,视图增删后两者自动保持一致"). The shape dates from #44 (v1.17 D keyboard accessibility), which added the keycap machinery without noticing that the guest item predates it and belongs to no registry; it survived #86 (i18n) and all the recent nav work.Changes
ui/js/app.js: the guest group draws from the registry it shares with the handler —items: NAV_ORDER.filter((it) => GUEST_VIEWS.includes(it.id)). The guest keycap becomes2and the tooltip agrees; the live and role paths are untouched (they already drew fromNAV/NAV_ORDER). Label rendering is unchanged (T(item.label)on the registry's key).src/state_gate.rs: new invariantthe_sidebar_advertises_only_digits_that_work, four rules with independent teeth:renderNavrenders no hand-built nav-item literal (id: "…"inside its body) — the sidebar may only render registry members;isGuest ? 2 : NAV_ORDER.indexOf(item) + 1, where the session picks a number nobody is responsible for making work);NAV_ORDER[…]) and it selects with the pressed digit (Number() — the two halves name the same array;NAV(NAV.flatMap(…)), not a second hand-written list.ui/README.md: the "键位只有一个真源" rule, including the guest branch, and the reason the static gate owns the shape while the probe owns the values.ui/index.html:app.js?v=20260915-11→-12.src/i18n_pack.rs: recalibrated positive controlsT_LITERAL_COUNT543 → 542 andT_LITERAL_DISTINCT434 → 433 — removing that one guestT("nav.marketplace")call site is the only change to theT()literal population (the key itself is still reachable dynamically throughT(item.label); both packs keep their 809 keys).//lines, multi-line/* … */bodies and inline pairs) — the new comment and the C2141 doc section both mentionNAV_ORDER[and the old literal, so a naivecontainswould red-flag its own documentation (ledger pitfall #296/#309).Related Issue
No issue exists for this; found during Recon. No linked issue is expected.
Tests
cargo test— 285 passed (283 before; +2 gate tests).cargo fmt --check— clean.cargo clippy --all-targets— only the pre-existingcollapsible_matchatsrc/protocol.rs:662.jsdom probe (real
index.html+ the four real scripts,fetchstubbed and logged, real "browse as guest" button, real nav clicks and realkeydownevents; the "working digit" is measured on the same apparatus by pressing 0–9 and watching for a re-render, never copied):afc9c6fadvertised="0", no switch)advertised="2")keycap="0", measured["2"])"Shortcut 0 · Marketplace", measured2)role=user): 6 items, every keycap opens its own view, keycaps are the registry positions 1-5,8role=admin): the role item has a title but no badge, and the digit in that title opens the view9,0) switch nothing — i.e. A1 fails for the right reasonRejected competitor fixes (probe, both red;
--basepins the expectations to the explicit baseline):m_visiblegroups.flatMap(g => g.items).indexOf(item) + 1){A1,A2,A3,B1,B2}1is still dead (1is the dashboard ⇒ lock toast), and it breaks the live path:settingskeycap becomes6while the handler still indexesNAV_ORDER(8)m_nokey{A1,A2,A3}Shortcut 0(A3), so the sidebar still lies — only about a shorter distanceGate A/B (
ui/js/app.jsmutated in place, restored byte-for-byte, md5 checked before/after every leg):const short = isGuest ? 2 : NAV_ORDER.indexOf(item) + 1;const NAV_ORDER = [...NAV[0].items, ...NAV[1].items];m_visible(badge from visible order)Restored
ui/js/app.jsmd5 == pristine md5 after every leg. Both instruments agree on the fix and reject both competitors; the gate is deliberately the stricter of the two on shape (rule 1), while the probe owns the values.Checklist
fix/<描述>)fix(ui): …)src/i18n_pack.rs仅两个阳性对照常量重新校准)