Use a discriminated SyncDecision union so connection.ts no longer needs
non-null assertions on the cached state. Thread the resolved CliConfig
through withConnection's callback to drop duplicate resolveConfig calls
in the sync and budgets commands. Extract an errorCode helper and
replace the existsSync+readdirSync TOCTOU pattern in the reader-wait
polling loop with a single readdir that tolerates ENOENT.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every `actual <cmd>` call currently delta-syncs the budget with the
sync server via `api.downloadBudget`, which hits the server's
500-req/min rate limit on scripted workflows. Actual is local-first:
once the budget is on disk, most read commands do not need fresh
server data.
Introduce a CLI-only cache layer inside `withConnection` that
decides per invocation whether to skip, sync, or re-download:
- Cache state lives at `{dataDir}/.actual-cli/{syncId}/state.json`,
keyed by `syncId` to avoid the chicken-and-egg of not knowing the
on-disk `budgetId` before the first download. The on-disk id is
resolved via `api.getBudgets()` and persisted after first download.
- Read commands (list, balance, query run, …) skip the `/sync`
call while `now - lastSyncedAt < cacheTtl`. Write commands
(create, update, delete, set-*, etc.) sync before and after the
operation to keep server state consistent.
- Encrypted budgets force a sync per call since `api/load-budget`
does not re-verify the password.
- New `proper-lockfile`-backed shared/exclusive lock serializes
writes while allowing parallel reads. Reader markers live in
`{meta}/readers/`; writers sweep stale markers by PID.
New `actual sync` command with three modes: default (sync now),
`--status` (print cache age, TTL, stale flag), `--clear` (delete
cache, holding the exclusive lock to avoid racing writers).
New config surface, following the existing flag → env → config file
→ default precedence chain:
- `--cache-ttl <s>` / `ACTUAL_CACHE_TTL` / `cacheTtl` (default 60)
- `--refresh` / `--no-cache`
- `--lock-timeout <s>` / `ACTUAL_LOCK_TIMEOUT` / `lockTimeout` (10)
- `--no-lock` / `ACTUAL_NO_LOCK` / `noLock`
Every `withConnection` call site now passes an explicit
`{ mutates: boolean, skipBudget?: boolean }` so read/write intent is
visible at the edge.
The old `budgets sync` subcommand is removed — it silently diverged
from the new top-level `actual sync`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Disable bundle minification for readable production error messages
The desktop-client had dead terserOptions (no `minify: 'terser'` was set, so
Vite's default esbuild minifier ran with name mangling). The loot-core and
plugins-service workers used Terser with mangle:false but still compressed.
Set `minify: false` across all three browser build configs so production
stack traces are human-readable.
https://claude.ai/code/session_01VEywxebiNYAgJia35fygQx
* [AI] Rename release note to match PR number
https://claude.ai/code/session_01VEywxebiNYAgJia35fygQx
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] Disable fail-fast for Electron build matrices
Prevents cancellation of in-progress platform builds when one fails, so
Windows/macOS/Linux results are all visible on a single run.
* Add release notes for PR #7547
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Persist custom CSS overrides as a standalone global pref
Moves the custom CSS override out of the InstalledTheme JSON blob into
a dedicated customCssOverride global pref so that overrides survive
switching themes, clearing installed themes, or toggling auto/light/dark
mode. Includes a one-time migration that lifts the legacy overrideCss
field out of installedCustomLightTheme / installedCustomDarkTheme JSON.
- Add customCssOverride global pref (loot-core types + server defaults)
- Inject the override as a trailing style layer in CustomThemeStyle so
it layers on top of any installed custom theme
- Drop overrideCss from the InstalledTheme type; extractLegacyOverride
+ migrateLegacyOverride handle the one-time lift with whitespace trim
- Run the migration from CustomThemeStyle with an idempotent effect that
re-runs safely once prefs hydrate
- Bind the ThemeInstaller textarea directly to the new pref
- Add a "Custom CSS is active" indicator button next to the theme
selector that opens the installer for editing the override without
flipping auto mode to light
- Pre-switch out of auto when the user picks "Custom theme" from the
main selector, so the flag that used to distinguish entry points goes
away and handleInstall collapses to a pure slot dispatch
- Tests: hermetic Themes settings tests, expanded customThemes unit
tests covering extraction/migration/trim edge cases, updated
ThemeInstaller tests for the new pref binding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Address review feedback for custom CSS override installer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Defer auto-mode theme switch and guard stale installer callbacks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] sync-server: use workspace reference for @actual-app/crdt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update build script in sync-server package to use TypeScript's build mode
* Package electron
* [AI] crdt: add conditional exports so Node can load the built bundle
Before this change the root exports entry pointed at `./src/index.ts`, so
any pure-Node consumer (notably the sync server that Electron forks as a
utility process) failed to import `@actual-app/crdt` — Node can't execute
TypeScript source directly. Sync-server had been masking this by pulling
`@actual-app/crdt@npm:2.1.0` where `publishConfig.exports` resolves to
`./dist/index.js`; once sync-server switched to `workspace:*`, the
Functional Desktop App CI job timed out waiting for the sync server to
boot.
Switch to conditional exports in the same shape `@actual-app/api` already
uses:
- `types` → `./dist/index.d.ts` for TypeScript tooling
- `development` → `./src/index.ts` for Vite/Vitest (HMR, fast feedback)
- `default` → `./dist/index.js` for Node runtime
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add 'Last 30 days' date range option to custom reports
Add a rolling 30-day date range to the Live mode date filter in custom
reports. The option appears between 'Last month' and 'Last 3 months'
and is available for Daily, Weekly, and Monthly intervals.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* [autofix.ci] apply automated fixes
* Fix release note filename and author to match PR #7217
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
* [AI] Emit bundle stats from the crdt package
The crdt package was the only published library without a stats.json
artifact. Migrate its build to Vite (mirroring the api/cli setup), wire
in rollup-plugin-visualizer to emit dist/stats.json, and upload it from
the CRDT CI job. Declarations are still produced by tsgo via
--emitDeclarationOnly.
https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ
* Add release notes for PR #7537
* [AI] crdt: drop redundant rm -rf dist from build script
Vite's build.emptyOutDir: true already clears the output directory
before writing, so the leading rm -rf dist is unnecessary.
https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ
* [AI] Include crdt in the size-compare bundle stats table
Wait for the crdt build check on both the base branch and the PR,
download the crdt-build-stats artifact for each, and pass it to
bundle-stats-comment.mjs so the summary table rendered on the PR
includes a row for the crdt package alongside desktop-client,
loot-core, api, and cli.
https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Apply suggested fix to packages/desktop-client/src/components/modals/EditFieldModal.tsx from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Apply suggested fix to packages/desktop-client/src/components/modals/EditFieldModal.tsx from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* note
* fix typo in note
* remove useless conditions
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* [AI] Fix regex checkbox label contrast in notes modal (#7514)
Apply theme.menuAutoCompleteText color to the "Use Regular Expressions"
checkbox label in the find-and-replace tab so it contrasts with the
menuAutoCompleteBackground modal background.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rename release note file to match PR number
The CI expects the release note file to be named after the PR number
(7515), not the issue number (7514).
---------
Co-authored-by: liuren.lcy <liuren.lcy@antgroup.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* [AI] Refactor IS_GENERIC_BROWSER env var to --mode=browser
Replace the IS_GENERIC_BROWSER environment variable with Vite's built-in
--mode=browser flag to distinguish browser builds from Electron builds.
This aligns with the existing --mode=desktop pattern used for Electron
production builds.
Also fix build-shims.js to derive NODE_ENV from import.meta.env.DEV
instead of import.meta.env.MODE, so custom modes don't leak into
process.env.NODE_ENV.
https://claude.ai/code/session_014HvkpR59Ke4eUoiUzsUruv
* Add release notes for PR #7466
* [AI] Fix COOP/COEP headers not set with --mode=browser
The server.headers config was gated on mode === 'development', which
excluded --mode=browser. Since server.headers only applies during
vite serve (not builds), always set the COOP/COEP headers. These are
required for SharedArrayBuffer support used by the SQLite backend.
https://claude.ai/code/session_014HvkpR59Ke4eUoiUzsUruv
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Add scoped ErrorBoundary to Rules page to contain rendering crashes
* [autofix.ci] apply automated fixes
* docs: add release notes for ErrorBoundary PR
* fix(rules): add resetKeys to ErrorBoundary for route navigation reset
In react-error-boundary v6, the boundary does not auto-reset when the
user navigates away and back. Adding resetKeys={[location.pathname]}
ensures the error state clears on route changes.
This contribution was developed with AI assistance (Claude Code).
* fix(rules): show error message in fallback UI and log to console
Addresses reviewer feedback on #7437: surface error.message in the
FeatureErrorFallback so users can copy-paste when reporting issues,
and log the error to the console via useEffect so the error isn't
swallowed by the boundary.
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* [AI] Add publishConfig.imports sync validator with pre-commit integration
Add a TypeScript script that validates publishConfig.imports stays in sync
with imports in all packages/*/package.json files. Runs automatically in
the pre-commit hook via lint-staged with --fix mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Add release notes for #7469
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Guard main() with require.main and respect lint-staged file args
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Handle non-string imports targets in derivePublishImports
- Add type guard to check for non-string values in imports
- Throw descriptive error when conditional imports are encountered
- Update type signature to accept Record<string, string | object>
- Add test case for non-string imports error handling
This prevents TypeError when packages have conditional imports (e.g., #browser-preload in desktop-client)
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* [AI] Limit resolvePackageJsonPaths scope to packages directory
- Add packagesRoot constant to restrict path resolution
- Update while loop condition to only traverse within packages directory
- Add additional check to ensure candidate paths are under packages/
- Prevents resolution to repo root package.json for missing/deleted files
This ensures the validator only processes package.json files under packages/* and avoids accidentally targeting the monorepo root manifest.
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.6 (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] Fix type errors for API consumers by shipping .d.ts declarations from loot-core
Downstream consumers of @actual-app/api with strict: true get type errors
because @actual-app/core exports raw .ts source files. Consumers' tsc
follows the import chain into core's source (compiled with strict: false),
and skipLibCheck doesn't help since it only skips .d.ts files.
Add "types" conditions to all imports/exports entries in loot-core's
package.json, pointing to the pre-built declarations in lib-dist/decl/.
Add .npmignore to include lib-dist/decl/ in the published package.
Fixes#7410
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Use prepack/postpack scripts instead of inflating package.json
Replace the inline "types" conditions in imports/exports with a prepack
script that adds them at pack/publish time. This keeps the checked-in
package.json clean while still shipping .d.ts declarations to npm
consumers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Convert prepack/postpack scripts to TypeScript
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Add release notes for #7468
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Fix recursive ExportValue type and remove redundant comment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Rename scripts to .mts and inline types conditions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Make backup/restore scripts safer
- Check if backup exists before creating it in prepack
- Make restore idempotent by checking if backup exists in postpack
- Prevents overwriting existing backups from interrupted runs
- Addresses CodeRabbit review feedback
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* [AI] Type api-handlers.ts fields to drop implicit any
The `fields` / export-args slots in the ApiHandlers contract were
untyped, surfacing as TS7008 errors in strict consumers. Replace them
with the `Partial<APIXxxEntity>` shapes the `@actual-app/api` wrappers
already pass, and annotate the matching call sites in `api.ts` with
`@ts-expect-error` where the legacy helpers still declare full-entity
parameters despite accepting partial updates at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Replace vite-plugin-dts with tsgo for api types
Drops vite-plugin-dts in favor of running tsgo --emitDeclarationOnly
after the vite bundle, eliminating a heavy dev dependency tree
(api-extractor, volar, vue language-core) from the api package build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Add build script to loot-core to emit declarations via lage
`yarn build:cli` failed in CI with TS6305 because api's
`tsgo --emitDeclarationOnly` depends on loot-core's pre-built
`lib-dist/decl/*.d.ts`, but loot-core had no `build` script, so lage's
`^build` cascade silently skipped it. Add `"build": "tsgo -b"` so loot-core
slots into the dependency chain; its tsconfig already has
`emitDeclarationOnly: true`, so the output is declarations only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Simplify API build
* [AI] Document TypeScript moduleResolution requirement for @actual-app/api
The published declarations rely on package.json exports conditions, which
classic node / node10 resolvers don't honor. Document the supported modes
(bundler / nodenext / node16) in the package README and in the Getting
Started section of the API docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Type-guard default value in add-types-conditions prepack
`value.default` is typed `ExportValue | undefined`, which allows nested
conditional objects. The previous truthy check fell through to
`shouldSkip(defaultValue)` and would crash on `.endsWith()` if that shape
ever appeared. Replace with a `typeof === 'string'` narrowing and drop a
now-redundant "Insert types as the first key" comment.
No runtime change on current package.json — no nested `default` values
exist today — but the script is not covered by loot-core's tsconfig
include, so the latent type issue was silent.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Allow "nodenext" in docs spellcheck expect list
Referenced in the new TypeScript moduleResolution note in the API docs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Move loot-core declarations to @types and whitelist publish with files
Relocate loot-core's composite TypeScript output from lib-dist/decl to the
top-level @types directory, matching the api package's convention. Replace
the old .npmignore blacklist with an explicit package.json files whitelist.
- tsconfig.json: outDir @types, exclude test/mock dirs from decl emission
- scripts/add-types-conditions.mts: rewrite paths to ./@types/src/...
- package.json: files whitelist shipping only src, @types, migrations,
typings, default-db.sqlite; drop legacy typesVersions (docs now require
moduleResolution bundler/nodenext/node16, so the classic-resolution
fallback is unused)
- .gitignore: ignore the new @types build artifact
- lage.config.js: factor outputGlob into a shared BUILD_OUTPUT_GLOBS
constant and add @types/** so lage caches loot-core's decl output
- root tsconfig.json: tighten exclude from packages/api/@types to
packages/*/@types to cover both api and loot-core
- delete .npmignore entirely
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Build loot-core declarations inside prepack
yarn workspace @actual-app/core pack is the first non-setup step in the
publish workflow, running before any build. Without a build chained into
prepack the @types/ tree is empty at pack time, so the tarball shipped a
transformed package.json pointing at ./@types/src/... paths that didn't
exist. npm publish doesn't re-run hooks on a pre-packed tarball, so the
frozen snapshot must be self-contained; prepack now runs yarn build first
to populate @types/ before add-types-conditions rewrites the exports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* Update customThemeCatalog.json
* Update customThemeCatalog.json
* [autofix.ci] apply automated fixes
* Update customThemeCatalog.json
* 🎨 You Need A Theme Dark, based on 2026 nYNAB dark
* YNA Theme Dark
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>