Skip to content

fix(ui): let the ops key rows be named the way their own card names them - #298

Merged
argszero merged 1 commit into
mainfrom
fix/ops-key-count-names-the-keys-it-counts
Sep 25, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/ops-key-count-names-the-keys-it-counts

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The ops runtime card renders one mini-row per provider with
T("ops.keys.count", { total, on }). total is the backend's unfiltered COUNT(*) over the
keys table — every status: on, paused, and off (soft-deleted). The stat card directly
above it counts WHERE status = 'on', and that is what the product means by "listed": the sharing
page binds its 「上架中」/ "Listed" pill to status === 'on', and a count that includes non-on
rows is called 「历史」/ "history" there.

So the row read 6 listed keys · 5 enabled (zh: 6 个上架 key · 5 个启用) while the card above it
read Active keys: 5 — one word, one viewport, two numbers.

The card already names these rows "upstream key" in its own title (「上游 key 状态」/
"Upstream key status") and in its empty state (「尚未上架任何上游 key」/ "No upstream keys
listed yet"); only the count line and the subtitle drifted. The provenance is drift, not a
trade-off: the three pack keys were authored in 09e4121 (the settings/admin/ops redesign), which
renamed the prototype's wording — the prototype writes k.n + " 个上游 key" — while the data
source (COUNT(*), 85982e8) was untouched.

The fix is the word, not the number. total must keep counting every status: the row's own
pill is off === 0 ? "All enabled" : (off >= total ? "All disabled" : "{n} disabled"), so a
total that only counted on would make 「全部停用」 structurally unreachable — deleting the
promise instead of keeping it. The subtitle names the same number, so it moves with the count
line; the static data-i18n fallback and the i18n.js cache-bust token move with the pack.

Zero new i18n keys (the pack key set is byte-identical in count), zero backend change.

Related Issue

No linked issue — the repository has no open issues; this is a self-contained wording/correctness
fix found by scanning the ops view against its own data source (same shape as the previous
UI-contract fixes in this series).

Changes

  • ui/js/i18n.js — ops.keys.count and ops.keys.sub reworded in both packs (上架 key →
    上游 key, listed keys → upstream keys), plus a comment on the key stating why the count
    must keep every status
  • src/state_gate.rs — new gate the_ops_key_rows_are_named_the_way_their_own_card_names_them
    (noun derived from ops.keys.title — no wording written into the gate) with three
    companion tests: single-rule mutation legs, a scanner self-proof, and a comment-stripping
    control
  • ui/index.html — the data-i18n="ops.keys.sub" static fallback kept word-for-word in step
    with the pack, and the i18n.js cache-bust token moved
  • ui/README.md — new section: the contract, the gate's scope, and one leg declared blind
  • No configuration or data-structure change

Tests

  • cargo test — 387 passed / 0 failed (baseline 31bbff6 was 384; +3 tests)
  • cargo fmt --check — clean
  • cargo clippy --all-targets -- -D warnings — clean on both A/B legs
  • New tests added: the gate's rules and its two companion teeth tests, each mutation leg
    flipping exactly the rule it targets

Instrumentation (every instrument declares each leg's expectation up front; declared-vs-actual
and PASS/FAIL are printed as two separate facts, and a mismatch exits non-zero):

  1. jsdom probe — the real ui/index.html + the four real scripts, fetch stubbed and logged,
    session role ops, driving the real renderOps(). The fixture manufactures the divergence,
    because it is not on screen in dev today: key_health = {aliyun-bailian: total 6 / on 5 / off 1} and {deepseek: 4 / 0 / 4}, against active_keys: 5. For the row with off = 1 it
    reads the rendered count line, the rendered subtitle, the rendered pill and the stat card's
    value — and it derives the card's noun from the rendered title.
    7 legs × 2 languages: P0/P1 controls, D1 (the row's number is not the on-only number),
    A1 (count line uses the card's own noun), A2 (subtitle does), A3 (the printed total is
    on + off), R1 (reverse: the stat card's caption does not adopt the row noun).
    Six trees, 14/14 as declared each: landed (the shipped bytes) green; base red with
    exactly {A1, A2}; fix — a conforming but differently worded implementation — green
    (so the legs test the contract, not one paraphrase); m_one_line (fix the count line, leave
    the subtitle) red with exactly {A2}; m_total_only_on (write the number instead of the word)
    red with exactly {D1, A1, A2, A3}; m_card_widen (make the stat card's caption say
    "upstream key" too) red with exactly {R1}. Combinations outside that declared matrix are
    refused (rc 2) rather than judged.
  2. Compile gate, A/B on two trees — the shipped src/state_gate.rs on the pre-fix bytes
    (git archive 31bbff6) fails the axis test with the declared reading
    r2=false r3=false r4=true r5=true (the axis rules are false, both preconditions still hold —
    not a vacuous red); on this branch's tree it is green. clippy -D warnings rc 0 on both legs.
    (The legs share a target dir for the dependencies; that bit me once — see the note below.)
  3. Edit-sheet verifier — 26/26: each of the eight edits occurs exactly once in the pre-fix
    bytes and its replacement zero times; applying the sheet to those bytes reproduces this branch's
    ui/js/i18n.js and ui/index.html byte for byte (md5); the probe's anchors are read out of
    the probe file and equal the sheet's; the key set is unchanged (no new keys); the count line
    keeps both placeholders; the stat card's caption is untouched; the static fallback equals the
    pack value; the cache-bust token really moved; the tests the README names all exist.

One instrument bug worth stating: the first A/B run shared one CARGO_TARGET_DIR between the
two source roots, and cargo served the first leg's test binary to the second one — the crate
embeds the pack through include_str!. The run therefore reported the pre-fix reading on the fixed
tree: an A/B that would have "confirmed" the fix while measuring one tree twice. The legs now touch
the crate root before building, and the script asserts that the two legs' readings differ.

Scope, stated honestly: the gate is lexical — it proves the row count's noun and the card's
own title come from the same source. It does not prove what is on screen at any moment (that is
the jsdom probe's half), and it does not constrain the total algorithm (that is fixed by the
pill's structural requirement). One leg is declared blind and written into ui/README.md:
renaming ops.keys.title as well (so the card and the count agree on a wording that is wrong for
the data
— e.g. calling every-status rows "listed") keeps the gate green; what refuses it is the
design baseline and the probe's R1 leg, not this rule.

Checklist

  • Branch name follows the convention (fix/…)
  • Commit message uses Conventional Commits
  • Single responsibility, minimal change

The ops runtime card renders one line per provider with
`T("ops.keys.count", { total, on })`, and `total` is the backend's unfiltered
`COUNT(*)` over the `keys` table — every status, including `paused` and `off`
(soft-deleted). The stat card directly above it counts `WHERE status = 'on'`,
and that is what the product means by "listed": the sharing page binds its
"Listed" pill to `status === 'on'`, and a count that includes non-`on` rows is
called "history" there. So the row said "6 listed keys · 5 enabled" next to a
card saying "Active keys: 5" — one word, one viewport, two numbers.

The card already names these rows "upstream key" in its own title ("Upstream key
status") and in its empty state ("No upstream keys listed yet"); only the count
line and the subtitle drifted. Provenance is drift, not a trade-off: the three
pack keys were authored in 09e4121, which renamed the prototype's wording, while
the data source from `85982e8` was untouched.

Fix the WORD, not the number. `total` must keep counting every status —
otherwise the row's own pill (`off >= total` -> "All disabled") becomes
structurally always green, which deletes the promise instead of keeping it. No
new i18n keys (the key set is unchanged), no backend change; the subtitle, which
names the same number, moves with the count line, and the static `data-i18n`
fallback and the `i18n.js` cache-bust token move with the pack.

Gate `state_gate::the_ops_key_rows_are_named_the_way_their_own_card_names_them`
derives the noun from `ops.keys.title` (no wording is written into the gate, so
renaming the card does not need a gate edit), and `ui/README.md` states the
contract, the gate's scope and one leg declared blind to it.

Co-authored-by: argszero <argszero@argszerodeMac-mini.local>
@argszero
argszero merged commit 8f8fcdc into main Sep 25, 2026
1 check passed
@argszero
argszero deleted the fix/ops-key-count-names-the-keys-it-counts branch September 25, 2026 03:50
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