Repository navigation
fix(frontend): keep the bulk modal open and retry only failed buckets - #1390
Merged
Merged
Conversation
…ure and retry only failed buckets
When some buckets of a multi-bucket ("fan-out") purchase failed, the modal
closed and the selection was cleared before the outcome was classified, and
the toast truncated the reasons (3) and labels (5). Users had to find and
reselect the failed rows in a large table, at the risk of resubmitting the
buckets that had already gone through.
On any bucket failure the modal now stays open:
- each bucket section shows "Submitted: awaiting approval [from X]" /
"Submitted: joined existing request ..." or "Failed: <full reason>";
- submitted buckets are dimmed and their payment selects are locked;
- a banner (role=alert) states how many were submitted and failed, that
submitted buckets will not be sent again, and that closing the dialog
does not cancel them;
- the fan-out state is narrowed to the failed buckets, so the submit
button becomes "Retry N failed" and resubmits only those. A failed
bucket's payment can still be changed before retrying.
The sticky failure toast is skipped in this mode (the modal has the full
detail, and the toast covered the Retry button), and the Archera offer
waits until the purchase modal closes. A fully successful retry closes
the modal as before.
Buckets get a stable id (copies from getFanOutBuckets() lose identity)
and sections carry data-bucket-id so results map back to their section.
Callers without ids keep the previous close-and-toast behavior.
Refs LeanerCloud#757
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jest: the modal stays open with per-bucket status, submitted buckets are locked, the button reads "Retry N failed", a retry posts only the failed buckets and closes the modal (and opens the Archera offer) once all succeed, a second partial failure keeps earlier successes counted, an all-failed run also stays open, buckets without ids keep the old behavior, and failure reasons cannot inject markup. Playwright: three providers, Azure fails once, the dialog shows the failure, Retry 1 failed posts only the Azure bucket and closes. Refs LeanerCloud#757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Conflicts: - frontend/src/app.ts: both sides added an import after the closeModal import (this branch's ./fanout-retry, main's ./init-error from LeanerCloud#1338). Kept both. The submitFanOutBuckets/handleFanOutExecute retry changes merged cleanly with main's LeanerCloud#1352 account-labelled bucket names and LeanerCloud#1364 commitment lines; the toast those feed is still used when retry mode does not apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/app.ts: import block; kept this branch's ./fanout-retry import and main's ./purchasing-nav import (LeanerCloud#1336). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/app.ts: import block. Kept this branch's fanout-retry import (showFanOutRetry, fanOutRetryLabel, FanOutBucketOutcome) and main's setupMobileNav import from nav/mobile-nav (LeanerCloud#1340). handleFanOutExecute merged cleanly: main's combined bulk-approval totals and upfront-naming confirm label (LeanerCloud#1362) now sit alongside this branch's retry-label restore in the finally block. A retry passes only the narrowed failed buckets, so the confirm totals cover just the requests being resent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…etry e2e Main's LeanerCloud#1362 renamed the fan-out confirm button from "Send all for approval" to "Send for Approval ($X upfront on approval)", naming the combined upfront of the buckets being sent. The partial-failure spec still clicked the old label and timed out. It now matches the new label with the amount as a pattern, since the first send and the retry carry different totals. Refs LeanerCloud#757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved: - frontend/src/app.ts: import block only. Kept this branch's fanout-retry import and main's wireTablistKeyboard (LeanerCloud#730/LeanerCloud#1371) and setOpenAccountsHandler (LeanerCloud#1389) imports; the code using each merged cleanly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tatus Main's typography foundation (LeanerCloud#1378) forbids literal font weights in modals.css that duplicate a token. The fan-out status line used a literal 600; use var(--cudly-fw-semibold) instead, same weight. Refs LeanerCloud#757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): REQUEST_CHANGES Blocking:
|
…al is open showFanOutRetry only checked that #purchase-details still held fan-out sections. Closing the modal (Escape or Cancel) while the POSTs were in flight leaves those sections in the hidden modal, so a failed bucket put its banner there and suppressed the failure toast: the user got no notice at all. Bucket ids were also per-index and reused on every open, so results settling after the user opened a new bulk modal marked its sections submitted and narrowed its buckets to the old failed ids. Bucket ids now carry the purchase-modal open token, and retry mode is entered only when #purchase-modal is visible and still holds the submitted bucket ids. Otherwise the toasts fire as on main. If a newer purchase modal is open, it is not closed or cleared. The #1503 payment-adjustment toast also uses the default timeout in retry mode instead of staying on top of the Retry button. Refs LeanerCloud#757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
On the blocking re-review finding above: fixed in
|
Collaborator
Author
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
Collaborator
Author
|
Local verification at
|
IAmJSD
marked this pull request as ready for review
October 25, 2026 00:02
Collaborator
Author
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
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.
If some buckets of a multi-bucket (fan-out) bulk purchase failed, the dialog closed and the selection was cleared anyway, and the only record was a toast that cut the reasons off after 3 and the labels after 5. Retrying meant finding and reselecting the failed rows in a large table, at the risk of resubmitting buckets that had already gone through. This PR keeps the modal open with per-bucket status and the full reason, and turns the submit button into "Retry N failed", which resends only the failed buckets.
Closes #757
What changed
frontend/src/fanout-retry.ts:showFanOutRetry()marks each bucket section "Submitted: awaiting approval [from X]", "Submitted: joined existing request ; " or "Failed: "; dims submitted buckets and locks their payment select; adds arole="alert"banner ("N buckets submitted, M failed. Submitted buckets will not be sent again, and closing this dialog does not cancel them..."); and narrows the fan-out state to the failed buckets.#purchase-modalis visible and still holds the submitted bucket ids. If the user closed the dialog (Escape or Cancel) while the POSTs were in flight, the result is reported by toast exactly as on main.purchaseModalReplaced()detects that the user has already opened a newer purchase modal, so late results leave it alone.fanOutRetryLabel()turns the submit button into "Retry N failed".frontend/src/app.ts:submitFanOutBucketsbuilds per-bucket outcomes with the same success rule as the toast. On any failure, if the submitting modal is still open, it keeps the modal open (it used to callclosePurchaseModal/clearFanOutBucketsfirst) and skips the sticky failure toast. The #1503 payment-adjustment toast uses the default timeout instead of sticking in that case, so it doesn't sit on the Retry button. The Archera offer is held back while the modal is in retry mode and fires when a retry with no failures closes it. If a newer purchase modal is open when the results arrive, it is not closed or cleared, and the offer is skipped.handleFanOutExecuterestores the retry label. A fully successful retry closes the modal as before.frontend/src/recommendations.ts:FanOutBucketgets an optionalid,bucket-<open token>-<index>, which is unique per modal open (sincegetFanOutBuckets()returns copies, and so results from an earlier open can't match a later one). Sections getdata-bucket-id, and a newretainFanOutBuckets(ids)narrows state and refreshes the modal header.frontend/src/styles/modals.css: status, lock and banner styles.__tests__/app-fanout-retry.test.tsandtests-e2e/fanout-partial-failure.spec.ts, plus a case in__tests__/purchase-modal-submit.test.ts.Why this approach
Narrowing the existing module-level fan-out state reuses the whole submit path (confirm dialog, double-submit guard, payment handling, same POST body), so a retry can't drift from a normal submit and structurally can't resend a bucket that went through. Matching on a bucket id rather than an index is needed because
getFanOutBuckets()hands out copies and a retry submits a subset. The id includes the existing purchase-modal open token, so POSTs that settle after the dialog was closed and reopened can't mark or narrow the new one. The id is optional: callers without ids fall back to the old close-and-toast path, so existing flows and tests are unchanged. The sticky failure toast is dropped in retry mode because the e2e run showed it covering the Retry button, and the modal already shows the untruncated detail. Retry mode needs the dialog to be visible, so a user who closed it never loses the toast. The dialog also stays open when every bucket failed, since the same retry is needed there.Screenshots
Before is main at
a14dc8f2. Main has since added account labels to the failure toast (#1352) and combined totals to the confirm (#1362), but it still closes the dialog and leaves only that toast. After is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API, with 3 buckets (AWS/Azure/GCP) where the Azure POST returns 502.Verification
Run on this branch's head
e2858b94, which includes main10f2c8af:Lint: 0 errors (141 pre-existing warnings). Typecheck and build pass.
Jest (
--maxWorkers=2): 166 suites, 4385 passed, 1 skipped, 0 failed. Newapp-fanout-retry.test.tshas 9 cases:purchase-modal-submit.test.tsadds a case for the real modal: two opens get disjoint bucket ids, and the rendered sections carry the current ones. The last three app cases and the id case fail on the previous head and pass now.Playwright via
e2e-local.sh: 260 passed. The newfanout-partial-failure.spec.tsruns the real UI: 3 providers, Azure fails once, and "Retry 1 failed" posts only Azure and closes the dialog.git-secrets scan clean (planted key flagged);
test-git-secrets-allowlist.sh: 137 pass, 0 fail.Notes for review
Behavior changes to confirm:
Copy to approve: the banner text, the "Submitted: ..." / "Failed: ..." lines and the "Retry N failed" button.
Known rough edges:
Bulk purchase — 1 bucket(it describes what a retry will send) while all sections stay visible.Overlaps
The overlapping work has all landed on main and is merged into this branch: #711 (#1364, monthly and term totals), #707 (#1362, combined-totals confirm label), #434 (#1352, account-labelled failure toast), #741 (#1378,
modals.css) and #215 (#1340, an import next tocloseModal). To match them,ea0a590fupdates the e2e confirm label for #1362, andfa3d6bc2switches the bucket status weight tovar(--cudly-fw-semibold)for #1378.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code