Skip to content

fix(ux/home): re-query Savings-over-time chart on filter chip change - #500

Closed
cristim wants to merge 2 commits into
feat/multicloud-web-frontendfrom
fix/498-home-chart-filter-rerender
Closed

fix(ux/home): re-query Savings-over-time chart on filter chip change#500
cristim wants to merge 2 commits into
feat/multicloud-web-frontendfrom
fix/498-home-chart-filter-rerender

Conversation

@cristim

@cristim cristim commented May 19, 2026

Copy link
Copy Markdown
Member

Summary

The Home tab's "Savings over time" chart rendered identical data across
account selections. The dashboard subscribers fired loadDashboard() on
every filter chip change but lacked the active-tab guard + microtask
coalescing that PR #488 (closes #477) added to Opportunities, so the
provider-change ordering rule from #185 (clear accounts, then set
provider) caused back-to-back redundant reloads, and tab-switching could
produce stale renders.

Fix mirrors PR #488's pattern in dashboard.ts:

  • Active-tab guard: loadDashboard() only runs when #home-tab is
    .active. switchTab('home') reloads on entry, so a filter change
    while the user is on another tab can defer (avoids burning
    /dashboard/summary, /recommendations, /history/analytics
    round-trips for an unseen section).
  • Coalesce duplicate reloads: queueMicrotask collapses the
    account-then-provider subscriber pair (fired from one user action via
    the fix(dashboard): account dropdown change doesn't reliably refresh dashboard data #185 ordering rule in topbar-filters.ts) into one reload. Avoids
    stale-overwrite races if responses arrive out of order.
  • Re-check active-tab inside the microtask: a tab switch between
    chip click and microtask flush cancels the now-unneeded fetch.

URL persistence for provider/account is already global via
topbar-filters.ts (added in PR #488), so Home inherits it for free.

Test plan

New describe('subscriber wiring (issue #498)') in
dashboard.test.ts:

  • Wiring: setupDashboardHandlers registers callbacks with
    state.subscribeProvider and state.subscribeAccount.
  • Account chip change re-queries getSavingsAnalytics when
    #home-tab is active.
  • Provider chip change re-queries getSavingsAnalytics when
    #home-tab is active.
  • Inactive-tab guard: no fetch when #home-tab is not .active.
  • Coalescing: back-to-back provider+account fires trigger exactly
    one reload.
  • Bug-was-fixed: two consecutive account changes produce two
    fetches with distinct account_ids (['acct-A'] then ['acct-B']),
    proving the chart re-queries with fresh data rather than re-rendering
    the same payload.
  • npx tsc --noEmit clean.
  • webpack --mode production clean.
  • Full Jest suite: 36 dashboard tests pass (was 30, +6 new). One
    pre-existing unrelated failure in settings-purchasing-grace.test.ts
    (touches no files in this PR; failing on feat/multicloud-web-frontend
    before this change).
  • Manual verification post-merge: switch accounts on Home tab,
    confirm the Savings-over-time chart re-fetches with new data.

Out of scope (follow-up issues to be filed)

  • /history/analytics backend handler ignores provider query param.
    The frontend currently doesn't pass provider to
    getSavingsAnalytics because the backend doesn't honour it (Athena
    limitation). Switching provider chip clears accounts (per fix(dashboard): account dropdown change doesn't reliably refresh dashboard data #185), which
    effectively resets the chart to "all accounts", so the chart shape
    changes but isn't truly provider-filtered. Backend feature, separate
    ticket.
  • modules/savings-history.ts (Purchases tab's savings-history-chart)
    has the same subscriber-wiring gap and additionally ignores account
    selection entirely. Separate Purchases-tab ticket.

Closes #498.

Summary by CodeRabbit

  • Bug Fixes

    • Dashboard no longer reloads or re-queries charts when the Home tab is inactive.
    • Account switching now triggers distinct API requests with correct account information.
  • Performance

    • Multiple provider/account changes are coalesced into a single reload, reducing redundant API calls and visual flicker.
    • Queued reloads are cancelled if Home becomes inactive before they run.
  • Tests

    • Added tests covering subscriber wiring, coalescing behavior, and reload cancellation.

Review Change Stack

The Home tab's Savings-over-time chart rendered identical data across
account selections because the dashboard subscribers fired loadDashboard()
on every filter change without an active-tab guard or coalescing, mirroring
a gap the Opportunities page had before PR #488 (closes #477).

Apply the same pattern to dashboard.ts:

- Active-tab guard: only fire loadDashboard() when #home-tab is active.
  switchTab('home') already reloads on entry, so a filter change while the
  user is on Opportunities/Plans/Purchases can defer; this avoids burning
  /dashboard/summary, /recommendations, and /history/analytics round-trips
  for a section the user is not looking at.

- Microtask coalescing: topbar-filters.ts updates BOTH state slots on a
  provider chip change (clear accounts then set provider, per the #185
  ordering rule), firing both subscribers from one user action. Without
  coalescing this would trigger two loadDashboard() calls back-to-back
  plus a stale-overwrite risk if responses race. queueMicrotask runs once
  after the synchronous state-mutation chain settles.

- Re-check the active-tab flag inside the microtask so a tab-switch
  between chip click and microtask flush skips the now-unneeded fetch.

URL persistence is already global via topbar-filters.ts (PR #488); Home
inherits it for free.

Test coverage (dashboard.test.ts, new describe 'subscriber wiring (#498)'):

- Wiring assertion: setupDashboardHandlers registers callbacks with
  state.subscribeProvider and state.subscribeAccount.
- Account chip change re-queries getSavingsAnalytics when home-tab is
  active.
- Provider chip change re-queries getSavingsAnalytics when home-tab is
  active.
- Inactive-tab guard: no fetch when home-tab is not .active.
- Coalescing: back-to-back provider+account fires trigger one reload.
- Bug-was-fixed: two consecutive account changes produce two fetches
  with distinct account_ids ([A] then [B]), proving the chart re-queries
  with fresh data rather than the same payload.

Closes #498.
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4b234a6-c3c6-4029-a601-2faf30ae9099

📥 Commits

Reviewing files that changed from the base of the PR and between 4220ef6 and 8ae19d3.

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

📝 Walkthrough

Walkthrough

This PR fixes Issue #498 by gating dashboard reloads to when the Home tab is active and coalescing consecutive provider/account state changes into a single microtask-scheduled reload to avoid redundant api.getSavingsAnalytics calls and stale chart overwrites.

Changes

Home tab active gating and subscriber coalescing

Layer / File(s) Summary
Home tab active check and subscriber coalescing implementation
frontend/src/dashboard.ts
Introduces isHomeTabActive() helper to check the Home tab's active CSS class, then refactors setupDashboardHandlers to skip reloads when Home is inactive and uses queueMicrotask with a reloadQueued flag to coalesce multiple provider/account state change notifications into a single loadDashboard() execution per microtask flush.
Subscriber wiring validation and microtask coalescing tests
frontend/src/__tests__/dashboard.test.ts
Adds Issue #498 test suite that imports setupDashboardHandlers(), asserts subscriber registration with state.subscribeProvider/state.subscribeAccount, verifies chart re-queries only when the Home tab is active, confirms consecutive provider+account changes coalesce into one reload (and queued reloads cancel if Home deactivates before flush), and checks distinct getSavingsAnalytics requests with correct account_ids when switching accounts.

Sequence Diagram

sequenceDiagram
  participant StateProvider as State Provider
  participant Handler as setupDashboardHandlers
  participant ActiveCheck as isHomeTabActive()
  participant Microtask as queueMicrotask
  participant Dashboard as loadDashboard()
  participant API as api.getSavingsAnalytics

  StateProvider->>Handler: provider/account change notification
  Handler->>ActiveCheck: check Home tab active
  ActiveCheck-->>Handler: boolean result
  alt Home tab is active
    Handler->>Microtask: scheduleReload (queueMicrotask)
    StateProvider->>Handler: additional change (same microtask)
    Handler-->>Microtask: reloadQueued true → skip duplicate
    Microtask->>Dashboard: flush → call loadDashboard()
    Dashboard->>API: getSavingsAnalytics(account_ids)
    API-->>Dashboard: chart data
  else Home tab not active
    Handler-->>Handler: bail out, no scheduling
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #503: Implements similar subscription wiring, active-tab guard, and microtask coalescing for the Purchases/savings-history module — shares the same objectives and patterns as this PR.

Possibly related PRs

  • LeanerCloud/CUDly#190: Addresses ordering and state handling for provider/account changes within reload logic; related to correct account_ids handling.
  • LeanerCloud/CUDly#488: Wired tab-scoped subscriber behavior for Opportunities page; similar active-tab gating and subscriber wiring patterns.

Poem

🐰 A dashboard that's wise, now waits for Home's light
Microtasks gather, then call once—oh so right
Provider and account hop in a queue
One fetch at a time, the graph shows what's true
Hooray—no flicker, no duplicate view!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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 clearly and concisely describes the main fix: implementing chart re-querying on filter chip changes in the Home tab.
Linked Issues check ✅ Passed The PR fully addresses #498 by implementing subscriber wiring with active-tab guards and microtask coalescing for chart re-queries. Issue #477 is related but separate; this PR correctly focuses on Home rather than Opportunities.
Out of Scope Changes check ✅ Passed All changes are in-scope: dashboard.ts adds active-tab guard and coalescing logic; dashboard.test.ts adds regression tests for subscriber wiring and microtask behavior, all directly supporting #498 objectives.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/498-home-chart-filter-rerender

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

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect triaged Item has been triaged bug Something isn't working labels May 19, 2026
@cristim

cristim commented May 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 19, 2026

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

1 similar comment
@coderabbitai

coderabbitai Bot commented May 19, 2026

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

784-903: ⚡ Quick win

Add one regression test for “active at fire-time, inactive at microtask flush.”

Current tests validate active/inactive at callback time, but not the mid-flight tab switch that Line 77–81 specifically guards. Adding that case would lock in the cancellation behavior.

Suggested test addition
+    test('cancels queued reload when Home becomes inactive before microtask flush', async () => {
+      const tab = document.getElementById('home-tab')!;
+      tab.classList.add('active');
+
+      setupDashboardHandlers();
+      const accountCb = (state.subscribeAccount as jest.Mock).mock.calls[0]?.[0] as () => void;
+
+      (api.getDashboardSummary as jest.Mock).mockClear();
+      (api.getSavingsAnalytics as jest.Mock).mockClear();
+
+      accountCb();               // queued while active
+      tab.classList.remove('active'); // user switches tabs before flush
+
+      await new Promise((r) => setTimeout(r, 0));
+      await new Promise((r) => setTimeout(r, 0));
+
+      expect(api.getDashboardSummary).not.toHaveBeenCalled();
+      expect(api.getSavingsAnalytics).not.toHaveBeenCalled();
+    });
🤖 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__/dashboard.test.ts` around lines 784 - 903, Add a
regression test that exercises the “active at fire-time, inactive at microtask
flush” path: write a new test using setupDashboardHandlers() to grab the
subscriber callback (state.subscribeAccount or state.subscribeProvider), ensure
the `#home-tab` has .active, call the subscriber (e.g., accountCb()), then
synchronously remove the .active class from the `#home-tab` before yielding to the
microtask/macrotask flush (the two await new Promise(r => setTimeout(r,0)) calls
used elsewhere), and finally assert that api.getSavingsAnalytics and
api.getDashboardSummary were NOT called; place the test alongside the other
dashboard tests and follow the same mockClear/await pattern so it verifies the
mid-flight tab-switch cancellation guarded by setupDashboardHandlers.
🤖 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.

Nitpick comments:
In `@frontend/src/__tests__/dashboard.test.ts`:
- Around line 784-903: Add a regression test that exercises the “active at
fire-time, inactive at microtask flush” path: write a new test using
setupDashboardHandlers() to grab the subscriber callback (state.subscribeAccount
or state.subscribeProvider), ensure the `#home-tab` has .active, call the
subscriber (e.g., accountCb()), then synchronously remove the .active class from
the `#home-tab` before yielding to the microtask/macrotask flush (the two await
new Promise(r => setTimeout(r,0)) calls used elsewhere), and finally assert that
api.getSavingsAnalytics and api.getDashboardSummary were NOT called; place the
test alongside the other dashboard tests and follow the same mockClear/await
pattern so it verifies the mid-flight tab-switch cancellation guarded by
setupDashboardHandlers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 706a14e8-f391-456a-a9f1-5fe88fb19662

📥 Commits

Reviewing files that changed from the base of the PR and between 4220ef6 and 358aeb2.

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

Address CodeRabbit nitpick on PR #500: lock in the inside-microtask
active-tab re-check by exercising the path where the chip change fires
while Home is active but the user switches tabs before the microtask
flushes. The existing tests verified active/inactive at callback time
but not the mid-flight cancellation guarded by lines 77-81 of
dashboard.ts.
@cristim

cristim commented May 19, 2026

Copy link
Copy Markdown
Member Author

Addressed CR nitpick: added regression test covering the inside-microtask active-tab re-check (chip fires while Home active, user switches tab before microtask flushes, fetch is cancelled). 37 dashboard tests pass.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat frontend/src/dashboard.ts
echo "---FILE SEPARATOR---"
cat frontend/src/__tests__/dashboard.test.ts

Length of output: 71855


@cristim: The newly added regression test for the inside-microtask active-tab re-check is well-constructed — removing .active synchronously after accountCb() but before the microtask flushes correctly exercises the deferred cancellation path. The 37-test count and the six new cases all look sound. Here is the full review.


frontend/src/dashboard.ts

isHomeTabActive() — the optional-chaining + === true comparison is the right idiom; avoids both a null-dereference when #home-tab is absent and a truthy-shortcut that could misfire on a non-Boolean.

setupDashboardHandlers() — the closure-scoped reloadQueued flag, the entry-guard (!isHomeTabActive() || reloadQueued), the flag reset before the re-check inside the microtask, and the symmetrical subscribeProvider/subscribeAccount wiring all look correct.

One minor observation: subscribeProvider and subscribeAccount both return unsubscribe functions (the mock is mockReturnValue(() => {})), but setupDashboardHandlers discards the return values:

state.subscribeProvider(scheduleReload);  // return value dropped
state.subscribeAccount(scheduleReload);   // return value dropped

If setupDashboardHandlers is ever called more than once (e.g. hot-module reload, a test that doesn't fully isolate module state), listeners accumulate silently. A defensive capture would make the function safe to call repeatedly:

export function setupDashboardHandlers(): void {
  let reloadQueued = false;
  const scheduleReload = (): void => { /* ... */ };
  const unsubProvider = state.subscribeProvider(scheduleReload);
  const unsubAccount  = state.subscribeAccount(scheduleReload);
  setupSavingsTrendHandlers();
  // Return teardown so callers that need it can clean up.
  return () => { unsubProvider(); unsubAccount(); };
}

This is not a blocker if the app guarantees single initialization, but the pattern is worth aligning with if subscribeAccount/subscribeProvider already return unsubscribers.

loadSavingsTrendChart — multi-account silent omission (pre-existing, not introduced here): when 0 or 2+ accounts are selected the account_ids parameter is omitted and the backend returns all-accounts data. The comment documents that the backend only accepts a single account_id, so this is intentional, but selecting two accounts produces the same chart as selecting none. A follow-up backend ticket (mentioned in the PR description) will address this.


frontend/src/__tests__/dashboard.test.ts

The six new tests in describe('subscriber wiring (issue #498)') are correct:

Test What it verifies
registers callbacks subscribeProvider and subscribeAccount each called once with a function
account chip / active getDashboardSummary + getSavingsAnalytics each called once
provider chip / active getSavingsAnalytics called once
inactive-tab guard neither API called when .active absent
coalescing back-to-back provider+account fires produce exactly one fetch each
in-microtask re-check (new) getDashboardSummary/getSavingsAnalytics both suppressed when tab deactivated before microtask flushes
regression / distinct accounts two sequential chip changes → two distinct getSavingsAnalytics calls with acct-A then acct-B

The getCurrentAccountIDs mock in the regression test is set up with exactly three mockReturnValueOnce entries per account, which matches the three call sites reached per loadDashboard invocation (getDashboardSummary, getRecommendations, loadSavingsTrendChart). That accounting is correct.

The double await new Promise(r => setTimeout(r, 0)) flush pattern correctly drains the microtask queue and then the awaited Promise.allSettled chain — no concern there.


Summary: the implementation is solid and the test coverage is thorough. The only actionable item is the unsubscribe-return-value pattern; everything else is either correct or a pre-existing upstream concern. Happy to approve once you've considered the unsubscribe point (or confirmed single-initialization is guaranteed by the call site).

@cristim

cristim commented May 19, 2026

Copy link
Copy Markdown
Member Author

The nitpick is addressed: commit 8ae19d3 adds a regression test that exercises exactly the mid-flight tab-switch cancellation path (active at callback time, inactive before microtask flush). The test matches the suggested structure in the review comment.

@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

CodeRabbit review triage (single nitpick):

  • Nitpick (frontend/src/__tests__/dashboard.test.ts): "Add a regression test for active-at-fire-time, inactive-at-microtask-flush." — Addressed. This is already covered by the test cancels queued reload when Home becomes inactive before microtask flush (commit 8ae19d36c, lines 861-884), which queues the reload while Home is active, synchronously removes the active class before the microtask flushes, and asserts neither getDashboardSummary nor getSavingsAnalytics fire. The commit landed ~2 min after this review was submitted, so it predates CodeRabbit's incremental re-review.

No actionable items outstanding. Full dashboard suite green (37 passed). Pre-merge "Docstring Coverage" warning is non-blocking and unrelated to the diff's TS test/source.

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@cristim: Confirmed — the triage is accurate. The cancels queued reload when Home becomes inactive before microtask flush test (commit 8ae19d36c, lines 861–884) fully covers the mid-flight tab-switch cancellation path that was flagged. With the 37-test suite green and no outstanding actionable items, triggering the full review now.

✅ Actions performed

Full review triggered.

cristim added a commit that referenced this pull request May 22, 2026
…578)

The Purchases-tab Savings History chart (modules/savings-history.ts) only
wired its local period dropdown and refresh button, so changing the global
Provider or Account topbar chip did not refresh it (the tab had to be left
and re-entered), and it ignored the account filter entirely.

Mirror the PR #500 / #488 pattern:
- isPurchasesTabActive() guards the reload so a chip change while the user
  is on another tab defers (switchTab reloads on entry).
- queueMicrotask coalesces the account-then-provider subscriber pair fired
  from one user action (the #185 ordering rule), collapsing two reloads
  into one and avoiding stale-overwrite races.
- loadSavingsHistory() now forwards the selected account_id (single-select,
  matching the backend) and the provider chip to getSavingsAnalytics. The
  provider param is honoured by the backend once #502 lands; until then
  account_ids does the filtering.

Adds regression tests covering subscriber wiring, the active-tab guard,
coalescing, account_id passthrough, and two consecutive account changes
producing distinct fetches. Updates app.test.ts's state mock to expose the
new subscribe getters since it exercises the real savings-history module.

Closes #503
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

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

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

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

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

Superseded by #747 (already merged into feat/multicloud-web-frontend as 3931e9e). The isHomeTabActive() guard, queueMicrotask coalescing, and subscriber wiring are all present on the base branch. Closing to declutter.

@cristim cristim closed this May 27, 2026
cristim added a commit that referenced this pull request May 27, 2026
Address CodeRabbit nitpick on PR #500: lock in the inside-microtask
active-tab re-check by exercising the path where the chip change fires
while Home is active but the user switches tabs before the microtask
flushes. The existing tests verified active/inactive at callback time
but not the mid-flight cancellation guarded by lines 77-81 of
dashboard.ts.
@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working 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-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant