Skip to content

fix(frontend): low money-path display and SP coverage input fixes - #1345

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

IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-318

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

The 2026-09-02 audit grouped four low-severity money-path findings in this issue; three were still live on main. The Marketplace listing dialog rounded list price, AWS fee and net proceeds to whole dollars separately so they did not add up, the history tables showed the full upfront next to an amortized Monthly Cost that already includes it, and the per-Savings-Plan coverage inputs saved any number (for example 12.7). This PR addresses those three; the fourth (A12-044, the "Run now" dialog with no amount) is already addressed on main by #781 (6248db25).

Closes #318

What changed

  • 943422b4 (A11-006), frontend/src/settings.ts: a new invalidEditedCardCoverages() helper runs inside saveGlobalSettings next to the other numeric guards, before any PUT. Each edited SP coverage card must be a whole number from 0 to 100. If one is not, the save stops with the global field's toast text plus the card's heading, e.g. "(Compute Savings Plans)".
  • f67dc499 (A12-058), frontend/src/history.ts Marketplace dialog: list price and net proceeds are rounded to cents, the fee is list minus net, and all three (and the per-unit values) are shown with 2 digits.
  • ec847dfc (A12-059), frontend/src/history.ts: a new upfrontCell() helper, used by both Purchase History and the Approval Queue, renders the Upfront cell muted with a small "(in monthly)" line and the title "Included in the amortized monthly cost" when amortize is on and the upfront was actually folded in (monthly cost, upfront > 0 and term are known). The amortize flag is read once per render instead of once per row. New .upfront-in-monthly and .upfront-in-monthly-note classes in styles/components.css.
  • A12-044: no change; plans.ts runNowBody on main already shows the plan, count, term, payment and upfront.

Why this approach

  • Coverage: only edited cards are checked, using the same "edited" test the save already uses to decide what to send. An old fractional value already stored therefore cannot block every later save, and empty cards keep their current fallback to the stored value. The check runs before the global PUT, so a bad card never leaves a half-applied save.
  • Marketplace: the issue suggested digits: 2 on all three rows, but three separately rounded numbers can still be a cent apart (e.g. 100.505). Deriving the fee from the two rounded totals guarantees they reconcile, and the fee is the least important of the three.
  • Amortize: the issue asked for a muted cell with a title. A title only shows on hover, so a short visible "(in monthly)" note is added too. The Approval Queue has the same double-count and gets the same treatment.

Screenshots

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

Before After
amortized-upfront before amortized-upfront after
marketplace-cents before marketplace-cents after
sp-coverage before sp-coverage after
  • Amortized upfront: before, Upfront $1,200 at full weight next to Monthly Cost (amortized) $200, which already includes it. After, the Upfront cell is muted with an "(in monthly)" note and an explanatory title.
  • Marketplace dialog: before, list $143, fee $17, net $125, which do not add up. After, list $142.50, fee $17.10, net $125.40, which reconcile to the cent.
  • SP coverage 12.7: before, the inline hint shows but Save still sends 17 PUTs and toasts "Settings saved successfully". After, Save is blocked (0 PUTs) with "Target coverage must be a whole number between 0 and 100 (Compute Savings Plans)". The overlay shows the PUT count captured by the scenario.

Verification

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

  • Lint: 0 errors (142 warnings, all already on main). Typecheck and build pass.
  • Jest: 109 suites, 3494 passed, 1 skipped.
  • New jest tests: settings.test.ts +5 (12.7, -40 and 500 abort with no PUT and an error toast; 0 and 100 are saved), history.test.ts +2 (amortize on/off: Upfront cell class, title and text in both tables; monthly $200 vs $100), history-marketplace-sell-button.test.ts +1 (real formatter: $142.50 / $17.10 / $125.40, no $143). The new tests pass under --randomize. Some existing tests in settings.test.ts and history.test.ts are order-dependent and fail with some seeds; the same failures occur on a14dc8f2 in a clean worktree.
  • Playwright (e2e-local.sh): 158 passed, including the new tests-e2e/amortized-upfront.spec.ts (toggles amortize and checks the Upfront cell's class, title and note).
  • Secrets scan clean (planted key flagged); scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.

Notes for review

  • Copy to check: the "(in monthly)" note and title "Included in the amortized monthly cost"; the coverage toast "Target coverage must be a whole number between 0 and 100 ()".
  • Behavior change: Save on the Purchasing panel now refuses fractional or out-of-range SP card coverage. Before, fractional values were saved silently and -40/500 failed at the backend PUT.
  • An edited but emptied SP card still falls back to the stored value (unchanged), unlike the global field, which rejects empty input.
  • Out of scope: the sibling dialog defect LeanerCloud/cloud-commitments-cli#1951 (the list price rounds where the backend floors).
  • settings.ts (3,800+ lines) and history.ts (1,900+) are already far over the 500-line guideline; this adds about 30 lines to each rather than starting a split.

Overlaps

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 3 commits October 24, 2026 01:07
The four per-Savings-Plan coverage inputs sit outside the settings form,
so native min/max validation never runs for them and the inline hint
only paints a message. Save accepted any finite number, so a fractional
value like 12.7 passed the backend's 0..100 range check and was stored.

Apply the global coverage field's whole-number 0..100 rule to every
edited SP card before any PUT is sent, and abort the save with a toast
naming the offending card. Untouched and empty cards keep their existing
fallback behavior.

Refs LeanerCloud#318

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The marketplace listing dialog rounded the list price, AWS fee and net
proceeds to whole dollars independently, so the lines did not add up: a
$100.50 listing showed $101 = $12 + $88.

Render all three with cents and derive the fee from the rounded list and
net totals, so fee + net always equals the list price shown.

Refs LeanerCloud#318

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the amortize toggle on, Purchase History and the Approval Queue
showed the full upfront next to a Monthly Cost that already includes it,
and only the Monthly Cost header disclosed this. Upfront plus 12 times
Monthly then double-counted the upfront.

When the upfront is spread into the monthly figure, mute the Upfront
cell, add an "(in monthly)" note and an explanatory title. Also read the
amortize flag once per render instead of once per row.

Refs LeanerCloud#318

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/eventually No deadline impact/few Limited audience effort/s Hours type/bug Defect 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 7 billable files and costs up to $1.75.

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 ec847df.

Files selected for processing: 7
  • frontend/src/__tests__/history-marketplace-sell-button.test.ts
  • frontend/src/__tests__/history.test.ts
  • frontend/src/__tests__/settings.test.ts
  • frontend/src/history.ts
  • frontend/src/settings.ts
  • frontend/src/styles/components.css
  • frontend/tests-e2e/amortized-upfront.spec.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 ec847dfcb3be6300335590c8e61e499da5f19772

Blocking:

  • none
    Non-blocking:
  • frontend/src/history.ts:1600: the per-unit price is rounded on its own (listPricePerUnit, 2 digits). With count > 1, per-unit times count can differ from the shown total by a cent, for example "$47.51/unit ($142.52 total for 3 units)" when the exact values are 47.505 and 142.515.
  • frontend/src/settings.ts:3449: an edited-then-emptied SP card still silently keeps the stored value, unlike the global field (the body already says so).
    Attacked: Checked that upfrontCell's "folded" condition matches exactly when amortizedMonthly adds the upfront (term > 0, upfront non-null, monthly non-null). Checked that only static strings go into the new td/title, so there is no XSS. Checked that the SP coverage ids are in TRACKED_FIELDS, so savedSnapshot comparisons are real and an old fractional value cannot block every save. Grepped history.ts for DOM-text or CSV reads of the Upfront cell and found none. Confirmed A12-044 is already fixed on main (runNowBody uses buildCommitmentSummary), so Closes chore(frontend): low money-path findings from the 2026-09-02 audit #318 is justified.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at ec847dfc merged onto current main (512a7dee; merge-tree eb21ffaf; 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 ec847dfc (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:53
@IAmJSD
IAmJSD merged commit 2450ffe 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 ec847dfc.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
Conflicts:
- frontend/src/history.ts (Purchase History row and Approval queue row):
  kept LeanerCloud#181's no-commitment guards (dashOr/MUTED_DASH, NO_RECS_HTML in the
  Type/Service cell, termCell(p) dash for a 0 term) and main's changes
  (count escaped with a null fallback from LeanerCloud#1318, upfrontCell(p, amortize)
  with the "(in monthly)" note from LeanerCloud#1345, amortize hoisted out of the row
  loop). A no-commitment row renders a muted dash <td> instead of
  upfrontCell; real rows use upfrontCell unchanged.

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/s Hours impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/bug Defect urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(frontend): low money-path findings from the 2026-09-02 audit

1 participant