The "For next month" amount in the summary and its tooltip already show what's held, so the extra line on desktop and in the mobile overbudgeted banner repeated it.
A `buffered-managed` cell per month replaces three copies of the managed
rule: the `buffered-selected` formula, the server guard that queried the
preference and the `frozen` row, and the client's `isManagedMonth` with
its `month` prop plumbing and `monthFromSheet`. The hook now reads the
cell for the sheet it's rendered in, which also fixes frozen months never
reading as frozen on the client: the cell holds a boolean, not 1.
The freeze only covers months that have a sheet, so the budget range no
longer reaches back to the start month, and the blank-sheet guard that
reach-back needed goes with it. The sync handler keeps the file check
from the carryover seed and tracks only the synced spreadsheet for the
freeze gate.
The sheet answers after the modal's first render, so seeding the
selection from the mode picked Manual even while Automatic was on. A
Save without choosing then turned the mode off, which can't be undone.
The selection now follows the sheet until the user picks a choice.
The modal tests use the real sheet hooks with a fake server, so the
cells arrive late as they do in the app.
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>
* [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 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.
* [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>
* [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>
* [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] 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>
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 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>
* [AI] Replace html-to-image with snapdom for report snapshots
Clicking "Download Snapshot" on a report does nothing in Firefox (#5731).
The cause is inside html-to-image's web font embedding: a font value can be
undefined after splitting the font-family list in Firefox, so `font.trim()`
throws and the whole capture aborts before a download is produced. The
upstream fix (bubkoo/html-to-image#541) was closed without merging and the
package has had no maintainer response for months.
Switch to @zumer/snapdom, which is actively maintained, MIT licensed and has
no transitive dependencies. It has its own font embedding, so the failing
code path no longer exists. It was suggested on the issue by tinchox5.
The APIs are not quite interchangeable: html-to-image's `toPng` resolves to a
data URL string, while snapdom's resolves to an HTMLImageElement, so the
download link now reads `image.src`. Assigning the element directly would
have produced a broken download with no error.
One visible difference: snapdom rasterises at `devicePixelRatio` by default,
so on a HiDPI display the exported PNG is larger and sharper than before. No
options are passed, so the transparent-background default matches the old
behaviour.
html-to-image is removed; this was its only use in the repo. The download
link is left as it was rather than moved to snapdom's own `download()`, to
keep this change to the capture library alone.
* [AI] Add release note for report snapshot fix
The code is unchanged apart from the new names. This prepares for
separate fixes to the donut's hover, touch and label behaviour.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* [AI] Name the stored scale of amounts
- Add STORED_DECIMAL_PLACES: amounts are stored with two implied decimals whatever the currency, and a currency's own decimals only decide how many digits are shown
- Make integerToCurrencyWithDecimal with a currency divide by 100 and show that currency's digits
- Remove amountToCurrencyInteger, which had no callers and stored amounts with the currency's decimals
- Update the tests
Issue: actualbudget/actual#8858
* [AI] Show and read amounts at the stored scale in every currency
- Make useFormat divide and multiply by 100 for every currency, so balances, totals, budget cells, rules, schedules, filters and reconciliation agree with the register, imports and bank sync
- Take typed digits as whole units for currencies without decimals in the desktop and mobile amount inputs
- Read formula card results at the stored scale
- Add tests for useFormat and the mobile amount input in dollars and yen
Issue: actualbudget/actual#8858
* [AI] Use the stored scale in budget templates and goals
- Convert template, goal, limit and schedule adjustment amounts with two implied decimals for every currency, and round to whole units of the currency when decimals are hidden
- Remove the currency from the template engine, which no longer needs it
- Read and write the goal editors' amounts at the stored scale
- Update the yen template tests
Issue: actualbudget/actual#8858
* [AI] Show budget movement notes at the stored scale
- Divide amounts by 100 and show the currency's digits in the notes written when money moves between categories
- Add a yen test
Issue: actualbudget/actual#8858
* [AI] Document that amounts are always stored x100
- State in the API docs that amounts are always the value x 100, whatever the currency
- Add the release note
Issue: actualbudget/actual#8858
* [AI] Reduce tests to the stored-scale change
- Drop the JPY template tests: the template engine no longer reads the currency
- Drop the round-trip test of the removed amountToCurrencyInteger
- Keep one yen case each for useFormat and the mobile amount input
* [AI] Simplify the stored-scale change
- Remove STORED_DECIMAL_PLACES and the unused decimalPlaces parameter of integerToCurrency
- Format budget movement notes with integerToCurrencyWithDecimal
- Drop the toAmount/fromAmount wrappers in useFormat
- Mobile amount input: format through useFormat so currencies without decimals show whole units while editing and after saving, whatever the hideFraction setting
- Drop the movement notes test, now covered by the integerToCurrencyWithDecimal tests
* [AI] Compare goals at the stored scale
Master's goal helper (#9304) still divided by the currency's decimal places.
Round to the currency's displayed digits through the formatter instead, and
return the difference ×100.
* [AI] Skip separator reformatting for whole-unit mobile entry
The zero-decimal branch strips every non-digit before formatting, so
reapplying thousands separators first did nothing there. Only the
two-decimal branch needs the reformatted text.
* [AI] Tidy comments, API docs and release note for stored amounts
* [AI] Keep stored fractions in edit text
forEdit rounded a stored fraction to the currency's digits, so focusing and
leaving an amount field unedited saved the rounded value (¥1,500.50 became
¥1,501). Show two decimals while editing when the stored value has a
fraction, unless fractions are hidden.
* [AI] Keep the exact amount when an amount field is left unedited
With decimals hidden, the edit text rounds a stored fraction (¥1,500.50
shows as 1,501), and blur, Enter or Tab saved that rounded text. The
desktop amount input, the financial input and budget cells now keep the
original amount when the text is unchanged.
* [AI] Track edits instead of comparing amount text
Comparing the text with the edit form missed the display text left after
Enter or a sign toggle, and treated clearing and retyping the rounded value
as untouched. Track whether the user typed: the desktop amount input keeps
the exact amount its text stands for, and the financial input and budget
cells parse only edited text.
---------
Co-authored-by: Tim Smart <hello@timsmart.co>
* [AI] Center list items correctly near the edges of a list
- Center an item by the offset that puts it in the middle, kept within the list, instead of jumping to the top whenever the list can scroll less than half its height
- Add tests
Issue: rapastranac/actual#10
* [AI] Scroll to and highlight the other side of a transfer
- Pass the linked transaction along when the transfer arrow opens the other account
- Load more of that account until the transaction is there, open its split, center it and point it out briefly with a pulse, or a steady highlight with reduced motion; leave the account at the top when the current view can't show it
- Keep the transaction in view while it's pointed out, also while rows load above it; drop it when the user searches, and ignore results of a query replaced since
- Add tests
Issue: rapastranac/actual#10
* [AI] Document finding the other side of a transfer
- Explain the transfer arrow in the transfers docs and add a release note
Issue: rapastranac/actual#10
* [AI] Reduce the tests for pointing out the other side of a transfer
- Keep one list test for the case that used to jump to the top and one for a centered item
- Check the pulse and its end in a single table test
* [AI] Load up to the other side of a transfer in one query
Use PagedQuery.refetchUpToRow instead of paging until the transaction
shows up, and only look for it in the account's first query so a sort
or filter applied meanwhile drops it. refetchUpToRow keeps the data as
it is when the row isn't in the query instead of throwing.
* [AI] Keep loading the other side of a transfer through a sync refresh
A sync refresh while loading the account up to the transaction dropped
the loaded rows, and the next onData gave up on it. fetchData now tells
whether a newer request superseded it, and the account loads again in
that case, giving up only when the query doesn't have the transaction.
* [AI] Fetch up to a row through sync refreshes in PagedQuery
A refresh during refetchUpToRow now makes it fetch again instead of
replacing its rows, and refetchUpToRow resolves once the rows up to the
row are loaded. The account awaits it and reads the transaction from
the loaded rows instead of guessing from the order of onData calls,
which drops the retry loop and the superseded flag on fetchData.
* [AI] Find the other side of a transfer on the account's current query
React can call componentWillReceiveProps twice for one account change,
which fetches the account twice. The pending transaction was tied to the
first query, which the second replaced, so it was never shown. Look for
it on the first load of the current query instead, as long as the
account isn't sorted.
refetchUpToRow also refetches the loaded rows when the row is missing
instead of keeping them, so a row deleted by a sync during the fetch
doesn't stay listed.
* [AI] Tidy the transfer focus code and its comments
---------
Co-authored-by: Tim Smart <hello@timsmart.co>
* [AI] Fix long payee names being cut off in the rule editor on mobile
On narrow screens the rule editor squeezed condition and action values
next to the field and operator selects (a long payee got ~56-119px), and
the "This rule applies to these transactions" table left the payee
columns about 47px wide, with no hover tooltip on touch devices.
- Below the small breakpoint, a value editor needs at least 70% of its
row, so it wraps onto its own line next to the -/+ buttons instead of
being squeezed beside the selects.
- Below the small breakpoint, keep the results table at least 600px
wide and let it scroll sideways.
Wider screens are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #9335
* [AI] Make the narrow rule editor layout deterministic
Below the small breakpoint each condition and action row now keeps the
field and operator selects with the -/+ buttons on the first line and
drops the value editor to a full-width second line, instead of relying on
a percentage min-width that only wrapped cleanly at some widths.
The transactions table's narrow minimum width is derived from its columns
so the trailing column stays partly visible as a scroll hint.
Regenerated the mobile rules VRT snapshots.
* [AI] Count only rendered columns in the narrow table width
getTransactionFields can include fields such as `cleared` or `payee_name`
that SimpleTransactionsTable never renders, so the narrow minimum width
now counts only the flexible columns the table actually shows.
* [AI] Wrap the split-amount value editor on narrow screens
The split-amount row kept its value input beside the allocation select
below the small breakpoint, squeezing it to 45-70px and leaving the -/+
buttons alone on the next line. It now follows the other rows: controls
and buttons on the first line, the value at full width on the second.
* [AI] Keep the split method select on the first line when narrow
In the narrow rule modal the row is too tight for the "allocate" label,
the long method label and the -/+ buttons, so the select wrapped and left
the buttons on a line of their own above the value. The select now shares
the first line and truncates its label when there is no room.
* [AI] Simplify the narrow rule editor layout
Tighten the comments on the narrow layout helpers and skip the empty
split-amount value wrapper on narrow screens, where it only added a blank
flex line below the remainder row. Wide layouts keep the wrapper as the
spacer that pushes the buttons to the right.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tim Smart <hello@timsmart.co>
* [AI] Record a category's goal when its automations are saved
Saving automations in the editor now also stores the goal it projects
for that month, without changing the budgeted amount, so the goal
status and "Fund goal from To Budget" work without running templates.
Undo now also clears the goal on a newly created budget row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM
* [AI] Make the goal status agree with the balance colour
The balance menu's goal status, progress bar and "Fund goal from To
Budget" item now compare amounts as they are displayed, like the
balance colour does, using the currency's own decimal places so
currencies without decimals are not rounded to the nearest hundred.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM
* [AI] Drop an unneeded cast in the goal-on-save tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM
* [AI] Show the goal shortfall between the displayed amounts
With hidden decimals the goal status could read "Underfunded (0)", because
the label compared the displayed amounts while the amount in brackets used
the raw difference. Both now use the difference between the displayed
amounts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpAtycTVCRhkLcuD6AJeTM
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] Automatically extend planning in Tracking mode
* [AI] Shorten Tracking mode release note
* [AI] Simplify automatic budget range and support both modes
* [AI] Cap automatic budget range and stop shrinking it while open
- Cap the automatic range at 5 years ahead so stray far-future
amounts can't create an unbounded number of budget months
- Keep the range from shrinking while the budget is open; clearing
allocations shrinks it on the next load. Created months always
match the visible range again, so bulk actions (rollover, copy
until year end) reach every visible month
- Make getBudgetRange a pure function in base.ts again and revert
the getAllMonths and copyUntilYearEnd clamps in actions.ts
- Share a clampMonth helper between desktop and mobile
- Drop the extra loadPrefs on synced budget type changes
- Simplify the e2e test to the default budget type
When SimpleFIN is unreachable or down for maintenance, the sync server
answers with a SERVER_DOWN error. The client had no mapping for it, so the
account showed "An internal error occurred. Try to log in again", which
sends people to re-link a working account.
Persist the failure as a server-down bank sync status and show a message
saying SimpleFIN could not be reached and to try again later.