* [AI] feat: add balance forecast backend
* [AI] feat: add balance forecast report UI
* [AI] feat: gate balance forecast behind an experimental flag
* [AI] Include account-less schedules in balance forecast via explicit flag
- Add includeAccountlessSchedules to forecast/generate and normalize
schedules without an account into FORECAST_UNASSIGNED_ACCOUNT_ID
- When enabled, append synthetic bucket and rule stub; skip transfer legs
for unassigned schedules
- Balance forecast UI sets the flag when widget meta has no account filter
- Add loot-core tests for include vs exclude behavior
* [AI] Improve balance forecast chart refresh UX
Keep forecast charts stable during refetches and let the Y-axis scale to forecast data so balance changes remain visible.
* [AI] Document balance forecast report
Add experimental user documentation and navigation links for the new balance forecast report.
* [AI] Link balance forecast experimental flag to feedback issue #7669
* docs: add PR release notes
* [AI] chore: rerun CI
* [AI] fix: match no transactions when "has tags" input lacks `#` (closes#7797)
The SQL-side `hasTags` filter extracts `#tag` patterns from the user
input and `$and`s them together. When the input has no `#` (e.g. user
types `foo` instead of `#foo`), the extraction returns an empty array
and the resulting `$and: []` matches every transaction.
Mirror the empty-`oneOf` behaviour and return the match-nothing
sentinel (`{ id: null }`) in that case.
* [AI] Add release notes entry for #7808
---------
Co-authored-by: MaksZhukov <maks_zhukov_97@users.noreply.github.com>
* [AI] Fix flaky openid /config test from cross-worker auth race
Vitest runs sync-server test files in parallel workers that share
account.sqlite. Other files (e.g. app-account.test.js) insert 'openid'
auth rows, and auth.method is a PRIMARY KEY, so a concurrent INSERT in
app-openid.test.ts can hit UNIQUE constraint failed: auth.method.
Use INSERT OR REPLACE in the helper and clear the auth table in
beforeEach for a clean start.
* Add release notes for PR #7847
* Change category from Bugfixes to Maintenance
Fix OpenID authentication test flakiness by ensuring test isolation with INSERT OR REPLACE.
* [AI] Disable file parallelism for sync-server tests
The previous fix only patched insertOpenIdAuth in app-openid.test.ts,
but app-account.test.js's insertAuthRow helper also does plain
INSERT INTO auth ... 'openid' ... (lines 197, 203, 210, 229, 245).
With maxWorkers: 2 and a shared account.sqlite, either file's INSERT
can race the other's and hit UNIQUE constraint failed: auth.method.
Disable cross-file parallelism so test files run sequentially against
the shared DB. Within-file tests still run sequentially by default.
Test suite goes from ~20s to ~36s; trades some speed for stability.
* [AI] Revert openid test changes, reword release note
The fileParallelism: false change in vitest.config.ts already prevents
the auth.method UNIQUE-constraint race across files, so the INSERT OR
REPLACE and extra beforeEach cleanup in app-openid.test.ts are no longer
needed. Revert that file back to its original state and reword the
release note to describe the actual fix.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Stabilize size-compare job by pinning downloads to run_id
The compare job in .github/workflows/size-compare.yml was flaky because
fountainhead/action-wait-for-check matched a check by name from any run
on the branch, while dawidd6/action-download-artifact with branch:/pr:
filters and workflow_conclusion: '' resolved to the latest run regardless
of completion. When a new master build started in the seconds between
waiting and downloading, the action picked up the in-progress run and
failed with "artifact not found".
Replaces the eight wait-for-check steps with one actions/github-script
step that polls listWorkflowRuns for a successful build.yml run on
master and the PR head SHA in parallel via Promise.all, then pins all
eight downloads to those run_ids.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add release notes for PR #7780
* Change category to Maintenance in release notes
Updated category from 'Enhancements' to 'Maintenance'.
* [AI] Clean up comment to remove reference to previous implementation
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* [AI] Fix npm provenance for @actual-app/crdt and bump to 3.0.1
Add the missing repository field to packages/crdt/package.json so the npm
provenance bundle can validate the source against
https://github.com/actualbudget/actual. Without it, publishing fails with
"Error verifying sigstore provenance bundle: repository.url is \"\"".
* Add release notes for PR #7845
* [AI] Revert @actual-app/crdt version back to 3.0.0
* Fix metadata formatting in package.json for crdt
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Replace any-typed Modal in undo state with structural type
loot-core can't import @actual-app/web's Modal union, so the undo MRU
typed openModal as `any`. The undo system only stores the value and
reads `.name`, so a minimal structural shape `{ name: string; options?: unknown }`
is enough. desktop-client's full Modal still assigns to it, and the one
reader (global-events.ts) re-narrows back to Modal when handing the
value to replaceModal().
* Add release notes for PR #7813
* Update 7813.md
* [AI] Add TODO on Modal cast for future type consolidation
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Share cleanup-group helpers and let storeTemplates write cleanup_def
- Extract resolveCleanupGroup and tombstoneOrphanCleanupGroups out of
cleanup-template-notes.ts into a new cleanup-groups.ts so the
upcoming UI-driven create flow can reuse the resurrect-aware lookup.
- Let storeTemplates accept an optional cleanup array per category
(omitted = leave as-is, [] = clear, non-empty = replace), and run the
orphan tombstone sweep whenever cleanup_def is touched so groups
removed from the UI don't linger.
- Register budget/store-note-cleanups so the UI can migrate a single
category's notes on demand, and budget/create-cleanup-group so it can
create groups inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Add UI editor for end-of-month cleanup automations
- New cleanup row in the BudgetAutomations sidebar with read-only
summary; selecting it opens an editor with a Global scope card and
an optional named-group scope (single group per category for now,
since multi-group ordering depends on category sort).
- Each scope card has independent "send leftover" / "take a share"
toggles plus a weight; group scopes additionally support
"only enough to cover overspending".
- Group picker is a typeahead that creates groups inline via
budget/create-cleanup-group.
- useCategoryCleanup migrates notes to cleanup_def at modal-open for
unmigrated categories; useCleanupGroups streams the live list.
- Un-migrate flow renders cleanup_def back to #cleanup note lines and
drops rows whose group can't be resolved, so users never see UUIDs
in their notes.
- Sidebar/automation-button "has automations" probes also check
cleanup_def so cleanup-only categories still get the indicator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* note
* review pass 1
* bring automation logic in line with cleanup logic
* review pass 3
* coderabbit pass 1
* wording suggestions
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Require @actual-app/crdt version bump and auto-publish
Adds two workflows:
- crdt-version-check: fails PRs that modify files in packages/crdt/
without bumping the version in packages/crdt/package.json.
- publish-crdt: publishes @actual-app/crdt to npm when the version in
packages/crdt/package.json changes on master, tagging the release as
crdt-v<version>.
* [AI] Skip git tagging in @actual-app/crdt publish workflow
Remove the tag-and-push step and the now-unused version output;
downgrade contents permission to read.
* [AI] Simplify crdt version-bump workflows
- Drop the redundant explicit base-branch fetch (fetch-depth: 0 already
retrieves all remote branches).
- Remove the unreachable "no changes" guard; the pull_request paths
filter already scopes the workflow to packages/crdt changes.
- Replace the embedded Node semver comparison with `sort -V`.
- Read versions with `jq` instead of inline Node.
* [AI] Add release notes for crdt publish workflows
* [AI] Restrict GITHUB_TOKEN permissions in crdt workflows
Add top-level `permissions: contents: read` to both crdt workflows so
the implicit jobs no longer inherit overly broad permissions (flagged by
zizmor).
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] crdt: typecheck test files and clean up lint issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Replace google-protobuf with @bufbuild/protobuf
Swap the google-protobuf + ts-protoc-gen + protoc-gen-js toolchain for
@bufbuild/protobuf + @bufbuild/protoc-gen-es. The generator now emits a
single pure-TS sync_pb.ts (no .js sidecar, no globalThis.proto hack)
and a thin wrapper in proto/compat.ts preserves the SyncProtoBuf /
SyncRequest / etc. API so call sites stay unchanged. Removes the
loot-core CommonJS require polyfill that only existed to service
google-protobuf.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Align @bufbuild/protobuf version ranges with installed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: drop the SyncProtoBuf compat layer
The proto/compat.ts wrapper was introduced alongside the bufbuild
migration to avoid touching call sites. With bufbuild messages already
exposing fields as plain mutable properties, the wrapper was just
boilerplate hiding direct reads and writes — and it had drifted (e.g.
setMessagesList was called in a test but never defined).
Delete compat.ts and migrate the six call sites in loot-core and
sync-server to use @bufbuild/protobuf directly. The crdt package now
re-exports the sync_pb types/schemas and the three bufbuild runtime
helpers (create, fromBinary, toBinary) so consumers keep a single
import source.
Also switch sync-server's @actual-app/crdt dependency from the pinned
"2.1.0" to "workspace:*", matching api/loot-core — the npm pin was
pulling the stale published copy instead of the workspace source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] CI: drive sync-server build through lage so crdt deps are built
Before: the server job ran `yarn workspace @actual-app/sync-server build`
directly, which invokes tsgo without first emitting the workspace
dependencies' declarations. That worked when sync-server pinned crdt to
the published npm version (declarations bundled in the tarball), but
with `workspace:*` it fails with TS6305 because packages/crdt/dist/*.d.ts
hasn't been built yet.
Switch the CI command to `yarn build --to=@actual-app/sync-server`.
Lage respects the `dependsOn: ['^build']` pipeline and builds
@actual-app/crdt (and the other transitive deps) before sync-server.
Using --to rather than --scope keeps the build set minimal; --scope
would also include dependents like desktop-electron.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] sync-server: build project references via tsgo -b
The build script ran plain `tsgo`, which doesn't compile referenced
projects. With @actual-app/crdt now a `workspace:*` dep (no bundled
declarations from the npm tarball), the sync-server build fails with
TS6305 because packages/crdt/dist/index.d.ts doesn't exist yet.
Switch to `tsgo -b` so the sync-server build is self-contained: it
emits crdt's declarations into packages/crdt/dist on demand. This
mirrors what the sync-server `typecheck` script already does and fixes
all callers (`build:server`, docker-edge, publish workflows, the
direct `yarn workspace @actual-app/sync-server build` invocation in
build.yml) without needing per-workflow lage orchestration.
Revert the build.yml workaround added in the previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] sync-server: build @actual-app/crdt before tsgo
The previous tsgo -b approach emitted crdt's .d.ts via the project
reference but never produced dist/index.js — tsgo respects crdt's
tsconfig which has emitDeclarationOnly: true, and the actual JS
runtime is emitted by Vite in crdt's build script. So sync-server
compiled cleanly but crashed at runtime when forked by desktop-electron
(require('@actual-app/crdt') resolved to a package whose main pointed
at a nonexistent file, surfaced in e2e as the onboarding screen never
leaving the "Configure your server" state).
Unlike packages/api (which uses Vite with noExternal: true and bundles
crdt's source inline), sync-server uses plain tsgo compilation and
keeps its deps external — so crdt must be built ahead of time and be
resolvable via node_modules at runtime.
Chain `yarn workspace @actual-app/crdt build` before tsgo so every
caller of sync-server's build (build:server, docker-edge, publish
workflows, direct invocations in CI) gets a complete crdt dist. Revert
tsgo -b back to plain tsgo since crdt's build step now emits both the
JS and the declarations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: expose dist/ via conditional exports so Node can load it
The package's `exports` field pointed straight at `./src/index.ts`,
which works for TS tooling and bundlers (vite with noExternal, vitest)
but breaks at plain-Node runtime — Node can't execute `.ts` files and
resolves dependent `./crdt` as a directory import, failing with
ERR_UNSUPPORTED_DIR_IMPORT.
That was invisible before because sync-server pinned
`@actual-app/crdt@2.1.0` and ran against the published npm tarball
(whose `publishConfig.exports` had already been promoted to the main
`exports` by yarn pack). Switching sync-server to `workspace:*` made
the raw workspace exports win at runtime: the compiled server imported
crdt when desktop-electron forked it, Node hit the `.ts` entry, the
utility process crashed before emitting `server-started`, and the
onboarding flow stalled on "Configure your server".
Switch to the same conditional-exports pattern packages/api already
uses: types → dist/index.d.ts, development → src/index.ts (for vitest
runs that enable the `development` condition), default → dist/index.js
(Node runtime and any other consumer). `publishConfig.exports` still
collapses this to just types + default for the npm tarball.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: split exports per consumer (browser source, node dist)
Previous commit's conditional exports routed everything non-development
to ./dist/index.js. That broke the web build: rolldown runs with
conditions ['electron-renderer', 'module', 'browser', 'default'] — no
match for development, falls through to the dist entry, which isn't
built by bin/package-browser, and fails to resolve @actual-app/crdt
when bundling loot-core's server/undo.ts.
Split the entries so each consumer lands on the right artifact:
types → ./dist/index.d.ts (TypeScript, project references)
development → ./src/index.ts (vitest — both configs include it)
browser → ./src/index.ts (web rolldown bundles the source)
node → ./dist/index.js (sync-server forked by Node at
runtime — the failure that kicked
off this whole saga)
default → ./src/index.ts (fallback for bundlers like api's
vite build with conditions=['api'])
Verified: node resolves to dist, yarn build:browser succeeds from a
clean crdt/, sync-server build produces both dist/index.js and
build/app.js, loot-core (552) + sync-server (386) tests pass, full
typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] address review feedback on crdt/sync-server
- generate-proto: add `set -euo pipefail` so a protoc failure exits the
script non-zero instead of silently running oxfmt on whatever is in
src/proto/ from the previous run.
- sync.proto SyncRequest: field numbers jumped from 3 to 5; declare
`reserved 4;` so the slot can't be silently reused for a new field
with an incompatible type. Regenerated sync_pb.ts — the reservation
shows up in the encoded file descriptor.
- sync-simple.js: SQLite stores is_encrypted as a 0/1 integer and
better-sqlite3 hands it back as a number, but the bufbuild
MessageEnvelope schema types isEncrypted as bool. Coerce to boolean
when constructing the envelope so the JS value matches the field
type before toBinary runs.
Skipped the suggested `types` → ./src/index.ts swap in crdt's exports:
packages/api uses the same `types` → dist pattern and TypeScript's
bundler resolution already falls through when dist/*.d.ts doesn't yet
exist (verified — loot-core typecheck passes with packages/crdt/dist
removed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] address review feedback on encoder/app-sync test
- encoder.ts: prefs.getPrefs().encryptKeyId is `string | undefined`
(MetadataPrefs is a Partial<>). The bufbuild SyncRequestSchema's
keyId field is a non-optional proto3 string. Current code worked by
accident — passing undefined into `create(Schema, init)` falls back
to the schema default '' — but relied on bufbuild's undef-handling
and would break if someone dropped @ts-strict-ignore. Normalize to
'' explicitly.
- app-sync.test.ts: add a short WHY comment next to
`syncRequest.since = ''` in "returns 422 if since is not provided".
The test's intent (missing since) only matches the handler's
`requestPb.since || null` falsy-check because proto3 strips '' on
the wire and decodes it back to ''. Not obvious without the comment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: load source directly in dev, only use dist when published
Local exports point at src/index.ts so consumers (sync-server in
particular) never load a stale Vite bundle. publishConfig keeps the
dist/ mapping for npm consumers. Switched the Vite output to ESM and
added "type": "module" so the published bundle stays consistent.
Sync-server's existing extension-resolution loader is extended to
handle directory imports and is now registered at runtime via
--import ./register-loader.mjs, matching how tests already load it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] desktop-electron: register sync-server loader on the embedded fork
The Electron app starts the sync server via utilityProcess.fork, which
bypasses sync-server's `start` script. With crdt now loaded from
source, the fork needs the same `--import register-loader.mjs` that
the standalone server uses; otherwise it crashes on the extensionless
`from './crdt'` directory import. Adds the loader files to
sync-server's published `files` so they actually ship with the
packaged app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] sync-server: bootstrap entry that registers the loader for utilityProcess
Electron's utilityProcess.fork accepts execArgv but silently ignores
--import (verified with a minimal repro: the flag shows up in
process.execArgv but the preload module never executes), so the
previous attempt was a no-op and the embedded sync-server still
crashed on crdt's ESM directory imports. Add packages/sync-server/start.mjs
that statically imports register-loader.mjs and then dynamic-imports
build/app.js, so the loader is in place before the app's module graph
resolves. desktop-electron now points utilityProcess.fork at start.mjs
and drops the ineffective --import flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] fix: allow clearing pre-assigned category on new transactions
Add a "Nothing" button to the category autocomplete modal that allows
users to clear a pre-assigned category when adding or editing
transactions. Previously, when a payee had a pre-assigned category,
there was no way to remove it and leave the transaction uncategorized.
Closes#7390
* [AI] docs: add release notes for PR #7521
* [AI] chore: re-trigger CI for flaky test
The test failure in methods.test.ts (Budgets: successfully update budgets)
is a pre-existing flaky test caused by a race condition in
advanceSchedulesService. The async schedule service fires via
void runMutator() after a sync event, but the database can be closed
before the query completes. This is unrelated to the PR changes which
only touch desktop-client UI code.
* chore: retrigger CI (flaky api test)
* fix type issue, better text
* more type fixes
* actually fixed?
---------
Co-authored-by: youngcw <calebyoung94@gmail.com>
* moved the bank sync indicator to the right side of the text in mobile accounts view
* release notes
* moved spacing to the left again but made it smaller
* removed react from imports
* compressed space further
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7611
---------
Co-authored-by: Alec Bakholdin <alecbakholdin.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Add T keyboard shortcut for Make transfer
* [AI] Add release notes for #7750
* [AI] Switch Make transfer shortcut from T to R and document it
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7750
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7750
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7750
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Fix flaky upload-user-file test
The "uploads and updates an existing file successfully" test wrote the
old file content using the async callback form of fs.writeFile without
awaiting it. That write could land after the upload endpoint had already
written the new content, leaving the file with stale content and failing
the assertion. Use fs.writeFileSync so the setup completes before the
request is sent.
* [AI] Increase api test timeouts to fix flaky budget-load test
methods.test.ts loads a budget file and runs all DB migrations in each
test/hook. On busy CI runners this regularly approaches the default 5s
limit, and when it exceeds it the in-flight loadBudget keeps running after
teardown closes the database, producing a cascade of unhandled rejections
("database connection is not open", "no such table: v_schedules",
"Cannot read properties of undefined (reading 'timestamp')") that fail the
suite. Bump testTimeout/hookTimeout to 20s for the api package.
* [AI] Add release note for flaky test fixes
---------
Co-authored-by: Claude <noreply@anthropic.com>
The Merge VRT Patches job collects shard patches with the glob
`/tmp/shard-patches/*/vrt-shard.patch`, which assumes every downloaded
artifact lands in its own `path/<artifact-name>/` subdirectory. But
actions/download-artifact only does that when 2+ artifacts match the
pattern; when exactly one matches it unpacks the artifact directly into
`path`. So whenever a `/update-vrt` run touches snapshots in a single
shard (the common case) the patch ends up at
`/tmp/shard-patches/vrt-shard.patch`, the glob matches nothing, and the
job reports "No shard patches to merge" despite a patch having been
generated (e.g. run 25679233565).
Replace the glob with a recursive `find` so the patches are located
under either layout. `merge-multiple: true` is not an option here
because every shard artifact contains a file literally named
`vrt-shard.patch` and they would overwrite each other.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Release custom themes feature
Remove the customThemes experimental feature flag while keeping the
functionality intact (now enabled for all users).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revise custom themes release note to experimental
Updated the release notes to reflect the experimental status of the custom themes feature.
* [AI] Move custom themes docs out of experimental
Custom themes graduated from experimental in this release; move the
guide to /docs/custom-themes, drop the experimental warnings and the
flag-toggle instructions, and update the historical link target in
releases.md plus a brief pointer from settings/index.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update upcoming-release-notes/7775.md
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Add tests and handling income group in different budget types
* Handle income groups in reflect budget logic
* Add release note
* Lint fix
* Address coderabbit feedback
* Remove ts-strict-ignore
* Change test dates, and assert group existence
* fix naming
* fix typecheck
---------
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
* [AI] Load @actual-app/crdt from source in dev, only bundle for publish
@actual-app/crdt's local exports now point at src/index.ts so consumers
(sync-server, loot-core, desktop-client) never see a stale Vite bundle.
publishConfig keeps the dist/ mapping for npm consumers. crdt's
tsconfig switches to bundler module resolution to match the rest of
the workspace (no extensions in source imports).
Sync-server's existing extension-resolution loader is extended to also
handle directory-index imports (./crdt → ./crdt/index.ts), and the
standalone `start` / `start-monitor` scripts now invoke Node with
--import ./register-loader.mjs so the loader is in place before crdt's
source resolves.
Electron's utilityProcess.fork accepts execArgv but doesn't actually
preload --import modules, so a new packages/sync-server/start.mjs
bootstrap entry registers the loader imperatively and then dynamic-
imports build/app.js. desktop-electron's startSyncServer() points the
fork at start.mjs. sync-server's "files" array now ships start.mjs,
register-loader.mjs and loader.mjs so packaged Electron / npm
consumers actually receive them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add release notes for PR #7702
* [AI] Restructure sync-server to build with Vite
Replace the hand-rolled tsgo + add-import-extensions + copy-static-assets
+ runtime loader pipeline with a single Vite SSR build. Bundles every
entry (app, bin/actual-server, scripts/*) and inlines @actual-app/crdt
source so Node never has to resolve TS at runtime — the
MODULE_TYPELESS_PACKAGE_JSON warning that surfaced via crdt's source
exports is gone. Migrations and bank handlers move from readdir-based
dynamic imports to import.meta.glob; messages.sql becomes a ?raw import.
Drop loader.mjs, register-loader.mjs, start.mjs, and
bin/add-import-extensions.mjs. Electron's startSyncServer() forks
build/app.js directly. publishConfig.imports goes away (subpath imports
are resolved at build time and don't appear in the bundle).
In dev (start:server-dev) sync-server proxies to Vite, so loosen the CSP
to allow Vite's inline preamble script and HMR websocket — production
CSP is unchanged. desktop-client skips registerSW() in dev (and disables
vite-plugin-pwa's devOptions) so stale cached assets don't override
edits between page loads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Address review feedback
- sync-server CSP: drop 'unsafe-eval' from the production script-src;
the bundle has no genuine eval/new Function usage (only a defensive
branch in setimmediate's polyfill that's never hit). Keep it on the
dev branch where Vite's HMR runtime relies on it. Add a comment so
it's obvious which branch needs it and why.
- bank-factory: widen the loader glob to ./banks/*_*.{ts,js} so
TypeScript handlers are discovered too, mirroring migrations.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Restore 'unsafe-eval' in production CSP for Electron
The Electron app needs `'unsafe-eval'` at runtime, so revert the dev-only
restriction and keep `'unsafe-eval'` in both branches. Comment updated to
record the actual reason instead of marking it as removable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Revert bank-factory glob change
Widening the glob to ./banks/*_*.{ts,js} broke the desktop e2e tests in
CI even though every current handler is .js and the brace expansion
matches no .ts files locally. Reverting to ./banks/*_*.js — the change
had no behavioural benefit since there are no TS handlers, so the
nitpick isn't worth chasing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Strip CSP comment to restore identical state to 9513c1e16
The desktop e2e has been failing despite my prior commits being a strict
revert (only difference was a 2-line comment, which can't change runtime).
Removing even the comment so the branch matches 9513c1e16's relevant
files exactly, to isolate whether the failure is from the master merge
or from CI-environment drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Make rebuild-electron actually rebuild better-sqlite3
PR #7712 simplified rebuild-electron to just `electron-rebuild -f -o
better-sqlite3,bcrypt` from the repo root. Two problems with that:
1. Without `-m`, electron-rebuild scans the root workspace's package.json
for native deps. better-sqlite3 isn't a direct root dep — it lives
under packages/sync-server/ — so the scan returns no candidates and
the rebuild silently no-ops.
2. Without --build-from-source, electron-rebuild defers to
prebuild-install, which downloads a stale prebuilt binary keyed off
better-sqlite3's package.json (ABI 127) instead of recompiling
against Electron 39's bundled Node ABI 140. The download succeeds
and "Rebuild Complete" prints, but the resulting `better_sqlite3.node`
can't `dlopen` inside Electron's utility process — sync-server
crashes immediately on db init, the renderer's startSyncServer IPC
never resolves, and the e2e test hangs on "Configure your server".
Point -m at packages/desktop-electron (which transitively pulls in
better-sqlite3 and bcrypt via @actual-app/sync-server) and force a real
compile via --build-from-source. Verified locally: better-sqlite3
rebuilds to darwin-arm64-140 and the desktop e2e onboarding test passes
in 6s instead of hanging for 60s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Restore CSP unsafe-eval comment
Bring back the explanatory comment that was stripped diagnostically in
99682268c. Now that the desktop e2e regression is traced to
rebuild-electron and not to anything in this branch, we can keep the
documentation noting why 'unsafe-eval' is retained in both CSP branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Restore bank-factory glob to ./banks/*_*.{ts,js}
Re-apply the glob widening originally added in 145868f9d. It was
reverted in 531b1a191 because the desktop e2e was failing — that
failure is now traced to the rebuild-electron breakage (fixed in
6e8ac0784), not to this glob. Mirroring migrations.ts so future TS
bank handlers are picked up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Fix applyAppUpdate hanging in dev mode
In dev mode browser-preload's updateSW was () => undefined, so
applyAppUpdate() — which calls updateSW() and then awaits a
deliberately never-resolving promise (waiting for the SW-driven page
reload) — hung the renderer instead of refreshing. In prod the page
is replaced by the new service worker, so the never-resolving await is
fine. The dev path now triggers a plain window.location.reload() so
the page reloads and the never-settling await is irrelevant, matching
prod's effective behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Revert rebuild-electron to master version
* Revert "[AI] Revert rebuild-electron to master version"
This reverts commit 4b6baab79f.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* [AI] Update mobile bank sync indicators live during sync
Mobile's account list uses react-aria-components ListBox with the
items render-function pattern, which memoizes rows by item identity.
Without a dependencies prop, changes to syncingAccountIds,
failedAccounts, and updatedAccounts in Redux didn't cause the
per-account dots to re-render until the items array itself changed,
so the green/yellow/red indicators only updated after the full sync
finished.
Pass these Redux selections via the dependencies prop so the rows
re-render as state changes during sync. Also clear SimpleFin
accounts from accountsSyncing right after the batch call returns,
so their indicators reflect completion before the per-account loop
starts on the remaining accounts.
https://claude.ai/code/session_01DNkRSgqW5JEtYpZjxvj7Bi
* [AI] Update release notes filename and author
https://claude.ai/code/session_01DNkRSgqW5JEtYpZjxvj7Bi
* [AI] Drop verbose comment on SimpleFin sync dispatch
https://claude.ai/code/session_01DNkRSgqW5JEtYpZjxvj7Bi
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] Recover from BackendInitFailure and show a meaningful error
When the backend Worker fails to load (e.g., the hashed kcab.worker
asset can't be fetched), the SharedWorker would cache the
app-init-failure and replay it to every subsequent tab forever, while
the FatalError modal showed a misleading "browser version" message.
- Retry importScripts in production (3 attempts) so a transient blip
doesn't brick the SharedWorker.
- Clear lastAppInitFailure when the client acknowledges the failure,
when a backend later connects successfully (centralized in
broadcastConnect), and when a fresh init arrives with no active
groups (the failed leader is gone).
- Add a BackendInitFailure branch to FatalError's RenderSimple with a
message that points the user at reload / hard refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Remove support contact message from FatalError
Removed support contact message from FatalError component.
* [AI] Fix error propagation in importScriptsWithRetry
- Change Promise executor to accept both resolve and reject
- Properly propagate errors using .then(resolve).catch(reject)
- Fixes issue where errors from recursive retry calls were swallowed
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* [AI] CLI: hide hidden categories by default in list commands
The `categories list` and `category-groups list` commands now exclude
hidden entries by default. Pass `--include-hidden` to include them, mirroring
the existing `--include-closed` flag for `accounts list`.
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] Rename release note to 7785.md and update author
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] CLI: simplify category-groups list and consolidate test setup
- Flatten the include-hidden ternary on category-groups list into a
single filter chain, mirroring categories list.
- Consolidate duplicated stderr/stdout spy setup into one outer
describe in categories.test.ts.
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] Rename release note to 7786.md to match PR number
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] Push hidden-category filtering down to the API/query layer
Add an optional \`hidden\` filter to \`api.getCategories\` and
\`api.getCategoryGroups\`. When set, the AQL query filters category groups
by hidden status and nested categories are filtered to match. Internal
callers (no options) keep the existing "return everything" behavior.
The CLI \`categories list\` and \`category-groups list\` commands now pass
\`{ hidden: false }\` instead of filtering client-side after fetching.
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] Document new \`hidden\` option on getCategories and getCategoryGroups
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] getCategories: include hidden categories from visible groups in list
When \`hidden: true\` was requested, the flat list only contained hidden
categories that lived inside hidden groups, because it was derived from
the same already-filtered groups used for the grouped view. A hidden
category sitting in a visible group was silently dropped.
Fetch the unfiltered groups for the list view and filter by
\`category.hidden\` so the list reflects every hidden category regardless
of its parent group's hidden status. The grouped view is unchanged.
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
* [AI] getCategories: query categories table directly when hidden=true
Replace the second \`getCategoryGroups()\` call (which loaded every group
plus its nested categories just to be flattened and filtered) with a
direct \`q('categories').filter({ hidden: true })\` AQL query. Same
result, one targeted query instead of fetching all groups.
The non-hidden=true paths are unchanged.
https://claude.ai/code/session_01DhYiicACsWb5NGHX71Wv4F
---------
Co-authored-by: Claude <noreply@anthropic.com>
* added logic to prevent autocomplete from flickering on alt tab
* lint auto fixes
* removed import and linting autofix
* release notes
* flipped false to true for allowOpening
* updated release notes
* release note update to trigger CI
---------
Co-authored-by: Alec Bakholdin <alecbakholdin.com>
* Integrate Enable Banking as bank sync provider
Rewrite Enable Banking modal to match GoCardless pattern
Resolve Enable Banking bugs and improve auth flow
* [AI] Address code review feedback for Enable Banking integration
Bug fixes:
- Fix double-negative for DBIT transaction amounts (e.g. '--25.99')
- Fix payeeName counterparty mapping (CRDT→debtor, DBIT→creditor)
- Add missing state validation in EnableBankingCallback and /auth_callback
- Fix stuck loading state in useEnableBankingStatus with try/catch/finally
- Make session-expiry error matching case-insensitive
- Prefer CLAV balance type for startingBalance in /transactions route
- Guard setTimeout in post/del/patch when timeout is null
- Distinguish abort from network failure in post() catch
Credential handling:
- Add validateCredentials() to validate before persisting secrets
- Refactor client to use enablebanking-configure instead of manual secret-set
- Distinguish null (loading) from false (not configured) in setup checks
Poll-auth robustness:
- Add unique waiter IDs to prevent superseded waiter cleanup race
- Always cache results in completedAuths for retry resilience
- Add client disconnect cleanup via res.on('close')
- Cancel poll when Enable Banking modal closes via AbortController
- Prevent concurrent poll controller race with local reference check
Code quality:
- Extract buildSessionResult() to deduplicate auth_callback/complete-auth
- Add enabled parameter to useEnableBankingStatus to skip unused requests
- Add re-entrancy guard on onJump, reset bank on country change
- Refetch bank list after Enable Banking setup completes
- Type enableBankingConfigure config, make state required in completeAuth
- Add AbortError→TIMED_OUT test, fix startAuth test assertion
- Add afterAll vi.unstubAllGlobals() for test cleanup
- Add explanatory comments for bank-per-account model and in-memory maps
* [AI] Fix missing patterns in Enable Banking integration
- Add SyncServerEnableBankingAccount to ExternalAccount union and
getInstitutionName parameter type in SelectLinkedAccountsModal
- Use BankSyncProviders type in mobile BankSyncAccountsList instead of
hardcoded union missing enableBanking
- Add getSecretsError handling to EnableBankingInitialiseModal for
proper auth/permission error messages
- Replace hardcoded #666 color with theme.pageTextSubdued
- Wrap onConnectEnableBanking in try/catch with error notification and
init modal re-open, matching SimpleFin/PluggyAI pattern
- Translate hardcoded error string in enablebanking.ts
- Add 60s timeout to downloadEnableBankingTransactions matching PluggyAI
- Revert out-of-scope changes to del()/patch() in post.ts
- Revert shared starting balance dedup logic back to master pattern
* Forward PSU headers to Enable Banking API
* Fix Enable Banking re-auth dispatch
* Respect ASPSP maximum_consent_validity when starting Enable Banking auth
* Fix missing types for module jws
* Add upcoming release notes
* Fix format
Expected "sign" (value-import) to come before "Algorithm"
* Fix code review findings on Enable Banking integration
* [AI] Disable Enable Banking button while status is loading
* typo
* [AI] Migrate enable-banking files to subpath imports
Update all enable-banking files to use # subpath imports and
@actual-app/core paths, matching the migration done in master.
Add #enablebanking entry to desktop-client package.json imports map.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Add #app-enablebanking subpath imports to sync-server package.json
Register enablebanking service, utils, and root entries in both
the imports and publishConfig.imports maps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add jws to dependencies
* [AI] Harden Enable Banking OAuth callback handoff
Enforce exact OAuth state round-trip in the Enable Banking callback so
mismatched/missing state values no longer silently complete the flow.
Replace unsafe `as`/`!` assertions in the auth handoff with typed
locals so the callback path stays sound under strict TypeScript.
* [AI] Tighten Enable Banking type safety
Make the Enable Banking external-msg modal strict-ts compatible,
annotate the id type in linkEnableBankingAccount, derive
AccountSyncSource from a single SYNC_PROVIDERS list, and annotate the
return type of getJWTBody. No behaviour change.
* [AI] Fix Enable Banking poll lifecycle and abort handling
Make the popup-driven auth poll cancellable and isolated:
- Allow the popup retry path to abort the in-flight poll instead of
leaving it hanging on the previous attempt.
- Clear the Enable Banking stateRef when the retry attempt finishes so
a new attempt starts from a clean state.
- Start useEnableBankingStatus in loading state until the first fetch
resolves so the UI doesn't briefly flash "not connected".
- Cancel only the requested poll, not every in-flight Enable Banking
poll, so unrelated link attempts aren't affected.
- Skip writing the poll response when the client has already
disconnected, with a regression test covering the disconnect path.
* [AI] Tighten Enable Banking client/test plumbing
Misc code-quality improvements with no behaviour change:
- Parallelize Enable Banking secret reset calls so wiping multiple
secrets doesn't serialize the request chain.
- Use absolute imports in the enable-banking client module to match the
rest of desktop-client.
- Document externalSignal usage in the post helper.
- Tighten Enable Banking test fixtures with `satisfies` and dynamic
dates so they stop drifting when the real "now" moves.
* [AI] Fix Enable Banking initial-balance and post-link bookkeeping
Apply the standard post-sync bookkeeping when linking an Enable Banking
account so the new account picks up the same starting-balance
treatment as other bank-sync providers, and skip pending transactions
when computing the initial balance so the figure isn't inflated by
transactions that haven't cleared yet.
* [AI] Refine Enable Banking error model and bank-sync surface
Carry the human-readable Enable Banking message in
EnableBankingError.error_type and the machine-friendly identifier in
error_code, then map error_code to a bank-sync category in the
/transactions wire format so AccountSyncCheck can match on the same
categories as other providers.
* [AI] Improve Enable Banking bank-sync field mapping
Bring the Enable Banking transaction normalizer in line with how other
bank-sync providers feed the field mapper:
- Strip SEPA structured prefixes from remittance text so notes/payee
display the human-meaningful portion instead of the SEPA boilerplate.
- Return the notes field and spread the raw transaction so downstream
field mapping can reach the full payload.
- Expose Enable Banking raw fields in the bank-sync field mapper UI so
users can map any underlying property, not just the curated subset.
* [AI] Use req.ip for Enable Banking PSU header so trust-proxy whitelist applies
* [AI] Address Enable Banking CodeRabbit pass-3 follow-ups
Three small fixes from the latest CodeRabbit re-review:
- Guard the aspsps fetch in EnableBankingExternalMsgModal against stale
responses. Switching countries quickly could let an earlier in-flight
request overwrite the newer selection's bank list. Added a cleanup
flag in the useEffect so only the latest response updates state.
- Clear `enablebanking_auth_state` from localStorage when the auth flow
exits, but only if the stored value still matches this attempt's
state, so a concurrent retry can't wipe a newer session. Wrapping
the poll in try/finally covers every return path (success, timeout,
abort, body-level error).
- Use `Boolean(trans.booked)` in the Enable Banking initial-balance
predicate to match `normalizeBankSyncTransactions`. The Enable
Banking normalizer always sets `booked` to a boolean today, so this
is defensive rather than a live bug, but keeping the two predicates
aligned avoids surprises if the upstream shape ever loosens.
* [AI] Address Enable Banking CodeRabbit pass-3 follow-ups (round 2)
Two more findings from the latest CodeRabbit pass:
- Guard onJump against stale-retry completions. Token each call with a
monotonic jumpIdRef counter and gate every post-await write
(setError/setWaiting after onMoveExternal, the second setWaiting,
and the finally-block ref reset) on `myJumpId === jumpIdRef.current`.
Without this, a retry click while the previous poll was still
unwinding could surface the older call's error in the newer
attempt's UI and clear stateRef/isJumpingRef out from under it,
leaving the new poll un-cancellable.
- Translate the (beta) suffix on Enable Banking ASPSP names so
non-English locales don't surface a hardcoded English token in the
bank list. The existing `actual/no-untranslated-strings` rule misses
this case (regex requires a leading uppercase, and template-literal
interpolations aren't visited as standalone strings).
* [AI] Use SEPA prefix allowlist instead of catch-all regex
The previous `^[A-Z]{3,}\+` regex would incorrectly strip merchant
tokens like `BMW+`, `USB+`, or `COVID+` from the start of a remittance
line. Replaced it with an explicit allowlist of known SEPA / ISO 20022
prefixes and added a regression test covering the false-positive case.
* [AI] Use uuidv4 instead of crypto.randomUUID in Enable Banking
Aligns with master's revert in #7734 (crypto.randomUUID back to uuid
library). Two stray spots remained in Enable Banking code: the
link-account flow in loot-core/server/accounts/app.ts and the OAuth
state token in sync-server/app-enablebanking.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Merge VRT Patches job runs inside the Playwright container where
the default GitHub Actions shell is `sh -e {0}`, not bash. The merge
step uses bash-only constructs (`shopt -s nullglob`, array literals,
`${#patches[@]}`, `"${patches[@]}"`), so every /update-vrt run that
reaches the merge stage now exits 127 with `shopt: not found` (e.g.
run 25609625260).
Pin this step to `shell: bash` to match the explicit `shell: bash` we
already use elsewhere in the workflow. The sibling shard-patch creation
steps stay on the default sh because they only use POSIX features.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Sync server: harden CORS proxy method validation
The CORS proxy validated `method` against a fallback-normalized value but
forwarded the raw client-supplied value to fetch(), letting a non-string
input (e.g. ["POST"]) bypass the GET/HEAD allowlist via undici's String()
coercion. Reject non-string method, pass the validated normalized method
to fetch(), and drop the unreachable body-forwarding branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Polish release notes wording
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Rename 7787.md to 7788.md
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>