Skip to content

feat(frontend): wire Azure RI exchange preview and execute UI - #1325

Draft
IAmJSD wants to merge 7 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-81
Draft

IAmJSD wants to merge 7 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-81

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

The backend already serves the Azure RI exchange endpoints (POST /api/ri-exchange/azure-instances/compatible-offerings and /exchange), but the frontend never called them: the Azure exchangeable-reservations list on Inventory > RI Exchange was read-only. This PR adds selection on that list and an exchange modal that previews the priced quote, requires an explicit spending limit and currency, and executes the exchange.

Closes #81

What changed

  • API client (0a3e8d7c): frontend/src/api/types.ts, api/riexchange.ts, api/index.ts add typed request/response shapes mirroring internal/api/handler_ri_exchange.go (amounts stay number | null) and getAzureCompatibleOfferings / executeAzureExchange. ExchangeableAzureRI gains billing_scope_id, which the backend already returns. New frontend/src/lib/azure-exchange.ts holds pure helpers: the subscription to exchange under (from the sources' billing_scope_id; all sources must share it, as the backend's ownership gate requires), form and guardrail validation matching the backend checks (1-50 items, quantities, P1Y/P3Y/P5Y terms, non-negative plain decimal cap, 3-letter currency), and user-facing error text (422 guardrail refusals get a specific hint for cap exceeded, currency mismatch, missing net payable and Azure policy; 403, 404 and 400 are explained).
  • Modal (8d25988d): new frontend/src/riexchange-azure-modal.ts, built with DOM/textContent only. "Return" rows set a quantity per selected reservation (max = its quantity); "Buy instead" target rows take VM size, region, term and quantity (seeded from the first source; add/remove targets). Preview exchange shows net payable, refunds total, purchases total, the compatible-offerings table and Azure policy errors. Execute appears only when the preview has no policy errors and a non-null net payable, and only for holders of execute:ri-exchange (isRIExchanger()). It requires a typed maximum payment due and currency (never prefilled; the quoted values appear only as placeholders) and goes through a destructive confirmDialog naming the sources, targets, quoted net payable and the cap. Editing any source or target discards the preview. The execute outcome is shown below the preview.
  • List wiring (ad57119d): new frontend/src/riexchange-azure.ts plus small changes in riexchange.ts. For users with view:purchases, the Azure table gets a checkbox column and an "Exchange selected (N)" button. The modal module is loaded on demand with import(). The tab reloads after a successful exchange. Styles in styles/components.css.
  • Playwright spec tests-e2e/azure-ri-exchange.spec.ts (4ac6e996).
  • Uncertain execute outcomes (a6b14b3b): only a 4xx from execute is reported as "Exchange not executed" (every backend gate, the guardrails, the idempotency claim and Azure's client-fault rejections answer 4xx). A 5xx, gateway timeout, client timeout (status 0) or network error is reported as "The exchange may have been submitted to Azure. Verify the reservation state in the Azure portal (the reservation order's history) before trying again; the RI exchange history here does not list Azure exchanges." plus the detail. This matches how the backend audits those failures (directExchangeFailureStatus records them as unknown). After such a failure the tab reloads the list and history, and the modal hides Execute and drops the preview, so trying again needs a new preview. frontend/src/lib/azure-exchange.ts (isAzureExchangeOutcomeUnknown), riexchange-azure-modal.ts. The same commit shows a toast if the modal chunk fails to load ("Could not load the exchange dialog. Reload the page and try again.") and gives the checkbox column's header a visually hidden "Select for exchange" name (.sr-only).
  • Execute outcome and timeout (8ffadd67): the unknown-outcome text points only at the Azure portal, matching the backend's own 500 text, because direct Azure executions are audited in ri_exchange_direct_executions, which the tab's history (ri_exchange_history) does not read. If the modal is closed while execute is in flight, the outcome is shown as a toast: success, a 4xx refusal as an error, and an unknown outcome as a warning that stays until dismissed. executeAzureExchange gets a 5-minute client timeout (api/riexchange.ts), since the backend's detached execution budget is 4 minutes and the default 90s would report slow successful exchanges as unknown.

Why this approach

The flow matches the AWS exchange modal (quote, confirm, execute; quote invalidated on edit; destructive confirm with Cancel focused), but the Azure backend contract differs: targets are free-form SKU/location/term with no offering-ID picker, and the cap and currency are mandatory. So it is a separate module rather than more branches inside the 2,400-line openExchangeModal.

The subscription comes from each reservation's own billing scope rather than the header account chip: the backend refuses any source not billed to subscription_id, and with "All accounts" selected there is no chip to use. Mixed-subscription selections are refused up front with a clear message. Preview is gated on view:purchases and Execute on execute:ri-exchange, the permissions each backend endpoint checks.

The modal is lazy-loaded because the eager version pushed app.js from 497 to 509 KiB, over webpack's 500 KiB size hint. With import() the main bundle is 498 KiB plus an 11 KiB chunk.

Screenshots

Before is main (a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.

Before After
azure-list before azure-list after
azure-preview before azure-preview after
(no equivalent on main) azure-blocked after
(no equivalent on main) azure-guardrail-confirm after
(no equivalent on main) azure-guardrail after
  1. Azure reservations table. Before: read-only, no way to exchange. After: a selection column and an "Exchange selected (2)" action above the table.
  2. Before: the full RI Exchange page for Azure, where nothing opens a preview. After: the exchange modal with the source quantity, a target row, the priced preview (net payable, refunds, purchases), the compatible-offerings table and the required spending limit and currency.
  3. A preview with an Azure policy error is marked as not acceptable and no Execute is offered.
  4. The destructive confirm names what is returned and bought, the quoted net payable and the cap.
  5. A 422 "cap exceeded" refusal is explained with a next step, below the preview.

Verification

Run on this branch's head 8ffadd67 on top of main a14dc8f2, from frontend/:

  • npm run lint: 0 errors (142 pre-existing warnings). Typecheck passes. Build passes with the one pre-existing entrypoint-size warning (app.js 498 KiB vs 497 KiB on main).
  • Jest: 112 suites, 3555 passed, 1 skipped.
  • New src/__tests__/azure-exchange.test.ts: billing-scope parsing, mixed subscriptions, form and guardrail validation, null amounts never shown as 0, 422/403/404/400 messages. Execute failures with a 500, a 504, a client timeout (status 0) or a network error each assert the full unknown-outcome message with no "not executed" prefix. A 422 guardrail refusal and a 409 keep their definite wording, and a preview 504 stays "Could not preview".
  • New src/__tests__/riexchange-azure.test.ts (23 tests): toolbar selection and count, an error toast when the modal chunk fails to load, mixed-subscription refusal, exact compatible-offerings request body, preview rendering, no Execute on policy errors / null net payable / null preview, preview errors, invalidation on edit, cap and currency required, exact execute body with the cap and an upper-cased currency, cancelled confirm sends nothing, 422 text, a 504 and a client timeout on execute (unknown-outcome text, list/history refreshed, Execute hidden, a further click sends nothing), the outcome shown as a toast when the modal is closed mid-execute (success, 422 refusal, sticky unknown-outcome warning) and no toast while it is open, preview-only without execute:ri-exchange, add/remove targets. New src/__tests__/api-riexchange-azure.test.ts: execute is sent with a 5-minute timeoutMs, preview keeps the default. Plus one case in riexchange.test.ts (the Azure table shows the selection column for an admin, and its header has the visually hidden name).
  • The new suites pass with --randomize. riexchange.test.ts fails 27 tests under --randomize seed 1922165236, and main's copy of that file fails the same way with the same seed.
  • Playwright via e2e-local.sh: 159 passed (157 plus 2 new: the full preview, cap required, execute flow with exact request bodies; and a 422 guardrail refusal explained).
  • Secrets scan: clean, planted key flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.

Notes for review

  • Azure/GCP deploys are paused, so this has only been exercised against mocked responses; no real cloud API was called. It should be checked against a real Azure subscription once deploys resume (preview first; execute moves money and cannot be undone).
  • The guardrail inputs are left empty on purpose and the quoted amount only appears as a placeholder, so the cap has to be typed, per the issue's "explicit" requirement. A "use quoted amount" shortcut or a prefilled currency would be easy to add if preferred.
  • Visibility: the selection column and Preview show for view:purchases; Execute only for execute:ri-exchange (RI Exchanger group), with a note explaining that to everyone else. The AWS table still gates its Exchange button on admin:*, which no longer covers execute:ri-exchange since #1644. Aligning the two is a separate decision.
  • Targets are free-text SKU and region, since the backend has no Azure target-offering list endpoint. Azure validates them and errors come back as 400s with Azure's message. A SKU/region picker would need a backend endpoint.
  • With 4-eyes approval mode on, the backend refuses direct execution with a 403. The UI shows that message but does not hide Execute in advance.
  • Copy to review: modal title "Azure Reservation Exchange", section labels "Return" / "Buy instead", "Spending limit (required to execute)", and the guardrail hint texts.
  • The Azure list's Term column still shows raw P1Y/P3Y (unchanged); the modal shows "1 year" / "3 years".

Overlaps

No textual conflicts with any other UI branch (checked with git merge-tree). Branches for #1135, #136, #18, #186, #190, #284, #319, #321, #363, #37, #709, #727, #729, #737, #738, #740 and #751 also edit frontend/src/riexchange.ts, in other functions. Only #1135 and #18 touch Azure-adjacent lines (the reshape empty-state/popover code, not renderAzureRIsTable). components.css and api/types.ts get appends that merge cleanly.

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

IAmJSD and others added 4 commits October 24, 2026 02:12
The backend already serves POST /api/ri-exchange/azure-instances/
compatible-offerings and /exchange, but the frontend had no client for
them. Add typed request/response shapes that mirror
internal/api/handler_ri_exchange.go (nullable amounts stay nullable),
the two API calls, and pure helpers the exchange UI builds on: the
billing subscription a set of source reservations must be exchanged
under, form and guardrail validation matching the backend's checks, and
user-facing text for the 422 guardrail refusals and other failures.

Refs LeanerCloud#81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The modal takes the selected source reservations, lets the user set how
many of each to hand back and which SKU/region/term/quantity to buy,
and previews the exchange through compatible-offerings: net payable,
refunds and purchases totals, the compatible offerings and any Azure
policy errors. Execute is offered only for a preview with no policy
errors and a reported net payable, only to holders of
execute:ri-exchange, and only after the user types a maximum payment
due and currency (never prefilled from the quote), matching the
backend's mandatory guardrail. Any change to sources or targets
discards the preview. Execution goes through a destructive confirm and
its outcome, including 422 guardrail refusals, is shown below the
preview.

Refs LeanerCloud#81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…exchange modal

The Azure reservations table was read-only. For users who can preview
purchases it now has a selection column and an "Exchange selected"
action that opens the exchange modal for the ticked reservations and
reloads the tab after an exchange. The modal module is loaded on demand
so the main bundle stays under webpack's 500 KiB size hint.

Refs LeanerCloud#81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playwright spec with mocked Azure endpoints: select a reservation,
preview, get refused without a spending limit, execute with an explicit
cap and currency, and see a 422 guardrail refusal explained rather than
reported as a generic error.

Refs LeanerCloud#81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/this-quarter Within the quarter impact/few Limited audience effort/m Days type/feat New capability labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): REQUEST_CHANGES 4ac6e996122320c265f3e5b5791c00a5c00645d8

Blocking:

  • frontend/src/lib/azure-exchange.ts:123,135 (used at frontend/src/riexchange-azure-modal.ts:342-344): every execute failure is labelled "Exchange not executed: ...", including 5xx, gateway timeouts and client timeouts/network errors (status 0). For those cases the backend itself records the outcome as unknown (ri_exchange_audit.go directExchangeFailureStatus; handler_ri_exchange.go executeAzureExchange runs ExecuteExchange on a detached 4-minute context and says it "may already have been submitted"). Failure scenario: an Azure exchange LRO runs past the container's 30s per-request timeout (or the 90s client timeout). The user sees "Exchange not executed: HTTP 504" (or "...timed out after 90000ms"), while the exchange keeps running and commits on the server. On a 500 the text even contradicts itself ("Exchange not executed: exchange execution failed and may already have been submitted to Azure"). onExecuted() isn't called, so the list isn't refreshed, and Execute stays enabled, which invites a retry of an irreversible money operation. Fix: say "not executed" only for 4xx; for other failures say the outcome is unknown, point to the Azure portal or exchange history, and refresh the list. The test at tests/azure-exchange.test.ts:108 only matches the "may already have been submitted" substring, so it passes despite the contradictory prefix.
    Non-blocking:
  • frontend/src/riexchange-azure.ts:53: void import(...) has no .catch. If the chunk fails to load (for example a stale tab after a deploy changes the content hashes), clicking "Exchange selected" silently does nothing and leaves an unhandled rejection.
  • frontend/src/riexchange.ts renderAzureRIsTable: the new checkbox column has an empty , so its column header has no accessible name. The per-row checkboxes do have aria-labels.
  • Inputs stay editable while an execute is in flight. An edit runs invalidate() and clears the status box mid-request; harmless, but disabling the form during execute would be cleaner.
  • With 4-eyes mode on, Execute is shown and then refused with a 403 (already noted in the PR body).
    Attacked: Diffed the request/response types and validation against internal/api/handler_ri_exchange.go: body fields, the 50-item caps, P1Y/P3Y/P5Y terms, guardrail 422 message texts and the subscription_id/billing_scope ownership rule all match. The modal builds the DOM with textContent only, adds no inline styles (CSP style-src 'self'), and the lazy chunk loads from publicPath '/' under script-src 'self'. Checked the preview revision guard against stale responses, invalidation on edits, the double-submit guards, and that the cap/currency are never prefilled. Closes feat(frontend): wire Azure RI exchange compatible-offerings + execute UI (refs #596) #81 matches the issue scope apart from the blocking item.

…ot executed"

A 5xx, gateway timeout, client timeout or network error on execute does
not prove the exchange was refused. The backend runs ExecuteExchange on a
detached context and audits those failures as outcome unknown
(directExchangeFailureStatus), so labelling them "Exchange not executed"
was wrong. It also left Execute enabled on the same preview and skipped
the list refresh, which invited a retry of an irreversible exchange.

Only a 4xx is now reported as a refusal. Any other failure says the
exchange may have been submitted and asks the user to check the history
or the Azure portal. It also refreshes the list and history and retires
the preview, so a retry needs a fresh preview.

Also show a toast when the exchange dialog chunk fails to load, and give
the selection column's header a visually hidden accessible name.

Refs LeanerCloud#81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

On the blocking review finding above: fixed in a6b14b3b.

  • Only a 4xx is reported as "Exchange not executed". Every refusal before submit in handler_ri_exchange.go returns a 4xx: validation, scope, ownership and 4-eyes checks, the 409 duplicate submit, and the 422 guardrails. Azure's own client-fault rejections also come back as 4xx.
  • A 5xx, gateway or client timeout, or network error is now treated as an unknown outcome. The message says the exchange may have been submitted to Azure and asks the user to check the RI exchange history or the Azure portal before trying again, with no "not executed" prefix. That matches how directExchangeFailureStatus audits those failures.
  • After an unknown outcome: the tab reloads the list and history, and the modal hides Execute and drops the preview, so retrying needs a fresh preview.
  • New tests: 500, 504, status 0, network error, 422 and 409. On 4ac6e996, 8 of the 136 tests in those suites fail; on a6b14b3b, all 136 pass.
  • Non-blocking items also fixed: the lazy import now shows an error toast if it fails, and the checkbox column header has a visually hidden "Select for exchange" label.

Checks at a6b14b3b: lint 0 errors, typecheck and build pass, jest 3549 passed, Playwright 159 passed, and both git-secrets checks pass.

This PR is Azure-only. Azure deploys are paused, so it will be kept merge-ready but held out of the current merge train.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): REQUEST_CHANGES a6b14b3b483d90ea35bd648a108b0b32917707df

Blocking:

  • frontend/src/lib/azure-exchange.ts:137 (rendered at frontend/src/riexchange-azure-modal.ts:355): the unknown-outcome text tells the user to "Check the RI exchange history (or the Azure portal) before trying again", but that history can never show this exchange. The tab's history (riexchange.ts loadExchangeHistory -> GET history -> config GetRIExchangeHistory) reads only the ri_exchange_history table. executeAzureExchange audits direct executions only in ri_exchange_direct_executions (ri_exchange_audit.go beginDirectExchange/finalizeDirectExchange), and nothing serves that table. Failure scenario: execute hits the 90s client timeout while the 4-minute LRO is still running. onExecuted() reloads the list right away, so the source reservation is still shown, and the history panel shows "No exchange history found." / AWS rows only. The user follows the first instruction, concludes the exchange did not happen, makes a fresh preview with a slightly different quantity or target (a new idempotency key), or waits out the 15-minute claim window, and executes again. That is a second irreversible commitment, which is the false-negative-then-retry this fix was meant to prevent. Fix: point only at the Azure portal (the reservation's exchange/order history), as the backend's own 500 text does, or at a view that really lists direct executions. Then update the two tests that pin the string (azure-exchange.test.ts:114, riexchange-azure.test.ts:254).
    Non-blocking:
  • Earlier blocking item otherwise resolved: isAzureExchangeOutcomeUnknown (azure-exchange.ts:128) treats only 4xx as definite, which matches directExchangeFailureStatus exactly. 5xx, status 0, a network TypeError and a 2xx with an unparseable body all count as unknown. After an unknown outcome the list is refreshed, Execute is hidden and the preview is dropped. The 409 idempotency text is accurate under the "Exchange not executed:" prefix.
  • riexchange-azure-modal.ts:342-356: if the user closes the modal while execute is in flight, sessionOpen=false suppresses every outcome message (success, 4xx refusal or unknown). Only the silent list refresh remains. A toast when !sessionOpen would keep the outcome visible.
  • api/riexchange.ts executeAzureExchange uses the default 90s client timeout, while the backend's detached execute budget is 4 minutes, so a slow but successful exchange will often be reported as "may have been submitted". Consider a longer timeoutMs for this call.
  • Pre-submit 500s (audit-row save failure, idempotency-store failure in beginDirectExchange) are definitely not submitted but are shown as unknown. That is conservative and acceptable.
  • With 4-eyes mode on, Execute is still offered and then refused with a 403. This is noted in the PR body.
    Attacked: Traced every 4xx/5xx path in executeAzureExchange, mapAzureExchangeError, beginDirectExchange and claimExchangeSubmit against isAzureExchangeOutcomeUnknown/describeAzureExchangeError. Read apiRequest (no retries; status 0 on timeout; a JSON parse failure carries no status) and followed where "RI exchange history" actually reads from. Re-read the whole modal for double-submit (Execute disabled before the confirm), stale preview responses (revision guard), invalidation on edit, listener scoping on a fresh root per open, the shared-modal onClose, and DOM built with textContent only (no innerHTML, no inline styles). Confirmed the lazy-import .catch toast and that the sr-only header label exists in components.css.

…ortal

The unknown-outcome text told the user to check the RI exchange history,
but that history reads only ri_exchange_history, while direct Azure
executions are audited in ri_exchange_direct_executions, which nothing
serves. The history could never show the exchange, inviting the user to
conclude it did not happen and execute again. Point only at the Azure
portal, as the backend's own 500 text does, and say the history here does
not list Azure exchanges.

Closing the modal while execute was in flight dropped the outcome; it is
now shown as a toast (a sticky warning when the outcome is unknown).

Execute used the default 90s client timeout against a 4-minute backend
execution budget, so slow successful exchanges were reported as unknown;
it now gets a 5-minute timeout.

Refs LeanerCloud#81

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

On the re-review above: fixed in 8ffadd67.

  • Blocking (history text): the unknown-outcome message no longer mentions the RI exchange history. That history reads only ri_exchange_history, and direct Azure executions are audited in ri_exchange_direct_executions, so it could never show the exchange. The message now sends the user to the Azure portal (the reservation order's history), the same advice as the backend's own 500 text (handler_ri_exchange.go:1295). It also says the history here does not list Azure exchanges. Both tests that pin the string are updated.
  • Modal closed while execute is in flight: the outcome now appears as a toast. Success shows a success toast and a 4xx refusal an error toast. An unknown outcome shows a warning toast that stays until dismissed. New tests close the modal mid-request and settle it each way, and another test checks that nothing is toasted while the modal is open.
  • 90s client timeout: executeAzureExchange now passes a 5-minute timeoutMs, longer than the backend's 4-minute detached budget (ri_exchange_detached.go:25). Preview keeps the default, and a new API test pins both.

Fail-before: 10 of the 68 new or updated tests fail on a6b14b3b. All 68 pass on 8ffadd67.

Checks at 8ffadd67:

  • lint: 0 errors
  • typecheck and build: pass
  • jest: 3555 passed
  • Playwright: 159 passed
  • git-secrets: both checks pass

This is still held out of the merge train while Azure deploys are paused. A fresh re-review of 8ffadd67 follows.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE 8ffadd673c88a743c2243fe3d41ac0915f24570f

Blocking:

  • none
    Non-blocking:
  • The round-2 blocking item is resolved. frontend/src/lib/azure-exchange.ts:140 no longer points at the tab's RI exchange history. It sends the user to the Azure portal (the reservation order's history), which matches the backend's own 500 text (handler_ri_exchange.go executeAzureExchange). It also says outright that the history here does not list Azure exchanges. Both tests that pin the string are updated (azure-exchange.test.ts:114, riexchange-azure.test.ts:255).
  • frontend/src/riexchange-azure-modal.ts:350: a success toast reads "Exchange ", and the status can be a pending, accepted state (the provider's exchangeStatusAccepted lets non-terminal statuses through). A pending exchange is then shown with kind 'success'. The in-modal path does the same thing, so this is not new in this commit; wording such as "Exchange submitted (Pending)" could be a follow-up.
  • The 5-minute client timeout (api/riexchange.ts AZURE_EXCHANGE_EXECUTE_TIMEOUT_MS) only helps when nothing in between cuts the request sooner. In container mode the Go server's WriteTimeout is 30s (internal/server/http.go:76), and executeAzureExchange does not extend it the way scheduled tasks do. So a slow but successful exchange there still ends as a network error and the unknown-outcome message. The message is correct and conservative, so the timeout change does no harm; the real fix is on the backend (lift the write deadline for this route).
  • When the modal is closed mid-execute and an error toast appears for a 4xx, it uses the default 30s timeout. The unknown-outcome toast is sticky (timeout: null), and that is the case that matters.
  • Carried over: pre-submit 500s (audit or idempotency-store failures) are shown as unknown, which is conservative. With 4-eyes mode on, Execute is still offered and then refused with a 403; this is noted in the PR body.
    Attacked: Read the a6b14b3..8ffadd6 delta and re-read the whole riexchange-azure-modal.ts, riexchange-azure.ts and the riexchange.ts wiring at the new head. Checked that showToast accepts kind 'warning' and timeout null (sticky) and renders through textContent. Checked that apiRequest honours options.timeoutMs. Traced the backend execute path through to the provider's ExecuteExchange status handling, and the server WriteTimeout. Checked the closed-mid-execute paths: the sessionOpen closure is per open, the detached statusBox is never touched, and a reopened modal is not affected by the old in-flight call. Also re-scanned for innerHTML and inline styles, and listener scoping on a fresh root per open. Ran the 3 Azure suites in a scratch worktree: 68/68 pass. The close-mid-request tests hold the promise open, assert no toast before it settles, and check kind, sticky timeout and onExecuted for each outcome, so they test what they claim.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

The Azure merge hold on this PR is lifted. The owner released it on 2026-10-20 ("merging is allowed; Azure/GCP apply stays paused", see #1114), and IAmJSD's team had missed that. This PR is frontend-only, so it needs no Azure deploy. It goes back into the normal queue: main merge, fresh CI, review, merge. Infra PRs that wait on the first Azure/GCP deploy are tracked in #1413.

Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(frontend): wire Azure RI exchange compatible-offerings + execute UI (refs #596)

1 participant