Skip to content

fix(ui): let the login submit button restore its own label - #293

Merged
argszero merged 1 commit into
mainfrom
fix/login-submit-restores-its-own-label
Sep 24, 2026
Merged

argszero merged 1 commit into
mainfrom
fix/login-submit-restores-its-own-label

Conversation

@argszero

Copy link
Copy Markdown
Owner

What this is

ui/index.html ships the login submit button as

<button type="submit" … data-i18n="login.enter">进入平台</button>

zh "进入平台" / en "Enter", filled by applyStatic() (innerHTML = t(key),
called from setLang() and DOMContentLoaded).

The #login-form submit handler rewrote that element's label twice: to
T("login.logging") while the request was in flight, and — in its finally —
to T("login.submit") (zh "登 录" / en "Sign in"). login.submit had
exactly one consumer in the whole repository: that restore line.

So after any submit attempt — a wrong password is enough — the button says
something other than what it ships with, and the login screen has no language
switcher (that control lives in the settings view), so the element never heals
within the session. The screen shows the inline 401 error right next to the
freshly mis-labelled button.

Provenance: drift, not a tradeoff

8bd1063 (the P2-A login wiring) introduced both strings in one commit: the
markup always said "进入平台", while the handler hand-wrote
btn.textContent = "登 录". 68f9f70 (the i18n packs) faithfully turned each
literal into its own key, freezing the split into two keys. The design prototype
draws that button with "进入平台".

The repository's own convention is a capture: withLoading() reads
const orig = btn.innerHTML and writes it back, the forgot-password handler
reads const orig = btn.textContent, and three more sites do the same. The login
restore was the only one naming a key that is not the element's own.

The fix

const btn = e.target.querySelector('button[type="submit"]');
const origLabel = btn ? btn.textContent : "";
…
} finally {
  if (btn) { btn.disabled = false; btn.textContent = origLabel; }
}

The busy write is untouched, and login.submit is deleted from both packs
(net −1 key per pack; login.enter keeps its markup consumer).

The gate

state_gate::the_control_that_rests_owns_its_label — three rules, each derived
from the artifacts under test; no key name is written into the gate (the
button's own key is read out of ui/index.html):

  1. axis — the last label write to a markup-declared control must derive from
    that control's own source: a capture read of it, or T(k) where k is the
    key the markup declares;
  2. class — every form whose submit button carries data-i18n (five today)
    must resolve to exactly one submit handler and obey (1); "cannot be
    resolved" is a loud failure, not a pass;
  3. reverse — a member that writes its label must write it at least twice
    (busy + rest), so "delete the busy state" cannot pass.

Evidence

Instruments are run against the tree in this PR; base legs are fed the pre-fix
tree (git show HEAD:ui/js/app.js).

  • Rust gate — the_control_that_rests_owns_its_label is red on the
    pre-fix ui/js/app.js and green here. The red reading names the defect:

    r1=false r2=false r3=true | members=5 axis={"login-form"} thin={} unresolved={}
    foreign={"login-form": "FOREIGN: T(\"login.submit\")"}
    

    the_r169_rules_have_teeth builds four synthetic mutants on the live tree
    (every anchor asserted unique), each flipping exactly the rule it targets:
    a foreign restore → R1 + R2; deleting the busy write → R3; restoring with the
    element's own key → all green (the other accepted shape, fix_key); an
    unresolvable sibling binding → R2 only. the_r169_roster_is_real is the
    positive control: five forms derived from the markup, the login button's own
    key read as login.enter, all five handler bodies sliced correctly (each ends
    on its closing brace and contains preventDefault).

  • jsdom probe (r169_probe.js; real index.html + the four real scripts, no
    token, a real submit event, once per language):
    ---- 20 legs, 0 misdeclared ---- RESULT: ALL LEGS AS DECLARED on the landed
    bytes; on the pre-fix tree exactly B1 / B2 / D1 are red in both packs.
    Its C4 leg samples before the first await and still sees
    disabled=true label="登录中…" / "Signing in…", so the busy state is proven
    to have happened rather than assumed.

  • cargo test: 376 passed / 0 failed (373 before — the three new gate
    tests). cargo fmt --check clean; cargo clippy --all-targets rc=0.

  • src/i18n_pack.rs positive controls are re-read from the gates' own output,
    not recomputed: ZH_KEY_COUNT/EN_KEY_COUNT 792 → 791,
    T_LITERAL_COUNT 545 → 544, T_LITERAL_DISTINCT 434 → 433 (both
    STATIC_ATTR_* are unchanged — login.submit was never a data-i18n
    attribute).

Scope (stated, not implied)

The gate is lexical: it proves that the resting expression and the markup key
are the same source; it does not prove the label rendered at that moment
(that half belongs to the jsdom probe). Handles are recognised by the
repository's dominant spelling querySelector('button[type="submit"]') (login,
register, verify, share today); #forgot-form uses
querySelector("button[type=submit]") and is therefore not counted as a
label-writing member — recorded in ui/README.md rather than papered over.
Relabelling the markup to login.submit (self-consistent and gate-green) is
rejected by the probe's D2 leg against the design prototype.

Related Issue

None. No issue in this repository is open (all are closed); this defect comes
from this task's own recon. The needs:issue bot check therefore cannot pass —
by design, not by omission.

Checklist

  • branch name follows the convention (fix/…)
  • commit message uses Conventional Commits
  • single responsibility, minimal diff

`ui/index.html` ships the login submit button as `data-i18n="login.enter"`
(zh "进入平台" / en "Enter"), but the `#login-form` submit handler restored it to
`T("login.submit")` (zh "登 录" / en "Sign in") in its `finally` — a *foreign*
key. After any submit attempt (a wrong password is enough) the button says a
different thing, and the login screen has no language switcher, so it never
heals within the session.

The fix captures the label before the busy write and restores the captured
value — the repository's own idiom (`withLoading` reads `orig = btn.innerHTML`,
the forgot-password handler reads `const orig = btn.textContent`, plus three
more capture sites). `login.submit` had exactly one consumer in the whole repo
(that restore line) and is deleted from both packs; `login.enter` keeps its
markup consumer.

Gate: `state_gate::the_control_that_rests_owns_its_label`, three rules, all
derived from the artifacts under test (no key name is written into the gate):
(1) the last label write to a markup-declared control must derive from that
control's own source — a capture read of it, or `T(k)` with `k` equal to the
key `ui/index.html` declares; (2) every form whose submit button carries
`data-i18n` must resolve to exactly one submit handler and obey (1); (3) a
member that writes its label must write it at least twice (busy + rest), so
"delete the busy state" cannot pass. The axis test is red on the pre-fix tree
(`FOREIGN: T("login.submit")`) and green after; each rule has its own synthetic
mutant in `the_r169_rules_have_teeth`.

Tests: `cargo test` 376 passed / 0 failed (373 before). The jsdom probe
`r169_probe.js` reports 20/20 legs as declared on the landed bytes (its `C4`
leg samples before the first `await`, so the busy swap is still proven at
runtime).
@argszero
argszero merged commit 25b9967 into main Sep 24, 2026
1 check passed
@argszero
argszero deleted the fix/login-submit-restores-its-own-label branch September 24, 2026 15:29
@argszero argszero mentioned this pull request Sep 24, 2026
10 tasks
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