Skip to content

fix(ui): stop reading a key's status count as a health verdict - #269

Merged
argszero merged 1 commit into
mainfrom
fix/ops-key-health-states
Sep 21, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/ops-key-health-states

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

The ops view's upstream-key card renders enable/disable counts as health verdicts. Its three states read
Healthy, {n} failing and All failing — and all-off is painted pill-danger (red).

The data behind it has no health information at all: keys has no health/error column and /api/ops/runtime
returns only total / on / off (off = total − on, computed server-side). So an ordinary user action —
pausing or re-listing their own key — turns the operator's card red and "all failing" for that provider.

The card's own subtitle and count line already name it correctly ("aggregated by provider · enable status and
total listings", "{total} keys listed · {on} enabled"), so this is drift, not a design choice: the verdict
vocabulary was written before the counters it sits next to.

This change makes the pill say what the data says — the state it counts — and drops the failure colour, because
red is the vocabulary of faults and the data cannot support a fault.

Related Issue

Changes

  • ui/js/i18n.js (both packs) — the three verdict keys are renamed and reworded, so the key names can no
    longer lie either (a gate can only pin a name that means what it says):

    before after (zh / en)
    ops.keys.healthy = 健康 / Healthy ops.keys.allOn = 全部启用 / All enabled
    ops.keys.abnormal = {n} 个异常 / {n} failing ops.keys.someOff = {n} 个停用 / {n} disabled
    ops.keys.failed = 全部失败 / All failing ops.keys.allOff = 全部停用 / All disabled

    Title ops.keys.title: 上游 key 健康 → 上游 key 状态 / Upstream key health → Upstream key status.
    "启用" is not a new word — the untouched count line already uses it.

  • ui/js/app.js — the provider row renders the new keys; all-off now uses the neutral pill-muted instead of
    pill-danger. off === 0 keeps pill-ok, partial stays pill-warn. The comment above the block is
    corrected too (it described a "health / N abnormal / all failed" three-state that the data does not have).

  • ui/index.html — the static fallback text of the card title is synced with the pack (the two are compared by
    an existing gate); app.js / i18n.js cache-bust bumped.

  • src/i18n_pack.rs — new gate the_ops_key_health_pill_names_the_state_it_counts, three rules, each with its
    own tooth:

    1. Forbidden vocabulary is matched on key names, not on text: no pack key may match
      ops\.keys\.(healthy|abnormal|failed|fail|error|down|unhealthy). Rewording a value while keeping a
      misleading key name still fails.
    2. Consumer key set ⊆ registered state-key set: the ops.keys.* keys appearing in the key_health
      consumer block must be a subset of {allOn, someOff, allOff, count, empty}.
    3. Reverse direction (blocks the "just delete the pill" escape): all three state keys must actually be
      rendered at least once.
      Plus a negative control on synthetic input, and the measured scope in ui/README.md.

No config / data-structure changes. No backend change — ops.rs reports the facts correctly; only the client's
wording was wrong.

Deliberate behaviour notes (recorded, not hidden):

  • Colour choice. All-off could also be pill-warn ("this provider has no usable key"). pill-muted is used
    because red/warning is the vocabulary of faults and the data supports neither; if observability is wanted it
    should come from the real health signal, not from painting "disabled" red. Either choice must avoid
    pill-danger. pill-muted already exists in style.css.
  • Not in this PR: feeding the real health signal (the in-memory KeyRouter cooling set) into
    /api/ops/runtime — that is a product/interface extension (new field, provider mapping, semantics for an
    in-memory set that resets on restart) and a separate change.
  • The gate is lexical: it proves the key names and the key set, not that the rendered sentence matches the
    data. Stated in ui/README.md.

Tests

  • cargo test passes — expected 299 passed / 0 failed (297 after the previous change; this
    change adds two tests: the gate plus its negative control). The exact number is taken from
    the run, not assumed.
  • cargo fmt --check passes
  • New unit tests added (2 — the gate, plus a synthetic-input control proving both scanners can fail)
  • Gate verified before landing, against a mirror of the real tree (compile + run the real
    src/i18n_pack.rs test binary, one arm per rule): on the unfixed tree the new gate is the
    only failure and it opens on rule 1 (24 passed / 1 failed); with the edit set applied the whole
    module is green (25 passed / 0 failed); each rule also has its own isolating arm — consumer-only
    (rule 1), an unregistered key (rule 2), and a deleted pill (rule 3) — and each opens exactly that rule
  • Invariants after the edit (measured before → after): the three verdict keys 1 → 0 in app.js; the three
    new keys 0 → 1; pill-danger occurrences in app.js 3 → 2 (the two remaining are legitimate users:
    PILL_CLS and the department-quota card); ops.keys.count untouched; both packs gain/lose 3 keys each
    (net 0, pack key total unchanged at 811)
  • DOM probe (jsdom, real index.html + four real scripts, ops session, fetch stubbed and accounted):
    the all-off row never shows "failing" in either language; the zero-off row still shows a positive
    "All enabled"; the all-off pill is not pill-danger; and a mechanism leg drives the real control —
    pausing a key on the sharing page then returning to ops must not turn the provider row into a red fault

Verified locally with the exact CI invocation:
cargo fmt --check (exit 0), cargo clippy --all-targets -- -D warnings (exit 0), cargo test (see above).

Checklist

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

The ops view's upstream-key card rendered enable/disable counts as health
verdicts: `Healthy` / `{n} failing` / `All failing`, with all-off painted
`pill-danger`. The data behind it has no health information at all — `keys`
has no health/error column and `/api/ops/runtime` reports only `total` / `on`
/ `off` — so a user pausing their own key showed the operator a red
"all failing" alarm.

Rename and reword the three verdict keys in both packs (the key names could
lie, so the gate could not pin them), drop `pill-danger` for all-off, and add
`i18n_pack::the_ops_key_health_pill_names_the_state_it_counts` (three rules,
each with its own tooth) plus its synthetic-input control. Measured scope in
`ui/README.md`.
@argszero

Copy link
Copy Markdown
Owner Author

Self-review (Committer; allow_self_merge: true).

Verified locally, at the commit being merged (0be288a):

  • cargo test302 passed / 0 failed (baseline main = 300; +2 = the gate and its synthetic-input control).
  • cargo fmt --check — clean. cargo clippy --all-targets -- -D warnings — clean (exactly CI's invocation).
  • PR CI 35589670722 — green.

Instrument readings taken against this exact tree:

  • Gate A/B (c2158_gate_ab.py, compiles and runs the real src/i18n_pack.rs test binary from a mirror): ALL ARMS AS DECLARED
    base RED (opens on rule 1, "判定词"), fix GREEN, and each isolating arm opens exactly the rule it targets
    (m_consumer_only → rule 1, m_unregistered → rule 2, m_no_pill → rule 3, m_text_only → rule 1).
    The harness also asserts working tree == pristine + edit sheet: YES before running.
  • jsdom DOM probe (c2158_probe.js, real index.html + four real scripts, real controls, real PATCH):
    base arm 6 axis legs red / 4 controls green; fix arm 10/10 green. Leg C1 is the mechanism leg —
    it drives the real pause control on the sharing page, issues the real PATCH /api/sharings/1, returns to ops,
    and asserts the provider row is not a red fault.

Complementarity measured, not asserted: the competitor fix m_text_only (right wording, key names still claim
health) is accepted by the probe (the on-screen sentence is correct) and rejected by the gate (the key name
still lies). The gate is lexical and says so in ui/README.md; the sentence-vs-data half is the probe's.

Scope notes: no backend change (ops.rs reports the facts correctly — only the client's wording was wrong);
Related Issue is intentionally empty (this is a finding, not a tracked issue); the two pill-danger users that
remain are the PILL_CLS table and the department-quota card, both legitimate and asserted by the invariant check.

@argszero
argszero merged commit e5ee178 into main Sep 21, 2026
1 check passed
@argszero
argszero deleted the fix/ops-key-health-states branch September 21, 2026 10:40
@argszero argszero mentioned this pull request Sep 21, 2026
12 tasks
argszero added a commit that referenced this pull request Sep 21, 2026
Ships the 10 PRs merged since v0.7.25 (#261-#270). No schema change, no config
change, so the deployment-side config.toml needs no edit.

One fact, one source / display must equal what it consumes (frontend, 6 places)
- #261 read the spendable half of the wallet payload when refreshing your own
  balance; #262 the transactions payload signature covers the time range, with
  one reload trigger shared by the four controls; #263 the settings controls are
  either wired or explicitly inert; #265 the re-list outcome comes from the same
  entry as its action; #268 the sharing form shows a plan's label, not its
  config id; #269 the ops card stops reading a key's status count as a health
  verdict.

i18n reachability
- #266 every pack key must reach a consumer (the gate), and #270 drops the 23
  keys that gate proved unreachable: ZH/EN key count 811 -> 788, sunset list
  59 -> 36.

Gateway
- #267 applies the body limit where axum actually reads it (per-route
  DefaultBodyLimit, 8 MiB on the three gateway routes; unauthenticated
  endpoints keep the 2 MiB default). This is the application half of rant
  2026-09-18T09:14:18. It also corrects the false v0.7.10 "raised to 70MB"
  CHANGELOG line, which described installing a layer rather than raising a limit.

- Cargo.toml / Cargo.lock: 0.7.25 -> 0.7.26.
- CHANGELOG.md: v0.7.26 entry plus the v0.7.10 correction.
- ui/index.html cache-bust left as-is: this release touches no UI file; the live
  values are app.js 20260921-2 / i18n.js 20260921-2.

cargo test 302 passed; cargo fmt --check clean; clippy -D warnings clean.
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