Skip to content

fix(frontend): stop rendering placeholder values on pending purchases - #1329

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

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

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

A pending execution with no recommendations comes back from /api/history with count 0, term 0, "0 commitment(s)", provider/region "multiple" and $0 costs, and the Purchases page (Approval queue and Purchase History) printed those as data. A real mixed-term basket also showed "0 Years". On Plans, a pending scheduled purchase past its date showed a plain "Pending" badge with no sign it missed its run. This PR guards those renders and flags overdue planned purchases with a next action.

Closes #181

What changed

  • Purchases (e63a94b9): new frontend/src/lib/purchase-row-guards.ts (hasNoCommitments for count 0, isRealTerm, isPastScheduledDate, and the shared "No recommendations attached" copy). In frontend/src/history.ts, both the Purchase History table and the Approval queue render a no-commitment row with muted dashes for provider, service, region, count, term, payment, costs and savings, and "No recommendations attached" in the Type (history) or Service (queue) cell. A 0 term renders as a dash everywhere, including the Term column-filter labels. Real rows keep main's upfront cell with its "(in monthly)" note (chore(frontend): low money-path findings from the 2026-09-02 audit #318); a no-commitment row gets a muted dash in its place.
  • Plans (23a900a1): frontend/src/plans.ts renderPlannedPurchaseRow adds an "Overdue" badge (the existing status-badge badge-danger classes, as on plan cards) next to Pending/Notified when scheduled_date is before today, plus a hint "Not run on its date. Run now or pause." (or just "Not run on its date." without run permission). frontend/src/styles/tables.css adds .status-badges and .overdue-hint.
  • Tests: src/__tests__/purchases-placeholders-181.test.ts and src/__tests__/plans-overdue-181.test.ts.

Why this approach

The backend contract for these synthetic rows is deliberate (handler_history.go comments say a 0 term should render blank), so the guard belongs in the renderer, as the issue asks ("guard the render regardless"). count == 0 is the reliable marker: synthetic rows set count to the number of recommendations, and a real purchase never has 0. formatTerm itself is unchanged (a unit test pins formatTerm(0) == '0 Years' and other callers pass real terms); the call sites that can see 0 are guarded instead. The overdue check compares YYYY-MM-DD text against the local date to avoid the UTC off-by-one that new Date('YYYY-MM-DD') causes. On Plans, "Various (Multiple)", 0 count and $0 for no-recommendation steps and the "0 Years" term were already handled by #609 and the existing term > 0 guard.

Screenshots

Screenshots were taken before this branch was merged with main; before = main at a14dc8f. After is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API.

Before After
purchases-empty-execution before purchases-empty-execution after
plans-overdue before plans-overdue after
  1. /purchases/history with an empty pending execution, a mixed-term basket and a normal row. Before: the empty execution shows "Multiple", Count 0, "0 commitment(s)", "0 Years" and $0 in both the Approval queue and Purchase History, and the mixed-term basket shows "0 Years". After: the empty execution shows dashes and "No recommendations attached" in both tables, the mixed-term basket keeps its real count and costs with a dash for term, and the normal row is unchanged.
  2. /plans planned purchases with one pending step dated 2026-09-15 (past) and one in 2027. Before: both show only "Pending". After: the past step shows "Pending" plus "Overdue" with "Not run on its date. Run now or pause."; the future step is unchanged.

Verification

Run on this branch's head 686288c2, from frontend/:

  • Merged with main at 5b52c4e; conflicts in frontend/src/history.ts resolved, see the merge commit.
  • npm run lint: 0 errors. Typecheck and build pass.
  • Jest: 3873 of 3874 passed, 1 skipped.
  • New purchases-placeholders-181.test.ts (6): the helpers including date edge cases; history and Approval queue rows driven through loadHistory with the real formatters; the mixed-term basket.
  • New plans-overdue-181.test.ts (3): overdue vs future pending, the copy without run permission, paused rows not flagged.
  • Before the merge, both new suites passed with --randomize.
  • Playwright via e2e-local.sh: 200 passed.
  • Secrets scan: clean, planted key flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.

Notes for review

  • Copy to check: "No recommendations attached" (reused from Plans, fix(plans): a plan with no matching recommendations schedules zero-count purchases instead of failing #609), the "Overdue" badge, and the hint "Not run on its date. Run now or pause."
  • No-commitment rows still offer Approve/Cancel in the Approval queue. Whether an empty basket should be approvable is a backend/policy question and is not changed here.
  • Overdue applies to pending and notified rows, not paused ones. Main's plan-card isPlanOverdue parses the date as UTC midnight, so in UTC-negative zones it can say "Overdue" on the scheduled day while the row here does not. Having isPlanOverdue reuse isPastScheduledDate would align them; not changed here.
  • The Home upcoming-purchases widget is not changed; the issue does not name it.
  • The issue's source document (docs/ui-ux-review.md) is no longer in the repo, so the fix follows the issue text.
  • Split into two commits to keep each under 400 changed lines.

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 2 commits October 23, 2026 22:27
A pending execution that carries no recommendations comes back from
/api/history as count 0, term 0, "0 commitment(s)", provider/region
"multiple" and $0 costs, and the Approval queue and Purchase History
tables printed those as data ("0 Years", "0", "Multiple", "$0"). A real
mixed-term basket (term collapsed to 0) also showed "0 Years", because
formatTerm only blanks null/NaN.

Rows with no commitments now render dashes and a "No recommendations
attached" label (the same copy Plans uses since LeanerCloud#609), and a 0 term
renders as a dash, including in the Term column-filter labels. The
guards live in a small lib module so the already-large history.ts only
gains call sites.

Refs LeanerCloud#181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scheduled purchase whose date had passed without running still showed
a plain "Pending" badge, which reads as on schedule. Pending/notified
rows past their date now get an "Overdue" badge next to the status and a
short next-action hint ("Not run on its date. Run now or pause."); users
without run permission see only "Not run on its date." The date check
compares calendar days so a date-only value is not shifted by UTC
parsing. Paused rows are not flagged.

Refs LeanerCloud#181

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged urgency/this-quarter Within the quarter impact/many Affects most users effort/s Hours type/bug Defect labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

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>
@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 686288c29966df9f79b848b065d2ee9aedf873cf

Blocking:

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 686288c2 merged onto current main (11b372a8; merge-tree 77f0b010; 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 dc090cc2, main plus 17 PRs) passes npm ci, lint, typecheck, build, jest (1 skipped, 4097 passed, 4098 total), and the full Playwright suite (232 passed).
  • CI is green on 686288c2 (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 19:00
@IAmJSD
IAmJSD merged commit 49c6d47 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 686288c2.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
Conflicts:
- frontend/src/history.ts (Purchase History and approval-queue rows):
  kept main's no-commitments handling (LeanerCloud#1329: hasNoCommitments, dashOr,
  NO_RECS_HTML, termCell) and this branch's num classes on the
  count/upfront/monthly/savings cells and NO_VALUE for the missing monthly
  cost, created-by, account and payment cells. The upfront placeholder for
  an empty row also gets class num, matching upfrontCell.
- frontend/src/recommendations.ts (sortable header): kept main's LeanerCloud#755
  header structure (plain <th> with aria-sort, sort-btn and filter button
  siblings) and added this branch's num class for numeric columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
… main added

Main's LeanerCloud#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 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>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
…Retry confirms

Main's LeanerCloud#181 guards (LeanerCloud#1329) stopped the Approval queue and History rows
from printing the placeholder values of an execution with no
recommendations: count 0, "0 commitment(s)", "multiple" provider/service/
region and $0 costs. The Cancel and Retry confirm summary added for LeanerCloud#728
still printed them, so a row reading "No recommendations attached" opened
a confirm reading "multiple 0 commitment(s) in multiple ... Upfront: $0".

The summary now uses NO_RECOMMENDATIONS_LABEL for such a row and leaves
out upfront, service and region, drops a "multiple" region the same way
as a "multiple" provider, and only shows a term isRealTerm accepts.

Tests: Cancel and Retry on the EMPTY_PENDING fixture shape, plus a real
row checked with the real currency formatter ("Upfront: $4,200"). The
Cancel describe is renamed to the Approval queue, where Cancel now lives.

Refs LeanerCloud#728

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit that referenced this pull request Oct 24, 2026
…#1369)

* fix(frontend): fix stale and inconsistent admin copy

The header said "Logout" (a noun) where it means "Log out". The
Purchasing policies tab opened on a panel headed "Purchasing Settings",
and the service overrides dialog pointed at "Settings → Purchasing",
a path that no longer exists. The add-override dialog said "Only AWS
services are supported today" although Azure and GCP overrides are
supported, and the Azure Workload Identity Federation option claimed it
uses an RSA certificate assertion, contradicting the secret-free OIDC
federated credential the form actually configures.

Refs #728

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

* fix(frontend): pause toasts while hovered or focused, lengthen override Undo

Toasts kept counting down while the pointer was over them or focus was
on their buttons, so a long message or an Undo action could vanish
mid-read (WCAG 2.2.1 Timing Adjustable). The countdown now pauses on
hover and on focus inside the toast, and resumes with the time that
was left (at least one second) when both leave. The override-delete
Undo toast goes from 5 s to 10 s so there is time to reach Undo.

The 30 s default timeout is unchanged: it was set as an explicit owner
choice, so shortening success toasts or making errors sticky is left
for the owner to decide.

Refs #728

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

* fix(frontend): name the purchase in History Cancel and Retry confirms

"Cancel this pending purchase?" and "Retry this failed purchase?" did
not say which row they applied to, so in a long table it was easy to
confirm the wrong one. The confirm now opens with a summary of the row
(provider, service, resource and count, region, account, term, upfront
cost and date) above the existing explanation, built by a small helper
module so history.ts does not grow further.

Adds a Playwright spec for the named confirm and for a toast staying
while hovered.

Refs #728

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

* fix(frontend): refresh the ladder timeline after kill-switch and config save

The planned-purchases timeline captured the configs and the global
kill-switch state when it was mounted, so after enabling laddering or
saving an account config it kept saying "Ladder planning is disabled"
or "No ladder configuration" until a page reload. ladder.ts now calls
refreshLadderTimeline() after both, which updates the mounted
timeline's inputs and reloads it while keeping the selected provider,
account and filters.

Refs #728

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

* fix(frontend): click Cancel in the Approval queue in the #728 e2e

Main's #1331 made the Approval queue the only place to approve or cancel a
pending purchase; the History table now shows a 'Review in queue' link
instead. The confirm-summary and toast-hover spec still clicked Cancel in
#history-list, so it timed out after the merge. It now clicks the queue's
Cancel button. findHistoryRow already falls back to the queue's rows, so
the confirm still names the purchase.

Refs #728

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

* fix(frontend): use the "Log out" label in tests merged from main

Main's crosstab-storage-clear.spec.ts (#317) waited for a button named
"Logout", which this branch renames to "Log out". Update it, the jest
fixtures in auth.test.ts and mobile-nav.test.ts, and the control names in
mobile-header-drawer.spec.ts so no test refers to the old label.

Refs #728

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

* fix(frontend): apply #181 no-commitments guards to Cancel/Retry confirms

Main's #181 guards (#1329) stopped the Approval queue and History rows
from printing the placeholder values of an execution with no
recommendations: count 0, "0 commitment(s)", "multiple" provider/service/
region and $0 costs. The Cancel and Retry confirm summary added for #728
still printed them, so a row reading "No recommendations attached" opened
a confirm reading "multiple 0 commitment(s) in multiple ... Upfront: $0".

The summary now uses NO_RECOMMENDATIONS_LABEL for such a row and leaves
out upfront, service and region, drops a "multiple" region the same way
as a "multiple" provider, and only shows a term isRealTerm accepts.

Tests: Cancel and Retry on the EMPTY_PENDING fixture shape, plus a real
row checked with the real currency formatter ("Upfront: $4,200"). The
Cancel describe is renamed to the Approval queue, where Cancel now lives.

Refs #728

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/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(frontend): impossible/placeholder values surfaced for scheduled and pending purchases

1 participant