Skip to content

[AI] feat: add an experimental balance forecast report#7310

Open
samaluk wants to merge 5 commits intoactualbudget:masterfrom
samaluk:001-balance-forecast-report
Open

[AI] feat: add an experimental balance forecast report#7310
samaluk wants to merge 5 commits intoactualbudget:masterfrom
samaluk:001-balance-forecast-report

Conversation

@samaluk
Copy link
Copy Markdown

@samaluk samaluk commented Mar 28, 2026

Description

Adds an experimental Balance Forecast report and dashboard widget so users can project future balances from scheduled transactions without leaving Actual. This makes it easier to see upcoming balance trends, spot the forecasted low point, and inspect the projection at monthly or daily granularity.

This PR includes:

  • forecast generation on the backend with RPC wiring
  • a new Balance Forecast report page
  • a new dashboard card for the forecast
  • report routing and dashboard widget registration
  • an experimental feature flag to gate the UI until it is ready for broader rollout
  • tests covering the report, widget registration, routing, and forecast generation

Related issue(s)

  • Relates to the experimental balance forecast feature work

Testing

Manual:

  • enabled the experimental Balance Forecast Report flag in Settings
  • added the Balance Forecast widget from the Reports dashboard
  • verified the dashboard card renders forecast data and lowest balance information
  • verified the full report page renders with future range presets
  • verified switching between monthly and daily granularity updates the chart

Automated:

  • yarn typecheck
  • yarn lint:fix
  • targeted tests for forecast generation, report routing, widget registration, and report interactions

Checklist

  • Release notes added (see link above)
  • No obvious regressions in affected areas
  • Self-review has been performed - I understand what each change in the code does and why it is needed

Bundle Stats

Bundle Files count Total bundle size % Changed
desktop-client 28 12.17 MB → 12.22 MB (+45.4 kB) +0.36%
loot-core 1 4.83 MB → 4.85 MB (+20.69 kB) +0.42%
api 4 4.06 MB → 4.08 MB (+20.36 kB) +0.49%
cli 1 7.88 MB 0%
View detailed bundle stats

desktop-client

Total

Files count Total bundle size % Changed
28 12.17 MB → 12.22 MB (+45.4 kB) +0.36%
Changeset
File Δ Size
src/components/reports/reports/BalanceForecast.tsx 🆕 +12 kB 0 B → 12 kB
src/components/reports/reports/BalanceForecastCard.tsx 🆕 +8.37 kB 0 B → 8.37 kB
src/components/reports/reports/balanceForecastChartData.ts 🆕 +2.74 kB 0 B → 2.74 kB
node_modules/es-toolkit/dist/function/debounce.js 🆕 +1.53 kB 0 B → 1.53 kB
node_modules/es-toolkit/dist/compat/function/debounce.js 🆕 +1.26 kB 0 B → 1.26 kB
node_modules/es-toolkit/dist/compat/function/throttle.js 🆕 +502 B 0 B → 502 B
node_modules/es-toolkit/compat/throttle.js 🆕 +187 B 0 B → 187 B
node_modules/recharts/es6/component/ResponsiveContainer.js 📈 +6.28 kB (+2115.79%) 304 B → 6.58 kB
node_modules/recharts/es6/component/responsiveContainerUtils.js 📈 +1.82 kB (+752.63%) 247 B → 2.06 kB
src/components/reports/Header.tsx 📈 +4.15 kB (+32.61%) 12.74 kB → 16.89 kB
src/components/reports/ReportRouter.tsx 📈 +623 B (+11.81%) 5.15 kB → 5.76 kB
src/components/reports/reportRanges.ts 📈 +444 B (+10.12%) 4.28 kB → 4.72 kB
src/hooks/useFeatureFlag.ts 📈 +31 B (+6.25%) 496 B → 527 B
locale/zh-Hans.json 📈 +4.75 kB (+5.50%) 86.32 kB → 91.07 kB
src/components/reports/Container.tsx 📈 +48 B (+3.90%) 1.2 kB → 1.25 kB
src/components/reports/Overview.tsx 📈 +806 B (+3.25%) 24.23 kB → 25.02 kB
src/components/settings/Experimental.tsx 📈 +184 B (+1.82%) 9.87 kB → 10.05 kB
src/components/reports/graphs/CashFlowGraph.tsx 📈 +52 B (+0.59%) 8.58 kB → 8.63 kB
src/components/reports/spreadsheets/cash-flow-spreadsheet.tsx 📈 +10 B (+0.17%) 5.71 kB → 5.72 kB
src/components/reports/spreadsheets/net-worth-spreadsheet.ts 📈 +6 B (+0.12%) 5 kB → 5 kB
src/components/reports/graphs/CalendarGraph.tsx 📈 +8 B (+0.08%) 9.5 kB → 9.51 kB
src/components/accounts/AccountEmptyMessage.tsx 📈 +2 B (+0.08%) 2.47 kB → 2.47 kB
src/components/reports/ReportSummary.tsx 📈 +2 B (+0.05%) 3.56 kB → 3.56 kB
src/components/reports/ReportSidebar.tsx 📈 +4 B (+0.02%) 16.7 kB → 16.7 kB
src/components/formula/QueryManager.tsx 📈 +2 B (+0.01%) 22.53 kB → 22.53 kB
src/components/reports/reports/CustomReport.tsx 📉 -2 B (-0.00%) 41.28 kB → 41.28 kB
src/components/reports/reports/Crossover.tsx 📉 -6 B (-0.02%) 38.68 kB → 38.67 kB
src/components/reports/reports/Calendar.tsx 📉 -7 B (-0.03%) 26.64 kB → 26.63 kB
src/components/reports/spreadsheets/custom-spreadsheet.ts 📉 -2 B (-0.04%) 5.54 kB → 5.54 kB
src/components/reports/reports/Spending.tsx 📉 -8 B (-0.04%) 21.66 kB → 21.65 kB
src/components/reports/reports/NetWorth.tsx 📉 -6 B (-0.04%) 13.55 kB → 13.54 kB
src/components/reports/spreadsheets/crossover-spreadsheet.ts 📉 -4 B (-0.05%) 7.93 kB → 7.92 kB
src/components/reports/reports/CrossoverCard.tsx 📉 -6 B (-0.06%) 9.47 kB → 9.47 kB
src/components/reports/reports/BudgetAnalysis.tsx 📉 -16 B (-0.08%) 19 kB → 18.99 kB
src/components/reports/reports/Summary.tsx 📉 -26 B (-0.10%) 24.77 kB → 24.75 kB
src/components/reports/reports/CashFlow.tsx 📉 -10 B (-0.11%) 8.67 kB → 8.66 kB
src/components/reports/spreadsheets/calendar-spreadsheet.ts 📉 -6 B (-0.12%) 4.95 kB → 4.95 kB
src/components/reports/DateRange.tsx 📉 -8 B (-0.26%) 3.06 kB → 3.06 kB
src/components/reports/spreadsheets/summary-spreadsheet.ts 📉 -16 B (-0.28%) 5.52 kB → 5.51 kB
src/components/reports/graphs/BudgetAnalysisGraph.tsx 📉 -93 B (-0.72%) 12.59 kB → 12.5 kB
src/components/EditablePageHeaderTitle.tsx 📉 -106 B (-3.92%) 2.64 kB → 2.54 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
static/js/ReportRouter.js 1.02 MB → 1.05 MB (+28.9 kB) +2.76%
static/js/index.js 3.23 MB → 3.24 MB (+11.74 kB) +0.36%
static/js/zh-Hans.js 86.32 kB → 91.07 kB (+4.75 kB) +5.50%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
static/js/BackgroundImage.js 119.98 kB 0%
static/js/FormulaEditor.js 846.44 kB 0%
static/js/TransactionList.js 81.29 kB 0%
static/js/ca.js 182.91 kB 0%
static/js/da.js 104.66 kB 0%
static/js/de.js 174.79 kB 0%
static/js/en-GB.js 7.16 kB 0%
static/js/en.js 170.76 kB 0%
static/js/es.js 182.18 kB 0%
static/js/fr.js 177.47 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.46 kB 0%
static/js/it.js 166.25 kB 0%
static/js/narrow.js 354.5 kB 0%
static/js/nb-NO.js 152.2 kB 0%
static/js/nl.js 108.93 kB 0%
static/js/pl.js 88.34 kB 0%
static/js/pt-BR.js 177.84 kB 0%
static/js/resize-observer.js 18.03 kB 0%
static/js/sv.js 80.58 kB 0%
static/js/th.js 179.94 kB 0%
static/js/theme.js 30.68 kB 0%
static/js/uk.js 213.14 kB 0%
static/js/useTransactionBatchActions.js 4.29 MB 0%
static/js/wide.js 418 B 0%
static/js/workbox-window.prod.es5.js 7.28 kB 0%

loot-core

Total

Files count Total bundle size % Changed
1 4.83 MB → 4.85 MB (+20.69 kB) +0.42%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/forecast/app.ts 🆕 +20.67 kB 0 B → 20.67 kB
home/runner/work/actual/actual/packages/loot-core/src/server/main.ts 📈 +8 B (+0.17%) 4.64 kB → 4.65 kB
home/runner/work/actual/actual/packages/loot-core/src/server/filters/app.ts 📈 +4 B (+0.10%) 3.78 kB → 3.78 kB
home/runner/work/actual/actual/packages/loot-core/src/server/schedules/find-schedules.ts 📈 +4 B (+0.05%) 7.16 kB → 7.16 kB
node_modules/hyperformula/es/DateTimeHelper.mjs 📈 +4 B (+0.05%) 8.42 kB → 8.43 kB
home/runner/work/actual/actual/packages/loot-core/src/server/accounts/app.ts 📈 +4 B (+0.02%) 21.6 kB → 21.6 kB
View detailed bundle breakdown

Added

Asset File Size % Changed
kcab.worker.BP3A5IFD.js 0 B → 4.85 MB (+4.85 MB) -

Removed

Asset File Size % Changed
kcab.worker.CwpE34S5.js 4.83 MB → 0 B (-4.83 MB) -100%

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged
No assets were unchanged


api

Total

Files count Total bundle size % Changed
4 4.06 MB → 4.08 MB (+20.36 kB) +0.49%
Changeset
File Δ Size
home/runner/work/actual/actual/packages/loot-core/src/server/forecast/app.ts 🆕 +20.34 kB 0 B → 20.34 kB
home/runner/work/actual/actual/packages/loot-core/src/server/main.ts 📈 +8 B (+0.23%) 3.44 kB → 3.45 kB
home/runner/work/actual/actual/packages/loot-core/src/server/filters/app.ts 📈 +4 B (+0.11%) 3.7 kB → 3.7 kB
node_modules/hyperformula/es/DateTimeHelper.mjs 📈 +4 B (+0.05%) 8.25 kB → 8.25 kB
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger

Asset File Size % Changed
index.js 3.84 MB → 3.86 MB (+20.36 kB) +0.52%

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
from-Bl-Hslp4.js 167.73 kB 0%
multipart-parser-BnDysoMr.js 8.1 kB 0%
src-iMkUmuwR.js 43.64 kB 0%

cli

Total

Files count Total bundle size % Changed
1 7.88 MB 0%
View detailed bundle breakdown

Added
No assets were added

Removed
No assets were removed

Bigger
No assets were bigger

Smaller
No assets were smaller

Unchanged

Asset File Size % Changed
cli.js 7.88 MB 0%

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 28, 2026

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 491993b
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget/deploys/69d000a72219b30008a1eb17
😎 Deploy Preview https://deploy-preview-7310.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@samaluk
Copy link
Copy Markdown
Author

samaluk commented Mar 28, 2026

Related issues for maintainer context: #4244, #517

@github-actions
Copy link
Copy Markdown
Contributor

👋 Hello contributor!

We would love to review your PR! Before we can do that, please make sure:

  • ✅ All CI checks pass
  • ✅ The PR is moved from draft to open (if applicable)
  • ✅ The "[WIP]" prefix is removed from the PR title
  • ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.

We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.

For more information, please see our Contributing Guide.

@github-actions

This comment has been minimized.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from a8878d9 to 882ce7f Compare March 28, 2026 02:01
@github-actions

This comment has been minimized.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch 2 times, most recently from b0ff03b to a8878d9 Compare March 28, 2026 03:31
@github-actions

This comment has been minimized.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from a8878d9 to 0825ec6 Compare March 28, 2026 03:46
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch 3 times, most recently from a65adfa to e2c67d8 Compare March 28, 2026 14:19
@github-actions

This comment has been minimized.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch 3 times, most recently from e5adb14 to 7c52bbb Compare March 28, 2026 21:17
@github-actions

This comment has been minimized.

@samaluk
Copy link
Copy Markdown
Author

samaluk commented Mar 28, 2026

/update-vrt

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from 7c52bbb to e7741ac Compare March 30, 2026 04:40
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 30, 2026

Deploy Preview for actualbudget-website ready!

Name Link
🔨 Latest commit e7741ac
🔍 Latest deploy log https://app.netlify.com/projects/actualbudget-website/deploys/69c9feaa163dba0008a4e0fe
😎 Deploy Preview https://deploy-preview-7310.www.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@samaluk samaluk marked this pull request as ready for review March 30, 2026 04:53
@coderabbitai coderabbitai bot added the API Issues with the @actual-app/api package label Mar 30, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

A new Balance Forecast Report feature is added end-to-end: frontend UI (page + card + header/range helpers + widget menu), backend forecast generation endpoint with schedule handling, types and prefs updates, E2E tests and page-model helpers, utility/chart logic and tests, and a feature flag + settings toggle to gate the feature.

Changes

Cohort / File(s) Summary
E2E Page Models & Tests
packages/desktop-client/e2e/page-models/reports-page.ts, packages/desktop-client/e2e/page-models/settings-page.ts, packages/desktop-client/e2e/reports.test.ts
Added page-model helpers to locate and open the Balance Forecast card, select granularity, and add widgets; tightened visibility waits in settings model; added balance forecast E2E tests that enable the experimental flag, add the widget, navigate to the report, and assert screenshots.
Frontend — Report Components
packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx, packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
Introduced full report page and dashboard card components handling widget loading, timeframe and filter state, forecast requests, loading/error states, chart rendering (daily/monthly), lowest-point display, privacy masking, and widget actions (rename/remove/copy).
Frontend — Charting & Utilities
packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts, packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
Added functions to build daily/monthly chart series from forecast results and to count scheduled occurrences; comprehensive unit tests for aggregation, carry-forward semantics, and deduplication of transfer schedules.
Frontend — Widget Menu, Overview & Router
packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts, packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts, packages/desktop-client/src/components/reports/Overview.tsx, packages/desktop-client/src/components/reports/ReportRouter.tsx
Added a widget-menu builder with conditional insertion of balance-forecast-card, tests verifying inclusion when feature flag enabled, wired Overview to render the card when enabled, and registered /forecast routes guarded by the feature flag.
Frontend — Header & Ranges
packages/desktop-client/src/components/reports/Header.tsx, packages/desktop-client/src/components/reports/reportRanges.ts
Extended Header props with showFutureRange and hideModeToggle, refactored presets into Past/Future helper components; added helpers getNextRange and getFullFutureRange for future-range computations.
Frontend — Feature Flag & Settings UI
packages/desktop-client/src/hooks/useFeatureFlag.ts, packages/desktop-client/src/components/settings/Experimental.tsx
Added balanceForecastReport to default feature-flag state and to experimental toggles UI so it can be enabled from settings.
Backend — Forecast Module & Tests
packages/loot-core/src/server/forecast/app.ts, packages/loot-core/src/server/forecast/app.test.ts
Implemented forecast/generate handler that resolves accounts, applies rule-based conditions, enumerates schedule occurrences, simulates scheduled transactions (including transfers), aggregates per-account daily deltas, computes running balances and lowest-point, and returns a ForecastResult; comprehensive server tests validate scenarios, schedules, transfers, and filtering.
Server Integration & Types
packages/loot-core/src/server/main.ts, packages/loot-core/src/types/handlers.ts
Registered the forecast app in server composition and added ForecastHandlers into the global Handlers type.
Types & Models
packages/loot-core/src/types/models/forecast.ts, packages/loot-core/src/types/models/index.ts, packages/loot-core/src/types/models/dashboard.ts, packages/loot-core/src/types/prefs.ts
Added forecast data contracts (ForecastDataPoint, ForecastTransaction, BalanceForecastConfig, ForecastResult), re-exported forecast types, introduced BalanceForecastWidget in dashboard models, and added 'balanceForecastReport' to FeatureFlag union.
Release Notes
upcoming-release-notes/7310.md
Added a release-notes entry announcing the new Balance Forecast Report feature.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as "BalanceForecast Page"
    participant API as "forecast/generate"
    participant DB as Database

    User->>UI: Navigate to /reports/forecast/:id
    UI->>UI: load widget config (id) via useDashboardWidget
    UI->>API: get-earliest-transaction
    API->>DB: query earliest transaction
    DB-->>API: earliest month
    API-->>UI: earliest month

    User->>UI: change timeframe / granularity / filters
    UI->>API: forecast/generate(startDate,endDate,conditions,accountIds)
    API->>DB: resolve accounts, load transactions & schedules
    DB-->>API: transactions & schedules
    API->>API: enumerate schedule occurrences, run rules, aggregate deltas
    API->>API: compute per-account running balances, lowest balance
    API-->>UI: ForecastResult (dataPoints, lowestBalance)
    UI->>UI: build chart data (Daily|Monthly) and render
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I nibble at months both near and far,
I map the coins where all your ledgers are,
Charts hop and shimmer under moonlit art,
Daily or monthly — tomorrow, take heart! ✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding an experimental balance forecast report feature.
Description check ✅ Passed The PR description clearly articulates the purpose, scope, and testing of the balance forecast feature, providing sufficient context about the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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: 7

🧹 Nitpick comments (6)
packages/loot-core/src/server/forecast/app.ts (2)

103-146: N+1 query pattern for account balances.

Each account triggers a separate query to compute its balance. This is acceptable for typical budget use cases with a handful of accounts, but could become slow with many accounts.

Consider batching balance computation into a single query with grouping:

♻️ Optional optimization to batch balance queries
// Instead of N separate queries, use a single grouped query:
const { data: balances } = await aqlQuery(
  q('transactions')
    .filter({ 
      'account.id': { $oneof: accountData.map(a => a.id) }, 
      tombstone: false 
    })
    .groupBy('account')
    .select({ account: 'account', total: { $sum: '$amount' } }),
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/forecast/app.ts` around lines 103 - 146,
getAccounts currently issues an N+1 set of aqlQuery calls (one per account) to
compute balances; replace that per-account balance loop with a single grouped
query using aqlQuery that filters transactions by the set of accountData.map(a
=> a.id) and groups by account to compute the sum (referencing symbols:
getAccounts, accountData, aqlQuery, transactions), then map the grouped results
back to accountData to produce accountsWithBalances; ensure tombstone filtering
is preserved and default missing balances to 0.

359-364: Potential timezone edge case with new Date(endDate).

When endDate is a string like "2024-12-31", JavaScript's new Date() parses it as UTC midnight, which can cause off-by-one errors in western timezones when compared against local dates.

Consider using the local-time-aware monthUtils.parseDate consistently:

🛡️ Optional fix for timezone consistency
   const today = new Date();
   const todayString = format(today, 'yyyy-MM-dd');
   const forecastStartDate = startDate || format(today, 'yyyy-MM-dd');
   const forecastEndDate = endDate || format(addMonths(today, 12), 'yyyy-MM-dd');
-  const endDateObj = endDate ? new Date(endDate) : addMonths(today, 12);
+  const endDateObj = endDate ? monthUtils.parseDate(endDate) : addMonths(today, 12);
   const forecastMonths = getMonthsInRange(forecastStartDate, forecastEndDate);

Based on learnings: "Avoid using date-fns with string dates in 'yyyy-MM-dd' format in the codebase, as JavaScript may parse these as UTC midnight causing incorrect results in western time zones."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/forecast/app.ts` around lines 359 - 364, The
code creates endDateObj with new Date(endDate) which parses "yyyy-MM-dd" as UTC
and can cause off-by-one issues; replace that usage with the local-aware parser
used in the codebase (e.g., monthUtils.parseDate) and ensure
forecastStartDate/forecastEndDate and any calls to getMonthsInRange use parsed
Date objects consistently (parse forecastStartDate/forecastEndDate with
monthUtils.parseDate instead of relying on string/new Date) so all month-range
calculations use local-time Dates.
packages/desktop-client/src/components/reports/Header.tsx (1)

328-335: Consider validating latestTransaction for the "All future" preset.

When latestTransaction is a past date, getFullFutureRange(latestTransaction) will produce an end date before the start date (current month). While this edge case may not occur in practice if callers always pass future dates, the function doesn't guard against it.

🛡️ Optional defensive fix in getFullFutureRange
 export function getFullFutureRange(latestMonth?: string) {
   const start = monthUtils.currentMonth();
-  const end = latestMonth ?? monthUtils.addMonths(start, 24);
+  const defaultEnd = monthUtils.addMonths(start, 24);
+  const end = latestMonth && latestMonth > start ? latestMonth : defaultEnd;
 
   return [start, end, 'full'] as const;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/Header.tsx` around lines 328 -
335, Guard against a past latestTransaction before calling getFullFutureRange
from the "All future" Button: in the Button onPress (where
onChangeDates(...getFullFutureRange(latestTransaction)) is invoked) validate
latestTransaction and if it's earlier than the current month (or earlier than
now) clamp it to the current date/start-of-current-month (or otherwise ensure
end >= start) before calling getFullFutureRange; alternatively add the same
defensive check inside getFullFutureRange so it returns a valid start/end when
passed a past latestTransaction.
packages/desktop-client/e2e/reports.test.ts (1)

73-80: Move granularity toggle steps into ReportsPage page model.

Line 73-80 uses raw page selectors in the spec. Extract this into a page-model method (e.g., reportsPage.selectForecastGranularity('Daily')) to reduce selector brittleness and keep specs intent-focused.

Based on learnings: Applies to packages/desktop-client/e2e/**/*.test.ts : E2E tests using Playwright should use page models from e2e/page-models/ for reusable page interactions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/e2e/reports.test.ts` around lines 73 - 80, Extract
the raw Playwright steps in the test into a ReportsPage page-model method (e.g.,
add a method selectForecastGranularity(granularity: string) on the ReportsPage
class in e2e/page-models/) that encapsulates clicking the current granularity
button, waiting for the options, selecting the requested option, and waiting for
the new label to be visible; then replace the inline selectors in
packages/desktop-client/e2e/reports.test.ts with a call like
reportsPage.selectForecastGranularity('Daily') so the spec uses the page-model
API and keeps selectors centralized and reusable.
packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts (1)

92-95: Add a full-range length assertion for daily output.

Line 92–95 checks key boundary points, but it doesn’t guarantee all intermediate days are present. Add an explicit length check to prevent sparse daily series regressions.

Suggested test hardening
     expect(chartData[0]).toEqual({ date: '2024-03-01', balance: 1000 });
     expect(chartData[30]).toEqual({ date: '2024-03-31', balance: 1000 });
     expect(chartData[31]).toEqual({ date: '2024-04-01', balance: 1200 });
     expect(chartData.at(-1)).toEqual({ date: '2024-04-30', balance: 1200 });
+    expect(chartData).toHaveLength(61);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts`
around lines 92 - 95, Add a full-range length assertion to the test that
constructs chartData to ensure every day is present: verify chartData has the
expected number of daily entries (61 for 2024-03-01 through 2024-04-30) by
adding an assertion like expect(chartData).toHaveLength(61) (or equivalent)
alongside the existing boundary checks for chartData[0], chartData[30],
chartData[31], and chartData.at(-1).
packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts (1)

34-39: Harden findItemIndex against missing anchors.

If the anchor item is ever absent, splice(-1, 0, ...) will silently insert before the last element. Consider a safe fallback (e.g., append).

Suggested defensive tweak
 function findItemIndex(
   items: MenuItem<DashboardWidgetMenuName>[],
   name: string,
 ) {
-  return items.findIndex(item => item !== Menu.line && item.name === name);
+  const index = items.findIndex(
+    item => item !== Menu.line && item.name === name,
+  );
+  return index === -1 ? items.length : index;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts`
around lines 34 - 39, findItemIndex currently returns -1 when the anchor is
missing which causes callers to do splice(-1, 0, ...) and insert before the last
element; change findItemIndex (the function handling
MenuItem<DashboardWidgetMenuName>[] and checking item !== Menu.line && item.name
=== name) so it returns a safe append index (e.g., items.length) when no
matching anchor is found, or alternatively have callers check for -1 and use
items.length; update the function signature/implementation accordingly so
callers can safely append instead of inserting before the last element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-client/e2e/page-models/reports-page.ts`:
- Around line 43-57: The second scan uses the original count and can miss newly
rendered cards after the scroll-to-bottom fallback; recompute the number of grid
items (gridItems.count() or equivalent) into a fresh local variable before the
for loop that iterates from count-1 downwards, then use that refreshed value
when accessing gridItems.nth(i) and checking heading visibility (references:
gridItems, count, targetItem, page.evaluate, item.scrollIntoViewIfNeeded,
heading.getByRole).

In `@packages/desktop-client/src/components/reports/Overview.tsx`:
- Around line 816-824: The BalanceForecastCard branch renders without a
feature-flag guard; update the conditional that checks widget.type ===
'balance-forecast-card' to also require the corresponding feature flag (e.g. &&
balanceForecastReportEnabled) so it mirrors the pattern used by
crossoverReportEnabled and budgetAnalysisReportEnabled; locate the render branch
for BalanceForecastCard and change the conditional to include the boolean flag
while keeping the existing props and callbacks (BalanceForecastCard, widget.type
=== 'balance-forecast-card', onMetaChange, onRemoveWidget).
- Around line 592-595: Missing feature-flag guard: the 'balance-forecast-card'
menu item is added unconditionally in the Overview component; add and use the
balanceForecastReportEnabled flag like the other widgets (e.g., crossover-card
and budget-analysis-card) by adding balanceForecastReportEnabled to the feature
flags collection at the top of the component and conditionally include the menu
entry (spread it only when balanceForecastReportEnabled is true) so users only
see "Balance forecast" when the feature is enabled.

In `@packages/desktop-client/src/components/reports/ReportRouter.tsx`:
- Around line 59-60: The ReportRouter contains duplicate Route entries for the
Formula route; remove the redundant Route with path="/formula" and
path="/formula/:id" that were copied again (the ones duplicating the existing
routes) so only one pair of <Route path="/formula" element={<Formula />} /> and
<Route path="/formula/:id" element={<Formula />} /> remains in the ReportRouter
component; locate the duplicate Route elements in ReportRouter and delete the
extra occurrences to avoid conflicting/duplicate routes.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 363-366: The tickFormatter currently calls new Date(value) which
parses the 'yyyy-MM-dd' strings as UTC and shifts dates; update the Daily branch
inside tickFormatter (where granularity === 'Daily') to call
monthUtils.parseDate(value) instead of new Date(value) so d.format receives a
local-aware Date; ensure you keep using d.format(...) and that
monthUtils.parseDate is used only for the Daily case.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`:
- Around line 78-99: Saved account filters (meta.conditions and
meta.conditionsOp) are not being forwarded to the forecast request, so the card
ignores saved report filters; update the payload sent by
send('forecast/generate', ...) in BalanceForecastCard to include meta.conditions
and meta.conditionsOp (or, alternatively, derive selectedAccountIds from
meta.conditions against the accounts list before sending) while keeping the
existing selectedAccountIds logic so that the request uses the saved filter
criteria when present.

In `@packages/loot-core/src/server/forecast/app.test.ts`:
- Line 1: Replace the ad-hoc "as never" cast on the fixture array in
packages/loot-core/src/server/forecast/app.test.ts with a proper TypeScript
"satisfies" annotation: locate the fixture array that currently uses "as never"
and change its typing so the array satisfies RuleConditionEntity[] (e.g., use
"satisfies RuleConditionEntity[]") so the fixture keeps its literal types while
conforming to RuleConditionEntity[], removing the unsafe assertion.

---

Nitpick comments:
In `@packages/desktop-client/e2e/reports.test.ts`:
- Around line 73-80: Extract the raw Playwright steps in the test into a
ReportsPage page-model method (e.g., add a method
selectForecastGranularity(granularity: string) on the ReportsPage class in
e2e/page-models/) that encapsulates clicking the current granularity button,
waiting for the options, selecting the requested option, and waiting for the new
label to be visible; then replace the inline selectors in
packages/desktop-client/e2e/reports.test.ts with a call like
reportsPage.selectForecastGranularity('Daily') so the spec uses the page-model
API and keeps selectors centralized and reusable.

In `@packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts`:
- Around line 34-39: findItemIndex currently returns -1 when the anchor is
missing which causes callers to do splice(-1, 0, ...) and insert before the last
element; change findItemIndex (the function handling
MenuItem<DashboardWidgetMenuName>[] and checking item !== Menu.line && item.name
=== name) so it returns a safe append index (e.g., items.length) when no
matching anchor is found, or alternatively have callers check for -1 and use
items.length; update the function signature/implementation accordingly so
callers can safely append instead of inserting before the last element.

In `@packages/desktop-client/src/components/reports/Header.tsx`:
- Around line 328-335: Guard against a past latestTransaction before calling
getFullFutureRange from the "All future" Button: in the Button onPress (where
onChangeDates(...getFullFutureRange(latestTransaction)) is invoked) validate
latestTransaction and if it's earlier than the current month (or earlier than
now) clamp it to the current date/start-of-current-month (or otherwise ensure
end >= start) before calling getFullFutureRange; alternatively add the same
defensive check inside getFullFutureRange so it returns a valid start/end when
passed a past latestTransaction.

In
`@packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts`:
- Around line 92-95: Add a full-range length assertion to the test that
constructs chartData to ensure every day is present: verify chartData has the
expected number of daily entries (61 for 2024-03-01 through 2024-04-30) by
adding an assertion like expect(chartData).toHaveLength(61) (or equivalent)
alongside the existing boundary checks for chartData[0], chartData[30],
chartData[31], and chartData.at(-1).

In `@packages/loot-core/src/server/forecast/app.ts`:
- Around line 103-146: getAccounts currently issues an N+1 set of aqlQuery calls
(one per account) to compute balances; replace that per-account balance loop
with a single grouped query using aqlQuery that filters transactions by the set
of accountData.map(a => a.id) and groups by account to compute the sum
(referencing symbols: getAccounts, accountData, aqlQuery, transactions), then
map the grouped results back to accountData to produce accountsWithBalances;
ensure tombstone filtering is preserved and default missing balances to 0.
- Around line 359-364: The code creates endDateObj with new Date(endDate) which
parses "yyyy-MM-dd" as UTC and can cause off-by-one issues; replace that usage
with the local-aware parser used in the codebase (e.g., monthUtils.parseDate)
and ensure forecastStartDate/forecastEndDate and any calls to getMonthsInRange
use parsed Date objects consistently (parse forecastStartDate/forecastEndDate
with monthUtils.parseDate instead of relying on string/new Date) so all
month-range calculations use local-time Dates.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2ce4d7cc-2b67-4feb-afea-9e96133467ba

📥 Commits

Reviewing files that changed from the base of the PR and between 4820331 and e7741ac.

⛔ Files ignored due to path filters (6)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (27)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/server/forecast/app.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/loot-core/src/types/models/index.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md

Comment on lines +59 to +60
<Route path="/formula" element={<Formula />} />
<Route path="/formula/:id" element={<Formula />} />
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.

⚠️ Potential issue | 🔴 Critical

Remove duplicate /formula routes.

Lines 59-60 duplicate the /formula routes already defined at lines 51-52. This appears to be a copy-paste error when adding the forecast routes.

🐛 Proposed fix to remove duplicate routes
       {balanceForecastReportEnabled && (
         <>
           <Route path="/forecast" element={<BalanceForecast />} />
           <Route path="/forecast/:id" element={<BalanceForecast />} />
         </>
       )}
-      <Route path="/formula" element={<Formula />} />
-      <Route path="/formula/:id" element={<Formula />} />
     </Routes>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Route path="/formula" element={<Formula />} />
<Route path="/formula/:id" element={<Formula />} />
{balanceForecastReportEnabled && (
<>
<Route path="/forecast" element={<BalanceForecast />} />
<Route path="/forecast/:id" element={<BalanceForecast />} />
</>
)}
</Routes>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/ReportRouter.tsx` around lines
59 - 60, The ReportRouter contains duplicate Route entries for the Formula
route; remove the redundant Route with path="/formula" and path="/formula/:id"
that were copied again (the ones duplicating the existing routes) so only one
pair of <Route path="/formula" element={<Formula />} /> and <Route
path="/formula/:id" element={<Formula />} /> remains in the ReportRouter
component; locate the duplicate Route elements in ReportRouter and delete the
extra occurrences to avoid conflicting/duplicate routes.

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (4)
packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx (1)

214-217: Type assertion for forecast result.

Similar to the card component, this uses as ForecastResult. Consider adding a more robust type guard or ensuring the send function returns properly typed results.

Suggested improvement
       .then(result => {
-        if (!cancelled && result) {
-          setForecastData(result as ForecastResult);
+        if (!cancelled && result && 'dataPoints' in result && 'lowestBalance' in result) {
+          setForecastData(result);
         }
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
around lines 214 - 217, The code in BalanceForecast.tsx currently force-casts
the RPC response with "as ForecastResult" when calling setForecastData; instead
add a runtime type guard (e.g., isForecastResult(result)) and only call
setForecastData when the guard passes, or update the send function to return a
properly typed ForecastResult so the cast isn't needed; locate the promise
resolution in the .then(...) block where result is checked, implement or call a
type guard (isForecastResult) that validates required fields of ForecastResult
before setting state, and handle the invalid case (log/error) rather than
blindly using "as ForecastResult".
packages/desktop-client/src/components/reports/Header.tsx (1)

182-337: Consider extracting the preset button groups into separate components.

The conditional rendering block for past vs future presets is quite long (~155 lines). While the logic is correct, extracting PastRangePresets and FutureRangePresets as separate internal components would improve readability and maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/Header.tsx` around lines 182 -
337, The long conditional JSX for past vs future presets should be extracted
into two small components (e.g., PastRangePresets and FutureRangePresets) to
improve readability; create those components inside the same file (or as small
subcomponents) and move the corresponding blocks that call getLatestRange,
getLiveRange, convertToMonth, getFullRange (for past) and getNextRange,
getFullFutureRange (for future) into them, then replace the big conditional with
{showFutureRange ? <FutureRangePresets ...props/> : <PastRangePresets
...props/>}; ensure you forward props used by the handlers (onChangeDates,
show1Month, earliestTransaction, latestTransaction, firstDayOfWeekIdx,
allMonths) so behavior of the existing Button onPress callbacks remains
identical.
packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx (2)

47-47: Unused variable _privacyMode.

The _privacyMode variable is assigned but never used. Consider removing it or using it for privacy filtering if intended.

Suggested fix
-  const _privacyMode = usePrivacyMode();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`
at line 47, In BalanceForecastCard remove the unused assignment to _privacyMode
(the result of calling usePrivacyMode()) or, if privacy behavior is intended,
wire that hook into the component's rendering/logic (e.g., apply _privacyMode to
filter/mask displayed values in the component's JSX or forecasting logic).
Locate the usePrivacyMode() call in the BalanceForecastCard component and either
delete the _privacyMode variable or replace its placeholder usage by
conditioning display/formatting of amounts/labels on it.

102-105: Consider using a type guard instead of type assertion.

The as ForecastResult assertion bypasses type checking. Per coding guidelines, prefer satisfies or type guards for type narrowing.

Suggested fix using type guard
       .then(result => {
-        if (!cancelled && result && 'dataPoints' in result) {
-          setForecastData(result as ForecastResult);
+        if (!cancelled && result && 'dataPoints' in result && 'lowestBalance' in result) {
+          setForecastData(result);
         }
       })

Alternatively, ensure the send return type is properly typed so no assertion is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`
around lines 102 - 105, Replace the type assertion "result as ForecastResult"
with proper type narrowing: add a type guard function like
isForecastResult(result): result is ForecastResult (or refine the send() return
type) and use it in the .then handler to check that result satisfies
ForecastResult before calling setForecastData; reference the result variable and
setForecastData and ForecastResult (or update send) so the compiler can infer
the type without using "as".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/desktop-client/src/components/reports/Header.tsx`:
- Around line 182-337: The long conditional JSX for past vs future presets
should be extracted into two small components (e.g., PastRangePresets and
FutureRangePresets) to improve readability; create those components inside the
same file (or as small subcomponents) and move the corresponding blocks that
call getLatestRange, getLiveRange, convertToMonth, getFullRange (for past) and
getNextRange, getFullFutureRange (for future) into them, then replace the big
conditional with {showFutureRange ? <FutureRangePresets ...props/> :
<PastRangePresets ...props/>}; ensure you forward props used by the handlers
(onChangeDates, show1Month, earliestTransaction, latestTransaction,
firstDayOfWeekIdx, allMonths) so behavior of the existing Button onPress
callbacks remains identical.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 214-217: The code in BalanceForecast.tsx currently force-casts the
RPC response with "as ForecastResult" when calling setForecastData; instead add
a runtime type guard (e.g., isForecastResult(result)) and only call
setForecastData when the guard passes, or update the send function to return a
properly typed ForecastResult so the cast isn't needed; locate the promise
resolution in the .then(...) block where result is checked, implement or call a
type guard (isForecastResult) that validates required fields of ForecastResult
before setting state, and handle the invalid case (log/error) rather than
blindly using "as ForecastResult".

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`:
- Line 47: In BalanceForecastCard remove the unused assignment to _privacyMode
(the result of calling usePrivacyMode()) or, if privacy behavior is intended,
wire that hook into the component's rendering/logic (e.g., apply _privacyMode to
filter/mask displayed values in the component's JSX or forecasting logic).
Locate the usePrivacyMode() call in the BalanceForecastCard component and either
delete the _privacyMode variable or replace its placeholder usage by
conditioning display/formatting of amounts/labels on it.
- Around line 102-105: Replace the type assertion "result as ForecastResult"
with proper type narrowing: add a type guard function like
isForecastResult(result): result is ForecastResult (or refine the send() return
type) and use it in the .then handler to check that result satisfies
ForecastResult before calling setForecastData; reference the result variable and
setForecastData and ForecastResult (or update send) so the compiler can infer
the type without using "as".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3f9f183-1b2a-4336-8035-12ebdbf2e31e

📥 Commits

Reviewing files that changed from the base of the PR and between e7741ac and 55d36ff.

⛔ Files ignored due to path filters (6)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (27)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/server/forecast/app.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/loot-core/src/types/models/index.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (8)
  • upcoming-release-notes/7310.md
  • packages/loot-core/src/types/models/index.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/loot-core/src/types/models/forecast.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/loot-core/src/types/handlers.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/loot-core/src/server/forecast/app.ts

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from 55d36ff to e368cf6 Compare April 1, 2026 04:53
Copy link
Copy Markdown
Contributor

@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 (3)
packages/desktop-client/src/components/reports/Overview.tsx (1)

821-830: Consider adding onCopy prop for consistency with other widget cards.

The BalanceForecastCard is missing the onCopy callback that other cards (NetWorthCard, CrossoverCard, CashFlowCard, etc.) have, which enables the "copy to dashboard" functionality. This may be intentional for an experimental feature, but consider adding it for feature parity.

♻️ Suggested addition
                        <BalanceForecastCard
                          widgetId={item.i}
                          isEditing={isEditing}
                          accounts={accounts}
                          meta={widget.meta}
                          onMetaChange={newMeta => onMetaChange(item, newMeta)}
                          onRemove={() => onRemoveWidget(item.i)}
+                         onCopy={targetDashboardId =>
+                           onCopyWidget(item.i, targetDashboardId)
+                         }
                        />

This would also require updating BalanceForecastCard props and menu items.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/Overview.tsx` around lines 821
- 830, The BalanceForecastCard usage in Overview.tsx is missing the onCopy prop
used by other widgets; add onCopy={() => onCopyWidget(item.i)} (or the
appropriate handler) to the BalanceForecastCard JSX alongside
widgetId/isEditing/accounts/meta/onMetaChange/onRemove to enable
copy-to-dashboard parity, then update the BalanceForecastCard component's props
interface (prop name/type) and its internal menu/handlers to call that onCopy
prop when the "Copy" menu item is selected so the copy flow matches
NetWorthCard/CrossoverCard/CashFlowCard.
packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx (1)

412-420: Consider using a theme color instead of hardcoded #fff.

The stroke="#fff" on the ReferenceDot uses a hardcoded white color, which may not work well with all themes (e.g., midnight theme with dark backgrounds).

♻️ Suggested fix
                      <ReferenceDot
                        x={lowestPoint.date}
                        y={lowestPoint.balance}
                        r={6}
                        fill={theme.errorText}
-                       stroke="#fff"
+                       stroke={theme.cardBackground}
                      />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
around lines 412 - 420, Replace the hardcoded white stroke on the ReferenceDot
with a theme value so it adapts to different themes: locate the ReferenceDot
render inside BalanceForecast (the block using lowestPoint and theme.errorText)
and change stroke="#fff" to use a color from the theme object (e.g.,
stroke={theme.background} or another high-contrast theme token your design
system exposes) so the dot outline remains visible across light/dark themes.
packages/desktop-client/src/components/reports/Header.tsx (1)

218-250: Consider narrowing the props type for FutureRangePresets.

The component signature declares RangePresetProps but only uses show1Month, latestTransaction, and onChangeDates. The unused props (earliestTransaction, firstDayOfWeekIdx, allMonths) are passed at the call site (lines 367-370) but ignored here.

♻️ Optional: Use a narrower type
-function FutureRangePresets({
-  show1Month,
-  latestTransaction,
-  onChangeDates,
-}: RangePresetProps) {
+function FutureRangePresets({
+  show1Month,
+  latestTransaction,
+  onChangeDates,
+}: Pick<RangePresetProps, 'show1Month' | 'latestTransaction' | 'onChangeDates'>) {

Then update the call site to pass only the required props:

 <FutureRangePresets
   show1Month={show1Month}
-  earliestTransaction={earliestTransaction}
   latestTransaction={latestTransaction}
-  firstDayOfWeekIdx={firstDayOfWeekIdx}
-  allMonths={allMonths}
   onChangeDates={onChangeDates}
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/Header.tsx` around lines 218 -
250, The FutureRangePresets component currently types its props as
RangePresetProps but only reads show1Month, latestTransaction and onChangeDates;
create a narrower props type (e.g., FutureRangePresetsProps) that includes only
those three fields and update the FutureRangePresets signature to use it, then
update the call site that renders FutureRangePresets (where extra props like
earliestTransaction, firstDayOfWeekIdx, allMonths are being passed) to supply
only show1Month, latestTransaction and onChangeDates so unused props are
removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`:
- Around line 192-203: The hardcoded fallback string in the ReportCardName usage
(name={meta?.name || 'Balance Forecast'}) should be translated; replace the
literal with the component's translation call (e.g., name={meta?.name ||
t('Balance Forecast')}) and ensure the translation hook/import (useTranslation
or i18n.t) is available in this file and a corresponding translation key exists
in the locale files; update ReportCardName usage and add the locale entry for
"Balance Forecast".

---

Nitpick comments:
In `@packages/desktop-client/src/components/reports/Header.tsx`:
- Around line 218-250: The FutureRangePresets component currently types its
props as RangePresetProps but only reads show1Month, latestTransaction and
onChangeDates; create a narrower props type (e.g., FutureRangePresetsProps) that
includes only those three fields and update the FutureRangePresets signature to
use it, then update the call site that renders FutureRangePresets (where extra
props like earliestTransaction, firstDayOfWeekIdx, allMonths are being passed)
to supply only show1Month, latestTransaction and onChangeDates so unused props
are removed.

In `@packages/desktop-client/src/components/reports/Overview.tsx`:
- Around line 821-830: The BalanceForecastCard usage in Overview.tsx is missing
the onCopy prop used by other widgets; add onCopy={() => onCopyWidget(item.i)}
(or the appropriate handler) to the BalanceForecastCard JSX alongside
widgetId/isEditing/accounts/meta/onMetaChange/onRemove to enable
copy-to-dashboard parity, then update the BalanceForecastCard component's props
interface (prop name/type) and its internal menu/handlers to call that onCopy
prop when the "Copy" menu item is selected so the copy flow matches
NetWorthCard/CrossoverCard/CashFlowCard.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 412-420: Replace the hardcoded white stroke on the ReferenceDot
with a theme value so it adapts to different themes: locate the ReferenceDot
render inside BalanceForecast (the block using lowestPoint and theme.errorText)
and change stroke="#fff" to use a color from the theme object (e.g.,
stroke={theme.background} or another high-contrast theme token your design
system exposes) so the dot outline remains visible across light/dark themes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c9d09975-3e08-4392-bbb4-d87a031ccf45

📥 Commits

Reviewing files that changed from the base of the PR and between 55d36ff and e368cf6.

⛔ Files ignored due to path filters (6)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (7)
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • upcoming-release-notes/7310.md
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from e368cf6 to 1675f15 Compare April 1, 2026 05:16
Copy link
Copy Markdown
Contributor

@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 (3)
packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx (3)

82-93: Consider removing manual memoization hooks.

Per project guidelines, the React Compiler automatically memoizes component bodies in desktop-client. The useMemo hooks on lines 82-85, 87-89, and 91-93 can be simplified to direct assignments.

♻️ Suggested simplification
-  const selectedAccountIds = useMemo(
-    () => (meta?.accounts?.length ? meta.accounts : accounts.map(a => a.id)),
-    [accounts, meta?.accounts],
-  );
+  const selectedAccountIds = meta?.accounts?.length
+    ? meta.accounts
+    : accounts.map(a => a.id);

-  const startDate = useMemo(() => {
-    return start + '-01';
-  }, [start]);
+  const startDate = start + '-01';

-  const endDate = useMemo(() => {
-    return monthUtils.lastDayOfMonth(end);
-  }, [end]);
+  const endDate = monthUtils.lastDayOfMonth(end);

As per coding guidelines: "In desktop-client, the React Compiler automatically memoizes component bodies; omit manual useCallback, useMemo, and React.memo when adding or refactoring code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`
around lines 82 - 93, The three uses of useMemo (computing selectedAccountIds,
startDate, and endDate) are unnecessary per the desktop-client guideline;
replace the useMemo blocks by direct constant assignments (compute
selectedAccountIds using meta?.accounts?.length ? meta.accounts : accounts.map(a
=> a.id), set startDate to start + '-01', and set endDate to
monthUtils.lastDayOfMonth(end)) and remove the import/usage of useMemo so the
component relies on the React Compiler's automatic memoization; touch the
symbols selectedAccountIds, startDate, endDate, and references to meta,
accounts, start, end, and monthUtils.lastDayOfMonth to make these direct
assignments.

139-161: Consider removing useMemo for chartData.

The React Compiler handles memoization automatically. This computation could be a direct assignment.

As per coding guidelines: "In desktop-client, the React Compiler automatically memoizes component bodies; omit manual useCallback, useMemo, and React.memo when adding or refactoring code."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`
around lines 139 - 161, Replace the useMemo wrapper around chartData with a
direct computation: remove useMemo and compute chartData synchronously from
forecastData (keep the same grouping logic that creates groupedByDate, dates,
and mapped balances), assign the resulting array to const chartData and remove
useMemo from imports; ensure the computed variable still reads forecastData and
handles the empty/null case the same way as in the current useMemo
implementation.

133-134: Consider removing useCallback wrappers.

These simple state setters don't need manual memoization with the React Compiler.

♻️ Suggested simplification
-  const onCardHover = useCallback(() => setIsCardHovered(true), []);
-  const onCardHoverEnd = useCallback(() => setIsCardHovered(false), []);
+  const onCardHover = () => setIsCardHovered(true);
+  const onCardHoverEnd = () => setIsCardHovered(false);

As per coding guidelines: "In desktop-client, the React Compiler automatically memoizes component bodies."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`
around lines 133 - 134, The two handlers onCardHover and onCardHoverEnd are
unnecessarily wrapped in useCallback; replace them with plain functions (e.g.
const onCardHover = () => setIsCardHovered(true) and const onCardHoverEnd = ()
=> setIsCardHovered(false)) and remove the now-unused useCallback import if it
becomes unused; ensure any places that reference onCardHover/onCardHoverEnd
continue to work with the direct function references.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-client/src/components/reports/reportRanges.ts`:
- Around line 184-190: getFullFutureRange currently returns mode 'full' which
causes calculateTimeRange to recompute and ignore the precomputed end; fix by
updating calculateTimeRange (the function that computes start/end based on mode)
so it only recalculates the end when mode === 'full' AND the end argument is
undefined or null, otherwise preserve and use the provided end value from
getFullFutureRange; this ensures the 24-month defaultEnd computed in
getFullFutureRange is honored.

---

Nitpick comments:
In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`:
- Around line 82-93: The three uses of useMemo (computing selectedAccountIds,
startDate, and endDate) are unnecessary per the desktop-client guideline;
replace the useMemo blocks by direct constant assignments (compute
selectedAccountIds using meta?.accounts?.length ? meta.accounts : accounts.map(a
=> a.id), set startDate to start + '-01', and set endDate to
monthUtils.lastDayOfMonth(end)) and remove the import/usage of useMemo so the
component relies on the React Compiler's automatic memoization; touch the
symbols selectedAccountIds, startDate, endDate, and references to meta,
accounts, start, end, and monthUtils.lastDayOfMonth to make these direct
assignments.
- Around line 139-161: Replace the useMemo wrapper around chartData with a
direct computation: remove useMemo and compute chartData synchronously from
forecastData (keep the same grouping logic that creates groupedByDate, dates,
and mapped balances), assign the resulting array to const chartData and remove
useMemo from imports; ensure the computed variable still reads forecastData and
handles the empty/null case the same way as in the current useMemo
implementation.
- Around line 133-134: The two handlers onCardHover and onCardHoverEnd are
unnecessarily wrapped in useCallback; replace them with plain functions (e.g.
const onCardHover = () => setIsCardHovered(true) and const onCardHoverEnd = ()
=> setIsCardHovered(false)) and remove the now-unused useCallback import if it
becomes unused; ensure any places that reference onCardHover/onCardHoverEnd
continue to work with the direct function references.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fd6f1871-28ff-4ec4-808f-a7a9868217a3

📥 Commits

Reviewing files that changed from the base of the PR and between e368cf6 and 1675f15.

⛔ Files ignored due to path filters (6)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (6)
  • upcoming-release-notes/7310.md
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/loot-core/src/server/forecast/app.test.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/loot-core/src/types/prefs.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from 1675f15 to fea4d58 Compare April 1, 2026 05:33
Copy link
Copy Markdown
Contributor

@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)
packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx (1)

1-1: Drop the manual useMemos here.

In desktop-client, these two derived values do not need hand-managed memoization, and removing them also lets you drop useMemo from Line 1.

As per coding guidelines, "In desktop-client, the React Compiler automatically memoizes component bodies; omit manual useCallback, useMemo, and React.memo when adding or refactoring code".

Also applies to: 152-157, 237-244

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
at line 1, Remove manual useMemo usage and import from the BalanceForecast
component: delete useMemo from the React import and replace each useMemo-wrapped
derived value with a plain computed const (e.g., change const memoizedSomething
= useMemo(() => computeSomething(...), [...]) to const something =
computeSomething(...) and do the same for the second memoized value). Ensure you
remove any corresponding dependency arrays and references to the former memoized
identifiers are unchanged. This applies to the two derived values currently
wrapped with useMemo in the component (previously around lines 152–157 and
237–244); keep the original compute logic but as direct computations so the
React Compiler's automatic memoization handles it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 196-230: The effect can leave stale forecastData after a failed
reload; when starting a new request and when a request errors you should clear
the previous forecast state. Inside the useEffect that calls
send('forecast/generate'), call setForecastData(null) (or the empty state you
use) right after setIsLoading(true) to clear prior data, and also in the .catch
branch (before setError) call setForecastData(null) if not cancelled; keep the
cancelled checks around these calls so you only clear state for active requests.
- Around line 169-179: The months list is capped to currentMonth + 24 in
loadMonths(), causing allMonths[0] (and thus
getFullFutureRange(latestTransaction)) to never reflect the actual forecast
horizon; change loadMonths() so futureEndMonth is computed using the real
forecast end when available (e.g., use forecastLastMonth or the last month
returned by forecast/generate) instead of always
monthUtils.addMonths(currentMonth, 24), then rebuild allMonthsArray (and calls
that rely on it such as the logic around latestTransaction and the code at the
other occurrence) so setAllMonths receives a range that extends to the true
forecast end when present. Ensure references to futureEndMonth, allMonthsArray,
loadMonths(), setAllMonths, getFullFutureRange, and latestTransaction are
updated accordingly.

---

Nitpick comments:
In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Line 1: Remove manual useMemo usage and import from the BalanceForecast
component: delete useMemo from the React import and replace each useMemo-wrapped
derived value with a plain computed const (e.g., change const memoizedSomething
= useMemo(() => computeSomething(...), [...]) to const something =
computeSomething(...) and do the same for the second memoized value). Ensure you
remove any corresponding dependency arrays and references to the former memoized
identifiers are unchanged. This applies to the two derived values currently
wrapped with useMemo in the component (previously around lines 152–157 and
237–244); keep the original compute logic but as direct computations so the
React Compiler's automatic memoization handles it.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 641963eb-314f-41e3-a30d-0d33159dc777

📥 Commits

Reviewing files that changed from the base of the PR and between 1675f15 and fea4d58.

⛔ Files ignored due to path filters (6)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (8)
  • upcoming-release-notes/7310.md
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/loot-core/src/server/forecast/app.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/types/prefs.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx

@samaluk samaluk force-pushed the 001-balance-forecast-report branch 2 times, most recently from b156a03 to dbfd1d7 Compare April 1, 2026 06:01
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

VRT tests ❌ failed. View the test report.

To update the VRT screenshots, comment /update-vrt on this PR. The VRT update operation takes about 50 minutes.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from dbfd1d7 to eecac40 Compare April 2, 2026 13:52
@samaluk
Copy link
Copy Markdown
Author

samaluk commented Apr 2, 2026

@CodeRabbit resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

✅ Actions performed

Reviews resumed.

Copy link
Copy Markdown
Contributor

@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 (3)
packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx (1)

463-480: Empty state message could be misleading.

The message "No scheduled transactions found. Add some schedules to see your forecast" implies the chart requires schedules, but the forecast also shows historical posted transactions. If a user has posted transactions but no schedules, they might still expect to see data. Consider a more inclusive message.

Alternative message suggestion
                 <Trans>
-                  No scheduled transactions found. Add some schedules to see
-                  your forecast.
+                  No forecast data available. The forecast shows your balance 
+                  from posted and scheduled transactions.
                 </Trans>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
around lines 463 - 480, The current empty-state in BalanceForecast.tsx (render
branch that uses theme.pageTextLight) says "No scheduled transactions found..."
which wrongly implies the chart has no data even when historical posted
transactions exist; update the UI to be inclusive by changing the message text
to mention both schedules and historical posted transactions (e.g., "No
scheduled transactions. Add schedules to see future forecasts — historical
posted transactions are shown if available.") or, better, add conditional logic
in the BalanceForecast component to detect posted/historical transactions (use
the same data source/props the chart uses) and render a different message when
posted transactions exist but schedules do not; modify the text inside the
<Trans> block and adjust the conditional rendering accordingly.
packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts (1)

90-102: Consider the insertion order when multiple flags are enabled.

When both budgetAnalysisReportEnabled and balanceForecastReportEnabled are true, the menu order will be: budget-analysis, balance-forecast, markdown-card (since each finds and inserts before markdown-card sequentially). If a specific order is desired (e.g., alphabetical or by importance), you may want to document this or adjust the insertion sequence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts`
around lines 90 - 102, The current code inserts each card before 'markdown-card'
using findItemIndex and items.splice which yields the final order depending on
which flag runs first; to enforce a deterministic order, gather the enabled
flags (budgetAnalysisReportEnabled, balanceForecastReportEnabled) into an
ordered list (e.g., desiredOrder =
['budget-analysis-card','balance-forecast-card']), filter by the enabled flags,
then insert them into items before 'markdown-card' in that order using
findItemIndex(items,'markdown-card') once (or by computing the index and
splicing sequentially while incrementing the insertion index) instead of calling
splice twice independently.
packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts (1)

34-45: Consider using monthUtils.parseDate for string date parsing.

Per codebase conventions, date-fns functions like parseISO can produce incorrect results in western timezones when parsing 'yyyy-MM-dd' strings (they're interpreted as UTC midnight). The repository's monthUtils.parseDate provides local-time awareness.

However, since the iteration uses the parsed Date object and immediately formats it back to a string on the same day, the practical impact may be minimal for the while-loop logic.

Consider using monthUtils for consistency
-    const startDate = d.parseISO(start + '-01');
-    const endDate = d.parseISO(monthUtils.lastDayOfMonth(end));
+    const startDate = monthUtils.parseDate(start + '-01');
+    const endDate = monthUtils.parseDate(monthUtils.lastDayOfMonth(end));

Based on learnings: "Avoid using date-fns (e.g., getDay) with string dates in 'yyyy-MM-dd' format in the codebase, as JavaScript may parse these as UTC midnight causing incorrect results in western time zones."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts`
around lines 34 - 45, Replace the direct date-fns ISO parsing with the repo's
local-aware parser: change the creation of startDate and endDate to use
monthUtils.parseDate instead of d.parseISO (i.e., use monthUtils.parseDate(start
+ '-01') and monthUtils.parseDate(monthUtils.lastDayOfMonth(end')) so the
while-loop that references startDate/endDate, current, and combinedBalanceByDate
stays timezone-safe and consistent with project conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 440-448: The ReferenceDot x-value uses lowestPoint.date in
'yyyy-MM-dd' but the chart's Monthly data keys are 'yyyy-MM', so when
granularity === 'Monthly' compute a matching x string (truncate or reformat
lowestPoint.date to 'yyyy-MM') before passing to ReferenceDot; update the render
in BalanceForecast.tsx to derive a formattedX (based on granularity) from
lowestPoint.date and use that for x so the dot matches forecastData's keys
(adjust code near the ReferenceDot usage and any date-format helper used for
chart data).

---

Nitpick comments:
In `@packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts`:
- Around line 90-102: The current code inserts each card before 'markdown-card'
using findItemIndex and items.splice which yields the final order depending on
which flag runs first; to enforce a deterministic order, gather the enabled
flags (budgetAnalysisReportEnabled, balanceForecastReportEnabled) into an
ordered list (e.g., desiredOrder =
['budget-analysis-card','balance-forecast-card']), filter by the enabled flags,
then insert them into items before 'markdown-card' in that order using
findItemIndex(items,'markdown-card') once (or by computing the index and
splicing sequentially while incrementing the insertion index) instead of calling
splice twice independently.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 463-480: The current empty-state in BalanceForecast.tsx (render
branch that uses theme.pageTextLight) says "No scheduled transactions found..."
which wrongly implies the chart has no data even when historical posted
transactions exist; update the UI to be inclusive by changing the message text
to mention both schedules and historical posted transactions (e.g., "No
scheduled transactions. Add schedules to see future forecasts — historical
posted transactions are shown if available.") or, better, add conditional logic
in the BalanceForecast component to detect posted/historical transactions (use
the same data source/props the chart uses) and render a different message when
posted transactions exist but schedules do not; modify the text inside the
<Trans> block and adjust the conditional rendering accordingly.

In
`@packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts`:
- Around line 34-45: Replace the direct date-fns ISO parsing with the repo's
local-aware parser: change the creation of startDate and endDate to use
monthUtils.parseDate instead of d.parseISO (i.e., use monthUtils.parseDate(start
+ '-01') and monthUtils.parseDate(monthUtils.lastDayOfMonth(end')) so the
while-loop that references startDate/endDate, current, and combinedBalanceByDate
stays timezone-safe and consistent with project conventions.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 61e3c731-8620-41b4-bb01-39b43a7ae81e

📥 Commits

Reviewing files that changed from the base of the PR and between 1675f15 and eecac40.

⛔ Files ignored due to path filters (6)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (6)
  • packages/loot-core/src/server/dashboard/app.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • upcoming-release-notes/7310.md
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/loot-core/src/types/prefs.ts
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/settings/Experimental.test.tsx

Comment on lines +440 to +448
{lowestPoint && lowestPoint.balance < 0 && (
<ReferenceDot
x={lowestPoint.date}
y={lowestPoint.balance}
r={6}
fill={theme.errorText}
stroke={theme.cardBackground}
/>
)}
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.

⚠️ Potential issue | 🟡 Minor

ReferenceDot x-value may not match chart data in Monthly granularity.

The lowestPoint.date from forecastData is in 'yyyy-MM-dd' format (e.g., '2024-03-01'), but when granularity === 'Monthly', the chart data uses 'yyyy-MM' format (e.g., '2024-03'). The ReferenceDot x-value won't find a matching data point, so the dot may not render correctly in Monthly mode.

Proposed fix to handle granularity-dependent date format
                     {lowestPoint && lowestPoint.balance < 0 && (
                       <ReferenceDot
-                        x={lowestPoint.date}
+                        x={
+                          granularity === 'Monthly'
+                            ? monthUtils.monthFromDate(lowestPoint.date)
+                            : lowestPoint.date
+                        }
                         y={lowestPoint.balance}
                         r={6}
                         fill={theme.errorText}
                         stroke={theme.cardBackground}
                       />
                     )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
around lines 440 - 448, The ReferenceDot x-value uses lowestPoint.date in
'yyyy-MM-dd' but the chart's Monthly data keys are 'yyyy-MM', so when
granularity === 'Monthly' compute a matching x string (truncate or reformat
lowestPoint.date to 'yyyy-MM') before passing to ReferenceDot; update the render
in BalanceForecast.tsx to derive a formattedX (based on granularity) from
lowestPoint.date and use that for x so the dot matches forecastData's keys
(adjust code near the ReferenceDot usage and any date-format helper used for
chart data).

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from eecac40 to eab5d4c Compare April 2, 2026 16:58
Copy link
Copy Markdown
Contributor

@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 (5)
packages/desktop-client/src/components/settings/Experimental.test.tsx (1)

3-3: Remove explicit Vitest globals import.

The test can rely on configured Vitest globals (describe, it, expect, vi), so this import is unnecessary. Vitest is configured with globals: true in the vite config.

♻️ Proposed change
-import { describe, expect, it, vi } from 'vitest';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/settings/Experimental.test.tsx` at
line 3, Remove the explicit Vitest globals import in Experimental.test.tsx:
delete the line that imports { describe, expect, it, vi } from 'vitest' and rely
on the configured Vitest globals (globals: true); ensure no other code depends
on that import and run the tests to confirm globals are resolved at runtime.
packages/desktop-client/src/components/reports/ReportRouter.test.tsx (1)

9-11: Reset the hoisted mock between tests to prevent cross-test state bleed.

useFeatureFlagMock is shared across test cases; add a reset hook so future tests stay isolated.

Suggested change
+beforeEach(() => {
+  useFeatureFlagMock.mockReset();
+});
+
 describe('ReportRouter', () => {

Also applies to: 51-79

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/ReportRouter.test.tsx` around
lines 9 - 11, The hoisted mock useFeatureFlagMock declared via vi.hoisted is
shared across tests and must be reset between cases to avoid state leakage; add
an afterEach (or beforeEach) test hook in ReportRouter.test.tsx that
clears/resets the hoisted mock (call useFeatureFlagMock.mockReset() or
mockClear()) and also run vi.clearAllMocks() / vi.restoreAllMocks() as
appropriate so each test starts with a fresh useFeatureFlagMock state.
packages/loot-core/src/server/forecast/app.ts (1)

218-292: Consider adding a safeguard for infinite loops in getFutureOccurrences.

The while loop at line 269 depends on getNextDate progressing forward. If getNextDate returns a date that doesn't advance (due to malformed recurrence config), this could loop indefinitely.

Proposed safeguard
   const dates = [schedule.next_date];
   let day = monthUtils.parseDate(schedule.next_date);
+  const maxIterations = 1000; // Reasonable limit for multi-year forecasts
+  let iterations = 0;

-  while (day <= endDate) {
+  while (day <= endDate && iterations < maxIterations) {
+    iterations++;
     const nextDate = getNextDate(dateCondition, day);

     if (!nextDate || monthUtils.parseDate(nextDate) > endDate) {
       break;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/forecast/app.ts` around lines 218 - 292,
getFutureOccurrences can enter an infinite loop if getNextDate returns the same
or earlier date; add a safeguard that breaks the loop when progress stalls
(e.g., if nextDate is equal to or not greater than the current day) and/or after
a reasonable maxIterations cap (e.g., 10k) to prevent runaway loops; update the
while loop that uses day, dates and getNextDate to check that parsed nextDate >
parsed day before pushing and advancing day, and break if iterations exceed the
cap.
packages/desktop-client/e2e/page-models/reports-page.ts (1)

80-88: Consider making the initial button selector dynamic.

Line 81 hardcodes clicking the 'Monthly' button, which assumes that's always the current selection. If the default changes or a test needs to switch from 'Daily', this would fail.

Proposed fix to handle any current selection
   async selectForecastGranularity(granularity: string) {
-    await this.pageContent.getByRole('button', { name: 'Monthly' }).click();
+    // Click the granularity dropdown trigger (current selection button)
+    await this.pageContent
+      .getByRole('button', { name: /^(Monthly|Daily)$/ })
+      .click();
     const option = this.page.getByRole('button', { name: granularity });
     await option.waitFor({ state: 'visible' });
     await option.click();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/e2e/page-models/reports-page.ts` around lines 80 -
88, selectForecastGranularity currently clicks a hardcoded 'Monthly' button
which breaks if the default changes; update the initial click to target the
currently displayed/selected granularity button inside this.pageContent (e.g.,
query for the visible/selected button within pageContent rather than
getByRole('button', { name: 'Monthly' }) ), then proceed to locate and click the
requested granularity option (keep the existing waits for visibility). Modify
the logic in selectForecastGranularity to use a dynamic selector that finds the
visible/selected button before opening the options.
packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts (1)

1-4: Consider removing explicit Vitest import.

Per coding guidelines, test files use Vitest globals (describe, it, expect) without needing imports. The Vitest configuration across the project has globals: true enabled, so the explicit import on line 1 is redundant.

Suggested removal
-import { describe, expect, it } from 'vitest';
-
 import { buildBalanceForecastChartData } from './balanceForecastChartData';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts`
around lines 1 - 4, Remove the redundant Vitest import by deleting the "import {
describe, expect, it } from 'vitest';" line in the test file so the test uses
the Vitest globals (describe, it, expect) provided by the project's globals:
true config; ensure the file still imports buildBalanceForecastChartData and all
tests run.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-client/src/components/reports/ReportRouter.test.tsx`:
- Line 5: The import statement in ReportRouter.test.tsx currently imports
describe, it, and expect from 'vitest' even though those are provided as
globals; remove describe, it, and expect from the import and keep only vi (i.e.,
change the import to import { vi } from 'vitest') so tests rely on the global
test helpers while retaining the vi mock utility.

---

Nitpick comments:
In `@packages/desktop-client/e2e/page-models/reports-page.ts`:
- Around line 80-88: selectForecastGranularity currently clicks a hardcoded
'Monthly' button which breaks if the default changes; update the initial click
to target the currently displayed/selected granularity button inside
this.pageContent (e.g., query for the visible/selected button within pageContent
rather than getByRole('button', { name: 'Monthly' }) ), then proceed to locate
and click the requested granularity option (keep the existing waits for
visibility). Modify the logic in selectForecastGranularity to use a dynamic
selector that finds the visible/selected button before opening the options.

In `@packages/desktop-client/src/components/reports/ReportRouter.test.tsx`:
- Around line 9-11: The hoisted mock useFeatureFlagMock declared via vi.hoisted
is shared across tests and must be reset between cases to avoid state leakage;
add an afterEach (or beforeEach) test hook in ReportRouter.test.tsx that
clears/resets the hoisted mock (call useFeatureFlagMock.mockReset() or
mockClear()) and also run vi.clearAllMocks() / vi.restoreAllMocks() as
appropriate so each test starts with a fresh useFeatureFlagMock state.

In
`@packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts`:
- Around line 1-4: Remove the redundant Vitest import by deleting the "import {
describe, expect, it } from 'vitest';" line in the test file so the test uses
the Vitest globals (describe, it, expect) provided by the project's globals:
true config; ensure the file still imports buildBalanceForecastChartData and all
tests run.

In `@packages/desktop-client/src/components/settings/Experimental.test.tsx`:
- Line 3: Remove the explicit Vitest globals import in Experimental.test.tsx:
delete the line that imports { describe, expect, it, vi } from 'vitest' and rely
on the configured Vitest globals (globals: true); ensure no other code depends
on that import and run the tests to confirm globals are resolved at runtime.

In `@packages/loot-core/src/server/forecast/app.ts`:
- Around line 218-292: getFutureOccurrences can enter an infinite loop if
getNextDate returns the same or earlier date; add a safeguard that breaks the
loop when progress stalls (e.g., if nextDate is equal to or not greater than the
current day) and/or after a reasonable maxIterations cap (e.g., 10k) to prevent
runaway loops; update the while loop that uses day, dates and getNextDate to
check that parsed nextDate > parsed day before pushing and advancing day, and
break if iterations exceed the cap.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7b22a9bb-2d6d-4ad8-9193-ac8e375e2c19

📥 Commits

Reviewing files that changed from the base of the PR and between eecac40 and 4a0c046.

⛔ Files ignored due to path filters (7)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (26)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.test.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.test.tsx
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/server/forecast/app.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/loot-core/src/types/models/index.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (8)
  • upcoming-release-notes/7310.md
  • packages/loot-core/src/types/models/index.ts
  • packages/loot-core/src/server/main.ts
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/loot-core/src/server/forecast/app.test.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/loot-core/src/types/prefs.ts
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts

import { MemoryRouter } from 'react-router';

import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify explicit Vitest globals imports in test files
rg -nP "import\s+\{[^}]*\b(describe|it|expect|beforeEach)\b[^}]*\}\s+from\s+['\"]vitest['\"];" --iglob "packages/**/*.test.{ts,tsx,js}"

Repository: actualbudget/actual

Length of output: 2521


Remove unnecessary Vitest globals imports.

Line 5 explicitly imports describe, it, and expect from Vitest, but these are provided as globals in test files and should not be imported. The vi utility object should remain imported as it is a module export, not a global.

Suggested change
 import '@testing-library/jest-dom/vitest';
 import { MemoryRouter } from 'react-router';
 
 import { render, screen } from '@testing-library/react';
-import { describe, expect, it, vi } from 'vitest';
+import { vi } from 'vitest';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, expect, it, vi } from 'vitest';
import { vi } from 'vitest';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/ReportRouter.test.tsx` at line
5, The import statement in ReportRouter.test.tsx currently imports describe, it,
and expect from 'vitest' even though those are provided as globals; remove
describe, it, and expect from the import and keep only vi (i.e., change the
import to import { vi } from 'vitest') so tests rely on the global test helpers
while retaining the vi mock utility.

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from 4a0c046 to 2262b41 Compare April 2, 2026 20:37
Copy link
Copy Markdown
Contributor

@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)
packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx (1)

69-79: Initial latestTransaction empty string may cause brief incorrect date range.

When latestTransaction is empty string (''), calculateTimeRange is called with it at line 90 before the fetchLatestTransaction effect completes. This could cause an initial forecast request with an incorrect date range, followed by a corrected one after the state updates.

Consider initializing to a sensible default:

Proposed fix
-  const [latestTransaction, setLatestTransaction] = useState<string>('');
+  const [latestTransaction, setLatestTransaction] = useState<string>(
+    monthUtils.currentDay(),
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`
around lines 69 - 79, latestTransaction is initialized to an empty string
causing calculateTimeRange to run with an invalid date before
fetchLatestTransaction completes; initialize latestTransaction to a sensible
default (e.g., monthUtils.currentDay()) instead of '' and keep the existing
useEffect/fetchLatestTransaction that overwrites it, or ensure
calculateTimeRange is only invoked after fetchLatestTransaction resolves (update
the logic around calculateTimeRange usage). Update the state initializer for
latestTransaction and verify calculateTimeRange calls reference the initialized
value (functions/variables to look for: latestTransaction, setLatestTransaction,
useEffect, fetchLatestTransaction, calculateTimeRange).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/loot-core/src/server/forecast/app.ts`:
- Around line 47-53: The 'matches' case in the switch (in forecast/app.ts)
currently tests the regex against account.name directly, making it
case-sensitive and inconsistent with Condition.eval(); change the test to use a
lowercased field value (e.g., test against account.name.toLowerCase()) so the
behavior matches the canonical implementation in Condition.eval(); keep the
try/catch and ensure you convert account.name to a string before lowercasing if
necessary.

---

Nitpick comments:
In
`@packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx`:
- Around line 69-79: latestTransaction is initialized to an empty string causing
calculateTimeRange to run with an invalid date before fetchLatestTransaction
completes; initialize latestTransaction to a sensible default (e.g.,
monthUtils.currentDay()) instead of '' and keep the existing
useEffect/fetchLatestTransaction that overwrites it, or ensure
calculateTimeRange is only invoked after fetchLatestTransaction resolves (update
the logic around calculateTimeRange usage). Update the state initializer for
latestTransaction and verify calculateTimeRange calls reference the initialized
value (functions/variables to look for: latestTransaction, setLatestTransaction,
useEffect, fetchLatestTransaction, calculateTimeRange).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 197b8cfc-e6af-4aa5-9d32-02159b0e157b

📥 Commits

Reviewing files that changed from the base of the PR and between 4a0c046 and 2262b41.

⛔ Files ignored due to path filters (7)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/server/forecast/app.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/loot-core/src/types/models/index.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (12)
  • upcoming-release-notes/7310.md
  • packages/loot-core/src/server/main.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/loot-core/src/types/models/index.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/loot-core/src/types/models/forecast.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/desktop-client/e2e/reports.test.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/prefs.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts

@samaluk samaluk force-pushed the 001-balance-forecast-report branch from 2262b41 to b78c19f Compare April 3, 2026 17:59
@samaluk samaluk force-pushed the 001-balance-forecast-report branch from fa0101a to 491993b Compare April 3, 2026 18:02
Copy link
Copy Markdown
Contributor

@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: 3

🧹 Nitpick comments (2)
packages/desktop-client/src/components/reports/Header.tsx (1)

93-221: Move the new preset components out of Header.tsx.

PastRangePresets and FutureRangePresets are new desktop-client components, so keeping both inline makes Header carry two extra component bodies and makes future preset changes harder to isolate. I’d split them into sibling files before this grows further.

As per coding guidelines, "Create new components in their own files".

Also applies to: 223-255

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/Header.tsx` around lines 93 -
221, The PastRangePresets (and similarly FutureRangePresets) component bodies
should be extracted from Header.tsx into their own files: create
PastRangePresets.tsx (and FutureRangePresets.tsx) that export the component
(preserve the PastRangePresetsProps typing and all JSX exactly), import required
symbols used inside (Button, Trans, getLatestRange, getLiveRange, getFullRange,
convertToMonth, earliestTransaction types, allMonths, firstDayOfWeekIdx,
onChangeDates) at the top of the new files, then replace the inline component in
Header.tsx with an import statement and use the imported component; ensure the
component exports match how Header.tsx consumes them (named or default), update
any relative import paths, and run TypeScript checks to fix any missing
prop/type imports.
packages/desktop-client/src/components/reports/Overview.tsx (1)

593-600: Use the shared widget-item builder here too.

This new entry is now maintained in both Overview.tsx and getDashboardWidgetItems.ts, so the tested helper and the live menu can drift the next time ordering or feature-flag rules change.

As per coding guidelines, "Prefer iteration and modularization over code duplication".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/Overview.tsx` around lines 593
- 600, Replace the duplicated inline widget object in Overview.tsx with the
shared widget-item builder from getDashboardWidgetItems.ts: import the exported
builder (the function used to produce dashboard items in
getDashboardWidgetItems.ts), and inside the balanceForecastReportEnabled
conditional call that builder with the same identity ('balance-forecast-card')
and label (t('Balance forecast')) instead of constructing the object literal;
keep the feature-flag check (balanceForecastReportEnabled) and ensure the
builder is passed any required metadata the helper expects so ordering and
feature-flag rules remain centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`:
- Around line 159-208: The loadMonths() async effect can have multiple in-flight
requests and a late response may overwrite newer state; add a stale-response
guard (e.g., a local "cancelled" boolean or a request counter/token captured
when loadMonths starts) inside the useEffect so that after awaiting
send('get-earliest-transaction') and before calling setAllMonths you verify the
token/!cancelled and skip setAllMonths if stale; ensure you also clean up by
setting cancelled=true (or incrementing the token) in the effect cleanup so only
the latest loadMonths invocation updates state.
- Around line 249-255: The catch block currently sets forecastData to null and
setError, but the render falls back to the empty-state UI (showing "No
transactions are included in this report") when chartData/forecastData is empty;
change the render logic in BalanceForecast.tsx to short-circuit on the error
state (the error variable set via setError) and render the error UI instead of
the empty-state; specifically, update the component's conditional that decides
between showing the chart, the "no transactions" message, and the footer to
first check if error (or error !== null/undefined) and render the error message
+ footer accordingly, and ensure any checks that treat forecastData or chartData
as falsy differentiate between an actual empty dataset and a load failure so the
error path wins.

In `@packages/loot-core/src/server/forecast/app.ts`:
- Around line 201-209: The selection logic in getAccounts treats an explicit
empty accountIds array as "all accounts" because it checks accountIds &&
accountIds.length > 0; change the condition so that only undefined/null means
"all accounts" and an empty array yields an empty result: use a strict check
like if (accountIds === undefined) { selectedAccounts = accounts } else {
selectedAccounts = accounts.filter(a => accountIds.includes(a.id)) } (apply the
same fix to the other occurrence that uses accountIds at the later block around
the 263-265 logic), and ensure selectedAccounts becomes [] when accountIds is
[].

---

Nitpick comments:
In `@packages/desktop-client/src/components/reports/Header.tsx`:
- Around line 93-221: The PastRangePresets (and similarly FutureRangePresets)
component bodies should be extracted from Header.tsx into their own files:
create PastRangePresets.tsx (and FutureRangePresets.tsx) that export the
component (preserve the PastRangePresetsProps typing and all JSX exactly),
import required symbols used inside (Button, Trans, getLatestRange,
getLiveRange, getFullRange, convertToMonth, earliestTransaction types,
allMonths, firstDayOfWeekIdx, onChangeDates) at the top of the new files, then
replace the inline component in Header.tsx with an import statement and use the
imported component; ensure the component exports match how Header.tsx consumes
them (named or default), update any relative import paths, and run TypeScript
checks to fix any missing prop/type imports.

In `@packages/desktop-client/src/components/reports/Overview.tsx`:
- Around line 593-600: Replace the duplicated inline widget object in
Overview.tsx with the shared widget-item builder from
getDashboardWidgetItems.ts: import the exported builder (the function used to
produce dashboard items in getDashboardWidgetItems.ts), and inside the
balanceForecastReportEnabled conditional call that builder with the same
identity ('balance-forecast-card') and label (t('Balance forecast')) instead of
constructing the object literal; keep the feature-flag check
(balanceForecastReportEnabled) and ensure the builder is passed any required
metadata the helper expects so ordering and feature-flag rules remain
centralized.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 898a15d2-bccd-40c5-ab33-9bb7e5c735bc

📥 Commits

Reviewing files that changed from the base of the PR and between 2262b41 and 491993b.

⛔ Files ignored due to path filters (7)
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-loads-balance-forecast-report-with-monthly-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-1-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-2-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-balance-forecast-switches-to-daily-granularity-3-chromium-linux.png is excluded by !**/*.png
  • packages/desktop-client/e2e/reports.test.ts-snapshots/Reports-custom-reports-Switches-to-Line-Graph-and-checks-the-visuals-1-chromium-linux.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • packages/desktop-client/e2e/page-models/reports-page.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/Header.tsx
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.ts
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/loot-core/src/server/forecast/app.test.ts
  • packages/loot-core/src/server/forecast/app.ts
  • packages/loot-core/src/server/main.ts
  • packages/loot-core/src/types/handlers.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/loot-core/src/types/models/index.ts
  • packages/loot-core/src/types/prefs.ts
  • upcoming-release-notes/7310.md
✅ Files skipped from review due to trivial changes (6)
  • upcoming-release-notes/7310.md
  • packages/loot-core/src/types/models/index.ts
  • packages/desktop-client/src/components/settings/Experimental.tsx
  • packages/desktop-client/src/components/reports/getDashboardWidgetItems.test.ts
  • packages/loot-core/src/types/models/forecast.ts
  • packages/desktop-client/src/components/reports/reports/BalanceForecastCard.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/loot-core/src/server/main.ts
  • packages/desktop-client/e2e/page-models/settings-page.ts
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/hooks/useFeatureFlag.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.test.ts
  • packages/loot-core/src/types/models/dashboard.ts
  • packages/desktop-client/e2e/reports.test.ts
  • packages/desktop-client/src/components/reports/reports/balanceForecastChartData.ts

Comment on lines +159 to +208
useEffect(() => {
async function loadMonths() {
const currentMonthLocal = monthUtils.currentMonth();

const earliestTransactionResponse = await send(
'get-earliest-transaction',
);

const earliestMonth = earliestTransactionResponse
? monthUtils.monthFromDate(d.parseISO(earliestTransactionResponse.date))
: monthUtils.subMonths(currentMonthLocal, 12);

let futureEndMonth = monthUtils.addMonths(currentMonthLocal, 24);
if (end > futureEndMonth) {
futureEndMonth = end;
}
if (forecastData?.forecastEndDate) {
const forecastEndMonth = monthUtils.monthFromDate(
forecastData.forecastEndDate,
);
if (forecastEndMonth > futureEndMonth) {
futureEndMonth = forecastEndMonth;
}
}

const allMonthsArray = monthUtils
.rangeInclusive(earliestMonth, futureEndMonth)
.map(month => ({
name: month,
pretty: monthUtils.format(month, 'MMMM, yyyy', locale),
}))
.reverse();

setAllMonths(prev => {
if (
prev &&
prev.length === allMonthsArray.length &&
prev.every(
(p, i) =>
p.name === allMonthsArray[i]?.name &&
p.pretty === allMonthsArray[i]?.pretty,
)
) {
return prev;
}
return allMonthsArray;
});
}
void loadMonths();
}, [locale, end, forecastData?.forecastEndDate]);
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.

⚠️ Potential issue | 🟠 Major

Add a stale-response guard to loadMonths().

Because this effect depends on forecastData?.forecastEndDate at Line 208, initial load can have one get-earliest-transaction request in flight with the 24-month fallback and a second one in flight with the real forecast horizon. If the first request resolves last, setAllMonths overwrites the longer range with the shorter one and the future presets regress back to the capped list.

🩹 Proposed fix
   useEffect(() => {
+    let cancelled = false;
+
     async function loadMonths() {
       const currentMonthLocal = monthUtils.currentMonth();
 
       const earliestTransactionResponse = await send(
         'get-earliest-transaction',
@@
       const allMonthsArray = monthUtils
         .rangeInclusive(earliestMonth, futureEndMonth)
         .map(month => ({
           name: month,
           pretty: monthUtils.format(month, 'MMMM, yyyy', locale),
         }))
         .reverse();
 
+      if (cancelled) {
+        return;
+      }
+
       setAllMonths(prev => {
         if (
           prev &&
           prev.length === allMonthsArray.length &&
           prev.every(
@@
         }
         return allMonthsArray;
       });
     }
     void loadMonths();
+
+    return () => {
+      cancelled = true;
+    };
   }, [locale, end, forecastData?.forecastEndDate]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
around lines 159 - 208, The loadMonths() async effect can have multiple
in-flight requests and a late response may overwrite newer state; add a
stale-response guard (e.g., a local "cancelled" boolean or a request
counter/token captured when loadMonths starts) inside the useEffect so that
after awaiting send('get-earliest-transaction') and before calling setAllMonths
you verify the token/!cancelled and skip setAllMonths if stale; ensure you also
clean up by setting cancelled=true (or incrementing the token) in the effect
cleanup so only the latest loadMonths invocation updates state.

Comment on lines +249 to +255
.catch(err => {
if (!cancelled) {
setForecastData(null);
setError(
err instanceof Error ? err.message : t('Failed to load forecast'),
);
}
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.

⚠️ Potential issue | 🟠 Major

Don’t fall through to the empty-state UI after a load failure.

Lines 249-255 already distinguish an RPC failure from an empty dataset, but the render still shows “No transactions are included in this report” and the generic footer once chartData is empty. That turns a backend/load error into a misleading empty-result message.

Also applies to: 357-360, 515-548

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/desktop-client/src/components/reports/reports/BalanceForecast.tsx`
around lines 249 - 255, The catch block currently sets forecastData to null and
setError, but the render falls back to the empty-state UI (showing "No
transactions are included in this report") when chartData/forecastData is empty;
change the render logic in BalanceForecast.tsx to short-circuit on the error
state (the error variable set via setError) and render the error UI instead of
the empty-state; specifically, update the component's conditional that decides
between showing the chart, the "no transactions" message, and the footer to
first check if error (or error !== null/undefined) and render the error message
+ footer accordingly, and ensure any checks that treat forecastData or chartData
as falsy differentiate between an actual empty dataset and a load failure so the
error path wins.

Comment on lines +201 to +209
async function getAccounts(
accountIds?: string[],
): Promise<AccountWithComputedBalance[]> {
const accounts = await db.getAccounts();

const selectedAccounts =
accountIds && accountIds.length > 0
? accounts.filter(account => accountIds.includes(account.id))
: accounts;
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.

⚠️ Potential issue | 🟠 Major

Treat accountIds: [] as “no accounts”, not “all accounts”.

The accountIds && accountIds.length > 0 check collapses an explicit empty selection into the same path as undefined. If a widget/report saves accountIds: []—or earlier filtering reduces the set to empty—this will forecast the entire budget instead of returning no data.

Suggested fix
 async function getAccounts(
   accountIds?: string[],
 ): Promise<AccountWithComputedBalance[]> {
   const accounts = await db.getAccounts();

   const selectedAccounts =
-    accountIds && accountIds.length > 0
-      ? accounts.filter(account => accountIds.includes(account.id))
-      : accounts;
+    accountIds === undefined
+      ? accounts
+      : accounts.filter(account => accountIds.includes(account.id));

   if (!selectedAccounts || selectedAccounts.length === 0) {
     return [];
   }
 async function getTransactions(
   accountIds: string[] | undefined,
   filterInfo: ForecastFilterInfo,
 ) {
   let query = q('transactions')
     .filter({ tombstone: false })
     .select('*')
     .options({ splits: 'grouped' });

-  if (accountIds && accountIds.length > 0) {
+  if (accountIds !== undefined) {
+    if (accountIds.length === 0) {
+      return [];
+    }
     query = query.filter({ 'account.id': { $oneof: accountIds } });
   }

Also applies to: 263-265

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/loot-core/src/server/forecast/app.ts` around lines 201 - 209, The
selection logic in getAccounts treats an explicit empty accountIds array as "all
accounts" because it checks accountIds && accountIds.length > 0; change the
condition so that only undefined/null means "all accounts" and an empty array
yields an empty result: use a strict check like if (accountIds === undefined) {
selectedAccounts = accounts } else { selectedAccounts = accounts.filter(a =>
accountIds.includes(a.id)) } (apply the same fix to the other occurrence that
uses accountIds at the later block around the 263-265 logic), and ensure
selectedAccounts becomes [] when accountIds is [].

@matt-fidd matt-fidd force-pushed the master branch 2 times, most recently from 5c7c70d to d262f7d Compare April 5, 2026 17:13
@youngcw
Copy link
Copy Markdown
Member

youngcw commented Apr 8, 2026

This seems interesting, can you fix the merge conflicts so we can do a real review?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants