Repository navigation
fix(frontend): make Home and Opportunities agree on Potential Savings - #1328
Conversation
… Savings Both cards sum the same per-cell range, but over different rows. Opportunities drops recommendations for providers switched off in Settings -> Enabled Providers (LeanerCloud#463); Home summed every provider, so with a provider disabled the two same-named KPIs disagreed. Home's "N recommendations" line also came from the backend total, which ignores that setting. Home now reads the config and applies the same filter through a shared helper (lib/enabled-providers.ts), and counts the rows it summed. A config fetch failure falls back to no filter, as on Opportunities. When column filters narrow the Opportunities list, its savings card now says "Filtered: X of Y recommendations", so a deliberately narrower scope is labeled instead of looking like a disagreement. Refs LeanerCloud#180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/dashboard.ts: import block. Kept this branch's filterByEnabledProviders import and main's formatTrendTooltipTitle (LeanerCloud#1323) on the combined chart-utils import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): REQUEST_CHANGES Blocking:
|
…pushed to the server The "Filtered: X of Y" line only appeared when the visible count was below the loaded count. A single-value service or region filter is sent to the server (LeanerCloud#162), so the loaded list is already narrowed, the two counts match, and the card showed a smaller total than Home with no label: the LeanerCloud#180 symptom on every load after the first. The card now decides "filtered" from the active column filters and from the filters the last fetch pushed to the server. When the loaded list was narrowed server-side the unfiltered total isn't known, so the line names the filter instead of inventing a total, e.g. "Filtered: 6 recommendations (Service: ec2)". The e2e recs mock now applies service/region like the backend does. Refs LeanerCloud#180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
On the blocking re-review finding above: fixed in
This branch will merge main again before landing, and that head gets a fresh review. |
Conflicts:
- frontend/src/dashboard.ts:
- imports: kept this branch's filterByEnabledProviders import and all of
main's new imports (upcoming-scope LeanerCloud#1365, sparse-trend helpers LeanerCloud#1334,
chart-a11y LeanerCloud#1381, first-run empty state LeanerCloud#1389).
- loadDashboard Promise.allSettled: combined both sides into five
fetches; this branch's loadEnabledProviders() (LeanerCloud#180) plus main's
listAccountsMinimal() for the first-run onboarding card (LeanerCloud#1389).
- call site: kept renderDashboardSummary(summaryData, recs, recsCount)
and main's renderHomeOnboarding(...). The onboarding check uses the
Enabled-Providers-filtered recs, matching the Opportunities first-run
check, which also runs on the preference-filtered list.
- helpers: kept both loadEnabledProviders() and main's
renderHomeOnboarding(); renderDashboardSummary keeps this branch's
recsCount parameter.
- savings tile detail: this branch's filtered count with main's
NO_VALUE placeholder (LeanerCloud#1376): recsCount ?? total_recommendations ??
NO_VALUE; Active Commitments takes main's NO_VALUE.
- frontend/src/recommendations.ts: import block; main's NO_VALUE added to
the utils import, kept this branch's enabled-providers import.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… savings tile Main's LeanerCloud#1376 replaced the '--' placeholder with NO_VALUE (an em dash). This branch's LeanerCloud#180 fallback test still expected '--' for the savings tile value when the recommendations fetch fails. Refs LeanerCloud#180 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/dashboard.ts: import block only; kept both this branch's filterByEnabledProviders import and main's buildKpiTile import (LeanerCloud#743). renderDashboardSummary merged cleanly: main's buildKpiTile loop now renders this branch's filtered savings range and count. - frontend/src/recommendations.ts: renderRecommendationsSummary. Took main's renderKpiTiles (LeanerCloud#743) instead of the innerHTML cards, and passed this branch's LeanerCloud#180 scope line as the savings tile's `detail` (only when a scope text exists). The detail element gets data-testid="savings-scope" after rendering so the jest and Playwright assertions still find it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 |
Home and Opportunities compute Potential Monthly Savings the same way but over different rows: Opportunities hides recommendations for providers switched off in Settings > Enabled Providers (#463) and applies the user's saved column filters, while Home summed every provider and took its "N recommendations" count from the backend total. This PR makes Home honor Enabled Providers through a shared helper, so both cards show the same figure, and labels the Opportunities card when column filters narrow it.
Refs #180
Partial: the exact disagreement in the review (different ranges for the same 47 recommendations) could not be reproduced on current code; this fixes the two scope differences that exist today.
What changed
frontend/src/lib/enabled-providers.ts:filterByEnabledProviders, permissive when the list is empty or absent. Used by both pages.frontend/src/dashboard.ts:loadDashboardfetches/api/configalongside the other calls (a failure falls back to no filter, as on Opportunities) and filters recommendations before the KPI range and the per-service chart. The savings tile's "N recommendations" now counts the rows it summed, and only falls back to the backend total if the recommendations fetch failed.frontend/src/recommendations.ts:loadRecommendationsuses the shared helper.renderRecommendationsSummarytakes the loaded count and adds a scope line under the savings value whenever a column filter is active (not a selection). When the loaded list is complete it reads "Filtered: X of Y recommendations". A single-value service or region filter is sent to the server (fix(frontend): RI-exchange account chip sends CloudAccount UUID where AWS and Azure handlers expect the provider account ID #162), so the loaded list is already narrowed and the unfiltered total isn't known; then the line names that filter instead of a total, e.g. "Filtered: 6 recommendations (Service: ec2)", plus "+N more filters" for any other active column filters.loadRecommendationsrecords which filters the fetch pushed.src/__tests__/dashboard.test.ts; insrc/__tests__/recommendations.test.ts, scope-line assertions in the existing fix(rekey): any zero-key decrypt failure counts as already re-keyed, so the run succeeds #272 filter test plus a new case where the service filter is sent to the server and the response holds only matching rows; and a new Playwright spectests-e2e/savings-kpi-parity.spec.ts. The e2e recommendations mock (tests-e2e/fixtures/recs.ts) now appliesserviceandregionthe way the backend does.Why this approach
Making Home honor the same Enabled Providers setting fixes the disagreement at its source, and one shared helper keeps the two screens from drifting again. Column filters are a deliberate user choice on Opportunities, so rather than forcing Home to read them (or Opportunities to ignore them), the narrower scope is labeled on the card, which is the issue's second acceptable outcome. Moving the computation server-side would need a backend change (the summary endpoint still sums every variant); this stays frontend-only.
Screenshots
Before is main at
a14dc8f2, after is this branch, both taken before current main was merged in. They are cropped to the KPI cards, which main's later changes (including #1335's per-service chart) don't touch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API, with a fixture of 19 recommendations across AWS, Azure and GCP and Enabled Providers set to AWS only.service=ec2to the server, and the line then reads "Filtered: 6 recommendations (Service: ec2)".Verification
Run on this branch's head
0be9d788, which has main51aa5b13merged in:npm run lint: 0 errors (existing warnings only). Typecheck and build pass.recommendations.test.tsfails without the scope-line change (no label is rendered) and passes with it. Newdashboard.test.tscases: disabled providers dropped before the range and count; empty list means no filter; config fetch failure means no filter and the dashboard still renders; recommendations fetch failure falls back to the backend count.--randomize. A whole-file randomized run ofrecommendations.test.tshas one order-dependent failure in the savings-trend "daily interval" test, which also fails on an unmodified copy of that code.e2e-local.sh): 211 passed, includingsavings-kpi-parity.spec.ts(Home and Opportunities show the same dollar figure and count with Azure and GCP disabled; a column-filtered Opportunities card shows the scope line; after leaving and returning to Opportunities, the reload sendsservice=ec2and the card still shows "Filtered: 6 recommendations (Service: ec2)"). That last test fails without the scope-line change.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Notes for review
GET /api/configper load (Opportunities already does the same).by_service, which does not know about Enabled Providers. Fixing that needs a backend change if it matters.Refsrather thanCloses.Overlaps
chart-utilsimport line, from fix(dashboard): include the year in the savings-trend tooltip title #1323.frontend/src/dashboard.ts(fix(frontend): dashboard charts have no text alternative and layers differ only by shade #744 the per-service chart, fix(frontend): charts rebuilt on every change; dashboard trend has no stale guard #747 chart rebuilds and the trend loader). This branch only touchesloadDashboardandrenderDashboardSummary, so conflicts should be small and textual. The dynamicaria-labelfrom fix(frontend): dashboard charts have no text alternative and layers differ only by shade #744 reads the same filtered recommendations.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