Repository navigation
fix(frontend): honest purchase-modal Eff. % and resilient view tables - #1327
Conversation
The capacity clone scaled count, upfront, monthly cost and savings but carried on_demand_cost through unscaled, so at 50% capacity a 47.4% row showed 23.7% at the moment of authorisation. on_demand_cost is a per-rec total like savings, so it now scales with the same ratio. The Totals row re-derived its weighted Eff. % with the AWS reconstruction that effectiveSavingsPct deliberately refuses, so it printed a figure the rows above showed as an em-dash. It now averages the per-row displaySavingsPct values (weighted by each row's on-demand baseline) and shows an em-dash when no selected row has a known %. A stored bulk-purchase capacity is now accepted only as an integer, like every other numeric parse in the module; a hand-edited 33.33 falls back to 100 instead of reaching the scaling and the audited capacity_percent. Refs LeanerCloud#136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ables - Reshape recommendations: an omitted utilization_percent made toFixed throw inside the row map, replacing the panel with "Cannot read properties of undefined"; it also styled unknown as red. Unknown now renders a neutral em-dash, mirroring the active-RI table. - API keys: new Date(bad).toISOString() threw a RangeError while building the rows, so one malformed last_used_at hid every key, Revoke buttons included. The title falls back to empty and the cell to "--". - Plans: Progress rendered "0/1 steps" when total_steps was missing; it now shows "--", like the Term cell beside it. - index.html: the API Docs link gets rel="noopener noreferrer". - riexchange.ts: drop the unused MODE_VALUES and the void that hid it from lint (A12-068). Refs LeanerCloud#136 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 10 billable files and costs up to $2.50. Or wait 59 minutes for your next included review. View limit details
Comment |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Local verification at
|
|
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 |
Since this branch was cut, main added placeholders that bypass NO_VALUE: - plans.ts formatRampProgress (LeanerCloud#1327) returned "--" for a missing step count; it now returns NO_VALUE like the other plan-card fields. - trends.ts (new Trends sub-tab, LeanerCloud#1317) showed "--" on its Latest and Average savings stat cards; they now use NO_VALUE, matching the monthly table, whose null cells come from formatCurrency and already render it. - trends.test.ts, trends.spec.ts, plans.test.ts and nullable-purchase-cost.spec.ts expected the old "--"/"-" text from formatCurrency(null) and the muted monthly cell; they now expect "—". Refs LeanerCloud#739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1376) * fix(frontend): one money formatter, one absent-value placeholder, right-aligned money columns Money was printed three ways: formatCurrency (separators), the cost period cells' `$${x.toFixed(n)}` (so a yearly figure read "$123456"), and savings-history's private helper. Absent values used "--" (from formatCurrency and about a dozen literals), "—" and, in the Purchases tables, "-"; the empty Home showed "--" in savings green next to a bold "—". All money columns were left-aligned, so digits did not line up. - utils.ts: export NO_VALUE ("—"); formatCurrency returns it for null/NaN/Infinity. - recommendations.ts: formatCostForPeriod and formatScaledRange go through formatCurrency with the per-period precision, so every period groups thousands. - dashboard.ts, history.ts, plans.ts, recommendations.ts: every "--" / "-" placeholder becomes NO_VALUE; an absent figure is no longer styled as a saving. - tables.css: th.num/td.num right-align with tabular-nums; the Opportunities numeric columns (by COLUMN_DEFS kind) and the Purchase History and approval queue count/money columns use it. - index.html: the Purchases savings stat cards start at "—", not "$0.00". The savings-history module's own formatter is left to #171/#749. Refs #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): use the one absent-value placeholder in code main added Since this branch was cut, main added placeholders that bypass NO_VALUE: - plans.ts formatRampProgress (#1327) returned "--" for a missing step count; it now returns NO_VALUE like the other plan-card fields. - trends.ts (new Trends sub-tab, #1317) showed "--" on its Latest and Average savings stat cards; they now use NO_VALUE, matching the monthly table, whose null cells come from formatCurrency and already render it. - trends.test.ts, trends.spec.ts, plans.test.ts and nullable-purchase-cost.spec.ts expected the old "--"/"-" text from formatCurrency(null) and the muted monthly cell; they now expect "—". Refs #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): use the one absent-value placeholder in Purchases code main added Main's #1329 (no-commitments rows) added a muted "-" placeholder (MUTED_DASH) for Purchase History and approval-queue cells and a "-" Term column-filter label for a 0 term. They now use NO_VALUE ("—"), like the rest of the absent values on these tables, so a no-commitments row no longer mixes "-" with "—". purchases-placeholders-181.test.ts expects "—". Refs #739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This takes a coherent subset of 8 items from the 2026-07-28 view-module review tracker. The main one is the capacity-scaling bug reconfirmed in the issue comments: at 50% capacity the purchase modal's Eff. % halved (47.4% shown as 23.7%) because savings scaled and the on-demand baseline did not, and the Totals row printed a percentage built from the AWS formula the rows themselves refuse to show. It also stops a single malformed field from wiping the reshape panel or the whole API-key list (Revoke buttons included).
Refs #136
Partial: 8 of the tracker's items are fixed here; the remaining items are listed under "Notes for review".
What changed
Commit
2e74e524(purchase modal,frontend/src/recommendations.ts):scaleRecForCapacity(now exported for tests) also scaleson_demand_costby the capacity ratio, so the per-row cell, the totals and the fan-out/variant pricing paths (pricedCellVariant) all pick it up.updatePurchaseModalTotalsuses a newweightedDisplaySavingsPct(recs): it averages each row'sdisplaySavingsPct, weighted by its on-demand baseline (on_demand_costwhen known, otherwise the baseline implied by the row's own %), skips rows that show an em-dash, and returnsnull(rendered as an em-dash) when no row contributes.loadBulkPurchaseStateaccepts only an integer stored capacity and otherwise falls back to 100, like every other numeric parse in the module.Commit
bdaa21f2(other views):frontend/src/riexchange.ts: reshape rows resolve utilization tonumber | nulland render a neutral em-dash for unknown, mirroring the active-RI table, instead of throwing intoFixedand styling unknown as red. Also drops the deadMODE_VALUESand thevoidthat hid it from lint (A12-068).frontend/src/apikeys.ts: anisoOrEmpty()title and--cell for an unparseablelast_used_at, instead of aRangeErrorthat hid every key.frontend/src/plans.ts:formatRampProgressrenders--unlesstotal_stepsis a positive number (was "0/1 steps"), like the Term cell beside it.frontend/src/index.html: the API Docs link getsrel="noopener noreferrer".recommendations.test.ts,riexchange.test.ts,apikeys.test.ts,plans.test.tsandhtml.test.ts.Why this approach
Each fix copies the in-repo reference the issue names: the active-RI table's null utilization handling, the Term cell's
--, and the AWS-IAM link'srelattribute. For the Totals row, averaging the values the rows actually display (rather than only adding the AWS bail) means the total can never disagree with the rows above it, including rows where the provider'ssavings_percentageis preferred over the reconstruction. Scalingon_demand_costin the sharedscaleRecForCapacityfixes every consumer at once.Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.utilization_percentreplaced the whole panel with "Cannot read properties of undefined (reading 'toFixed')". After: both rows render, and the unknown utilization is a neutral em-dash.last_used_atgave "Failed to load API keys", so no keys and no Revoke buttons. After: both keys are listed, the bad timestamp shows--, and both Revoke buttons are reachable.total_stepsshowed a fabricated "Progress 0/1 steps". After: Progress shows--.Verification
Run on this branch's head
bdaa21f2on top of maina14dc8f2:-t 136, 13 tests) pass with--randomize. Full-file--randomizeof the recommendations/riexchange/plans suites also fails on unmodified main (e.g. seed 2130656337 gives 33 failures in baserecommendations.test.ts), so that order dependence predates this branch.savings_percentage, still includes the Azure reconstruction; Eff. % is invariant at 25/50/75/100% capacity withon_demand_costscaled and an absent one kept absent; stored capacity 33.33/150/0/"abc" falls back while 50 is kept; a missing reshape utilization renders an em-dash with no color class; a badlast_used_atkeeps both rows and both Revoke buttons; Progress shows "2/4 steps" or--; everytarget=_blanklink inindex.htmlhasrel.test-git-secrets-allowlist.sh: 137 pass.Notes for review
savings_percentagebut noon_demand_costare weighted by the implied baseline (savings / pct). Rows showing an em-dash are excluded, so the total covers only rows with a known %, with no visual marker for partial coverage; a footnote may be worth adding.titletooltip is still ISO, as decided in fix(auth): reject API keys minted before the owner's last password change #492; it is empty for a bad value.loadRecommendations/loadHistory/loadPlans/loadConvertibleRIs(plus A12-011); the deadsavingsMin === nullbranch andpageLevelRangenullability (plus the A12-023 docstring);CSS.escapeinusers/userList.tsandplans.tsselectors;NaNinstead of?? 0in numeric filter extractors; the E2E coverage item; A12-004 (SP count-1 recs vanish under capacity scaling, needs hourly-commitment scaling); A12-026 shared cancel gate; A12-037/A12-043 test coverage; A11-017 create-path integer check; A11-018 api-keys shape fallback; A11-020 deadusers/*code; A12-057 marketplace dialog without amount; A12-060 completed-count fallback; A12-061 deep-link scoping; A12-062 redundant admin term; A12-063 post-filter empty state; A12-065 utilization failure state; A12-070 staleness banner copy.Overlaps
A
git merge-treecheck against every other ui branch found no textual conflicts. Branches that edit the same files:recommendations.ts: fix(frontend): Potential Monthly Savings differs between Home and Opportunities for the same recommendations #180, chore(frontend): UI polish backlog from the 2026-08 UX review #191, frontend/recs: mountBottomActionBox does not re-sync purchase visibility after in-session permission change #20, fix(frontend): monthly cost and term total never shown, bulk shows upfront 0 #711, fix(frontend): filter Clear hides all rows; user/group form validation toast-only #727, fix(frontend): three money formatters, mixed placeholders, left-aligned money columns #739, fix(frontend): Enter on a column-filter button sorts instead of opening the filter #755, fix(frontend): first-run empty state says no rows match filters with no next step #756, fix(frontend): partial bulk purchase failure closes the modal, no retry of failed buckets #757riexchange.ts: refactor(frontend): consolidate column-filter popover plumbing into shared lib #18, chore(frontend): standardize loading indicators and add slow/empty fallbacks #186, feat(frontend): Admin Purchasing policies page is very long with a mid-page partial load #190, fix(frontend): RI-exchange cost chips and totals hardcode "$" and discard currency_code #284, chore(frontend): loop-invariant work repeated per row in table renders #319, chore(frontend/riexchange): low correctness findings from the 2026-09-02 audit #321, chore(riexchange): make exchange region required in TS types and openapi (follow-up to #355) #363, SUP-03: Two escaping-convention gaps in innerHTML templates (defense-in-depth only) #37, fix(frontend): RI exchange confirm lists offering UUIDs, no instance type or cost delta #709, fix(frontend): filter Clear hides all rows; user/group form validation toast-only #727, fix(frontend): low-severity accessibility fixes (focus, aria, target size) #729, fix(frontend): status badges use four shapes and conflicting colors per status #737, fix(frontend): RI exchange result is not announced and the exchange ID vanishes after 2s #738, fix(frontend): RI exchange target picker says type a UUID but offers no field #740, fix(frontend): RI exchange reshape empty state can show a stale RI count #1135plans.ts: refactor(frontend): consolidate column-filter popover plumbing into shared lib #18, SUP-03: Two escaping-convention gaps in innerHTML templates (defense-in-depth only) #37, fix(frontend): impossible/placeholder values surfaced for scheduled and pending purchases #181, fix(frontend): filter Clear hides all rows; user/group form validation toast-only #727, fix(frontend): status badges use four shapes and conflicting colors per status #737, fix(frontend): three money formatters, mixed placeholders, left-aligned money columns #739, fix(plans): plan list ramp label ignores percent/interval and disagrees with Edit #1185apikeys.ts: Sweep toISOString().split('T')[0] -> toLocalDateInputValue across frontend date inputs #51, chore(frontend): standardize loading indicators and add slow/empty fallbacks #186, fix(frontend): confirm dialog focuses destructive button, lacks a11y, native dialogs remain #705, fix(frontend): raw transport and developer errors reach users instead of friendly messages #748None of them touch
scaleRecForCapacityor the purchase-modal totals. A later fix for A12-004 (SP capacity scaling) will edit the samescaleRecForCapacityfunction. Those PRs will be opened alongside this one and merged in sequence, with conflicts resolved on the later branch.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