Skip to content

feat(frontend/recs): summary cards narrow on selection + drop redundant banner (closes #278 #279)#283

Merged
cristim merged 6 commits into
feat/multicloud-web-frontendfrom
fix/recs-ui-polish-followup
May 5, 2026
Merged

feat(frontend/recs): summary cards narrow on selection + drop redundant banner (closes #278 #279)#283
cristim merged 6 commits into
feat/multicloud-web-frontendfrom
fix/recs-ui-polish-followup

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 5, 2026

Summary

Closes #278, closes #279. First slice of the post-#275 UX polish — rest
of the issues (#280 chevron, #281 action-summary, #282 Payment column +
drop bulk dropdown) coming as additional commits on this branch as I
work through them.

#279 — summary cards narrow on selection in real time
The four cards on the Recommendations header now all recompute client-
side from the same source on every list rerender:

  • Source = selected ∩ visible when ≥1 row is ticked, else
    visible.
  • All four cards are range-shaped where applicable (savings, upfront,
    payback) — cell-by-cell min/max sums, not flat variant sums.
  • Card titles flip from "Total/Potential …" to "Selected …" on the
    narrow so the user knows why the number moved.
  • "Showing X of Y" filter-status line surfaces the selection count too:
    1 selected · Showing 2 of 2 recommendations.

Real-time updates land for free: the existing checkbox change handler
already calls renderRecommendationsList(...) on every toggle, which
calls renderRecommendationsSummary (from #272 follow-up).

#278 — drop redundant "Recommended range" banner
Same data is now front-and-centre on the savings card; the banner
under the table is just visual noise. Removed.

Test plan

  • summary cards narrow to selection in real time (closes #279)
    4 recs / 2 cells; asserts no-selection (range), single-tick (collapse
    to single value, title flips to "Selected …"), double-tick (sum of
    two selected variants).
  • "Showing X of Y" surfaces selection count (closes #279).
  • page-level range banner is removed (closes #278) — replaces
    the earlier "banner appears" assertion.
  • npm test -- --testPathPattern=recommendations — 149 / 149
    pass.
  • npm run typecheck — clean.

Followups in scope on this PR (incoming commits)

🤖 Generated with claude-flow

Summary by CodeRabbit

  • New Features

    • Summary cards switch between "Total" and "Selected" metrics and show min–max savings, upfront cost, and payback ranges.
    • Payment column added to bundle tables with header, sorting, human-readable labels, and filtering.
  • Improvements

    • Filter status bar announces selected count; live "Showing X of Y" prefixes with "N selected".
    • Removed redundant "Recommended range" banner; header is canonical.
    • UI refinements: chevron placement and expanded-state behavior; bottom action summary shows dollar impacts and global bulk Payment dropdown removed.

…loses #278 #279)

Two related fixes that together turn the Recommendations summary
header into a real-time projection of the user's current intent:

**#279 — all four cards + filter-status line narrow on selection**

Previously only the Potential Monthly Savings card recomputed
client-side from the visible set (#272). The other three (Total
Recommendations, Total Upfront Cost, Payback Period) still rendered
the API's flat `summary.*` fields, which sum every (term, payment)
variant of every cell — overstating achievable totals by ~6× on a
typical fan-out, the same bug class #272 closed for savings alone.

Now every card on the header recomputes from the same source on every
list rerender:
  - Source = `selected ∩ visible` if ≥1 row is ticked, else
    `visible`.
  - All four values are cell-by-cell range sums (savings, upfront,
    payback) — the user buys at most one variant per cell, so the
    achievable total is bounded by `sum(cell.{min,max})` not
    `sum(every variant)`.
  - When narrowing by selection, card titles flip from
    "Total/Potential" to "Selected" so the user knows why the number
    moved. Card title switches automatically on the toggle.

Real-time updates: the existing checkbox change handler at
`recommendations.ts:2643` already calls `renderRecommendationsList(...)`
on every selection toggle, and `renderRecommendationsList` calls
`renderRecommendationsSummary` (from #272 follow-up). So selection
changes refresh the cards without any new wiring.

The "Showing X of Y" filter-status line now also surfaces the
selection count: "1 selected · Showing 2 of 2 recommendations".

`pageLevelRange()` extended with `upfrontMin/Max`, `paybackMonthsMin/
Max` (cell-by-cell range sums; payback months = upfront / savings,
clamped to 0 on zero-savings to avoid Infinity). `formatPaybackRange`
helper added for the new range-shaped Payback Period card.

The unused `summary` arg on `renderRecommendationsSummary` is renamed
`_summary` for now — it can be dropped from the function signature
(and the `RecommendationsSummary` field references) in a follow-up,
but keeping the call-site shape stable for this PR.

**#278 — drop redundant "Recommended range" banner**

The banner under the table (`<div class="rec-range-banner">`) showed
"Recommended range: $X – $Y/mo across N cells" — same data the
Potential Monthly Savings card now renders prominently in the header.
Removed.

**Tests added**:
- `summary cards narrow to selection in real time (closes #279)` —
  builds 4 recs / 2 cells, asserts cards reflect the full visible set
  with no selection (savings $300–$400, upfront $0–$3000, count 2),
  narrow to a single rec on tick (savings $150, upfront $1000, count
  1), and sum two selected variants on second tick. Card titles flip
  from "Total/Potential ..." to "Selected ..." on the narrow.
- `"Showing X of Y" surfaces selection count (closes #279)` — pins
  the filter-status line's selection-aware copy.
- `page-level range banner is removed (closes #278)` — replaces the
  earlier "banner appears" assertion; verifies the banner DOM node is
  gone and the savings card carries the range.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c43ef81-bb93-4c98-9a42-8807c3da9042

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR moves header summary computation client-side: header cards (count, savings, upfront, payback) are computed from visible recommendations or the intersection of selected∩visible, the under-table “Recommended range” banner is removed, a Payment column/filter is added, the global bulk Payment dropdown is removed, and tests updated to cover these behaviors.

Changes

Selection-Driven Summary, Table, and Actions

Layer / File(s) Summary
Types / API
frontend/src/recommendations.ts
Adds exported PageLevelRange type (savingsMin/savingsMax, upfrontMin/upfrontMax, paybackMonthsMin/paybackMonthsMax, cellCount).
Range Computation
frontend/src/recommendations.ts
pageLevelRange() expanded to compute min/max envelopes for savings, upfront, and payback by aggregating per-cell envelopes; formatPaybackRange() added.
Rendering / Wiring
frontend/src/recommendations.ts
renderRecommendationsSummary() now computes cards from selected ∩ visible when non-empty (otherwise visible); cards display min–max ranges and switch titles to “Selected …” when applicable; removes .rec-range-banner; filter status ARIA live includes selected count.
Table Columns / Row Markup
frontend/src/recommendations.ts, frontend/src/state.ts
Adds payment to RecommendationsColumnId; introduces Payment column rendering, display labels (PAYMENT_DISPLAY_LABELS + formatPayment), updates TABLE_COL_COUNT and column sort/filter wiring to include payment and term.
Bottom Action Box
frontend/src/recommendations.ts
Removes global bulk Payment dropdown; bottom action box now shows financial impact (min–max savings/upfront) computed from the same target set (selected∩visible or visible).
Styles
frontend/src/styles/components.css
Adjusts .rec-cell-chevron to larger, centered inline-flex layout with padding and min-dimensions.
Tests
frontend/src/__tests__/recommendations.test.ts
Extends coverage: asserts summary-card narrowing and title changes on selection, “Showing X of Y” includes selection prefix when applicable, Payment column header/cells/sort/filter behavior, removal of .rec-range-banner, multi-variant cell chevron placement and aria-expanded transitions, bottom action box text updates, and a regression test ensuring per-cell payback min uses paired-variant logic.

Sequence Diagram(s)

(omitted — conditions for sequence diagrams are not met)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #278: ux(frontend/recs): drop redundant 'Recommended range' banner under the table — this PR removes .rec-range-banner as requested.
  • #279: feat(frontend): summary cards + 'Showing X of Y' narrow on selection in real time — this PR implements selection-driven narrowing and live-status updates.

Possibly related PRs

Poem

🐰 A hop, a click, the headers change,
Selected rows now narrow range.
The banner’s gone, the chevron’s bright,
Payments show and filters bite.
A rabbit cheers — the view’s polite.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: summary cards narrowing on selection and removing the redundant banner, which are the primary objectives of #278 and #279.
Linked Issues check ✅ Passed The PR implements both #278 (banner removal) and #279 (selection-driven narrowing) requirements: card recomputation, selection visibility, filter-status updates, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed Changes are scoped to recommendations module (header cards, filter status, banner removal, test updates, styling). Payment column and dropdown changes (from #282) align with related issues referenced in PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/recs-ui-polish-followup

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

@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/feat New capability type/chore Maintenance / non-user-visible labels May 5, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
frontend/src/__tests__/recommendations.test.ts (1)

241-328: ⚡ Quick win

Exercise the checkbox-driven rerender path in the #279 tests.

These assertions swap the selection mock and call loadRecommendations() again, so they only re-test initial load with different state snapshots. They would still pass if the checkbox change handler stopped rerendering the cards/live text in real time. Prefer toggling the rendered checkbox and asserting the DOM updates from that event path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/__tests__/recommendations.test.ts` around lines 241 - 328, The
tests currently flip selection by changing the
state.getSelectedRecommendationIDs mock and re-calling loadRecommendations(),
which skips the real checkbox change handler path; instead, after the initial
loadRecommendations() call, find the rendered checkbox element corresponding to
a recommendation (use the DOM input[type="checkbox"] whose value or data
attribute equals the rec id, e.g. for 'cell1-pricey'), simulate a user
toggle/click on that checkbox to trigger the actual change handler, then assert
the summary cards/live text update; repeat for the second checkbox to test
cumulative selection. Keep loadRecommendations() only for the initial render and
do not replace the state.getSelectedRecommendationIDs mock between assertions so
the DOM event path is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/recommendations.ts`:
- Around line 196-202: Replace the current global-selection logic with a
visible-selection intersection: compute selectedVisible by filtering
recommendations with state.getSelectedRecommendationIDs() (e.g., const selected
= state.getSelectedRecommendationIDs(); const selectedVisible =
recommendations.filter(r => selected.has(r.id));), then set target =
selectedVisible.length > 0 ? selectedVisible : recommendations, compute groups
via groupRecsByCell(target) and plr = pageLevelRange(groups), and set
isSelectionView = selectedVisible.length > 0 && plr.cellCount > 0; apply the
same change to the other occurrence referenced (lines 1296-1303) so both
surfaces derive from the selectedVisible slice rather than the global selected
set.
- Around line 359-392: The current pageLevelRange() computes payback by summing
savingsMin/savingsMax and upfrontMin/upfrontMax independently, which can produce
unattainable cross-combinations; change it to compute attainable total
(upfront,savings) pairs across cells and derive payback from those. Concretely:
in pageLevelRange(), for each group (variants: LocalRecommendation[]), build the
per-variant (upfront, savings) pairs (you can use the same metrics used in
cellSummary()), then perform a fold/convolution across groups starting from a
single total (0,0) and for each cell add every variant to every existing total
to produce the new set of achievable totals; after processing all groups compute
paybackMonthsMin/paybackMonthsMax from the achievable totals (upfront_total /
savings_total) for totals with savings_total>0, clamping to 0 when no positive
savings exist. Keep the other aggregates (savingsMin/Max, upfrontMin/Max) if
needed for summary cards but compute payback only from the attainable totals
computed above; update pageLevelRange to return those payback values.

---

Nitpick comments:
In `@frontend/src/__tests__/recommendations.test.ts`:
- Around line 241-328: The tests currently flip selection by changing the
state.getSelectedRecommendationIDs mock and re-calling loadRecommendations(),
which skips the real checkbox change handler path; instead, after the initial
loadRecommendations() call, find the rendered checkbox element corresponding to
a recommendation (use the DOM input[type="checkbox"] whose value or data
attribute equals the rec id, e.g. for 'cell1-pricey'), simulate a user
toggle/click on that checkbox to trigger the actual change handler, then assert
the summary cards/live text update; repeat for the second checkbox to test
cumulative selection. Keep loadRecommendations() only for the initial render and
do not replace the state.getSelectedRecommendationIDs mock between assertions so
the DOM event path is exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7ad49a4f-50b2-44a6-9960-611f12ffef06

📥 Commits

Reviewing files that changed from the base of the PR and between bb619f2 and 0b6f3b5.

📒 Files selected for processing (2)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts

Comment thread frontend/src/recommendations.ts Outdated
Comment thread frontend/src/recommendations.ts Outdated
cristim added 3 commits May 5, 2026 11:31
…ction count (closes #281)

The bottom-action-box summary line used to read "(1 selected)" — the
selection count is the least useful info at this point, since the
user can already see selection state from the row checkboxes and the
visible count is in the filter status bar.

Surface the *financial* impact of the current action target instead:
"($100/mo · $500 upfront across 1 cell)". Same source-of-truth as the
summary cards above (selected ∩ visible if ≥1 selected, else the
visible set), via `pageLevelRange` on the target's cell groups. The
action box is prime real estate for the dollar figures the user is
about to authorise — purchase / plan flows commit to these numbers.

Empty / no-selection states keep their previous copy:
  - 0 loaded → "(No recommendations loaded)"
  - 0 visible → "(0 visible — adjust filters)"
  - 0 selected → "(Select cells to act on)" (#273 prompt)

Updated the existing test that pinned the old "1 selected" copy to
assert the new format ($/mo + upfront + N cell{s}).
…umn (closes #280)

Move the rec-cell-chevron button from inside the summary-content td into
the checkbox-col td so it occupies the same column as variant row
checkboxes. Increase hit target to 24x24 px minimum (font-size 1.2em,
padding 4px, min-width/min-height 24px, display inline-flex). colspan on
the summary-content td is unchanged (TABLE_COL_COUNT - 4 = 8) because the
chevron was an inline child of that cell, not a separate td. Add a test
asserting the button is a descendant of td.checkbox-col and update the
expand test to also verify aria-expanded toggles on click.
…tion narrowing + attainable payback

Finding 1 — visible-selection narrowing (renderRecommendationsSummary,
renderFilterStatusBar):
- Summary cards now derive `selectedVisible = recommendations ∩ selected`
  (where `recommendations` is the already-filtered visible set passed in
  from `renderRecommendationsList`). Only when `selectedVisible.length > 0`
  do the cards switch to selection view; selections hidden by a column
  filter no longer drive card values or the "Selected …" title.
- `renderFilterStatusBar` intersects `state.getVisibleRecommendations()`
  with `state.getSelectedRecommendationIDs()` for the live-region "N
  selected" prefix. A selection that is entirely filtered out of view
  produces no prefix, matching the card behaviour.

Finding 2 — attainable payback range (pageLevelRange):
- Replaced the independent cross-extrema formula
  (upfrontMin / savingsMax) with per-cell paired variant selection.
  For each cell, payback-best picks the variant with the lowest
  upfront/savings ratio; payback-worst picks the highest. Sums of the
  paired (upfront, savings) pairs across cells produce bounds that
  correspond to purchase combinations the user can actually make.
  Full convolution is O(variants^cells) and impractical for pages with
  30+ cells; per-cell paired choice is O(cells × variants). The
  docstring explains the tradeoff for future readers.
- savingsMin/Max and upfrontMin/Max keep their independent cross-extrema
  (each is rendered on its own card with no cross-combinatorial concern).

Tests (+2, 150 → 152):
- "selection filtered out of view: cards and live line treat it as no
  selection" — column filter hides the selected row; asserts cards show
  the visible-set range and live line omits "N selected".
- "paybackMonthsMin uses per-cell paired variants, not cross-extrema" —
  two cells where the lowest-upfront variant ≠ the highest-savings
  variant; asserts the new formula produces the correct attainable ratio
  and not the old misleading one.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

Two CR findings addressed:

  1. Visible-selection narrowing (recommendations.ts:~196 + ~1300) — renderRecommendationsSummary and renderFilterStatusBar now derive a selectedVisible = recommendations.filter(r => selected.has(r.id)) slice and only enter selection-view / surface the "N selected" prefix when that intersection is non-empty. A selection hiding behind a column filter no longer collapses the cards or contradicts the "selected ∩ visible" semantics described in the PR.

  2. Attainable payback range (recommendations.ts:~359 pageLevelRange) — payback months no longer cross-combine upfrontMin / savingsMax across cells. For each cell we pick the variant minimising upfront/savings (best payback) and the one maximising it (worst), then sum those paired (upfront, savings) values across cells. The final ratio is from an attainable per-cell-variant combination the user could actually buy. Docstring explains why we didn't do the full O(variants^cells) convolution CR's prompt suggested — per-cell pairing is O(cells × variants) and produces correct attainable bounds.

Tests: 150 → 152 (focused regressions for both findings; both fail on the old code).

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim Thanks for the detailed summary of both fixes — the implementations look well-reasoned:

  1. Visible-selection narrowing — The selectedVisible intersection correctly enforces the selected ∩ visible invariant in both renderRecommendationsSummary and renderFilterStatusBar, so a selection obscured by a column filter no longer incorrectly collapses the cards. Clean and minimal.

  2. Attainable payback range — The per-cell pairing approach (O(cells × variants)) is a sound and practical solution. Picking the best/worst payback variant within each cell before summing avoids the false extremes that arise from cross-combining upfrontMin / savingsMax across different cells. The docstring rationale for not doing the full O(variants^cells) convolution is a valuable addition for future maintainers.

The two new regression tests that fail on the old code are a good safety net. Triggering a fresh review now.

[review]

… dropdown (closes #282)

Part A — Drop bulk Payment dropdown from bottom action box.
Each rec carries its own payment_option from the API fan-out, and the
per-cell radio enforcement caps purchase to one variant per cell, so a
global toolbar override was misleading and redundant. The `payment` field
is kept on BulkPurchaseToolbarState for the fan-out modal's override/fallback
logic (seeded from GlobalConfig; no longer persisted to localStorage).

Part B — Add sortable + filterable Payment column to the Recommendations table.
New column appears between Term and Monthly Savings. Raw values are capitalised
for display (all-upfront → All Upfront, etc.); missing values render as em-dash.
Wired into FILTERABLE_COLUMNS, SORTABLE_STRING_COLUMNS, categoricalCellValue,
numericCellValue, and SORT_HEADER_LABELS. TABLE_COL_COUNT updated 12 → 13.

Tests: +4 new (Payment column header/sort, cell label rendering, categorical
filter); updated 3 existing (column count 11 → 12, bottom-box dropdown
assertion inverted, filter-button column list updated).
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

Commit ba222f6 — closes #282

Two coordinated changes landed in a single commit:

Part A — Drop bulk Payment dropdown
Removed the <select id="bulk-purchase-payment"> from the bottom action box. Each rec carries its own payment_option from the API fan-out, and per-cell radio enforcement caps purchase to one variant per cell, so the global toolbar override was misleading. The payment field is retained internally on BulkPurchaseToolbarState (seeded from GlobalConfig, not persisted to localStorage) so the fan-out modal's override/fallback logic is unchanged.

Part B — Add Payment column
New sortable + filterable column between Term and Monthly Savings. Capitalises raw values for display (all-upfront → All Upfront, etc.); missing values render as —. Wired into FILTERABLE_COLUMNS, SORTABLE_STRING_COLUMNS, categoricalCellValue, numericCellValue, SORT_HEADER_LABELS. TABLE_COL_COUNT updated 12 → 13.

Tests: 155 passing (net +3 new: Payment column header/sort, cell label rendering, categorical filter; 3 existing updated: column count 11 → 12, bottom-box dropdown assertion inverted, filter-button column list).

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim Thanks for the detailed walkthrough of both parts. The approach is sound overall — a few things I'll be watching for in the review:

  1. Column count discrepancy — The commit description says TABLE_COL_COUNT was updated 12 → 13, but the test-update note mentions "column count 11 → 12". This is likely a different assertion (e.g., a rendered-visible count vs. the constant), but worth confirming the two numbers refer to different things and neither is stale.

  2. Payment value capitalisation coverageall-upfront → All Upfront is shown as the example; I'll verify the capitalisation logic covers all known payment_option values the API can return (e.g. partial-upfront, no-upfront, any others) and that the fallback is hit correctly for truly absent values vs. empty-string.

  3. BulkPurchaseToolbarState.payment retention — Retaining the field (seeded from GlobalConfig, not persisted) is clean for the fan-out modal. I'll confirm no UI path accidentally surfaces or re-enables the removed dropdown's value in an unexpected way post-removal.

Triggering a full review now.

[review]

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

@coderabbitai review

Re-ping for the full review on commits 6369745f6 (CR pass-1 fixes) and ba222f605 (Payment column + drop dropdown, closing #282). The watch-items preview at 09:54Z noted three things to confirm; pre-emptively audited:

  • Column count: TABLE_COL_COUNT = 13 (the <th> count: checkbox col + 12 data cols, used by colspan math). Test labelled "Bundle B + feat(frontend/recs): drop bulk Payment dropdown + add Payment column to table #282: 12 columns" asserts the 12 sortable data columns separately. 1 + 12 = 13 — consistent.
  • Payment value capitalisation: PAYMENT_DISPLAY_LABELS (recommendations.ts:1669) covers all 4 known values (all-upfront / partial-upfront / no-upfront / monthly). undefined / '' falls through to em-dash; unknown values fall through to the raw string (lookup ?? payment at :1678).
  • BulkPurchaseToolbarState.payment retention: type still has payment (:1873); loadBulkPurchaseState reads from cachedGlobalDefaultPayment (seeded from GlobalConfig.default_payment); saveBulkPurchaseState only persists capacity. No UI surface re-exposes the value.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim Thanks for the pre-emptive audit — all three watch items check out cleanly:

  • Column count: The 1 + 12 = 13 split (checkbox + 12 data cols for TABLE_COL_COUNT, separate assertion for the 12 sortable data cols) is consistent and unambiguous.
  • Payment capitalisation: Full coverage of all 4 known API values via PAYMENT_DISPLAY_LABELS, with the safe ?? payment raw-string fallback for any future unknowns, and the em-dash correctly catching undefined/''.
  • BulkPurchaseToolbarState.payment retention: Seeded from GlobalConfig.default_payment, only capacity persisted — no risk of the removed dropdown's value resurfacing anywhere.

Triggering the full review now across both commits.

[review]

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/src/__tests__/recommendations.test.ts (1)

241-328: ⚡ Quick win

Exercise the real selection event path here, not just a fresh load.

These cases swap getSelectedRecommendationIDs() and call loadRecommendations() again, so they only verify the narrowed cards/live text on a full rerender. They won’t catch the regression this PR is really guarding against: row-checkbox or select-all changes failing to update the summary/filter-status in place. I’d add at least one assertion that dispatches a checkbox change after the initial render and verifies the cards + live line update without reloading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/__tests__/recommendations.test.ts` around lines 241 - 328, The
test currently toggles selection by mocking state.getSelectedRecommendationIDs
and re-calling loadRecommendations(), but the reviewer asks to exercise the
actual DOM event path so checkbox changes update the summary in-place; update
the test (functions: loadRecommendations, document.querySelector /
.recommendations-filter-live, and the cardValue/cardTitle helpers) to after the
initial render find a row checkbox (or the select-all checkbox), change its
checked state and dispatch a 'change' event on that input, then assert the cards
(cardValue(/Recommendations/), cardValue(/Monthly Savings/),
cardValue(/Upfront/)) and the live text update accordingly without calling
loadRecommendations() again; ensure you exercise both selecting a single row
checkbox and toggling the select-all to cover incremental and bulk selection
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/state.ts`:
- Around line 16-19: The sort-state type is missing the new 'payment' column and
must be aligned with the column union: update RecommendationsSortColumn (or
wherever the sort union is declared) to include 'payment' (or replace the
explicit union with RecommendationsColumnId) so setRecommendationsSort({ column:
'payment', ... }) is type-safe; locate the declaration referenced by
RecommendationsSortColumn and add 'payment' to its union or refactor it to reuse
RecommendationsColumnId.

---

Nitpick comments:
In `@frontend/src/__tests__/recommendations.test.ts`:
- Around line 241-328: The test currently toggles selection by mocking
state.getSelectedRecommendationIDs and re-calling loadRecommendations(), but the
reviewer asks to exercise the actual DOM event path so checkbox changes update
the summary in-place; update the test (functions: loadRecommendations,
document.querySelector / .recommendations-filter-live, and the
cardValue/cardTitle helpers) to after the initial render find a row checkbox (or
the select-all checkbox), change its checked state and dispatch a 'change' event
on that input, then assert the cards (cardValue(/Recommendations/),
cardValue(/Monthly Savings/), cardValue(/Upfront/)) and the live text update
accordingly without calling loadRecommendations() again; ensure you exercise
both selecting a single row checkbox and toggling the select-all to cover
incremental and bulk selection paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 781e5e0e-c021-410b-ad03-853579b75d2a

📥 Commits

Reviewing files that changed from the base of the PR and between 0b6f3b5 and ba222f6.

📒 Files selected for processing (4)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
  • frontend/src/state.ts
  • frontend/src/styles/components.css
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/recommendations.ts

Comment thread frontend/src/state.ts
Comment on lines 16 to 19
export type RecommendationsColumnId =
| 'provider' | 'account' | 'service' | 'resource_type' | 'region'
| 'count' | 'term' | 'savings' | 'upfront_cost'
| 'count' | 'term' | 'payment' | 'savings' | 'upfront_cost'
| 'monthly_cost' | 'effective_savings_pct';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Align the sort-state type with the new Payment column.

This PR adds payment as a first-class recommendations column here, and the updated tests now drive setRecommendationsSort({ column: 'payment', ... }), but RecommendationsSortColumn above still excludes it. That leaves the sort API out of sync with the UI and forces unsafe casts around the new header path. At minimum, payment should be added to the sort union in the same pass; ideally the union should match the full set of sortable headers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/state.ts` around lines 16 - 19, The sort-state type is missing
the new 'payment' column and must be aligned with the column union: update
RecommendationsSortColumn (or wherever the sort union is declared) to include
'payment' (or replace the explicit union with RecommendationsColumnId) so
setRecommendationsSort({ column: 'payment', ... }) is type-safe; locate the
declaration referenced by RecommendationsSortColumn and add 'payment' to its
union or refactor it to reuse RecommendationsColumnId.

…nion + selection-event tests

**Sort union (state.ts)**
Refactored `RecommendationsSortColumn` to alias `RecommendationsColumnId`
rather than maintaining a separate union. The old union was missing `'payment'`
(and the string columns `provider`, `account`, `service`, `resource_type`,
`region`) added in PR #282/#283. The alias approach keeps the two in sync
automatically so any future column addition never requires a second edit.
The stale `'payback'` member (never a real table column) is removed; it was
unused at all call sites.

**Selection DOM event tests (recommendations.test.ts)**
Added two new tests that exercise the real checkbox `change` event path
without calling `loadRecommendations()` a second time:
- `row checkbox change event updates summary cards in place` — fires a
  `change` on a single variant's checkbox, asserts that
  `renderRecommendationsList` updates cards in-place. Uses single-variant
  cells (distinct `resource_type`) so checkboxes are directly in the DOM.
- `select-all change event updates summary cards in place` — fires `change`
  on `#select-all-recs`, asserts `pickBestVariantPerCell` is called once per
  cell and the Recommendations card shows 2 selected.

Both tests wire `addSelectedRecommendation`/`removeSelectedRecommendation`
mocks to mutate a shared `Set` that `getSelectedRecommendationIDs` reads
back, mirroring the pattern already in use for the evict-siblings tests.
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 5, 2026

CR pass-2 fixes (commit 31a55a5)

Finding 1 — sort union missing payment (state.ts)

Refactored RecommendationsSortColumn to alias RecommendationsColumnId rather than maintaining a separate union. The old declaration was missing 'payment' plus the other string columns added in #282. Aliasing keeps them permanently in sync — any future column addition only needs one edit. The stale 'payback' member (never an actual table column, unused at all call sites) was removed in the same change.

Finding 2 — exercise the real DOM event path (recommendations.test.ts)

Added two tests that dispatch real change events without a second loadRecommendations() call:

  1. row checkbox change event updates summary cards in place — renders single-variant cells (distinct resource_types so checkboxes land directly in the DOM, not hidden behind a collapsed cell-group chevron), dispatches change on one row checkbox, asserts cards update in place.

  2. select-all change event updates summary cards in place — dispatches change on #select-all-recs, asserts addSelectedRecommendation is called once per cell (via pickBestVariantPerCell) and the Recommendations card reflects the bulk selection.

Both tests wire addSelectedRecommendation/removeSelectedRecommendation mocks to mutate a shared Set that getSelectedRecommendationIDs reads back — the same coordination pattern already used in the evict-siblings tests.

Test count: 155 → 157 (+2). All pass. tsc --noEmit clean.

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

Tip

For best results, initiate chat on the files or code changes.

@cristim Thanks for the detailed walkthrough of both fixes. They look well-reasoned:

Finding 1 — RecommendationsSortColumn alias:
The alias approach is the cleanest long-term solution — one edit point for future columns, and the stale 'payback' removal is good hygiene. One thing I'll verify in the review: since RecommendationsColumnId now encompasses all column IDs (including non-sortable ones like the checkbox column), I'll confirm that any code path which previously relied on the narrower RecommendationsSortColumn type for compile-time safety (e.g., rejecting a non-sortable column as a sort key) is still adequately guarded at runtime by SORTABLE_STRING_COLUMNS / SORT_HEADER_LABELS. If the type was the only gate, widening it to the full union is a minor type-safety trade-off worth noting.

Finding 2 — Real DOM event path tests:
Dispatching real change events without re-invoking loadRecommendations() is the correct way to exercise the event-handler wiring rather than simulating a full reload. The detail of using distinct resource_types to surface checkboxes directly in the DOM (bypassing collapsed cell-group chevrons) is a thoughtful test setup. The shared-Set mock coordination is consistent with the evict-siblings pattern already in the suite, so it will be familiar to future test authors.

Triggering the full review now.

[review]

@cristim cristim merged commit df29074 into feat/multicloud-web-frontend May 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/chore Maintenance / non-user-visible type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant