fix(ui): derive the pager's ellipsis guards from the window - #285
Conversation
The compact pager (pages > 9) renders `1 … p-1 p p+1 … N`, but its two ellipsis guards were one notch tighter than the window they guard (`page > 4` / `page < pages - 3`). At `page = 4` and `page = pages - 3` two adjacent page numbers were printed with no ellipsis between them, so a real page disappeared: at pages = 12, page = 4 the old code renders `1 3 4 5 … 12` — page 2 is unreachable and nothing says so. The ellipsis is a non-clickable `<span>` and the repo has no prev/next, so the pager was the only way to reach that page. Derive both guards from the loop that defines the window (half-widths A = B = 1, clamped to [2, pages - 1]): a gap exists on the left iff `page > A + 2`, on the right iff `page < pages - (B + 1)`. Add `state_gate::the_pager_window_and_its_ellipsis_guards_agree`, which reads the window half-widths out of the loop and requires the guard literals to equal `A + 2` / `B + 1`. It is derivation-based, not a snapshot: it accepts any self-consistent window and rejects a gate that pins this edit's literals. `ui/README.md` records the convention.
|
Self-review (own PR — a plain comment, not a formal review). Delta: Verified on the landed tree (
Discrimination, both directions:
Honest scope: the Rust gate is lexical — it proves the guards follow the loop's window half-widths and accepts any self-consistent window; it does not prove DOM contiguity (that is the jsdom probe's job, since there is no JS runner in Risk: minimal — the production delta in |
Summary
The transactions-table pager renders a compact window —
1 … p-1 p p+1 … N— whenever there are more than 9 pages. Its two ellipsis guards were one notch tighter than the window they guard (page > 4andpage < pages - 3), so atpage = 4and atpage = pages - 3the pager printed two adjacent page numbers with no ellipsis between them: a real page silently vanished from the control.Walking the pager with real clicks on the unfixed tree (100 rows / 10 per page ⇒ 10 pages), reading the rendered
.pagerchildren:The ellipsis is a non-clickable
<span>(user-select: none) and the repository has no prev/next control, so the pager is the only way to reach those pages: the number disappears with nothing on screen saying it still exists.Fix — derive both guards from the loop that already defines the window instead of keeping two hand-tuned constants. The window is
Math.max(2, page - 1) … Math.min(pages - 1, page + 1), i.e.page - A … page + BwithA = B = 1:1and the window startpage - 1is2⇒ a gap exists iffpage - 1 > 2⟺page > A + 2page + 1and the pinnedpagesispages - 1⇒ a gap exists iffpage + 1 < pages - 1⟺page < pages - (B + 1)Hence
page > 3/page < pages - 2. The same click-walk on the fixed tree renderspage 4: 1 … 3 4 5 … 10andpage 7: 1 … 6 7 8 … 10, with zero invariant violations.Related Issue
None — found while scanning the transactions view (same family as the server-paging change
#135/052b60c: the pager was born in the static prototype and kept its hand-tuned constants).Changes
ui/js/app.js—pagerButtons(): both ellipsis guards now follow the window boundaries (page > 3,page < pages - 2); the header comment states the derivation, so the next person to change the window width knows these two numbers are not free.The production change is two literals plus comments — no restructuring, no change to the rendered shape.
ui/README.md— a new section (交易表分页器窗口约定(R167)) records the convention: the window is defined by the loop, and the guards must be derived from its half-widths.src/state_gate.rs— new gatethe_pager_window_and_its_ellipsis_guards_agree. It is derivation-based, not a snapshot: it reads the window half-widthsA/Bout of the loop and requires the left literal to beA + 2and the right literal to beB + 1. It therefore accepts any self-consistent window and rejects a gate that merely pins this edit's literals — a test that would go green on the unfixed tree is not a test.ui/index.html— cache-bust token bumped for theapp.jschange.Tests
cargo test— 347 passed / 0 failed (baselinee1cd51b: 342;+5= the new gate plus its self-proof tests)cargo fmt --check— clean (rc=0)cargo clippy --all-targets -- -D warnings— clean (rc=0)state_gate::the_pager_window_and_its_ellipsis_guards_agreewith its teeth self-proof (each rule has a mutant leg that must fail for its own reason).Two independent instruments, both directions:
index.html+ the four real scripts, a fixture that pages like the gateway, driven by realclick()on the rendered buttons — no touching internal state). The legs are phrased as the invariant, so the unfixed tree declares failure. On a materialized pre-fix tree (git archive e1cd51b ui,app.jsmd5947efc442ee8856555823febc254efec): 5 variants × 7 legs = 35 legs, 0 not-as-declared.base— reproduces the defect exactly: A1page4=["1->3"], A2page7=["8->10"], B1 sweep2/10fix— all seven pass, sweep0/10m_showall(just print every page) → rejected by the shape leg C2 (["1:10","2:10","3:10","4:10"]);m_left_only(fix only the left guard) → rejected by A2/B1 (the two boundaries are two separate literals, so pinning one page only proves half the fix);m_mark_only(leave the sequence alone, add an explanatory tooltip) → rejected by A1/A2/B1.RESULT: ALL LEGS AS DECLARED, negative control 16/16 /failed=0,rustfmtandclippy-driver -D warningslegs on both the pre-fix and the fixed tree.Honest scope (recorded in
ui/README.md): the Rust gate is lexical — it proves the guards follow the loop's window, not that the rendered DOM is contiguous; the latter is the jsdom probe's job (there is no JS runner incargo test).Checklist
fix/…)fix(ui): …)