Repository navigation
SPA: pending state on every mutating save — spinner, live announcement, double-submit guard (#236) - #242
Merged
Conversation
added 2 commits
July 27, 2026 02:10
Every save that takes seconds — a lock wait behind a currency change (#162), a cold DB, bad rural connectivity — used to look identical to a dead button (the only visual was :disabled opacity). Three hand-rolled busy conventions and one fully unguarded form (daily-entry create-flock) are replaced by one shared mechanism: - usePendingAction: busy + isPending(scope) + run(scope, action), with the double-submit guard on a synchronous ref (state cannot stop two calls in one tick). Idempotency keys and refresh-before-rotate stay in the screens' reviewed helpers, now rebased on the hook; boolean wrappers map a skipped run to false explicitly. - BusyButton: children untouched (dynamic labels stay the caller's), centered aria-hidden spinner, single-layer label dim, and an always-mounted sibling role=status region ('Working…', i18n'd) — sibling because aria-busy defers announcements INSIDE the busy element. CSS overrides the global :disabled opacity for busy buttons; without that the spinner would render at half opacity and the busy state would look exactly like the dead button this issue removes. - All 14 mutating screens migrated (~55 triggers): per-row spinners via scoped pending (composite scopes where payload-bound), whole screen inert via busy, confirm-dialog handoff (dialog settles before I/O, the originating row control carries the pending state), carve-outs for the logo file-input and the language select, LanguageSelector's fire-and-forget PUT now guarded. - Design doc (v3) reviewed by three reviewers pre-implementation; the review-round blockers (disabled-opacity cancellation, same-tick guard, aria-busy deferral, inventory drift) are all encoded above. - Tests: +32 (hook, BusyButton, and held-promise screen tests: double submit under flight, two-verbs-one-row isolation, scope isolation on Settings, dialog-close leaves nothing busy, skip-never-closes-dialog). Coverage floors ratcheted up (88/85/79/74). - Help page: one line — a spinning button means the save is still working. Closes #236
- Row edit/correct openers are non-mutating and no longer share the dialog
save scope — the opener spun (and announced) alongside the dialog submit
during updates; openers are plain buttons disabled while busy (codex).
- Inputs whose value is embedded in a pending scope lock with the flight:
the assign-flock select (cavecrew), the add-category name input and the
adjustment lot select (codex) — editing them mid-flight re-pointed
isPending at a scope nobody runs and dropped the live spinner. Audit
confirmed no other input feeds a visible pending scope.
- Settings logo status announces only the upload; removal is announced by
the Remove button own live region — the two regions both said Working
during a removal (codex).
- Sales order-list Open button gains disabled={busy} like every sibling
read control (reviewer consistency note).
- styles.css: the busy-opacity override now names the base rule it exists
to beat (pi).
Owner
Author
Review rounds outcomeThis PR ran the full requested pipeline: design → 3-reviewer design round → codex re-pass → subagent implementation (4 coding agents) → 4-reviewer code round → fixes. Design rounds (before any code)
Code round (codex + pi + 2 Claude agents)
Final state
|
Resolves conflicts between the #236 pending-state rollout and the F182 i18n sweeps (B4/B5 + es/tl catch-up): pending-state structure (BusyButton, usePendingAction scopes) kept, user-visible strings taken from the i18n catalogs. Both trailing test suites in SettingsPage.test.tsx (pending scopes + i18n wiring) kept.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35010655 | Triggered | Generic Password | 3f2d512 | web/src/routes/UsersPage.test.tsx | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
added 2 commits
July 27, 2026 22:11
Take main's fully externalized HelpPage; re-apply #236's additions on top: the common-namespace hook and the workingHint list item in the dialogs section (pinned by the existing catalog-override test).
The overlay version dimmed the label to 0.45 and stacked a 2px ring on top of it — on filled buttons it read as barely-there (owner feedback on PR #242). The ring now renders inline as the label wrapper's first child, 3px stroke, with the label at full brightness; the button widening slightly while busy is the accepted trade-off since it is disabled for the duration. Structure pinned by a new BusyButton test; reduced-motion still gets a static ring via the global animation kill.
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.
Closes #236 — pending/busy state on every mutating save. Found while testing #162: a save waiting on the account-row lock looked exactly like a dead button.
Process
Design-first, per request: the design doc (docs/superpowers/specs/2026-07-27-save-pending-design.md) went through three reviewers (codex, pi, architect agent) plus a codex re-pass before any code; implementation ran as four coding subagents (shared mechanism first, then three parallel screen sets on disjoint files); a 4-reviewer code round follows on this PR.
What
Shared mechanism (
web/src/components/)usePendingAction—busy+isPending(scope)+run(scope, action). Double-submit guard on a synchronous ref (state can't stop two same-tick calls — unit-tested with two un-awaitedrun()s → one invocation). Idempotency keys and refresh-before-rotate deliberately stay in the screens' review-hardened helpers; boolean wrappers map a skipped run (undefined) tofalseexplicitly, pinned by a test.BusyButton— children untouched (dynamic labels like Login's "Signing in…" stay the caller's; exact accessible names pinned), centeredaria-hiddenspinner, single-layer label dim, always-mounted siblingrole="status"region ("Working…", all 3 locales) — sibling becausearia-busydefers announcements inside the busy element; always-mounted because a region that mounts pre-populated is unreliably announced.button:disabled[aria-busy="true"] { opacity: 1 }— the design-review blocker; without it the global:disabledopacity halves the spinner and busy looks exactly like dead.Migration — all 14 mutating screens, ~55 triggers:
archive:<id>, compositeassign:<user>:<flock>,adjust:<item>:<lot>), whole screen inert viabusy, only the clicked verb spins (two-verbs-one-row tested).disabled+ its existing status region), LanguageSelector<select>(previously fire-and-forget with no guard, now guarded, no spinner), sign-out (excluded by decision).Tests
+32 (870 total, all green): hook lifecycle + same-tick guard; BusyButton a11y contract (exact accessible name, status region outside the button); held-promise screen tests — double submit under flight (Customers, DailyEntry), confirm-handoff (Flocks), two-verbs-one-row (Sales), scope isolation + focus-effect survival (Settings), dialog-close-leaves-nothing-busy (Sales, Products, Users), skip-never-closes-dialog (Grades). Existing 838 untouched-green (4 legitimately adjusted, reasons in commits).
Coverage rose on every axis; floors ratcheted up to 88/85/79/74 (standing re-baseline rule).
Docs
Help page: one line (spinning button = save still working; pressing again won't double-record), i18n'd in en/es/tl. GLOSSARY untouched — no new domain term.