Skip to content

fix(frontend): honest purchase-modal Eff. % and resilient view tables - #1327

Merged
IAmJSD merged 2 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-136
Oct 24, 2026
Merged

IAmJSD merged 2 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-136

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

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 scales on_demand_cost by the capacity ratio, so the per-row cell, the totals and the fan-out/variant pricing paths (pricedCellVariant) all pick it up.
  • updatePurchaseModalTotals uses a new weightedDisplaySavingsPct(recs): it averages each row's displaySavingsPct, weighted by its on-demand baseline (on_demand_cost when known, otherwise the baseline implied by the row's own %), skips rows that show an em-dash, and returns null (rendered as an em-dash) when no row contributes.
  • loadBulkPurchaseState accepts 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 to number | null and render a neutral em-dash for unknown, mirroring the active-RI table, instead of throwing in toFixed and styling unknown as red. Also drops the dead MODE_VALUES and the void that hid it from lint (A12-068).
  • frontend/src/apikeys.ts: an isoOrEmpty() title and -- cell for an unparseable last_used_at, instead of a RangeError that hid every key.
  • frontend/src/plans.ts: formatRampProgress renders -- unless total_steps is a positive number (was "0/1 steps"), like the Term cell beside it.
  • frontend/src/index.html: the API Docs link gets rel="noopener noreferrer".
  • Tests in recommendations.test.ts, riexchange.test.ts, apikeys.test.ts, plans.test.ts and html.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's rel attribute. 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's savings_percentage is preferred over the reconstruction. Scaling on_demand_cost in the shared scaleRecForCapacity fixes 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.

Before After
purchase-modal-capacity before purchase-modal-capacity after
reshape-missing-util before reshape-missing-util after
apikeys-bad-timestamp before apikeys-bad-timestamp after
plan-progress before plan-progress after
  • Purchase modal at 50% capacity: the m5.large row (savings $900, on-demand $1,900) showed 23.7% and Totals showed 27.9% even though the c5.large row is an em-dash. After: the row shows 47.4%, and Totals shows 47.4% from the rows it can price.
  • Reshape: one row without utilization_percent replaced the whole panel with "Cannot read properties of undefined (reading 'toFixed')". After: both rows render, and the unknown utilization is a neutral em-dash.
  • API keys: one malformed last_used_at gave "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.
  • Plans: a plan with no total_steps showed a fabricated "Progress 0/1 steps". After: Progress shows --.

Verification

Run on this branch's head bdaa21f2 on top of main a14dc8f2:

  • Lint: 0 errors (142 warnings, same as base). Typecheck and build clean.
  • Jest: 109 suites, 3506 passed, 1 skipped. The new tests (-t 136, 13 tests) pass with --randomize. Full-file --randomize of the recommendations/riexchange/plans suites also fails on unmodified main (e.g. seed 2130656337 gives 33 failures in base recommendations.test.ts), so that order dependence predates this branch.
  • New tests: Totals skips an em-dash AWS row (50.0% instead of 57.1%), shows an em-dash when nothing contributes, weights provider savings_percentage, still includes the Azure reconstruction; Eff. % is invariant at 25/50/75/100% capacity with on_demand_cost scaled 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 bad last_used_at keeps both rows and both Revoke buttons; Progress shows "2/4 steps" or --; every target=_blank link in index.html has rel.
  • Playwright: 157 passed. No new spec, since these are data-rendering fixes covered at the unit level.
  • git-secrets scan clean (planted key flagged); test-git-secrets-allowlist.sh: 137 pass.

Notes for review

  • Totals Eff. % is now an on-demand-weighted average of the per-row displayed %. Rows with a provider savings_percentage but no on_demand_cost are 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.
  • The stored-capacity parse now sends out-of-range or fractional values back to 100. Previously values were clamped (-5 became 1).
  • The API-key title tooltip 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.
  • The screenshots show a "Failed to load users and groups" toast; that comes from the screenshot fixture's mocked user list and is not a regression.
  • Left out (open, needing design decisions or larger work): stale-response generation guards on loadRecommendations/loadHistory/loadPlans/loadConvertibleRIs (plus A12-011); the dead savingsMin === null branch and pageLevelRange nullability (plus the A12-023 docstring); CSS.escape in users/userList.ts and plans.ts selectors; NaN instead of ?? 0 in 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 dead users/* 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-tree check against every other ui branch found no textual conflicts. Branches that edit the same files:

None of them touch scaleRecForCapacity or the purchase-modal totals. A later fix for A12-004 (SP capacity scaling) will edit the same scaleRecForCapacity function. 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

IAmJSD and others added 2 commits October 24, 2026 01:24
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>
@IAmJSD IAmJSD added effort/l Weeks type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/many Affects most users labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 10 billable files and costs up to $2.50.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 61be5b6 and bdaa21f.

Files selected for processing: 10
  • frontend/src/__tests__/apikeys.test.ts
  • frontend/src/__tests__/html.test.ts
  • frontend/src/__tests__/plans.test.ts
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/__tests__/riexchange.test.ts
  • frontend/src/apikeys.ts
  • frontend/src/index.html
  • frontend/src/plans.ts
  • frontend/src/recommendations.ts
  • frontend/src/riexchange.ts
  • 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): APPROVE bdaa21f217ae76af5a2761f96ad8870ea601ff2f

Blocking:

  • none
    Non-blocking:
  • frontend/src/apikeys.ts:63-78: isoOrEmpty was inserted between renderApiKeysListError's JSDoc and that function, so the old doc block now sits orphaned above isoOrEmpty's JSDoc.
  • frontend/src/plans.ts:1088-1100: same problem. formatRampProgress landed between the "renderPlanCard generates..." comment block and renderPlanCard.
  • The Totals Eff. % now silently covers only rows with a known %, with no partial-coverage marker. The PR notes this; a footnote would be a reasonable follow-up.
  • "Refs chore(frontend/views): medium/low findings from the 2026-07-28 full review #136" with a partial note is correct for this tracker.
    Attacked: Recomputed weightedDisplaySavingsPct against effectiveSavingsPct/displaySavingsPct: AWS rows without on_demand_cost are excluded, provider savings_percentage uses the implied baseline, and negative and zero edge cases hold. Checked that the existing test was adjusted to Azure only to keep the reconstruction path, not to hide a regression. Confirmed on_demand_cost is a per-rec total: the backend purchase_pricing.go:155 scales it by the same count ratio, so scaling it in scaleRecForCapacity is consistent for the modal and pricedCellVariant. Checked that capacity is always persisted via parseInt (so the stricter integer parse cannot discard real user values), that formatRelativeTime returns '' for Invalid Date (so '--' renders), that total_steps is at least 1 on every backend create path, that MODE_VALUES had no other references, and that the escapeHtml coverage on changed interpolations is intact.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at bdaa21f2 merged onto current main (e3f2062a; merge-tree 76cf7fae; Linux, Node, headless Chromium):

  • This branch's own checks passed at this head: lint, typecheck, build, the full jest suite, the full Playwright e2e suite, and the git-secrets checks (see Verification in the PR body).
  • The combined tree of every UI PR in this merge wave (tree 89dd34d8, main plus 46 PRs) passes npm ci, lint, typecheck, build, jest (1 skipped, 3914 passed, 3915 total), and the full Playwright suite (208 passed).
  • CI is green on bdaa21f2 (CI Success, Run pre-commit hooks).
  • The independent review above approves this exact head.
  • Nothing was applied to any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 24, 2026 15:47
@IAmJSD
IAmJSD merged commit 52454bb into LeanerCloud:main Oct 24, 2026
29 checks passed
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
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 bdaa21f2.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
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>
IAmJSD added a commit that referenced this pull request Oct 24, 2026
…#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/l Weeks impact/many Affects most users priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant