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>
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.
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.
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>
- 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>
* [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>
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>
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>
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>
* [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>
* 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
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>
* [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.
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>
* [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>
* [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
* [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>
* #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>
* [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>
* [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>
Drops the deep import of EnrichedPendingTransaction, which 2.6.0 folds
into PendingTransaction.
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* [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>
* [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>
* fix: reject invalid category group updates (#8856)
* fix: validate category group existence on update
* test: mock existing category group in update unit test
* 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
* [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>
* [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>
* [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>
* [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>
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>
* [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
* [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>
* [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>
* [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>
* [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>