Commit Graph
3794 Commits
Author SHA1 Message Date
Tim SmartandNgoc Pham 8ca9b7a923 [AI] Add the automatic "For next month" UI
Desktop and mobile share one hook and one menu. `useFutureBufferMode`
reads `__global!future-buffer-start` and the month's `frozen` cell
through sheet bindings, so a mode change from another device shows up
without a reload, and `isManagedMonth` is a pure rule.

- To Budget menu item "For next month: Automatic…" / "Manual…" opening
  the mode modal. Shown with the experimental flag, and always once the
  mode is on.
- Mode modal with the Manual and Automatic choices, the start month, and
  the consequences of switching either way.
- Managed months hide Hold, Reset, Disable current auto hold, the income
  auto-hold toggle and the rollover arrow, explain "For next month" in a
  tooltip, and say how much is held for future months under
  "Overbudgeted". Frozen months keep Hold and Reset and lose the
  auto-hold controls.
- The budget-type switch says automatic mode is turned off first.
- `useSheetValue` observes the sheet a binding names, which the global
  cell needs.
- `month` reaches `ToBudgetAmount`, `TotalsList` and `BudgetSummary`,
  `TotalsList` provides its own sheet, and `monthFromSheet` maps a sheet
  back to its month.

Co-authored-by: Ngoc Pham <ngocphamm@fastmail.fm>
2026-11-06 10:33:34 +00:00
Tim Smart 2a61058bfd [AI] Compute the "For next month" buffer from future months
Adds a frozen column to zero_budget_months and a frozen cell per month,
loaded with the stored buffer. buffered-computed holds what the months
after a month need from it: next month's budgeted amount, less its
income, plus overspending that reaches it, plus its own need.

__global!future-buffer-start holds the futureBufferMode preference. It's
read on every budget open, after the months are created, and updated
when the preference row changes locally or through sync.
buffered-selected uses the computed buffer in managed months, the stored
buffer in frozen months, and today's rule otherwise.

setBuffer takes an optional frozen flag and only writes the columns
that change. envelope-budget-month also emits frozen and
buffered-selected.
2026-11-06 10:33:34 +00:00
Tim 669c4d9b23 Apply suggestion from @tim-smart 2026-11-06 08:38:48 +00:00
Tim Smart 312f3cc922 [AI] Make new rollover toggles persistent instead of seeding old windows
Switching rollover or auto hold on now stores 2, which following months
without a flag continue. A 1 keeps its old meaning of on for that month
only, so runs written by earlier versions end where they always did and
no existing balance changes. That removes the need for the carryover
seed, its post-sync and load-time scheduling, the seeded preference and
the budget table filter it relied on.
2026-11-06 08:38:48 +00:00
Tim 9efd688257 Apply suggestion from @tim-smart 2026-11-06 08:38:34 +00:00
Tim Smart c8537f814a [AI] Keep the carryover release note to the change itself 2026-11-06 08:38:34 +00:00
Tim SmartandClaude Opus 5.5 80c312002e [AI] Carryover no longer expires after 12 months
A month's carryover flag can now be NULL, meaning it inherits the previous
month's flag. The stored value moves to carryover-stored-<cat>, and
carryover-<cat> resolves the chain. Budget and goal inserts write NULL,
the one-month income reset pins the next month, undo of a created row
restores NULL, and cleanup templates read the resolved flag. Spreadsheet
caches from before this change are rebuilt on load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:38:18 +00:00
92865f32f3 [AI] Upgrade dependencies to resolve security advisories (#9450)
- Refresh yarn.lock within existing ranges for undici, js-yaml, qs,
  postcss-selector-parser, body-parser, serialize-javascript,
  http-proxy-middleware and express.
- Bump electron-builder 26.8.1 -> 26.18.0 (app-builder-lib,
  builder-util-runtime).
- Drop resolutions that no longer change the dependency tree
  (minimatch@3.1.2, socks, webpackbar, vite-plugin-node-polyfills).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 21:14:54 +00:00
fd0715a37a [AI] Show a goal progress ring beside underfunded category balances (#9297)
* [AI] Show goal funding status and an Assign action in balance menus

Use the existing Budget Automation engine to add a panel to category
balance menus with the month's recommendation, what is assigned so far,
what is still needed, and an Assign action that adds the money still
needed (undoable).

- budget/category-funding reads the dryRunCategoryTemplate projection
  without writing goal_def; categories without funding templates get
  null, and invalid automations fail closed.
- budget/fund-category recomputes on the server, assigns the full
  recommendation and goal in one batch, and is a no-op when already
  funded.
- Funding reads are gated by the goal templates flag, only run for
  categories with templates, and refresh after data changes and funding.
- Covers desktop and mobile envelope and tracking balance menus,
  including tracking income categories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJDNR72i3yEZttwMUuk4cq

* [AI] Fold goal funding into the existing goal status and never overbudget

Address review feedback on the category funding change:

- Replace the separate funding panel with the existing goal status. It
  now shows at the top of the desktop envelope and tracking balance
  menus (as it already did on mobile) with a thin progress bar, and the
  desktop hover tooltip is removed along with its tooltipDisabled
  workaround.
- Replace the Assign button with a "Fund goal from To Budget" balance
  menu item (envelope only, not for long-term goals) that uses the
  existing undoable transfer-available action, so it never moves more
  than To Budget and never lowers a budget. It is hidden when the goal
  is met or To Budget is empty.
- Drop the live dry-run endpoint, the fund-category endpoint, their
  client queries and refresh wiring, and the tracking income additions.
  Status, bar, colour and fund amount now all read the stored goal.
- Add transferAvailable tests and menu wiring tests, and update the
  budget automation docs for the moved goal status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJDNR72i3yEZttwMUuk4cq

* [AI] Show goal status as a colored pill on category balances

Draw the balance of a category with a goal as a pill with an icon:
yellow with a clock when underfunded, green with a check when funded
and red with an exclamation when overspent. Categories without a goal,
or with goal templates off, render as before.

- The pill, the balance colour and the goal status share one comparison
  (compareToGoal/getGoalFundingState), made on displayed amounts so they
  agree when decimals are hidden.
- Desktop envelope and tracking tables and the mobile budget table;
  narrow desktop columns drop the icon to keep the amount readable, and
  mobile amounts shrink to fit instead of being cut off.
- Status is announced to screen readers, not shown by colour alone.
- Pill colours get their own theme tokens so the funded pill keeps
  enough contrast in the midnight theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJDNR72i3yEZttwMUuk4cq

* [AI] Move GoalStatus into its own file

GoalStatus isn't specific to BalanceWithCarryover, so give it a
type-strict module of its own alongside its row helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Update goal status screenshots to show the balance menu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Clarify how funding a goal affects next month's To Budget

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Update docs screenshots to show the goal status pill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Describe the underfunded goal status as yellow in the docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Update mobile budget template VRT snapshots for the goal status pill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Replace the goal status pill with a goal progress ring

Keep category balances in their usual colours and draw a small ring
beside balances with a goal instead: a gold arc that fills as the goal
is funded, and a green check once it is met. Overspent balances stay red
like any other negative amount, without an extra marker.

The ring sits beside the amount in the balance cell on desktop, inside
the balance pill on mobile, and next to the heading of the goal status
in the balance menu, so one shape carries the status everywhere. The
pill-only theme tokens are removed, the docs text and screenshots and
the mobile VRT snapshots are updated, and the release note is replaced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4sNu2G32qCLcAe2YznUCM

* [AI] Only mark goals that still need money, with a filling ring

Draw the goal ring only beside balances whose goal is not met yet, so a
fully funded budget looks like one without goals and the one mark in the
table points at rows that still need money. The ring is a single gold
pie: a hollow outline when nothing is funded yet, a wedge that grows
with the funded share and that never closes, since the ring disappears
the moment the goal is met. Funded rows carry no mark and overspent
rows stay red like any other negative amount.

The glyph shrinks to 10px to sit within the cap height of the figures
and draws in one theme token, so it keeps 3:1 contrast on every surface
in all three themes. The amount keeps its full width in narrow month
columns; the ring is what yields. The balance menu shows the same ring
only beside "Underfunded", as its legend. The three-way funding state
is replaced by one helper that returns the progress to draw or nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4sNu2G32qCLcAe2YznUCM

* [AI] Keep the funded balance green, ring only while the goal is unmet

A category whose goal is met keeps the green balance the goal templates
feature already gives it, so a met goal still reads differently from no
goal. The ring stays the only mark on an unfunded goal; its amount keeps
the normal colour instead of upstream's gold text.

Docs text and screenshots, the release note and the mobile template VRT
snapshots follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B4sNu2G32qCLcAe2YznUCM

* [AI] Keep funded balances green in the mobile balance sheet and the ring in view

- Add an explicit isIncome prop instead of reading isDisabled as "income",
  so the mobile balance sheet header (which is only disabled because it is
  not clickable) colours a funded balance green again, and mobile income
  balances keep their hover style
- Let a long amount shrink beside the ring on desktop so the ring is not
  pushed out of the column
- Describe the ring instead of yellow balances in the goal directive docs
- Drop a snapshot change that had no visible difference

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Drop the ring from the goal status in the balance menu

The progress bar right below it already shows the same thing, so the menu
keeps its plain status line. Retake the balance menu screenshots in the
docs to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Keep the balance yellow while its goal is unmet

Per review, an unmet goal keeps the yellow balance text it has on master and
gains the progress ring beside it, so every category with a goal is
coloured. The balance colour logic is back to master's; the docs and their
screenshots describe and show yellow text with the ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM

* [AI] Show income goals only in tracking budgets

Envelope budgets don't apply templates to income categories, so the
balance now ignores an income goal only when the budget is not
tracking, instead of on every income category. Tracking income pills
get their goal colour back and show the ring when underfunded. The
envelope income balance sheet no longer shows a goal status, and the
mobile income group menu no longer offers "Overwrite with templates"
in envelope budgets.

The isIncome props on the tracking budget table and balance menu are
removed, since those only ever render expense categories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRjoaUi1WAUGsSNp3sb7p

* [AI] Keep goals from one budget type out of the other

Envelope and tracking budgets share spreadsheet cell names, so a goal
from one type could show up after switching to the other:

- Switching type (by sync, undo or redo) left the old type's cells in
  the saved spreadsheet cache, and goals came back from it on the next
  open. setType now clears the cache and waits for the budget values to
  reload.
- Changes to the inactive type's budget table were still applied to the
  sheet. Only the active table's rows are applied now.
- Applying templates to specific categories wrote amounts and goals for
  income categories in envelope budgets, and saving automations stored
  their goals. Envelope budgets now skip income categories in both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRjoaUi1WAUGsSNp3sb7p

* [AI] Show income goals on the desktop tracking budget

Desktop tracking budgets have no balance column for income categories,
so their goals showed nowhere, while the mobile income pill already
showed them. An income category with a goal now shows the goal colour
on its received amount, with the ring while underfunded. Income
categories without a goal, and budgets with goal templates off, keep
the plain received amount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MRjoaUi1WAUGsSNp3sb7p

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-11-05 19:48:15 +00:00
d9c184777e [AI] Document intended global bank-sync credential access in sync-server (#9439)
* [AI] Document intended global bank-sync credential access in sync-server

Global bank-sync credentials are usable by any authenticated user by
design; add a note so this stops being reported as a vulnerability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eQbJYThmFWQRnxw1Q6

* [AI] Add SECURITY.md with threat model and align bank-sync comments

Document how to report vulnerabilities, the trusted-users threat model
for multi-user servers, and that shared global bank-sync credentials are
out of scope. Point the sync-server comments at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eQbJYThmFWQRnxw1Q6

* [AI] Clarify per-budget credential advice in SECURITY.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eQbJYThmFWQRnxw1Q6

* [AI] Add release note for security policy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eQbJYThmFWQRnxw1Q6

* [AI] Tighten security note wording on route scope and per-budget credentials

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eQbJYThmFWQRnxw1Q6

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-05 18:27:25 +00:00
17466da6fe [AI] Hide the SharedArrayBuffer warning in the native mobile apps (#9433)
The warning in the top bar was only hidden on phones because the Titlebar
is not rendered at narrow widths. Rotating to landscape renders it, and the
Capacitor apps never have SharedArrayBuffer (they always run in the fallback
mode set in browser-preload/start.ts), so the warning showed up. Gate the
component on isNativeMobileApp, the same flag that enables the fallback.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 22:22:29 +00:00
80fb13131b [AI] Show the mobile nav bar again after navigating back from a settings page (#9434)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 22:22:15 +00:00
51bd2fb5ab [AI] Fix squished budget file list on mobile (#9436)
The file list wrapper only had a width inside the desktop media query, so
on narrow screens the centered parent shrank it to its content width.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 22:09:10 +00:00
11cbdf04db [AI] Improve budget page loading and initial sizing (#9121)
* [AI] Improve budget loading and initial sizing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* [AI] Address budget PR review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* drop the LoadComponent cache and wait for prewarming on every visit

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-11-04 21:24:16 +00:00
Michael ClarkandGitHub c9096f00ed Experimental OPFS - Phase 1 (#9269)
* phase 1

* bump wasm

* simplify the facade and engine

* Register the sqlite-wasm engine explicitly and accept opaque handles natively
2026-11-04 20:28:37 +00:00
502e61ef38 [AI] Fix OpenID login in the iOS and Android apps (#9394)
The login page used the web view's own origin (capacitor://localhost) as
the OpenID return URL and then navigated the web view to the provider, so
the callback had nowhere to land. Register an `actualbudget://` URL scheme
in both native shells, use it as the return URL inside the native apps,
and route the `appUrlOpen` callback from @capacitor/app to the existing
/openid-cb page, which writes the token and finishes the login. Navigating
the web view to the provider is what hands the sign-in to the system
browser: Capacitor opens any top-level navigation to an outside host with
the OS and keeps the web view on the login page.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 14:43:29 +00:00
3f5b93808d [AI] Sync and re-check the server when the mobile app resumes (#9384)
* [AI] Add isNativeMobileApp platform flag and gate web-only UI behind it

The native Capacitor builds were detected ad hoc with 'Capacitor' in window.
Expose a real isNativeMobileApp flag from shared/platform (false on electron
and the API) based on Capacitor.isNativePlatform(), which stays false on the
plain web even once @capacitor/core is bundled there.

Use it to skip what has no meaning in an App Store build: service worker
registration (assets ship in the bundle and update via the store), the
"new version of Actual is available" notification, the "learn how to set
up a server" hint on the server config screen, and the "Use the current
domain" shortcut, whose origin is the capacitor:// scheme.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Gate the GitHub release check itself in the native mobile apps

Skipping the fetch in getLatestVersion covers the update notification, the
Settings "Updates" row and the GitHub rate limit in one place, so the
per-consumer gate in FinancesApp goes away. Also inline the one-use type
alias in platform.ts and tighten the service worker comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Unregister any service worker left by earlier native mobile builds

Skipping registration alone leaves a worker installed by a previous native
build in control of page loads, serving the old precached bundle after a
store update. Drop existing registrations on native startup only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Revert "Unregister any service worker left by earlier native mobile builds"

The only native builds that ever registered a service worker were TestFlight
test builds, so there are no store users with a stale worker to clean up.

This reverts commit 2eb95f623f3bc0ba7a8dd7b0e2b94b6a4ebc0c0e.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Sync and re-check the server when the mobile app resumes

The Capacitor apps only synced on an interval or a manual pull, so returning
to the app after a while showed stale data. `useOnVisible` now also listens to
`@capacitor/app` `appStateChange` on native platforms, so the existing
visibility-driven sync and server-version check run on resume too. User data
is refreshed first so the online/offline status reflects the server's
reachability.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Track server reachability from sync events everywhere

The online/offline reconciliation lived in LoggedInUser, which the narrow
(mobile) layout never renders, so the status never updated there. Move it
into the global sync-event listener so every layout keeps the status in step
with sync results, and drop the extra user-data fetch from the visibility
callback. Also fold the native app-state listener into the existing
visibility effect and trim the test mocks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Use the isNativeMobileApp platform flag for the resume listener

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Apply CodeRabbit review feedback

Use a single foreground event source per platform in useOnVisible so a
resume in the native apps does not run the callback twice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Coalesce visibility events that arrive during a running callback

A resume event that lands while the previous callback is still running was
dropped, so if that run failed the resume sync was lost. Queue one follow-up
run instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Drop a queued visibility run once the hook is cleaned up

An event coalesced while the callback was still running would start one more
run after the hook unmounted or was disabled. Clear the pending flag in both
cleanup paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Ignore native app-state events that land after cleanup

Capacitor removes listeners asynchronously, so an appStateChange arriving
between cleanup and removal could still run the callback. Flag the listener
as removed in cleanup and skip events after that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Simplify the resume listener and guard the reachability refresh

Collapse useOnVisible into one effect with a shared active flag and a loop
for the coalesced follow-up run, so both platforms share the same cleanup.
Refresh user data one request at a time when sync errors arrive in a burst.
Trim the test scaffolding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Apply CodeRabbit review feedback

Keep a queued visibility run when the callback rejects, and drop the
in-flight guard on the reachability refresh so a sync error that lands
mid-validation is not lost and a stalled request cannot block later ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Return useOnVisible to the minimal native listener

Drop the per-platform event source, the coalesced follow-up run and the
cleanup guards added during review. The hook keeps its original logic and
only adds the native app-state listener next to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Restore LoggedInUser to its original sync handling

Keep the title bar's own user-data refresh so it stays in "Connecting..."
until validation completes. The global sync-events listener still covers the
mobile layout, which never renders this component.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Leave reachability reconciliation to the global sync listener

LoggedInUser only tracks sync start/stop for its loading state; the
re-fetch on a contradicting sync result lives in sync-events alone. The
status text may lag by one validate request until the store updates.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Resolve follow-redirects to 1.16.2 as on master

The merge conflict resolution kept the lockfile entry from the branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 14:19:53 +00:00
Michael ClarkandGitHub a4830bf639 Remove actual-appimage link from installation guide (#9429)
* Remove actual-appimage link from installation guide

* Remove AUR actual-appimage from install docs

Removed references to AUR actual-appimage from community installation documentation for clarity.

* Remove actual AppImage from documentation
2026-11-04 14:14:46 +00:00
b13f5c299e [AI] Release Akahu bank sync as a stable feature (#9426)
Remove the experimental akahuBankSync feature flag so the Akahu provider
is always shown on the Bank Sync page.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 14:13:03 +00:00
Michael ClarkandGitHub a3a1c7fc41 📈 Monte Carlo - Stop the min spending from raising the spending above planned (#9414)
* stop the min spending floor from raising the spending above planned

* feedback
2026-11-03 22:15:02 +00:00
TimandGitHub 479db6081b [AI] Retry incomplete schedule advancement without blocking healthy schedules (#9386)
* [AI] Reproduce swallowed schedule advancement failures

* [AI] Keep incomplete schedule advancement retryable

* [AI] Prevent schedule write failures from retrying themselves

* [AI] Simplify schedule advancement failure handling

Treat any RuleError as an expected rule problem, name the sync-triggered advancement run, and give the test helper named arguments.

* [AI] Consolidate schedule regressions into two cases

* [AI] Handle manual schedule posting failures

* [AI] Ignore apply-failure sync events when advancing schedules

* [AI] Restore active-run guard for schedule self-retries

Local writes emit sync errors other than apply-failure, such as
clock-drift, so filtering by subtype cannot prevent a run from
retrying its own failure.
2026-11-03 22:06:56 +00:00
RaymondandGitHub 821bcc2a13 fix: custom reports showing excluded empty category rows (#9402)
* Issue/3664 fix

* applied 256 character eval and return criteria per coderabbit review
2026-11-03 22:03:17 +00:00
Hrushikesh RamillaandGitHub 51880c71f0 fix: use selected file for option-driven re-parses in import modal (#8929) (#9401)
* fix: use selected file for option-driven re-parses in import modal (#8929)

Signed-off-by: hrushikeshramilla <ramillahrushikesh@gmail.com>

* fix: trigger parse on repeated file selection in import modal

Signed-off-by: hrushikeshramilla <ramillahrushikesh@gmail.com>

* fix: pass complete format options on repeated file selection retry

Signed-off-by: hrushikeshramilla <ramillahrushikesh@gmail.com>

---------

Signed-off-by: hrushikeshramilla <ramillahrushikesh@gmail.com>
2026-11-03 22:02:03 +00:00
15571bd6bb [AI] Update MASH documentation link (#9425)
MASH moved each service's documentation into its component directory, so docs/services/actual.md now 404s.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 21:27:25 +00:00
James NGitHubClaude Opus 5.5github-actions[bot] <github-actions[bot]@users.noreply.github.com>
288bf88f2f [AI] Allow the Reports page to be hidden from the sidebar (#9406)
* [AI] Allow the Reports page to be hidden from the sidebar

A hidden Reports stays out of the Settings nav. It shows in the sidebar
while it is open, and its row in Settings gets an "Open" button.

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

* [AI] Test that a hidden Reports leaves the sidebar after navigating away

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

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #9406

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-11-03 20:03:18 +00:00
TimandGitHub b450a9f269 [AI] Add "Average of previous weeks" budget automation (#9405)
* [AI] Add "Average of previous weeks" budget automation

* [AI] Keep weekly average unrounded and label fixed adjustments per week

* [AI] Simplify weekly average template plumbing

- Collapse the historical template type conversions in the automation
  reducer into one helper instead of six pairwise cases.
- Dispatch the adjustment update once from AmountAdjustment; the template
  type discriminant narrows the payload without branching.
- Drop the redundant early return in trimWeeklyAverageWindow: the general
  formula already returns the untouched window when activity predates it.

* Update upcoming-release-notes/average-of-previous-weeks-automation.md

* [AI] Default weekly average automations to 12 weeks

* [AI] Only carry percentage adjustments between monthly and weekly averages

* [AI] Run weekly averages through the average template and share the weekday count

* [AI] Share the weekday names with the date utils

* [AI] Trim weekly average tests to the essential cases

* Apply batched suggestions from code review

Co-authored-by: Tim <hello@timsmart.co>

* Apply suggestion from @tim-smart

* Apply suggestion from @tim-smart

* Apply suggestion from @tim-smart
2026-11-03 19:01:57 +00:00
767068b64e [AI] 🩹 handle dashboard widgets not showing on some devices (#9278)
* [AI] Fix dashboard widgets not syncing between devices

The multiple_dashboards migration seeded dashboard_pages with a uuid
generated at migration time. Migrations run locally on every device and
emit no sync messages, so each device ended up with its own "Main" page
id. Widgets do sync, carrying their dashboard_page_id along, so a widget
added on one device arrived on the other pointing at a page that device
had never heard of. The dashboard filters widgets on an exact page-id
match, so it was dropped from the UI entirely — permanently, not just
until a refresh.

Add a DEFAULT_DASHBOARD_PAGE_ID constant and a migration that re-points
the seeded page at it, so every device agrees on the default page, then
adopts the widgets orphaned in the meantime. The migration writes raw
queries on purpose: syncing these repairs would have the two devices
overwriting each other's dashboard_page_id values back and forth. Where
messages_crdt cannot identify the seeded page (sync-reset clears it) the
ids are left alone rather than guessed.

Also keep widgets whose page is missing visible on the default page, so
a message replay re-orphaning a widget cannot hide it again.

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

* [AI] Order dashboard pages instead of pinning a default page id

Replaces the constant DEFAULT_DASHBOARD_PAGE_ID and the migration that
rewrote the seeded page's primary key.

Give dashboard_pages a sort_order instead, following account_groups: the
migration backfills it in insertion order, so the page multiple_dashboards
seeded sorts first on every device, and dashboard-create appends after the
existing pages. Register it in the schema's defaultOrderBy so every query
returns pages in that order — until now the page order was whatever SQLite
happened to return, which could differ per device.

Widgets whose page does not exist locally still resolve onto the first
page, which is now deterministically the seeded one. That leaves the ids
alone: nothing rewrites a primary key, and no device has to guess from
messages_crdt which page another device seeded.

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

* [AI] Sort a dashboard page from an older client last

A client that predates the sort_order column syncs a page's id and name
but no sort_order, leaving the column NULL. SQLite orders NULL ahead of
every number, so such a page would have sorted before the seeded one and
taken over as the page that adopts widgets synced from another device.

Give the column a high default so those rows land at the end instead.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-11-03 17:03:37 +00:00
0b1eec8cce [AI] Reject sync messages too far in the future (#8905)
* #7106 - Reject incoming messages with explicit 400 error when they're going to cause permanent clock drift issues

* Generate release notes

* Simplify release note

* Use >= for future check, and use functional error factory vs creating new error type

* PR feedback: only check new messages for invalid timestamps

* PR feedback: check for invalid timestamps and use a separate error for them

* [AI] Reduce clock drift sync tests

* [AI] Simplify sync timestamp rejection

* [AI] Always restore real timers in sync resend test

* [AI] Assert rejected sync messages are rolled back

* [AI] Tidy clock drift comment and release note

* [AI] Match server future-drift limit to client maxDrift

* [AI] Test the five-minute future-drift boundary

* [AI] Match client drift comparison at the boundary

---------

Co-authored-by: Tim Smart <hello@timsmart.co>
2026-11-03 04:07:55 +00:00
ab0b5c40fa [AI] Keep mobile transaction search when going back from a transaction (#9172)
* [AI] Keep mobile transaction search when going back from a transaction

* [AI] Follow the search URL param on every change and add an e2e test

TransactionSearchInput now applies the `search` param whenever it
changes, not only on mount, so navigating to a different search on the
same page updates the box and the list. Moved it into its own file.

Adds an e2e test (mobile account page: search, open a transaction, Back)
and a unit test for a URL change after the first render.

* Apply the search before showing drill-down filtered transactions

---------

Co-authored-by: tabedzki <35670232+tabedzki@users.noreply.github.com>
2026-11-02 23:02:37 +00:00
4548275a68 [AI] Add isNativeMobileApp platform flag and gate web-only UI behind it (#9383)
* [AI] Add isNativeMobileApp platform flag and gate web-only UI behind it

The native Capacitor builds were detected ad hoc with 'Capacitor' in window.
Expose a real isNativeMobileApp flag from shared/platform (false on electron
and the API) based on Capacitor.isNativePlatform(), which stays false on the
plain web even once @capacitor/core is bundled there.

Use it to skip what has no meaning in an App Store build: service worker
registration (assets ship in the bundle and update via the store), the
"new version of Actual is available" notification, the "learn how to set
up a server" hint on the server config screen, and the "Use the current
domain" shortcut, whose origin is the capacitor:// scheme.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Gate the GitHub release check itself in the native mobile apps

Skipping the fetch in getLatestVersion covers the update notification, the
Settings "Updates" row and the GitHub rate limit in one place, so the
per-consumer gate in FinancesApp goes away. Also inline the one-use type
alias in platform.ts and tighten the service worker comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Unregister any service worker left by earlier native mobile builds

Skipping registration alone leaves a worker installed by a previous native
build in control of page loads, serving the old precached bundle after a
store update. Drop existing registrations on native startup only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Revert "Unregister any service worker left by earlier native mobile builds"

The only native builds that ever registered a service worker were TestFlight
test builds, so there are no store users with a stale worker to clean up.

This reverts commit 2eb95f623f3bc0ba7a8dd7b0e2b94b6a4ebc0c0e.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 22:09:23 +00:00
6330c716ce [AI] Upgrade akahu to 2.6.0 (#9411)
Drops the deep import of EnrichedPendingTransaction, which 2.6.0 folds
into PendingTransaction.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 21:09:34 +00:00
github-actions[bot]GitHubgithub-merge-queuegithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Julian Dominguez-Schatz
741d1615b7 🔖 (26.11.0) (#9321)
* 🔖 (26.11.0)

* Generate release notes for v26.11.0

* Generate release notes for v26.11.0

* [AI] Add November release highlights and upgrade notices

* Generate release notes for v26.11.0

---------

Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
2026-11-02 00:11:25 +00:00
5226912e39 [AI] Add native haptic feedback to the iOS and Android apps (#9382)
* [AI] Add native haptic feedback to the Capacitor mobile apps

The calculator keypad relied on navigator.vibrate, which iOS does not
support. Route keypad presses, swipe-action and pull-to-refresh threshold
crossings through a shared helper that uses @capacitor/haptics inside the
native apps and falls back to the Vibration API elsewhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eMnLyeJnMJFhQHDcCG

* [AI] Extract pull-to-refresh haptic into a named hook and drop helper test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8X9eMnLyeJnMJFhQHDcCG

* [AI] Keep swipe-action revealed state in sync when the spring start is a no-op

When a swipe overshoots the full reveal (or close), the spring is already
at its target on release, so react-spring treats the final start as a
no-op and never calls onRest. isRevealed then stayed stale, which made the
next drag jump instead of follow the finger and skipped the threshold
haptic. Set the state at release time instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Ignore the haptics plugin dependency in knip

The native app's package.json declares @capacitor/haptics so Capacitor
links the plugin, but no JS in that package imports it, same as the
splash-screen plugin.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Start swipe drags from the spring's live position

Read x.get() when a drag starts instead of tracking an isRevealed flag.
A swipe that interrupts a snap animation now continues from where the
item actually is, and the flag (which could go stale) is no longer needed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] Rename haptics helpers for clarity

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-11-01 14:21:56 +00:00
f2cc897782 [AI] Show a clear error when the API finds an out-of-sync local budget (#9393)
* [AI] Show a clear error when the API finds an out-of-sync local budget

getSyncError() had no case for the 'out-of-sync' reason, so API calls
(downloadBudget, loadBudget, sync) fell back to the generic "We had an
unknown problem opening" message. Add a specific message that explains
the local copy is out of sync and how to recover.

Fixes #9392

* reword

* Update packages/loot-core/src/shared/errors.ts

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

* fix test

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-10-31 22:30:44 +00:00
EMAD BASHARIandGitHub 5897cfa65a fix: reject invalid category group updates (#8856) (#9374)
* fix: reject invalid category group updates (#8856)

* fix: validate category group existence on update

* test: mock existing category group in update unit test
2026-10-31 16:31:59 +00:00
Junaid KhanandGitHub c5428ce2e9 fix(electron): align Linux StartupWMClass with window class (#9387)
* fix(electron): set StartupWMClass to match Linux window class

Electron uses the lowercase executableName (actual) as WM_CLASS, while
electron-builder defaulted StartupWMClass to productName (Actual). Align
the .desktop entry so GNOME can group the AppImage window with its launcher.

Fixes #9334

* docs: add release note for Linux StartupWMClass fix
2026-10-31 09:09:46 +00:00
doksanbesandGitHub 945fce10bb [AI] fix(core): fall back for non-finite arithmetic results (#9309) 2026-10-30 22:13:22 +00:00
e82ea4dc35 [AI] Fix schedule service shutdown race (#8791)
* [AI] fix: prevent duplicate schedule runs during shutdown

* docs: add schedule shutdown release note

* Restore current master changes

* Handle schedule mutator failures

* fix: drain background syncs and retry failed schedule metadata writes

* fix: drain background work for every budget close entrypoint

* fix: stop sync timers and drain late work before budget close

* [AI] Minimize schedule shutdown regression coverage

* [AI] Drain sync work in closeBudget instead of tracking background handlers

* [AI] Stop new syncs while closing the budget

* [AI] Flush mutators with a queued no-op instead of tracking them

* [AI] Track running mutators again so sync-error schedule work is drained

* [AI] Tighten shutdown comments and release note

* [AI] Keep only essential schedule shutdown regressions

* [AI] Drop rollback after completed schedule runs

---------

Co-authored-by: cakeni <211545599+cakeni@users.noreply.github.com>
Co-authored-by: Tim Smart <hello@timsmart.co>
2026-10-30 08:59:36 +00:00
a6a989823b [AI] fix transaction search treating digits inside a word as an amount (#9232)
* [AI] fix transaction search treating digits inside a word as an amount

* [AI] Reduce transaction search regression tests

* [AI] Keep amount search for the configured currency symbol

* [AI] Only strip a leading or trailing currency symbol from amount searches

* [AI] Tighten transaction search comment and release note

* [AI] Trim the search after removing a trailing currency symbol

---------

Co-authored-by: Tim Smart <hello@timsmart.co>
2026-10-30 08:08:46 +00:00
Dami OyeniyiGitHubgrzdevautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>Tim Smart
fa10ddb9de [AI] Fall back to IPv4 when binding to IPv6 is unavailable (#9226)
* [AI] Fall back to IPv4 when binding to IPv6 is unavailable

* [autofix.ci] apply automated fixes

* fix(sync-server): handle fallback listen errors and fix test typings

* [autofix.ci] apply automated fixes

* fix(sync-server): close server and propagate error on terminal listen failures

* [AI] Minimize sync server listen tests

* [AI] Use Node's default binding for IPv4 fallback

* [AI] Assert default host is omitted when listening

* [AI] Simplify listen test and startup message naming

* [AI] Tighten listen comment and release note

---------

Co-authored-by: grzdev <grzdev@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Tim Smart <hello@timsmart.co>
2026-10-30 04:36:28 +00:00
15a2e4dbaa [AI] Support Date inputs in current period checks (#9263)
* [AI] Support Date inputs in current period checks

* [AI] Simplify current period regression tests

* [AI] Simplify current period validity checks

---------

Co-authored-by: Tim Smart <hello@timsmart.co>
2026-10-30 03:56:42 +00:00
8e6eb272a7 [AI] Preserve skipped schedule dates when editing schedules (#9173)
* [AI] fix(schedules): preserve skipped dates on edit

* [AI] docs: add skipped schedule release note

* [AI] test(schedules): cover account changes via editor alias

* [AI] Simplify skipped schedule regression coverage

* [AI] Compare schedule account conditions without the field name

---------

Co-authored-by: Tim Smart <hello@timsmart.co>
2026-10-30 03:55:32 +00:00
Matt FiddamanandGitHub 1d9ec2d1d4 fix tab skipping to the cleared column after clicking a date (#9367)
* fix tab

* note
2026-10-29 20:36:50 +00:00
c7904f00db [AI] Keep transactions after sorting by date with hidden reconciled ones (#9363)
onSort queried before the new sort was in state, so canCalculateBalance
read the previous sort. Going to oldest first, hidden reconciled
transactions stayed in the query and filled the first page, leaving the
list empty. Going back to newest first, they were left out and the
running balance only counted the visible transactions. Query in the
setState callback instead.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 15:54:33 +00:00
Sebastián MalukandGitHub 4ad77e4517 [AI] Explain the largest contributors to balance forecasts (#9208)
* [AI] reports: explain balance forecast contributors

Group projected occurrences by schedule and reconcile their filtered net changes with recorded transactions. Keep original occurrence dates across rules so transfer legs count once, and exclude posted schedule occurrences independently of report filters.

* [AI] reports: improve forecast schedule name readability
2026-10-29 14:58:55 +00:00
72924f9b27 Fix template over-budget when a rule's schedule has a split with no category (#9316)
* [AI] Add failing test: schedule template counts uncategorized splits

A schedule template simulates the schedule's rule on a transaction whose
category is the budgeted category. A split that sets no category inherits
that category (makeChild), so e.g. a net-pay transfer split gets budgeted
in every category that uses the schedule template.

The test is expected to fail: it receives 10000 instead of 3000.

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

* [AI] Fix schedule templates budgeting uncategorized splits

The schedule template simulates the schedule's rule on a transaction. It
seeded that transaction with the budgeted category, and split transactions
inherit their parent's category unless a rule action sets one. Any split
without an explicit category (e.g. a transfer of the net pay) was therefore
counted toward every category using the template.

Leave the simulated parent uncategorized so only splits explicitly assigned
to the category are budgeted.

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

* fix rebase conflict

* Resolve coderabbit finding

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-29 14:54:09 +00:00
53a61855b6 Schedule forecast smoothing (#8862)
* [AI] refactor: extract getOccurrencesBetween from schedule preview logic

computeSchedulePreviewTransactions hand-rolled a loop enumerating a
schedule's occurrences between two dates. Extract it into a reusable,
exported function so the upcoming schedule-forecast budgeting feature
can enumerate occurrences the same way.

Fixes two bugs found while wiring up the refactor:

- getNextDate falls back to a schedule's last-ever occurrence when it
  finds nothing at or after the requested day (e.g. a recurring
  schedule whose end date has passed). That fallback date can be
  behind the search cursor, which made the new loop's
  `day = addDays(nextDate, 1)` advancement not-a-day-more-than the
  fallback date, only get repeated forever, and hang. Fixed by
  breaking out of getOccurrencesBetween's loop when the returned date
  is behind the cursor instead of treating it as a genuine next
  occurrence.
- getOccurrencesBetween's contract is `[startDay, endDay)` (endDay
  exclusive), but computeSchedulePreviewTransactions previously treated
  its `upcomingPeriodEnd` boundary as inclusive. Preserved that by
  passing the day after `upcomingPeriodEnd` as `endDay`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] fix: stop getOccurrencesBetween truncating weekendSolveMode 'before' schedules

The previous fix for the getOccurrencesBetween infinite loop (1c828a7a2)
broke out of the loop whenever the weekend-adjusted occurrence landed
behind the search cursor. That's correct when it means the schedule has
truly ended (getNextDate's stale last-occurrence fallback), but wrong
for a `weekendSolveMode: 'before'` schedule whose occurrence legitimately
resolves to the Friday before a weekend cursor date — that's not the end
of the schedule, just a normal adjustment, and further occurrences remain
in range.

Fix by probing the raw, unadjusted occurrence (getNextDate with
noSkipWeekend=true) to decide whether the schedule has ended, mirroring
the existing getNextDateAfter pattern. Only the raw occurrence is used to
detect "no more occurrences" and to advance the search cursor; the
weekend-adjusted date is used solely for the value pushed into the
result. Also removed the now-genuinely-reachable (not dead) startDay
bounds check, since the adjusted date can legitimately land before
startDay when the cursor sits exactly on a raw occurrence that gets
shifted backward.

Added a regression test reproducing the exact truncation the reviewer
described (weekly-on-Saturday, skipWeekend + weekendSolveMode: 'before',
queried starting from a raw Saturday occurrence) and verified it fails
against the prior commit's logic before this fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] refactor: carry dateConditions through createScheduleList entries

The upcoming schedule-forecast algorithm needs each schedule's raw
recurrence rule to enumerate every future occurrence, not just the
next one. Export createScheduleList and add dateConditions to its
per-schedule output rather than re-deriving it from the schedule rule
a second time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] feat: build a 60-month per-category outflow array for schedule forecasting

Enumerates each smoothed schedule's occurrences and any current/future
transactions already assigned to the category but not linked to a
schedule, collapsing them into a single monthly array. This is the
input the forecast-smoothing iteration (next commit) runs against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] feat: add runScheduleForecast smoothing algorithm

Projects each category's smooth (non-full-flag) schedule outflows 60
months forward and solves directly for the minimal monthly
contribution that keeps the projected balance non-negative throughout,
instead of today's independent-per-schedule sinking heuristic. Not yet
wired into the budget engine — that's the next commit, gated behind a
constant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] fix: normalize perScheduleMonthly weights and guard zero-total split

Fixes two issues from review of a52cdd2ee: the smooth-schedule share
split used raw per-occurrence entry.target as its weight, which
overstates a yearly (or other non-monthly) schedule's share relative
to a monthly one sharing the same category -- switched to the
existing getMonthlyBaseContribution normalization runSchedule already
uses. Also guards the case where smooth schedules in one category net
to a zero total monthly-equivalent weight (e.g. offsetting-sign
schedules), which previously could produce a non-finite
Infinity/-Infinity share; falls back to an even split across entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] feat: gate runScheduleForecast behind scheduleForecastConfig

Wires the new forecast-based smoothing algorithm into the schedule
template dispatch, defaulting to off so existing behavior is
unaffected until perf is measured and a rollout decision is made.
Exposed as a mutable config object (rather than a bare constant) so
both dispatch paths can be exercised explicitly and in parallel by
tests, instead of only reachable via a manual one-off toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] fix: correct schedule forecast smoothing query and aggregation bugs

Fixes from the final whole-branch review of schedule-forecast-smoothing:
convert month-shaped current_month to a day-shaped date before using it
as an AQL date filter bound (was throwing CompileError in production),
bucket occurrences in buildMonthlyOutflow by a new per-occurrence amount
instead of createScheduleList's monthly-aggregated target (fixes a
~4-25x over-forecast for weekly/daily schedules), and exclude off-budget
accounts from the unlinked-transaction query. Adds a weekly-schedule
regression test and an integration test that exercises the real AQL
query-compilation path. Also corrects the "build-time switch" wording
for scheduleForecastConfig, which is actually a mutable, test-settable
module-level switch read at call time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] perf: build the recurrence schedule once in getOccurrencesBetween

getNextDate rebuilds an RSchedule object from scratch on every call
(recurConfigToRSchedule + `new RSchedule(...)`), which is fine for its
normal single-occurrence use but was catastrophic for
getOccurrencesBetween's per-occurrence loop: a daily schedule over a
60-month window rebuilt the schedule ~3,650 times (2 getNextDate calls
per occurrence x ~1,825 occurrences), measured at 376ms for one
schedule. Build the schedule once and pull the whole [startDay, endDay)
range from it in a single pass instead, preserving the weekend-skip
adjustment semantics by querying a padded raw-date window and filtering
on the adjusted dates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] test: cover forward weekend-shift boundary and correct padding rationale

Addresses two review findings on 44aeddf11's getOccurrencesBetween
rewrite:

- The WEEKEND_SHIFT_PAD_DAYS comment only cited the 2-day weekend-shift
  bound, but that alone doesn't justify padding by 3: raw occurrences
  are timestamped at noon (recurConfigToRSchedule hardcodes
  byHourOfDay: [12]) while the query window is computed with
  d.startOfDay (midnight), so an occurrence dated exactly on endDay can
  already fall outside an unpadded, midnight-aligned queryEnd before
  any weekend adjustment is considered. Reworded the comment to explain
  both effects so the constant isn't re-derived from an incomplete
  premise later.
- All existing boundary tests only exercised weekendSolveMode:
  'before'. Added the 'after'-mode mirror: a raw occurrence shifting
  forward across startDay into range, and one shifting forward across
  endDay out of range.

No production logic changed; the padding constant was already
sufficient, just under-justified in its comment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] docs: drop code comments pointing at the internal design doc

Those comments referenced docs/superpowers/specs/2026-09-02-schedule-forecast-smoothing-design.md, which is this repo's own iteration artifact and isn't part of what ships upstream — a comment pointing at a file reviewers can't see is worse than no pointer at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] fix: address CodeRabbit review findings on #8862

- buildMonthlyOutflow passed the raw (possibly day-shaped) current_month
  to getOccurrencesBetween instead of the already-computed windowStart,
  inconsistent with the AQL query's window bound.
- aqlQuery test mocks returned only `data`, erased the mismatch with
  `as never`; supply the full { data, dependencies } shape instead.
- The schedule-template-gating dispatch tests asserted the correct
  algorithm's output equaled the dispatched result, but never confirmed
  the two algorithms actually differ for the fixture -- and for the
  original fixture (one pay-month-of monthly schedule + one yearly
  sinking schedule) they coincide exactly (this was a genuine, not
  hypothetical, false-positive risk). Swap the fixture for two
  non-monthly schedules with different due dates, and assert the two
  algorithms' totals differ before asserting which one was dispatched.
- A solveMonthlyContribution regression test's name and comment
  described the opposite of what it verifies (said month 0's threshold
  is larger and wins; the test actually asserts month 59's threshold,
  149 > 100, correctly wins) -- corrected the test's own documentation,
  behavior unchanged.
- Added a permanent regression test for actualbudget/actual#6513 (two
  annual schedules, different due dates, partial pre-existing balance),
  asserting runScheduleForecast's own non-negative-balance postcondition
  directly rather than comparing against runSchedule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] feat: default the schedule forecast algorithm on for this branch

Flips scheduleForecastConfig.enabled to true so this branch/PR runs the
new algorithm by default -- addresses CodeRabbit's Linked Issues check
concern that an opt-in-only fix doesn't resolve the linked bug for
normal users, and lets reviewers exercise it directly. Adds the
release note this defaulting requires.

No other schedule-type template tests assume the default value (the
gating tests set scheduleForecastConfig.enabled explicitly per test and
reset it in afterEach); full suite and typecheck pass unchanged with
the new default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] test: add default-gate dispatch coverage for schedule forecast

Addresses CodeRabbit feedback on #8862: the two existing dispatch tests
both set scheduleForecastConfig.enabled explicitly, so neither exercised
the module's initial true value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* [AI] fix: anchor schedule forecast to today when budgeting past or future months

The forecast assumed the month being budgeted was the current month.

- Budgeting ahead: the carried-over balance still holds money for
  scheduled occurrences between today and the budget month that haven't
  been paid. Count them against the budget month, from the schedule's
  stored next_date (bounded to the real current month). A linked payment
  already in the balance settles its nearest pending occurrence, since
  next_date only advances after the due date passes.
- Elapsed part of the budget month (all of it when re-running a past
  month): count the larger of the occurrences due by today and actual
  posted spending by today, linked or not. An unlinked payment can't be
  told apart from the scheduled bill, so it isn't counted twice; a bill
  that came in higher counts at its real amount; one that came in lower
  still reserves the scheduled amount. Linked payments settle their
  nearest occurrence in the month, so early payments aren't double-counted.
- Re-running a past month no longer counts unlinked spending posted in
  later months, which belongs to those months' own budgets; only
  future-dated unlinked transactions count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* [AI] docs: categorize schedule forecast release note as an enhancement

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* [AI] fix: apply both date bounds in schedule forecast transaction queries

AQL compiles only the first operator of an operator object, so
`date: { $gte, $lt }` silently dropped the upper bound. The posted-spending
query then counted every future-dated transaction as spent this month,
over-budgeting the month and leaving nothing for the next. Use the array
form, which ANDs the bounds.

Also address review feedback on countLinkedPayments: search far enough
either side of the range to find the true neighboring occurrence (a
one-month pad missed quarterly predecessors), and let several payments
nearest one occurrence settle it only once.

The unit-test mock now reads date filters the way AQL compiles them, and
a new test runs the forecast against a real database with future-dated
payments.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* [AI] fix: reserve unposted past-due schedules when budgeting ahead

A one-time schedule dated before the budget month was rejected as "in
the Past" and dropped before the forecast saw it, so budgeting ahead
treated the money carried in for it as free. With a $100 bill on Oct 1
not yet posted, budgeting November gave $0 instead of $36.66.

The forecast now keeps any not-completed schedule whose last occurrence
is before the budget month (one-time or ended recurring), so the
existing budgeting-ahead logic reserves it until it posts. Such
schedules only take a share of the per-schedule split when nothing
else in the category is left. runSchedule is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013KvKbXvv5NfsXJwNo5o2hA

* [AI] fix: only this category's linked payments settle a pending schedule

When budgeting ahead, an occurrence due between today and the budget
month is reserved out of the carried-over balance unless a linked
payment already settled it. That lookup filtered by schedule and date
only, so a payment booked in a different category counted. A schedule
linked from two categories (as in the test budget attached to #8862)
then had its unposted bill treated as paid in both, and November was
budgeted as if October's bill were already covered.

Filter the lookup by category and on-budget account like the other
transaction queries in buildMonthlyOutflow. A payment from another
category didn't come out of this balance, so this category still
reserves for the bill; over-funding is the safer failure for a schedule
linked from the wrong category, and it matches what the current-month
path already assumes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] fix: keep completed schedules in the forecast so their payment is covered

Once a one-time schedule's transaction has posted and its date has
passed, the schedule service marks it completed. createScheduleList then
dropped it, and a category whose only schedule template had completed
got no forecast at all: on Oct 3, "Test Bill A" ($100 due Oct 1, $75
carried in, payment posted) was budgeted nothing and sat at -25.

The forecast now keeps completed schedules, full-flag ones included.
They generate no future occurrences and nothing pending when budgeting
ahead, but the forecast still runs, so the month's posted spending is
covered the same way as any other spending in the budget month.
runSchedule is unchanged and still drops them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] fix: let a completed schedule's payments stand in for its occurrences

Completing a schedule stops its rule from generating occurrences, but
the forecast also stopped seeing what the schedule had already posted.
Re-running August on Oct 3 after Bill A ($100, Oct 1) completed asked
22.50 instead of 47.50, because Bill A's payment was linked (so not
unlinked spending) and no longer an occurrence. The same transactions
gave a different answer depending on whether the schedule service had
run yet.

For completed schedules, count their linked payments dated after the
elapsed part of the budget month at their own month, where the
occurrence would have been. Payments in the elapsed part are already in
the month's posted spending, so the two windows don't overlap.
Completion is then a no-op for the forecast.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] fix: count a completed schedule's occurrences up to its last known date

A completed schedule's payments standing in for its occurrences missed
the case where the payment isn't in this category at all. With Bill A
($100, Oct 1) completed and its payment uncategorized, budgeting
November on Oct 7 reserved nothing for it, although the money was still
in the carried-over balance. That is how the maintainer simulated
budgeting next month before this month's bill has come out.

Instead, treat a completed schedule like an active one up to a cutoff:
the next date it had when it was completed, or its last linked payment
in this category if that is later. Rule occurrences up to the cutoff
count as due, pending, or elapsed by the same rules as any other
schedule, so this category's own payments settle them and a payment
booked elsewhere doesn't. Occurrences after the cutoff are dropped: a
recurring schedule completed years ago still yields a date every month,
and none of them happened.

A one-off whose payment was deleted after completion now reserves its
amount again. From this category's side that is the same as a payment
booked elsewhere, so over-funding is the chosen failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [AI] fix: settle budget-month bills paid early in the previous month

A linked payment dated before the budget month is already out of the
carried-over balance, but the forecast only matched such payments against
pending occurrences when budgeting ahead. A bill due in the budget month
and paid early (e.g. due Nov 1, paid Oct 25) was charged again, both when
budgeting ahead and when budgeting the current month.

Query this category's linked payments before the budget month for every
schedule, and skip the budget-month occurrences they settle outright.
countLinkedPayments becomes settledOccurrences, returning the settled
dates so a bill paid in parts across the month boundary is settled once,
and finds the nearest occurrence by binary search since the lookback is
now wider.

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

* [AI] Budget zero for an overfunded schedule category; remove runSchedule

Per maintainer feedback on #8862: when a category already holds more
than its schedules need, budget nothing until the surplus is used up,
rather than spreading it over the 60-month window or returning it.
solveMonthlyContribution now compares the starting balance with the
lowest balance the window's average outflow covers; above it, the
contribution is the steady rate less the surplus, floored at zero.

Remove the old runSchedule path, the scheduleForecastConfig switch, and
the keepPastDue/keepCompleted options only it needed. Port master's
tracking-budget rule into the forecast: every schedule is budgeted in
the month it is due, since leftovers don't carry over. Port the
adjustment, completed-schedule and 360-day tests to the forecast and
replace the dispatch-gate tests.

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

* [AI] Update release note for zero-on-surplus schedule budgeting

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

* [AI] Speed up the schedule forecast without changing its results

- Generate each schedule's occurrences once, over every range the
  forecast needs, and slice ranges out of that list. The window and the
  linked-payment searches previously generated overlapping ranges
  separately, up to three times per schedule.
- Format occurrence dates directly instead of through date-fns format,
  which dominated getOccurrencesBetween for daily schedules.
- Compute an occurrence's month index from the date string instead of
  differenceInCalendarMonths.

Checked against the previous implementation on 1,500 randomized
scenarios (linked and unlinked payments, weekend shifts, completed
schedules, budgeting ahead and behind): identical results. Roughly 2x
faster with linked payments, 10-20% without.

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

* [AI] Only read completed-schedule payments after their next date

A payment only moves a completed schedule's cutoff when it is after the
schedule's stored next date, so the query no longer reads the whole
payment history when every completed schedule has a next date. On a
large budget file this query went from ~20ms to ~0.4ms per category.

Results are unchanged: checked against the previous implementation on
1,500 randomized scenarios, and a new real-database test covers a
payment entered after the next date.

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

* [AI] Don't fund a full-flag schedule twice alongside the forecast

A full-flag bill due this month is budgeted in full on top of the
forecast, but its amount was also taken off the balance the forecast
starts from, so the category was over-budgeted by the bill. When
budgeting the current month after the bill posted, its payment was also
counted as this month's spending.

Start the forecast from the real balance, and leave full-flag schedules'
payments out of this month's posted spending. Reported by CodeRabbit on
#8862.

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

* [AI] Save evenly for bills that start later; give the catch-up to schedules that are behind

Two issues from review of the attached test budgets:

- Monthly bills first due a few months out were funded almost in full
  up front, then a few cents for two months. The steady rate averaged in
  the empty months before the first bill, so it came out low, and the
  smallest constant amount over the whole 60-month window was set by its
  far end. The steady rate now comes from months 12 onward, and the
  catch-up runs at a constant amount only until a month whose bills
  reach the steady rate, then hands over to the steady rate.

- With several schedules in a category, a catch-up above the steady
  rate was split by each schedule's monthly equivalent, so a monthly
  bill that was on pace showed part of a yearly bill's catch-up. The
  amount above the steady rate now goes to the schedules whose bills
  through the catch-up exceed what their monthly equivalent puts by.

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

* [AI] Take the forecast's steady rate from each schedule's own rate

The steady rate was an average over the later months of the forecast,
which needed a cutoff month and could miss a schedule that recurs less
often than the averaged span. It is now the sum of each schedule's
monthly rate, and the per-schedule split uses the same rates.

Daily and weekly schedules recur every fixed number of days. Monthly and
yearly ones count their occurrences over whole months, twelve times the
interval long, so patterns such as the 1st and 15th count in full and a
schedule every few years is spread over its interval. This replaces
getMonthlyBaseContribution, which counted a weekly schedule by the
weeks in its next month.

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

* [AI] Reserve this month's bill when its linked payment isn't in the category yet

When budgeting ahead, the forecast only reserved occurrences on or after a
schedule's next_date. next_date moves on once any linked payment exists,
even one that is uncategorized or booked to another category, while the
category balance still holds that money. Budgeting next month before
categorizing this month's payment therefore treated it as surplus and
underbudgeted.

Now every occurrence from the start of the real current month up to the
budget month is reserved when it is on or after next_date, or when its
linked payment sits outside this category, unless a payment in this
category settles it. Skipped occurrences stay unreserved.

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

* [AI] Reserve unpaid full-flag bills when budgeting ahead

Only smoothed schedules had their unpaid occurrences between today and
the budget month reserved out of the carried-over balance. A full-flag
bill funded this month but not yet posted left its money in the balance,
and the forecast counted it as surplus, underfunding the smoothed
schedules sharing the category.

buildMonthlyOutflow now takes the full-flag entries and reserves their
pending occurrences with the same rules as smoothed ones. Their treatment
in the budget month itself is unchanged.

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

* retry ci

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-10-28 20:34:04 +00:00
7caedc7c48 [AI] Add Quarterly interval to custom reports (#9347)
* [AI] Add Quarterly interval to custom reports

Custom reports can now group by calendar quarter (Q1 = Jan-Mar),
alongside Daily, Weekly, Monthly and Yearly. Quarters are keyed by
their first month (e.g. Q3 2026 is '2026-07') and labelled
"Q3 '26". Transactions and budgeted amounts are queried by month
and rolled up into quarters, the same way Weekly rolls up days.

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

* [AI] Keep custom report From/To ranges on period boundaries

Picking To in the same month, quarter or year as From compared a full
date against a shorter period key, so it was treated as reversed and
From was moved back. Compare against the computed full dates instead.

When one end of a Quarterly range jumps past the other, the fallback
now spans four whole quarters instead of 365 days, so the range no
longer starts or ends partway through a quarter.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-10-28 18:11:09 +00:00
38ba95dcdc [AI] Keep the search applied after toggling reconciled transactions (#9360)
* [AI] Keep the search applied after toggling reconciled transactions

Toggling reconciled transactions rebuilt the query with the filters but
not the search, so the search text stayed in the box while the list was
no longer searched. Re-run the search like remove-sorting does.

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

* [AI] Re-run the current query when toggling reconciled transactions

Toggling reconciled transactions changes neither the filters nor the
root query, so there is no need to rebuild them. Re-running the current
query with the search avoids racing the async filter rebuild and keeps
the filters and sort.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:07:35 +00:00
Sebastián MalukandGitHub 6c41141434 [AI] Compare named balance forecast lines (#9209)
* [AI] reports: compare named balance forecast lines

* [AI] Refresh forecast snapshots after rebase

* [AI] reports: retain forecast results while filters update
2026-10-28 17:20:34 +00:00