Compare commits

..

288 Commits

Author SHA1 Message Date
Claude
22a5bff977 [AI] Add Capacitor iOS app wrapper for App Store publishing
Wraps the existing PWA mobile web app in a Capacitor iOS shell for
publishing to the iOS App Store. Uses the existing 54+ mobile-optimized
React components and local-first SQLite storage (via absurd-sql/IndexedDB).

Key additions:
- packages/mobile/ - New Capacitor workspace with iOS platform
- Custom ActualBridgeViewController for WKWebView configuration
- CrossOriginIsolationPlugin for SharedArrayBuffer support
- Build scripts: yarn build:mobile, yarn mobile:open, yarn mobile:run:ios
- iOS deployment target set to 16.0 (required for SAB in WKWebView)
- App Transport Security configured for local networking

https://claude.ai/code/session_01GAVpnQ21dPw6KJVf5c4f5D
2026-03-21 10:59:02 +00:00
Matiss Janis Aboltins
23adf06cb0 Add post-merge hook to auto-install dependencies (#7248)
* [AI] Add post-merge hook to auto-install when yarn.lock changes

Mirrors the existing post-checkout hook behavior: after a git pull or
merge, if yarn.lock changed between ORIG_HEAD and HEAD, automatically
runs yarn install to keep dependencies in sync.

https://claude.ai/code/session_01JHoMhGANLTc1q67s1dUHrt

* Add release notes for PR #7248

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-20 22:59:05 +00:00
Matiss Janis Aboltins
a8a2d23e63 [AI] Improve CLI query command with new flags, subcommands, and docs (#7240)
* [AI] Improve CLI query command with new flags, subcommands, and docs

Add --last, --count, --where, --offset, --group-by flags and field:desc
order-by syntax to make the query command more usable for both humans
and AI agents. Add query tables/fields subcommands for schema discovery.
Expand CLI and ActualQL documentation with comprehensive examples.

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

* [AI] Fix parseOrderBy empty field validation and misleading aggregate docs

Validate that order-by field names are non-empty (e.g. ":desc" now throws
a clear error). Move AVAILABLE_TABLES before first use. Update group-by
examples in CLI help text and docs to use --file with proper aggregate
JSON instead of misleading --select with plain field names.

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

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-20 19:01:20 +00:00
Matiss Janis Aboltins
f5a72448bd [AI] Refactor ThemeInstaller to handle pasted CSS more gracefully (#7236)
* [AI] Add baseTheme and overrideCss support to custom theme system

Add baseTheme field to InstalledTheme allowing users to choose which
built-in theme (light/dark/midnight) serves as the base for custom
themes. Add overrideCss field for layering additional CSS overrides
on top of a catalog theme's CSS.

ThemeStyle now respects the baseTheme field when rendering base
variables. CustomThemeStyle renders both cssContent and overrideCss
layers.

https://claude.ai/code/session_01PPAkAQB4xfeFCQbmNwvn2k

* [AI] Add base theme selection and CSS override layering for custom themes

- Add baseTheme field to CatalogTheme and InstalledTheme types, allowing
  catalog themes to declare which built-in theme (light/dark/midnight) they
  are based on
- Add overrideCss field to InstalledTheme for layering additional CSS
  overrides on top of a catalog theme
- Update ThemeStyle to render the correct base theme colors when a custom
  theme specifies a baseTheme
- Update CustomThemeStyle to render both cssContent and overrideCss layers
- Update ThemeInstaller UI: catalog selection and free-text CSS now coexist
  so users can pick a catalog theme (e.g. Matrix) and apply extra overrides
- Add baseTheme to all entries in customThemeCatalog.json
- Dynamic label: shows "Additional CSS overrides:" when a catalog theme is
  selected, "or paste CSS directly:" otherwise

https://claude.ai/code/session_01PPAkAQB4xfeFCQbmNwvn2k

* [AI] Remove baseTheme from catalog; derive base from mode instead

Base theme is now automatically determined from the catalog theme's
mode field: light mode themes use "light" as base, dark mode themes
use "dark" as base. No separate baseTheme field needed in catalog.

https://claude.ai/code/session_01PPAkAQB4xfeFCQbmNwvn2k

* Refactor ThemeInstaller to handle pasted CSS more gracefully

* Enhance ThemeInstaller and CustomThemeStyle to support CSS validation for both content and overrides. Refactor pasted CSS handling for improved clarity and efficiency.

* Implement validateAndCombineThemeCss function to streamline CSS validation and combination for light and dark themes in CustomThemeStyle. Refactor existing CSS handling to improve clarity and efficiency.

* Add cachedCatalogCss state to ThemeInstaller for improved CSS handling

* Update ThemeInstaller tests to ensure pasted CSS is preserved when a catalog theme is selected and modify onInstall behavior to correctly handle empty CSS content. Refactor test cases for clarity and accuracy.

* Enhance ThemeInstaller to support dynamic baseTheme selection based on catalog theme or user preference. Refactor CSS installation logic to prioritize selected catalog themes and improve handling of pasted CSS. Update dependencies in the installTheme function for better clarity and functionality.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 18:48:02 +00:00
erwannc
0793eb5927 Add Notes to Monthly Budget Cell (#6620)
* Add Notes to Monthly Budget Cell
Changed Modal menus layout to follow month menu on mobile

* Fixed rebase errors

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6620

* Addressed youngcw's comments (notes id format, notesButton defaultColor and modal layout)

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6620

* Updated mobile budget menu modal page model

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-03-19 00:42:30 +00:00
Matiss Janis Aboltins
a43b6f5c47 [AI] Experimental CLI tool for Actual (#7208)
* [AI] Add @actual-app/cli package

New CLI tool wrapping the full @actual-app/api surface for interacting with
Actual Budget from the command line. Connects to a sync server and supports
all CRUD operations across accounts, budgets, categories, transactions,
payees, tags, rules, schedules, and AQL queries.

* Refactor CLI options: replace `--quiet` with `--verbose` for improved message control. Update related configurations and tests to reflect this change. Adjust build command in workflow for consistency.

* Refactor tests: streamline imports in connection and accounts test files for improved clarity and consistency. Remove dynamic imports in favor of static imports.

* Enhance package.json: Add exports configuration for module resolution and publish settings. This includes specifying types and default files for better compatibility and clarity in package usage.

* Update package.json exports configuration to support environment-specific module resolution. Added 'development' and 'default' entries for improved clarity in file usage.

* Enhance CLI functionality: Update configuration loading to support additional search places for config files. Refactor error handling in command options to improve validation and user feedback. Introduce new utility functions for parsing boolean flags and update related commands to utilize these functions. Add comprehensive tests for new utility functions to ensure reliability.

* Update CLI TypeScript configuration to include Vitest globals and streamline test imports across multiple test files for improved clarity and consistency.

* Update CLI dependencies and build workflow

- Upgrade Vite to version 8.0.0 and Vitest to version 4.1.0 in package.json.
- Add rollup-plugin-visualizer for bundle analysis.
- Modify build workflow to prepare and upload CLI bundle stats.
- Update size comparison workflow to include CLI stats.
- Remove obsolete vitest.config.ts file as its configuration is now integrated into vite.config.ts.

* Enhance size comparison workflow to include CLI build checks and artifact downloads

- Added steps to wait for CLI build success in both base and PR workflows.
- Included downloading of CLI build artifacts for comparison between base and PR branches.
- Updated failure reporting to account for CLI build status.

* Update documentation to replace "CLI tool" with "Server CLI" for consistency across multiple files. This change clarifies the distinction between the command-line interface for the Actual Budget application and the sync-server CLI tool.

* Refactor configuration to replace "budgetId" with "syncId" across CLI and documentation

* Enhance configuration validation by adding support for 'ACTUAL_ENCRYPTION_PASSWORD' and implementing a new validation function for config file content. Update documentation to clarify error output format for the CLI tool.

* Enhance configuration tests to include 'encryptionPassword' checks for CLI options and environment variables, ensuring proper priority handling in the configuration resolution process.

* Update nightly versioning script to use yarn

* Align versions

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 18:22:38 +00:00
Matt Fiddaman
1f821d2849 ⬆️ bump github actions (#7234)
* actions/setup-node

* actions/cache

* actions/checkout

* docker/*

* actions/*-artifact

* actions/stale

* others

* note
2026-03-18 08:53:03 +00:00
Matt Fiddaman
beee16bc8c ⬆️ march dependency updates (#7222)
* @types/node (^22.19.10 → ^22.19.15)

* baseline-browser-mapping (^2.9.19 → ^2.10.0)

* eslint (^9.39.2 → ^9.39.3)

* lage (^2.14.17 → ^2.14.19)

* lint-staged (^16.2.7 → ^16.3.2)

* minimatch (^10.1.2 → ^10.2.4)

* oxlint (^1.47.0 → ^1.51.0)

* rollup-plugin-visualizer (^6.0.5 → ^6.0.11)

* @chromatic-com/storybook (^5.0.0 → ^5.0.1)

* @storybook/addon-a11y (^10.2.7 → ^10.2.16)

* @storybook/addon-docs (^10.2.7 → ^10.2.16)

* @storybook/react-vite (^10.2.7 → ^10.2.16)

* eslint-plugin-storybook (^10.2.7 → ^10.2.16)

* storybook (^10.2.7 → ^10.2.16)

* @codemirror/autocomplete (^6.20.0 → ^6.20.1)

* @codemirror/lang-javascript (^6.2.4 → ^6.2.5)

* @codemirror/language (^6.12.1 → ^6.12.2)

* @rolldown/plugin-babel (~0.1.7 → ~0.1.8)

* @swc/core (^1.15.11 → ^1.15.18)

* @swc/helpers (^0.5.18 → ^0.5.19)

* @tanstack/react-query (^5.90.20 → ^5.90.21)

* @uiw/react-codemirror (^4.25.4 → ^4.25.7)

* hyperformula (^3.1.1 → ^3.2.0)

* i18next (^25.8.4 → ^25.8.14)

* i18next-parser (^9.3.0 → ^9.4.0)

* react-i18next (^16.5.4 → ^16.5.6)

* react-virtualized-auto-sizer (^2.0.2 → ^2.0.3)

* fs-extra (^11.3.3 → ^11.3.4)

* @r74tech/docusaurus-plugin-panzoom (^2.4.0 → ^2.4.2)

* lru-cache (^11.2.5 → ^11.2.6)

* nodemon (^3.1.11 → ^3.1.14)

* eslint-plugin-perfectionist (^4.15.1 → ^5.6.0)

* downshift (9.0.10 → 9.3.2)

* react-router (7.13.0 → 7.13.1)

* @easyops-cn/docusaurus-search-local (^0.52.3 → ^0.55.1)

* peggy (5.0.6 → 5.1.0)

* @types/supertest (^6.0.3 → ^7.2.0)

* note
2026-03-18 08:37:04 +00:00
Karim Kodera
4cdb26f9a7 Adding Concentric Donut Pie Chart type to custom report charts (#7038)
* Initial commit for concentric donut chart implementation

* [autofix.ci] apply automated fixes

* Update upcoming-release-notes/7038.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix coderabbit comments (Recalculated total to avoid hidden cats, remove tooltip, add proper types)

* Update packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix zero total group

* lint issues fix

* Fix lint issues

* Empty commit to retriger the process

* [autofix.ci] apply automated fixes

* Removed line betweeen arc and label. I beleive the view is cleaner this way.

* Fixed line for outer donut

* split active shape for concentric circles to avoid impacting original chart

* [autofix.ci] apply automated fixes

* Fixing mid point to align with mid point on inner circle

* 1- make line always start inside the core circle
2- fix bug where inner circle label was showing below the outer circle

* - Fixed Dashboard issue when height too low.
- Rewrite of the activeShape part for simplicity.
- Centralize radius calculation.
- Provide differnt dimensions for compact vs standard rendering
- fix mid line point to auto fix at 70% of the inner radius
- More readable code

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7038

* Fixed distance issue for arc on single ring.

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7038

* Update packages/desktop-client/src/components/reports/graphs/DonutGraph.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fixing Code Rabbit Comments

* rerunning tests

* Added Group click through passing all categories iwth a workaorund on showActivity

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 20:10:30 +00:00
Matiss Janis Aboltins
15358b6b54 [AI] Remove development theme (#7232)
* [AI] Remove development theme

Delete the development theme that was only available in non-production
environments. Removes the theme file, its registration in the theme
registry, type definitions, preference validation, ThemeSelector icon
mapping, and Storybook configuration.

https://claude.ai/code/session_01A1aEippeWppuwoRSCBPwby

* Add release notes for PR #7232

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 19:18:47 +00:00
J B
6d9b1a1d72 Duplicate reimport fix in ui and API (#6926)
* add options to override reimportDeleted

* doc and default in ui to false

* pr note

* period

* wording

* [autofix.ci] apply automated fixes

* docs clarity

* actually test default behavior

* Update upcoming-release-notes/6926.md

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

* use new ImportTransactionOpts type for consistency

* Release note wording

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 17:25:22 +00:00
sys044
108ccc8aba Formula Card: Add budget analysis functions (#7078)
* feat(formula): Add QUERY_BUDGET function for budget-aware formula reporting

* docs(release): Add QUERY_BUDGET feature release notes

* refactor: decompose into multiple functions and support goal dimension

* [autofix.ci] apply automated fixes

* refactor: simplified code

* [autofix.ci] apply automated fixes

* updated release notes

---------

Co-authored-by: Your Name <tomgriffin@localhost>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-17 16:37:10 +00:00
scojo
ee8f8bfbba Add Formula Rule function to set-split-amount (#6414)
* Add Formula Rule function to set-split-amount fixed-amount and fixed-percent actions.

* Hide formula UI for remainder splits and simplify icon logic

* Guard against undefined options in set-split-amount else‑branch

* Show split amount formula in rules list

* Update tests based on changes introduced in 7fa1ff230e

* Change set-split-amount formula feature to work as a separate allocation method.

* [autofix.ci] apply automated fixes

* Update types for parent_amount

* Use a semantic non-interactive element instead of Button

* import organization

* [autofix.ci] apply automated fixes

* Fix tests

* Correctly hide when feature is disabled. Update release notes.

* Add tooltip documenting parent_amount formula variable
Change parent_amount to return in cents so it is consistent with amount
variable
Ensure balance variable is available in set-split-amount formula
Clean up parent_amount deletion to be consistent with balance

* Delete balance and parent_amount from subtransactions

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-03-17 16:28:14 +00:00
Matiss Janis Aboltins
c4ee71409e [AI] Add Yarn constraints to enforce consistent dependency versions (#7229)
* [AI] Add yarn constraints to enforce consistent dependency versions

Adds a `yarn.config.cjs` that uses Yarn 4's built-in constraints feature
to detect when the same dependency is declared with different version
ranges across workspaces. Workspace protocol references and
peerDependencies are excluded from the check.

Also adds a `yarn constraints` convenience script and the `@yarnpkg/types`
dev dependency for type-checked constraint authoring.

https://claude.ai/code/session_01B1xRjZXn6b18anZjo8cbqb

* Add release notes for PR #7229

* Add constraints job to GitHub Actions workflow

* Fix constraints

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 16:00:34 +00:00
Matt Fiddaman
dfd6e468a6 ⬆️ react-spring 10.0.3 (#7224)
* react-spring (10.0.0 -> ^10.0.3)

* note

* fix VRT

* fix more animations

* fix budget month colouring
2026-03-17 16:00:27 +00:00
Matiss Janis Aboltins
5b227f5fa1 feat: Add post-checkout hook to run yarn install if yarn.lock changes (#7230) 2026-03-17 15:58:02 +00:00
Julian Dominguez-Schatz
e1606b31ab Migrate get-next-package-version.js to TypeScript (#7227)
* Migrate `get-next-package-version.js` to TypeScript

* Add release notes

* Stronger type check

* Fix step ordering

* Fix typo

* Fix missed ordering
2026-03-17 13:56:47 +00:00
Michael Clark
4f7c3c51a5 🐛 Using a shared worker to coordinate multiple tabs (#7172)
* attempt to enable sync when multiple tabs are open

* allow multiple tabs to work

* release notes

* rehome the host if the tab closes

* ensure new tabs always receive failure  messages by broadcasting them on interval

* reject after retries are exhausted

* forwarding the logs from the worker to the main browser

* [autofix.ci] apply automated fixes

* add preflight fetch from main thread to server endpoint to trigger permission prompt if required

* remove the log prefix for cleaner logs

* adding heardbeat to detect closed tabs so they can be removed from the list

* store failure payload and broadcast for new tabs after timeout is cleared

* if a tab closes a budget, force other tabs to go to the budget list screen

* fix safari by detecting crossoriginisolated as a dependency for shared worker

* all ios to fallback to non-shared-worker implemenation

* coordinator and all backend work going through a leader tab to enable ios

* electing new leader tab when oone tab closes or is refreshed

* logic for standalone tabs to rejoin shared workers when on same budget

* remove the preflight request, shouldnt be needed now the code runs on the main process

* handling brand new tabs going to open budgets that are current standalone with no leader

* allowing budgets to be closed  without kickother others by transfering leadership to remaining oopened tabs

* remove unnedd comments

* change approach slightly - no more standalone, now every budget gets leader promotion automatically)

* adding tests and fixed minor bug to do with deleting budget with multiple tabs open

* fix worker not loading

* trouble with ts - moving to js

* reintroduce ts for the worker

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-17 09:30:34 +00:00
Matt Fiddaman
0e1fc07bf3 ⬆️ @types/react (#7223)
* bump @types/react

* note
2026-03-17 08:17:48 +00:00
Matiss Janis Aboltins
53cdc6fa48 [AI] Further hardening of "/change-password" endpoint (#7207)
* [AI] Fix OIDC privilege escalation in /change-password endpoint

Add admin role check and password auth_method session check to prevent
non-admin or OIDC-authenticated users from changing the server password.
Previously, any authenticated user could overwrite the password hash and
then login via password method to obtain an ADMIN session.

https://claude.ai/code/session_01Wne9FY2QnKp6JF7g61B1Sn

* Add release notes for PR #7207

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-17 08:16:46 +00:00
okxint
1d0281025d fix: preserve schedule link when merging transactions (#7177)
* [AI] fix: preserve schedule link when merging transactions

When merging two transactions where one is linked to a schedule,
the schedule field was not included in the merge update, causing
the schedule association to be silently dropped. This resulted in
duplicate transactions and incorrect "Due" status for scheduled
transactions.

Add `schedule: keep.schedule || drop.schedule` to both the normal
merge path and the subtransaction merge path, matching the existing
fallback pattern used for payee, category, notes, etc.

Add three test cases covering:
- Schedule preserved from dropped transaction when kept has none
- Kept transaction's schedule takes priority when both have one
- Schedule preserved when merging manual scheduled with banksynced

Fixes #6997

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

* Add release notes for PR #7177

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 05:14:16 +00:00
Matiss Janis Aboltins
f73c5e9210 [AI] Fix adm-zip dependency resolution in loot-core (#7219) 2026-03-16 21:11:51 +00:00
Julian Dominguez-Schatz
a4eb17eff2 Upgrade to Vite 8 (#7184)
* Upgrade to Vite 8

* Add release notes

* PR feedback

* [autofix.ci] apply automated fixes

* PR feedback

* fix: inject process.env

* Restore deleted release note

* Clean up and typecheck

* Fix dev server

* Fix type error

* Fix tests

* PR feedback

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 23:16:39 +00:00
Matiss Janis Aboltins
8a3db77cff [AI] api: simplify bundling by removing loot-core type inlining (#7209) 2026-03-15 20:07:36 +00:00
Matiss Janis Aboltins
f5a62627f0 [AI] Remove @actual-app/crdt Vite aliases and redundant config (#7195)
* [AI] Remove @actual-app/crdt Vite aliases and redundant config

* Release notes

* Enhance CRDT package configuration and clean up Vite settings

* Added `publishConfig` to `crdt/package.json` to specify exports for types and default files.
* Removed unused `crdtDir` references from `vite.config.ts` and `vite.desktop.config.ts` to streamline configuration.
2026-03-15 17:41:27 +00:00
Matiss Janis Aboltins
6c150cf28a [AI] Publish loot-core (@actual-app/core) nightly first in workflow (#7200)
* [AI] Publish loot-core (@actual-app/core) nightly first in workflow

* [autofix.ci] apply automated fixes

* Refactor imports and update configuration

- Updated .oxfmtrc.json to change "parent" to ["parent", "subpath"].
- Removed unnecessary blank lines in various TypeScript files to improve code readability.
- Adjusted import order in reports and rules files for consistency.

* Add workflow steps to pack and publish the core package nightly

* Remove nightly tag from npm publish command in workflow for core package

* Update post-build script comment to reflect correct workspace command for loot-core declarations

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 17:35:01 +00:00
Asger Mogensen
e069312ac3 Feature: Add a confirmation modal when users merge payees in /payees (#7188)
* Add a confirmation model when merging payees in /payee

* Added a confirmation modal when users merge payees in /payees

* Address coderabbit comments

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 15:59:57 +00:00
Pranay S
f266b761c2 [AI] Fix Spending Analysis budget table for tracking budgeting (#7191)
* [AI] Fix Spending Analysis budget table for tracking budgeting

Made-with: Cursor

* [autofix.ci] apply automated fixes

* [AI] Add release note for Spending Analysis tracking budgeting fix

Made-with: Cursor

* [autofix.ci] apply automated fixes

* [AI] Address CodeRabbit nitpicks: use typed narrowing instead of assertion for budgetType

Made-with: Cursor

---------

Co-authored-by: Pranay Mac M1 <pranayseela@yahoo.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-15 15:58:22 +00:00
Matiss Janis Aboltins
328b36f124 [AI] Fix navigator is not defined error in @actual-app/api for Node.js environments (#7202)
* [AI] Fix navigator is not defined error in @actual-app/api for Node.js environments

Add platform.api.ts to provide Node.js-safe defaults for platform detection,
which the API's Vite config resolves before the browser-only platform.ts.
Also guard navigator access in environment.ts isElectron() function.

Fixes #7201

https://claude.ai/code/session_015Xz2nHC12pNkADGjGZnSXd

* Add release notes for PR #7202

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-15 08:13:55 +00:00
Derek Gaffney
8934df0cb5 fix(docs/accounts): remove duplicated content (#7199)
* fix(docs): remove duplicated content

* release note file
2026-03-14 21:39:33 +00:00
dependabot[bot]
ab269fa4ea Bump undici from 7.18.2 to 7.24.1 (#7197)
Bumps [undici](https://github.com/nodejs/undici) from 7.18.2 to 7.24.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.18.2...v7.24.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.24.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 21:19:47 +00:00
Matiss Janis Aboltins
9c61cfc145 [AI] Switch typecheck from tsc to tsgo and fix Menu type narrowing (#7183)
* [AI] Switch typecheck from tsc to tsgo and fix Menu type narrowing

* [autofix.ci] apply automated fixes

* Add .gitignore for dist directory, update typecheck script in package.json to use -b flag, and remove noEmit option from tsconfig.json files in ci-actions and desktop-electron packages. Introduce typesVersions in loot-core package.json for improved type handling.

* Refactor SelectedTransactionsButton to improve type safety and readability. Updated items prop to use spread operator for conditional rendering of menu items, ensuring proper type annotations with MenuItem. This change enhances the clarity of the component's structure and maintains TypeScript compliance.

* Update tsconfig.json in desktop-electron package to maintain consistent formatting for plugins section. No functional changes made.

* [autofix.ci] apply automated fixes

* Update package.json and yarn.lock to add TypeScript 5.8.0 dependency. Adjust typesVersions in loot-core package.json for improved type handling. Enhance tsconfig.json in sync-server package to enable strictFunctionTypes for better type safety.

* Enhance tsconfig.json in ci-actions package by adding composite option for improved project references and build performance.

* [AI] Revert typescript to 5.9.3 for ts-node compatibility

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Update yarn.lock after TypeScript version change

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Refactor Menu component for improved type safety and readability. Updated type assertions for Menu.line and Menu.label, simplified type checks in filtering and selection logic, and enhanced conditional rendering of menu items. This change ensures better TypeScript compliance and maintains clarity in the component's structure.

* Refactor Select and OpenIdForm components to improve type safety and simplify logic. Updated item mapping to handle Menu.line more effectively, enhancing clarity in selection processes. Adjusted SelectedTransactionsButton to streamline item creation and improve readability.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-03-14 21:03:10 +00:00
Matiss Janis Aboltins
d86c9cf735 Add theme mode filtering to custom theme catalog (#7194)
* [AI] Add mode field to custom theme catalog for dark/light filtering

Each catalog theme now has a `mode: 'dark' | 'light'` field. When
installing a custom theme in auto mode, the ThemeInstaller filters the
catalog to only show themes matching the selected mode (light or dark).

https://claude.ai/code/session_01PtSEMRv3SpAEtdGzvYxzpa

* Add release notes for PR #7194

* Change category from Features to Enhancements

* [AI] Rename filter parameter to avoid shadowing useTranslation t()

Rename `t` to `catalogTheme` in the catalogItems filter to avoid
shadowing the `t` translation function from useTranslation().

https://claude.ai/code/session_01PtSEMRv3SpAEtdGzvYxzpa

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-14 14:57:47 +00:00
Stephen Brown II
f95cfbf82c Add drag&drop reordering for transactions within the same day (#6653)
* feat(desktop-client): add transaction drag-and-drop reordering

Add the ability to manually reorder transactions within a date group using
drag-and-drop in the TransactionsTable. This is useful for correcting the
order of transactions that occurred on the same day.

Prevent transaction reordering from falling back to the end of the day list
when dropping before a split parent. Parent-level moves now target parent
rows only, so regular transactions can be inserted between split parents on
the same date.

Key changes:

Frontend:
- Migrate drag-and-drop from react-dnd to react-aria hooks
- Add transaction row drag handles and drop indicators
- Implement onReorder handler in TransactionList
- Restrict reordering to same-date transactions only
- Disable drag-drop on aggregate views (categories, etc.)

Backend:
- Add transaction-move handler with validation
- Implement moveTransaction in db layer with midpoint/shove algorithm
- Add TRANSACTION_SORT_INCREMENT constant for consistent spacing
- Handle split transaction subtransactions automatically
- Inherit parent sort_order in child transactions during import

Refactoring:
- Remove useDragRef hook (replaced by react-aria)
- Remove DndProvider wrapper from App.tsx
- Update budget and sidebar components for new drag-drop API
- Fix sort.tsx @ts-strict-ignore by adding proper types

configure allowReorder for TransactionTable consumers

- Account.tsx: Enable reordering for single-account views, add sort_order
  as tiebreaker for stable ordering when sorted by other columns
- Calendar.tsx: Disable reordering in calendar report view (read-only context)

* void promises
2026-03-14 13:35:45 +00:00
Matiss Janis Aboltins
767f77fea3 [AI] Enable more lint rules as warn for gradual fix (7196) (#7196) 2026-03-14 01:47:17 +00:00
Matiss Janis Aboltins
d6dcc30e44 [AI] Promote typescript/no-for-in-array lint rule from warn to error (#7193)
* [AI] Promote typescript/no-for-in-array lint rule from warn to error

Convert the oxlint rule from "warn" to "error" as noted by the existing
TODO comment, and fix the three violations by replacing for-in loops
with for-of using .entries().

https://claude.ai/code/session_01N6F8DMzUVDxNJC56jMGknf

* Add release notes for PR #7193

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-13 21:06:06 +00:00
Matiss Janis Aboltins
541df52441 Enable restrict-template-expressions linting rule (#7181)
* [AI] Promote typescript/restrict-template-expressions to error and fix violations

Convert the oxlint rule from "warn" to "error" and fix all 42 violations
by wrapping non-string template expressions with String(). This ensures
type safety in template literals across the codebase.

https://claude.ai/code/session_01Uk8SwFbD6HuUuo3SSMwU9z

* Add release notes for PR #7181

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-13 18:36:58 +00:00
Julian Dominguez-Schatz
85e3166495 [AI] Remove deep-equal package (#7187)
* Remove `deep-equal` package

* Add release notes

* Add a few more tests

* Add release notes
2026-03-13 16:36:52 +00:00
Mats Nilsson
5d4bbc9ebb fix: mobile autocomplete modals (#6741)
When filtering for accounts in e.g. the net worth graph the modal closes
the filter the tooltip so it's impossible to add e.g. accounts to the
filter.
2026-03-13 15:03:38 +00:00
dependabot[bot]
031aac9799 bump ajv from 6.12.6 to 6.14.0 (#7044)
* Bump ajv from 6.12.6 to 6.14.0

Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0)

---
updated-dependencies:
- dependency-name: ajv
  dependency-version: 6.14.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-03-13 01:53:32 +00:00
Stephen Brown II
c53c5c2f36 [AI] Normalize apostrophe-dot thousandsSeparator for consistency (#7179)
* Normalize apostrophe-dot thousandsSeparator for consistency

* Also normalize keyboard apostrophes on the input path
2026-03-13 01:47:53 +00:00
Matiss Janis Aboltins
e968213977 Use TypeScript project references for incremental builds (#7180)
* [AI] Fix duplicate typechecking by consolidating into lage

Previously `yarn typecheck` ran:
1. `tsc -b` (type-checks all packages via project references)
2. `tsc -p tsconfig.root.json --noEmit` (checks root bin/*.ts)
3. `lage typecheck` (runs `tsc --noEmit` per package - duplicate!)

Now it runs:
1. `tsc -p tsconfig.root.json --noEmit` (checks root bin/*.ts)
2. `lage typecheck` (handles everything via dependency ordering)

Changes:
- Remove `tsc -b` from root typecheck script
- Add `dependsOn: ["^typecheck"]` to lage config for correct ordering
- Change per-package typecheck from `tsc --noEmit` to `tsc -b` so
  declarations are emitted for dependent packages

https://claude.ai/code/session_01P7mtAHphD6f1FsnQRwWBaW

* Add release notes for PR #7180

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-12 21:54:29 +00:00
Matiss Janis Aboltins
6b996c11d8 [AI] Add --quiet flag to oxlint commands (#7055)
* [AI] Separate lint and format into distinct commands

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update lint-staged configuration to use 'oxfmt' for formatting instead of 'yarn format:fix'

* [AI] Add format checks to CI workflows

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Refactor linting and formatting commands in package.json and GitHub workflows to streamline processes and add quiet mode for linting

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-03-12 18:01:10 +00:00
Matiss Janis Aboltins
282a99db2f lint: promote no-floating-promises and require-array-sort-compare to error and fix violations (#7168)
* lint: promote no-floating-promises and require-array-sort-compare to error and fix violations

- Set typescript/no-floating-promises and typescript/require-array-sort-compare to error in .oxlintrc.json
- Add explicit compare functions to all .sort() calls: migrations (localeCompare), crdt/merkle (localeCompare), FiltersMenu (tuple key), useScheduleEdit (two-arg comparator), exec.test (localeCompare), goal-template and category-template-context (numeric), main.test and transactions.test (localeCompare / amount+id)
- Fix invalid single-arg sort in useScheduleEdit to proper two-arg comparator

Made-with: Cursor

* refactor: update sorting functions to use two-argument comparator

* Update index.ts

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-03-12 17:50:37 +00:00
Matiss Janis Aboltins
3a22f1a153 refactor(api): fix cyclic dependencies (#6809)
* refactor(api): defineConfig vitest, api-helpers, drop vite.api build

- Wrap api vitest.config with defineConfig for typing/IDE
- Add loot-core api-helpers, use in YNAB4/YNAB5 importers
- Remove vite.api.config, build-api, injected.js; simplify api package

* refactor(api): update package structure and build scripts

- Change main entry point and types definition paths in package.json to reflect new structure.
- Simplify build script by removing migration and default database copy commands.
- Adjust tsconfig.dist.json to maintain declaration directory.
- Add typings for external modules in a new typings.ts file.
- Update comments in schedules.ts to improve clarity and maintainability.

* chore(api): update dependencies and build configuration

- Replace tsc-alias with rollup-plugin-visualizer in package.json.
- Update build script to use vite for building the API package.
- Add vite configuration file for improved build process and visualization.
- Adjust tsconfig.dist.json to exclude additional configuration files from the build.

* fix(api): update visualizer output path in vite configuration

- Change the output filename for the visualizer plugin from 'dist/stats.json' to 'app/stats.json' to align with the new directory structure.

* refactor(api): streamline Vite configuration and remove vitest.config.ts

- Remove vitest.config.ts as its configuration is now integrated into vite.config.ts.
- Update vite.config.ts to include sourcemap generation and adjust CRDT path resolution.
- Modify vitest.setup.ts to correct the import path for the CRDT proto file.

* feat(api): enhance build scripts and add file system utilities

- Update build scripts in package.json to include separate commands for building node, migrations, and default database.
- Introduce a new file system utility module in loot-core to handle file operations such as reading, writing, and directory management.
- Implement error handling and logging for file operations to improve robustness.

* Refactor typecheck script in api package and enhance api-helpers with new schedule and rule update functions. The typecheck command was simplified by removing the strict check, and new API methods for creating schedules and updating rules were added to improve functionality.

* Refactor API integration in loot-core by removing api-helpers and directly invoking handlers. Update typecheck script in api package to include strict checks, and refine TypeScript configurations across multiple packages for improved type safety and build processes.

* Refactor imports and enhance code readability across multiple files in loot-core. Simplified import statements in the API and adjusted formatting in YNAB importers for consistency. Updated type annotations to improve type safety and maintainability.

* Refactor handler invocation in YNAB importers to use the new send function from main-app. This change improves code consistency and readability by standardizing the method of invoking handlers across different modules.

* Refactor schedule configuration in loot-core to enhance type safety by introducing a new ScheduleRuleOptions type. This change improves the clarity of the recurring schedule configuration and ensures better type checking for frequency and interval properties.

* Update TypeScript configuration in api package to include path mapping for loot-core. This change enhances module resolution and improves type safety by allowing direct imports from the loot-core source directory.

* Update TypeScript configuration in api package to reposition the typescript-strict-plugin entry. This change improves the organization of the tsconfig.json file while maintaining the existing path mapping for loot-core, ensuring consistent type checking across the project.

* Update TypeScript configurations across multiple packages to enable noEmit option. This change enhances build processes by preventing unnecessary output files during compilation. Additionally, remove the obsolete tsconfig.api.json file from loot-core to streamline project structure.

* Update TypeScript configuration in sync-server package to enable noEmit option. This change allows for the generation of output files during compilation, facilitating the build process.

* Update api package configuration to streamline build process and enhance type safety. Removed unnecessary build scripts, integrated vite-plugin-dts for type declaration generation, and added migration and default database copying functionality. Adjusted vitest setup to comment out CRDT proto file import for improved test isolation.

* Update TypeScript configurations in desktop-client and desktop-electron packages to enable noEmit option, allowing for output file generation during compilation. Additionally, add ts-strict-ignore comments in YNAB importers to suppress strict type checking, improving compatibility with embedded API usage.

* Refactor api package configuration to update type declaration paths and enhance build process. Changed type definitions reference in package.json, streamlined tsconfig.json exclusions, and added functionality to copy inlined types during the build. Removed obsolete vitest setup file for improved test isolation.

* Revert to solution without types

* Update TypeScript configuration in API package to use ES2022 module and bundler resolution. This change enhances compatibility with modern JavaScript features and improves the build process.

* Update yarn.lock and API package to enhance TypeScript build process and add new dependencies

* Refactor inline-loot-core-types script to streamline TypeScript declaration handling and improve output organization. Remove legacy code and directly copy loot-core declaration tree, updating index.d.ts to reference local imports.

* Add internal export to API and enhance Vite configuration for migration handling

* Update Vite configuration in API package to target Node 18, enhancing compatibility with the latest Node features.

* Enhance inline-loot-core-types script to improve TypeScript declaration handling by separating source and typings directories. Update the copy process to include emitted typings, ensuring no declarations are dropped and maintaining better organization of loot-core types.

* Enhance migration handling by allowing both .sql and .js files to be copied during the migration process. Refactor file system operations in loot-core to improve error handling and streamline file management, including new methods for reading, writing, and removing files and directories.

* Refactor rootPath determination in Electron file system module by removing legacy case for 'bundle.api.js'. This simplifies the path management for the Electron app.

* Update API tests to mock file system paths for migration handling and change Vite configuration to target Node 20 for improved compatibility.

* Add promise-retry dependency to loot-core package and update yarn.lock

* Fix lint

* Refactor build script order in package.json for improved execution flow

* Feedback: API changes for "internal"
2026-03-12 17:43:39 +00:00
Tifan Dwi Avianto
d30162672c [AI] Correct API docs to match actual implementation (#7096)
* [AI] docs(api): fix API reference discrepancies in reference.md

Amp-Thread-ID: https://ampcode.com/threads/T-019ca316-33e2-75db-a333-baf62bb55f6c

* [autofix.ci] apply automated fixes

* docs: update API reference for init and updateRule methods

* docs(7096.md): add release notes for API reference documentation fix

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-11 19:04:19 +00:00
Matiss Janis Aboltins
db03d77e81 Handle missing accounts in SimpleFin batch sync (#7152)
* [AI] Fix SimpleFin batch sync crash when accounts are missing from response

When SimpleFin doesn't return data for all requested accounts during
batch sync, the code crashed with a TypeError accessing properties on
undefined, resulting in a generic "internal error" message for users.

This fix:
- Adds a guard in simpleFinBatchSync for missing account data, returning
  an ACCOUNT_MISSING error instead of crashing
- Propagates error entries from the SimpleFin response's errors map for
  accounts that have no data entry
- Adds a user-friendly ACCOUNT_MISSING error message in the UI suggesting
  to unlink and relink the account
- Adds test cases covering both scenarios

https://claude.ai/code/session_01XbHgxxrXYR3UTyW6VmYj47

* Add release notes for PR #7152

* [AI] Fix SimpleFIN batch sync error_code TypeError

Fix "Cannot read properties of undefined (reading 'error_code')" that
occurs during SimpleFIN batch sync by:

1. Adding null check for downloadSimpleFinTransactions result in
   simpleFinBatchSync (sync.ts) - the function can return undefined
   when user token is missing

2. Adding .catch() handler on individual processBankSyncDownload
   promises so a single account failure doesn't crash the entire
   batch via Promise.all rejection

3. Using optional chaining on syncResponse.res?.error_code in app.ts
   and handling the case where res is undefined with proper error
   reporting

https://claude.ai/code/session_01XbHgxxrXYR3UTyW6VmYj47

* [AI] Fix SimpleFin batch sync to emit ACCOUNT_MISSING for empty payloads

In the batch sync path, if a per-account download payload is an empty
object or is missing the transactions array, processBankSyncDownload
would crash and the error would be caught as INTERNAL_ERROR. Now we
check for these cases explicitly and emit ACCOUNT_MISSING instead,
while still allowing entries with error_code to propagate their
specific error.

https://claude.ai/code/session_01XbHgxxrXYR3UTyW6VmYj47

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 18:07:42 +00:00
Matiss Janis Aboltins
8a8fb2da51 [AI] Custom Themes - ability to define separate light/dark theme (#7145)
* [AI] Consolidate custom theme prefs and improve auto-mode UX

- Merge `installedCustomTheme` into `installedCustomLightTheme` so only
  two prefs exist (light + dark). The legacy asyncStorage key
  `installed-custom-theme` is preserved for backwards compatibility.
- In auto (System default) mode, the main Theme dropdown no longer
  surfaces the installed custom-light theme as an option; custom themes
  for light/dark are managed exclusively via the sub-selectors.
- Selecting "System default" resets both light and dark custom themes.
- Installing a custom theme from the main dropdown while in auto mode
  switches the base theme to "Light" so it applies directly.

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

* Add release notes for PR #7145

* Change category from Features to Enhancement

Custom Themes: separate light and dark theme options when selecting 'system default' theme.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7145

* Enhance ThemeSettings and UI components by adding maxWidth styling for better responsiveness. This change ensures that buttons and columns adapt to the full width of their containers.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 18:07:06 +00:00
Juulz
a65ab2b4ce Change Titlebar.tsx 'Sync' to Syncing icon only (#7005)
* Update Titlebar.tsx sync name to Server Sync

* Update Titlebar.tsx

* Create 7005.md

Change title Bar 'Sync' to 'Server Sync'.

* Update packages/desktop-client/src/components/Titlebar.tsx

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>

* Update packages/desktop-client/src/components/Titlebar.tsx

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>

* Update Titlebar.tsx

* Update Titlebar.tsx

* Update Titlebar.tsx

* [autofix.ci] apply automated fixes

* Update 7005.md

* Disable Server Sync button when offline in Titlebar component

* fix lint

* Add aria-disabled attribute to Server Sync button in Titlebar component

* Update titlebar sync icon and improve accessibility

Add disabled state and aria disabled label for offline mode.

---------

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-03-11 13:43:56 +00:00
Sylvercode
f29c031735 Add an option to swap payee/memo when importing transaction form file (#7101)
* Add swap payee-memo to ofx import

* Add mock files to test payee-memo swap

* Add swap payee-memo to qif import

* Add swap payee-memo to camt import

* Minor code cleanup for swap payee-memo on import

* Add release note

* lint fixing

* Fixe Payee and Memo capitalization

* change swapPayeeAndMemo to ofxSwapPayeeAndMemo

* correct  release note typo

* Add getSwapOption base on file type

* Support qfx

* Add CheckboxToggle to simplify ImportTransactionsModal options

* Fix split reac import

* Fix react import lint
2026-03-11 13:40:26 +00:00
Asherah Connor
c06f96f015 Add "only import transactions since" (#7139)
* Add "only import transactions since"

If specified, we filter out transactions before the given date.

* Address 🐰 comments

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7139

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-11 10:52:21 +00:00
Alieksieiev0
0b21b572fe fix item selection requiring double tap on mobile (#7166)
* fix item selection requiring double tap on mobile

* Update upcoming-release-notes/7166.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* extract getItemProps wrapper

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-11 10:34:31 +00:00
Matiss Janis Aboltins
bf1d220ced [AI] Lint: fix typescript/unbound-method problems (#7163)
* [AI] Refactor modal components and server modules

Made-with: Cursor

* Release notes
2026-03-10 18:06:50 +00:00
Vicente Cruz
94dd8f73c0 Add "Notion Dark Mode" custom theme (#7151)
* Update customThemeCatalog.json

Added custom theme inspired by Notion's Dark Mode

* [autofix.ci] apply automated fixes

* Create 7151.md release notes file

* Add Notion Dark Mode custom theme

Pushed a no-op change to get the tests to re-run

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-10 17:00:12 +00:00
Matiss Janis Aboltins
85e08b2e9e [AI] Fix privilege escalation in sync-server /change-password and getLoginMethod (#7155)
* [AI] Fix privilege escalation in sync-server /change-password and getLoginMethod

Made-with: Cursor

* Update upcoming-release-notes/7155.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix privilege escalation issue in change-password endpoint

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-09 20:33:20 +00:00
Dustin Brewer
60e2665fcc MVP for Payee Locations (#6157)
* Phase 1: Add payee locations database schema/types

 * Add migration to create payee_locations table with proper indexes
 * Add PayeeLocationEntity type definition
 * Update database schema to include payee_locations table
 * Export PayeeLocationEntity from models index

* Phase 2: Add payee location API/services

  * Add constants for default location behavior
  * Implement location service with geolocation adapters
  * Add new API handlers for payee location operations

* Phase 3: Add location-aware UI components/hooks

 * Update mobile transaction editing with location integration
 * Enhance PayeeAutocomplete with nearby payee suggestions and forget
   functionality
 * Implement location permission and placeholder unit of measurement hooks

* Phase 4: Add YNAB5 payee location import support

 * Extend YNAB5 types to include location data from payees
 * Implement location import logic in YNAB5 importer

* Phase 5: Add unit of measurement support

 * Add unit of measurement preference setting in Format.tsx
 * Implement distance formatting utilities for imperial/metric units
 * Add useUnitOfMeasurementFormat hook for accessing preferences

* Required release note about the PR

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6157

* Actually get syncing working

This was not obvious to me, esp. with 13 years of data, but the
locations I've been inserting were local only.

Everything appeared to work.

What I failed to notice is that the locations did not sync
across devices. Of course all the location data that was imported worked
fine, but nothing new unless it was created on device.

This changes the schema and uses the proper insert/delete methods such
that syncing works.

* Remove unit of measurement preference

Display feet and meters automatically, and don't bother to format based on miles/kilometers.

* Add payeeLocations feature flag

Place the location permissions check and thus user-facing functionality behind the feature flag

* Missed adding tombstone to payee location query

* Adjust migration name to pass CI

Adjust the indexes as well

* Unify location.ts

If CodeRabbit complains again, reply that we are actively choosing a unified file

* Add bounds testing

The validation is straightforward range-checking — if it's wrong, it'll be obvious quickly. Unless there's a plan to start adding broader test coverage for that file, I'd leave it untested for now

* Prefer camelCase for the method params

* Fix the nested interactive containers

* Fix the majority of CodeRabbit nits

The remainder seem to not be related to my code change (just a lint), outdated (sql migration comment), or infeasible (sql haversine query)

* More CodeRabbit nits

* Revert unnecessary YNAB5 zip import

Turns out the payee_locations were inside the exported budget all along!

* Additional guards and other CR fixes

* Match the pattern used elsewhere in file

* YNAB5.Budget -> Budget

Missed in the merge conflict

* ci: trigger rerun

* Change import from fetch to connection module

* Correct invalid border property

Ah. I never noticed this property wasn't working. I guess the button
looked OK to me!

* Only hide the button on success

* Update packages/loot-core/src/shared/location-utils.ts

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

* Update packages/loot-core/src/server/payees/app.ts

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

* Fully fix typo

Guess I shouldn't commit a suggestion after waking up

* Attempting to address feedback

Manual select nearby payee and save payee location buttons to make the UX more obvi

* Remove stale file that was moved

* Additional cleanup of remnant change

Removed the references to location from a few existing entities

* Additional cleanup of remnant change

* Show the Nearby payees button even when the field is disabled

If there are nearby payees, there's not a payee already selected, and the save button isn't needed

* runQuery is not async

* Add mockNearbyPayeesResult to test

Trying to utilize the real type in the mock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-03-09 20:26:48 +00:00
Matiss Janis Aboltins
102be1c54d Fix shared error array in SimpleFin batch sync error handling (#7125)
* Add failing tests for SimpleFin batch sync shared error array bug

Tests prove two bugs in simpleFinBatchSync() catch block (app.ts:1100-1115):
1. All accounts share the same errors array reference
2. Errors accumulate across accounts instead of being isolated

Related: #6623, #6651, #7114

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o

* Fix shared error array in SimpleFin batch sync catch block

When simpleFinBatchSync() threw an error, all accounts received the
same errors array by reference and errors accumulated across accounts.
Each account now gets its own isolated errors array with a single error
specific to that account, matching the pattern used by accountsBankSync().

Fixes #6623

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o

* Remove @ts-strict-ignore from bank sync tests

Use proper non-null assertions instead of disabling strict mode.

https://claude.ai/code/session_011ebiiXRMmbiKxYMohVXL6o

* Add release notes for PR #7125

* [AI] Replace test() with it() to follow repo convention

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Claude <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>
2026-03-09 19:50:18 +00:00
Julian Dominguez-Schatz
448da13cf5 Move migrations script to typescript (#7075)
* Move migrations script to typescript

* Add release notes

* Setup

* Fix

* PR feedback

* Make imports work as expected

* Rabbit
2026-03-09 07:58:03 +00:00
LeviBorodenko
41679235be [Mobile] Fix preview running balances not displaying on toggle (#7041)
* refactor(usePreviewTransactions): Move running balances to useMemo

* docs(relnotes): Add note for mobile running balance fix

* refactor(hooks): Remove unnecessary options ref
2026-03-08 22:04:52 +00:00
Matiss Janis Aboltins
73fa068fe9 [AI] Establish AI agent commit and PR guidelines (#7153)
* [AI] Extract PR/commit rules into shared agent skill

Deduplicate PR and commit instructions from AGENTS.md into a standalone
skill file at .github/agents/pr-and-commit-rules.md. This single source
of truth is consumed by both Claude Code (via CLAUDE.md @-import) and
Cursor (via .cursor/rules/pr-and-commit.mdc with alwaysApply: true).

AGENTS.md now references the shared file instead of repeating the rules
in three separate sections.

https://claude.ai/code/session_01KkHg7MYXrTyDkTw6u98Vam

* Add release notes for PR #7153

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 18:16:46 +00:00
Juulz
1fe588c143 🐞 Fix mobile transactions colors - fixes #7042 (#7047)
* Update amount styling with theme colors

* Clean up imports in TransactionListItem.tsx

Removed unused import of makeBalanceAmountStyle.

* Add release notes for bugfix in color variables

Fix color variables for mobile transaction list items.

* Change positiveColor in amount to use theme.tableText

* Change negative color style for running balance

* Fix negative color style in TransactionListItem

* Update color styles for transaction amount display

* Update upcoming-release-notes/7047.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #7047

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-08 15:04:30 +00:00
mibragimov
edce092ae8 fix(csv-import): trim whitespace from amount strings before parsing (#7149)
* fix(csv-import): trim whitespace from amount strings before parsing

looselyParseAmount relies on a regex anchored at $ to detect decimal
markers. Trailing whitespace (e.g. from Excel-saved CSVs) shifts the
pattern match so a thousands separator is misidentified as a decimal
point, producing wildly wrong values.

Adding trim() at the top of the function eliminates trailing/leading
whitespace before any regex logic runs.

Fixes actualbudget/actual#7121

* chore: add release notes for #7149
2026-03-07 20:41:47 +00:00
dependabot[bot]
77411394f6 Bump dompurify from 3.3.0 to 3.3.2 (#7143)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.0 to 3.3.2.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.3.0...3.3.2)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-03-07 20:24:41 +00:00
dependabot[bot]
235d94478f Bump express-rate-limit from 8.2.1 to 8.2.2 (#7140)
* Bump express-rate-limit from 8.2.1 to 8.2.2

Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.2.1 to 8.2.2.
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.2.1...v8.2.2)

---
updated-dependencies:
- dependency-name: express-rate-limit
  dependency-version: 8.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* [AI] Update express-rate-limit to 8.3.0 to fix GHSA-46wh-pxpv-q5gq vulnerability

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Add release notes for PR #7140

* [AI] Update release notes to reflect version 8.3.0

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-07 19:48:56 +00:00
Matiss Janis Aboltins
7e0edd43ec Sort theme catalog items alphabetically by name (#7144)
* [AI] Sort custom theme catalog options alphabetically in the UI

Sort catalog themes by name using localeCompare before rendering,
without modifying the underlying JSON data file.

https://claude.ai/code/session_01Y5SGaVYqsVWVsvXV8ZFXj3

* Add release notes for PR #7144

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 23:01:07 +00:00
Matiss Janis Aboltins
fdf5c8d0a9 [AI] Move window typings import to globals.ts (#7142)
* [AI] Remove parent path import of window.ts from desktop-client tsconfig

Replace the `../../packages/loot-core/typings/window.ts` include in
desktop-client's tsconfig.json with a proper package import. This adds
a `./typings/*` export to loot-core's package.json and creates a
globals.ts file in desktop-client that imports the window types via
the package name.

https://claude.ai/code/session_01GrgAzjWd3XvqwBTfXLerxc

* [autofix.ci] apply automated fixes

* Add release notes for PR #7142

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-06 23:00:50 +00:00
Matt Fiddaman
a8ec84ceac stop font size fluctuations showing in summary cards (#7092)
* stop summary cards from showing until font size settled

* note
2026-03-06 23:00:41 +00:00
Michael Clark
b727124603 Fix docker images (#7146)
* fix docker images

* release notes
2026-03-06 22:55:21 +00:00
dependabot[bot]
8bb7f207f2 Bump svgo from 3.3.2 to 3.3.3 (#7130)
Bumps [svgo](https://github.com/svg/svgo) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/svg/svgo/releases)
- [Commits](https://github.com/svg/svgo/compare/v3.3.2...v3.3.3)

---
updated-dependencies:
- dependency-name: svgo
  dependency-version: 3.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 21:56:01 +00:00
dependabot[bot]
6e0c15eb12 Bump immutable from 5.1.3 to 5.1.5 (#7129)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.3 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.3...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-03-06 21:51:14 +00:00
Michael Clark
4e2cec2c7a 🧪 Improving docker image test resiliency (#7141)
* improving test resiliency

* release notes
2026-03-06 21:02:31 +00:00
Matiss Janis Aboltins
078603cadf [AI] Implement sync recovery (#7111)
* [AI] Fix iOS/Safari sync recovery (fixes #7026): useOnVisible hook, re-fetch server version on visible, improved network-failure message

Made-with: Cursor

* Feedback: coderabbitai

* Refactor useOnVisible test: remove unnecessary resolve check and simplify callback definition
2026-03-04 23:27:15 +00:00
Joel Jeremy Marquez
b3a86b5392 Update remaining accounts hooks to return react query states (#7071)
* Update remaining accounts hooks to return react query states

* Add release notes for PR #7071

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-04 23:13:55 +00:00
Mats Nilsson
295a565e55 Fix budget analysis report padding (#7118)
* Fix budget analysis report padding

The padding on the report is too small when the value is large.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-04 22:08:30 +00:00
Matiss Janis Aboltins
387c8fce16 [AI] Enable TypeScript composite project references across monorepo (#7062)
* [AI] Enable TypeScript composite project references across monorepo

- Add composite and declaration emit to all package tsconfigs
- Wire root and per-package project references in dependency order
- Replace cross-package include-based typing with referenced outputs
- Fix api TS5055 by emitting declarations to decl-output
- Add desktop-client alias for tests; fix oxlint import order in vite.config
- Add UsersState.data null type and openDatabase return type for strict emit

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove obsolete TypeScript configuration for API and update build script to emit declarations directly to the output directory. This streamlines the build process and ensures compatibility with the new project structure.

* Refactor TypeScript configuration in API package to remove obsolete decl-output directory and update build scripts. The changes streamline the build process by directing declaration outputs to the @types directory, ensuring better organization and compatibility with the new project structure.

* Add TypeScript declaration emission for loot-core in desktop-electron build process

* Refactor TypeScript configuration in API package to utilize composite references and streamline build scripts. Update include and exclude patterns for improved file management, ensuring better organization of declaration outputs and migration SQL files.

* Refactor TypeScript configuration in loot-core and desktop-client packages to streamline path management and remove obsolete dependencies. Update paths in tsconfig.json files for better organization and compatibility, and adjust yarn.lock to reflect changes in workspace dependencies.

* Update desktop-electron package to utilize loot-core as a workspace dependency. Adjust TypeScript import paths and tsconfig references for improved organization and compatibility across packages.

* Enhance Vite configuration for desktop-client to support Electron-specific conditions and update loot-core package.json to include Electron as a platform for client connection. This improves compatibility for Electron builds.

* Refactor TypeScript configuration across multiple packages to streamline path management. Update tsconfig.json files in root, api, and loot-core packages to improve import paths and maintain compatibility with internal typings.

* Update package dependencies and Vite configuration across component-library and desktop-client. Add vite-tsconfig-paths to component-library and remove it from desktop-client. Refactor Storybook preview file to include a TODO for future refactoring.

* Remove Node-specific path from loot-core package.json for client connection, streamlining platform configuration for Electron.

* Remove loot-core as a workspace dependency from desktop-electron package.json

* Update tsconfig.json to remove reference to desktop-client

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 20:57:06 +00:00
YHC
c7ebfd8ad4 Add TWD Currency (#7095)
* Add New Taiwan Dollar to currency list

* Add New Taiwan Dollar to currency list

* Fix decimalPlaces for New Taiwan Dollar

Updated decimalPlaces for New Taiwan Dollar from 0 to 2, as suggested by coderabbitai, "Line 62 introduces TWD as zero-decimal, but this codebase currently has an unresolved zero-decimal conversion/storage issue. This risks incorrect persisted amounts for TWD transactions."

* Add upcoming-releass-notes

* Add New Taiwan Dollar (NT$) to available currency

* Add New Taiwan Dollar (NT$) to available currency
2026-03-04 18:46:20 +00:00
Matiss Janis Aboltins
e1f834371b [AI] Refactor YNAB importers to use server-side send() and handler API (#7050)
* [AI] Refactor YNAB importers to use server-side send() and handler API

Co-authored-by: Cursor <cursoragent@cursor.com>

* Rename 7049.md to 7050.md

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 18:38:33 +00:00
Matiss Janis Aboltins
4caee99955 [AI] Strengthen schedule rule types in loot-core (remove ts-expect-error) (#7051)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 18:38:20 +00:00
Matiss Janis Aboltins
286d05d187 [AI] Move ImportTransactionsOpts from api package to loot-core (#7053)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-04 18:38:09 +00:00
Matiss Janis Aboltins
cf05a7ea01 [AI] Typescript: low hanging fruit (#7091)
* [AI] Use loot-core workspace in desktop-electron and fix related types

- Add loot-core as workspace dependency in desktop-electron
- Import GlobalPrefsJson from loot-core package in desktop-electron
- Allow null in usersSlice data type (UsersState)
- Add explicit SQL.Database return type to openDatabase in sqlite electron

Made-with: Cursor

* Re-add loot-core as a workspace dependency in desktop-electron
2026-03-04 18:32:01 +00:00
Michael Clark
b373b612a4 :electron: Electron backups converted to zip for easy importing (#7069)
* e:electron: electron backups converted to zip for easy importing

* release notes

* fix lint

* suggestion from rabbit

* Change category from Maintenance to Enhancements
2026-03-04 18:00:07 +00:00
Copilot
3797cff716 [AI] Skip AI-generated release notes for release/ branch PRs (#7107)
* Initial plan

* [AI] Make AI-generated release notes not run on release/ branches

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* [AI] Use explicit startsWith(github.head_ref) if conditions instead of JS changes

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>

* Delete extra condition

* Add release notes for PR #7107

* [autofix.ci] apply automated fixes

* FIx note

* Add head branch information to PR details

* Refactor conditions for release notes generation

* Sec

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-04 17:40:32 +00:00
Michael Clark
9e2793d413 :electron: Flathub pr on publish of release assets (#7117)
* flathub pr on publish of release

* docs

* release notes

* rabbit feedback
2026-03-04 17:05:47 +00:00
Julian Dominguez-Schatz
3201819df9 Display/save limit/refill templates (#6693)
* Handle refill templates in automations modal

* Add release notes

* Fix typecheck

* Fix icon style

* Style fixes

* Fix migration

* Fix typecheck

* Rabbit

* Add tests
2026-03-04 16:41:07 +00:00
Josh Woodward
eca50f28b0 Fix skipping schedules that move before weekend (#7057)
* Pushing before weekend to Monday

* release notes

* lint

* Guard against null

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fixing AI commit and addressing second comment

* addressing nitpicks

* first attempt at a test

* [autofix.ci] apply automated fixes

* refactor to use condition and fix tests

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-03-04 16:40:41 +00:00
dependabot[bot]
c82ee91b12 bump bn.js from 4.12.2 to 4.12.3 (#7073)
* Bump bn.js from 4.12.2 to 4.12.3

Bumps [bn.js](https://github.com/indutny/bn.js) from 4.12.2 to 4.12.3.
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v4.12.2...v4.12.3)

---
updated-dependencies:
- dependency-name: bn.js
  dependency-version: 4.12.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-03-04 16:40:25 +00:00
Julian Dominguez-Schatz
cb8ff337dc Migrate sync server general utils to typescript (#7074)
* Migrate sync server general utils to typescript

* Add release notes

* Rabbit

* Fix
2026-03-04 16:40:24 +00:00
Matiss Janis Aboltins
c37a5a02aa [AI] Add CLAUDE.md with reference to AGENTS.md (#7105)
* [AI] Add CLAUDE.md with reference to AGENTS.md

Made-with: Cursor

* Add release notes for PR #7105

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-04 16:40:14 +00:00
Matiss Janis Aboltins
f9e09ca59b 🔖 (26.3.0) (#7097)
* 🔖 (26.3.0)

* Remove used release notes

* Add release notes for PR #7097

* Remove used release notes

* Remove used release notes

* Add release notes for version 26.3.0

* Add new terms to spelling expectation list

* Fix spelling and capitalization in release notes

Corrected spelling of 'reorganisation' to 'reorganization' and updated 'coderabbit' to 'CodeRabbit' for consistency.

* Update patterns.txt to allowlist 'CodeRabbit'

Add 'CodeRabbit' to allowlist of proper nouns.

* Clarify chart theming support in release notes

Updated the release notes to specify bar/pie chart theming support and added details about theme variables for customization.

* Remove 'CodeRabbit' from spelling expectations

* Refactor release notes and improve formatting

Reorganize release notes for clarity and update content.

* Create 2026-03-02-release-26-3-0.md

* Change release date to 2026-03-02

Updated the release date for version 26.3.0.

* Update release notes for version 26.3.0

---------

Co-authored-by: jfdoming <9922514+jfdoming@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Julian Dominguez-Schatz <julian.dominguezschatz@gmail.com>
2026-03-03 01:23:12 +00:00
Matiss Janis Aboltins
8081b8829e [AI] Make merge-freeze-unfreeze workflow work on fork PRs (#7104)
* [AI] Make merge-freeze-unfreeze workflow work on fork PRs via pull_request_target

Made-with: Cursor

* Add release notes for the "unfreeze" workflow functionality in fork PRs

* Add empty permissions block to unfreeze job in merge-freeze-unfreeze workflow
2026-03-01 20:46:58 +00:00
Matt Fiddaman
f2f79d378c fix bugfix categorisation in contributor points counting script (#7103)
* s/bugfix/bugfixes

* note

* add alias functionality

* note update
2026-03-01 16:33:58 +00:00
Julian Dominguez-Schatz
c5cca67399 Revert "feat(currency): Add Vietnamese Dong (VND) currency" (#7100)
* Revert "feat(currency): Add Vietnamese Dong (VND) currency (#6902)"

This reverts commit 7fa9fa900b.

* Add release notes for PR #7100

* Delete 7100.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-01 07:31:29 +00:00
Matiss Janis Aboltins
eabf09587f [AI] Github action for unfreezing PRs (#7094)
* [AI] Add GitHub Action to add PR to Merge Freeze unblocked list when unfreeze label is added

Made-with: Cursor

* Rename 7093.md to 7094.md

* Add concurrency control to unfreeze job in merge-freeze-unfreeze workflow

- Introduced concurrency settings to prevent overlapping executions of the unfreeze job based on labels.
- Updated error handling to abort the process if fetching the current merge freeze status fails, ensuring unblocked PRs are not overwritten.

* Refactor Merge Freeze workflow to simplify PR unblocking process

- Updated the workflow to directly post the PR to the unblocked list without fetching the current freeze status.
- Improved error handling by ensuring the access token is set before proceeding with the API call.
2026-02-28 21:15:11 +00:00
Matiss Janis Aboltins
6022929551 [Cursor] Development environment setup (#7088)
* [AI] Add Cursor Cloud specific instructions to AGENTS.md

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Add release notes for PR #7088

* [AI] Fix Node.js minimum version requirement in AGENTS.md (#7089)

* [AI] Fix outdated Node.js version requirement in Build Failures section

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* [AI] Add test budget tip to Cursor Cloud instructions

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-02-27 20:38:37 +00:00
Copilot
e65429497d [AI] Remove 'suspect ai generated' label and associated workflow (#7087)
* Initial plan

* [AI] Remove 'suspect ai generated' label and associated workflow

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* [AI] Remove 'suspect ai generated' label from coderabbit config

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add release notes for PR #7087

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-26 20:51:47 +00:00
Matiss Janis Aboltins
3758d72b65 Mobile rules item alignment (#7081)
* [AI] Fix mobile rules list items to be full width and left-aligned

- Override Button's default justifyContent/alignItems centering in
  ActionableGridListItem to use flex-start alignment
- Add width: 100% to RulesListItem's SpaceBetween to fill the item width

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Add release notes for PR #7081

* Change category from Enhancements to Bugfix

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-26 20:22:29 +00:00
Matiss Janis Aboltins
032d10ac42 [AI] Fix API build output path (dist/index.js instead of dist/api/index.js) (#7084)
* [AI] Fix API build output path (dist/index.js instead of dist/api/index.js)

- Set rootDir in packages/api/tsconfig.json so output is under dist/ not dist/api/
- Remove loot-core pegjs.ts from include; add local typings/pegjs.d.ts
- Use mkdir -p in build:migrations for idempotent build
- Exclude **/@types/** so declaration output does not conflict with input

Made-with: Cursor

* Update TypeScript configuration in api package to refine exclude patterns
2026-02-26 20:20:38 +00:00
Michael Clark
f97a89dc28 🐛 Fix file path on windows (#7076)
* fix file path on windows

* file path in migrations

* release notes
2026-02-25 15:00:10 +00:00
Juulz
a4bd301ec6 🐞 Midnight theme: Change menuAutoCompleteTextHover color - Fixes #7029 (#7048)
* Change menuAutoCompleteTextHover color to green400

* Change menuAutoCompleteTextHover color to green400 in Midnight theme.

Change menuAutoCompleteTextHover color to green400 in Midnight theme.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-24 17:29:09 +00:00
Julian Dominguez-Schatz
18072e1d8b Validate file IDs for correctness (#7067)
* Validate file IDs for correctness

* Add release notes
2026-02-24 15:32:50 +00:00
Juulz
a1e0b3f45d Rename theme 'Okabe Ito' to 'Color-blind (dark)' (#7058)
* Rename theme 'Okabe Ito' to 'Color-blind (dark)'

* Rename 'Okabe Ito' theme to 'Color-blind (dark)'

* Fix capitalization in theme name for consistency
2026-02-23 18:49:45 +00:00
Matiss Janis Aboltins
0b361e45b4 [AI] Bump version to 26.2.1 (#7052)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 15:52:58 +00:00
Matiss Janis Aboltins
b3052dda05 v26.2.1: critical security fix for simplefin, pluggy and multi-user (#7043)
* Add release notes for version 26.2.1, including critical security fixes for SimpleFin, Pluggy, and multi-user setups. Remove outdated upcoming release notes for related bugfixes.

* Add release notes for PR #7043

* Delete upcoming-release-notes/7043.md

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-22 14:43:50 +00:00
Matiss Janis Aboltins
31a027fc64 [AI] Add per-package tsconfigs and typescript-strict-plugin for typecheck (#7019)
* [AI] Add per-package tsconfigs and typescript-strict-plugin for typecheck

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update TypeScript configuration across multiple packages to correct plugin path key from "path" to "paths" and add reference to process-worker typings in index.electron.ts.

* Remove reference to process-worker typings in index.electron.ts and add new process-worker typings file for global Process augmentation.

* Refactor TypeScript build configurations across multiple packages by removing tsconfig.dist.json files and updating build scripts to use default TypeScript compilation. Adjusted compiler options to target ES2021 and enable declaration generation.

* Update TypeScript configuration in api package to refine include and exclude patterns for better file management.

* Update build script in api package to ensure migration SQL files are copied to the correct directory by creating the destination folder if it doesn't exist.

* Update TypeScript configurations in crdt and desktop-electron packages to refine include and exclude patterns for improved file management.

* Update TypeScript dependencies across multiple packages to include typescript-strict-plugin for enhanced type checking and maintain consistency in package.json files.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 21:26:22 +00:00
Julian Dominguez-Schatz
cfc18c240a Add limit/refill automation types (#6692)
* Add limit/refill automation components

* Add release note

* Fix typecheck

* Rabbit PR feedback

* Review
2026-02-21 20:58:54 +00:00
Matiss Janis Aboltins
a68b2acac3 [AI] Enforce file access authorization on sync API endpoints (#7040)
* [AI] Enforce file access authorization on sync API endpoints

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor file deletion authorization to return error message as text

* Refactor file upload validation to improve error handling

* Add tests to allow admin users to retrieve encryption keys and sync files for other users

- Implemented a test for admin access to retrieve encryption keys for another user's file in the /user-get-key endpoint.
- Added a test for admin users to sync another user's file in the /sync endpoint, ensuring proper response and headers.

These changes enhance the authorization checks for admin actions on user files.

* Refactor file cleanup in tests to use onTestFinished for better error handling

* Enhance admin capabilities in file management tests

* Add migration to backfill file owners with admin ID

* Enhance file access authorization in sync API

* Update migration to backfill file owners with admin ID to ensure consistent ordering in the query

* Refactor access control tests for file downloads in sync API

* Add test for non-owner file download access via user_access in sync API

This test verifies that users with appropriate access can download files owned by others, utilizing the requireFileAccess logic and UserService.countUserAccess. It ensures correct response headers and content delivery for shared files.

* Refactor file cleanup in upload and download tests to utilize onTestFinished for improved error handling

This update consolidates file cleanup logic in the test suite, ensuring that temporary files are removed after each test execution. The changes enhance the reliability of tests by consistently managing file state across various scenarios.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 08:45:48 +00:00
Matiss Janis Aboltins
a25be5c95c [AI] Remove usage of 'web' file types (#7033)
* [AI] Desktop client, E2E, loot-core, sync-server and tooling updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor database handling in various modules to use async/await for improved readability and error handling. This includes updates to database opening and closing methods across multiple files, ensuring consistent asynchronous behavior. Additionally, minor adjustments were made to encryption functions to support async operations.

* Refactor sync migration tests to utilize async/await for improved readability. Updated transaction handling to streamline event expectations and cleanup process.

* Refactor various functions to utilize async/await for improved readability and error handling. Updated service stopping, encryption, and file upload/download methods to ensure consistent asynchronous behavior across the application.

* Refactor BudgetFileSelection component to use async/await for onSelect method, enhancing error handling and readability. Update merge tests to utilize async/await for improved clarity in transaction merging expectations.

* Refactor filesystem module to use async/await for init function and related database operations, enhancing error handling and consistency across file interactions. Updated tests to reflect asynchronous behavior in database operations and file writing.

* Fix typo in init function declaration to ensure it returns a Promise<void> instead of Proise<void>.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6987

* Update tests to use async/await for init function in web filesystem, ensuring consistent asynchronous behavior in database operations.

* Update VRT screenshot for payees filter test to reflect recent changes

* Update filesystem module to remove web-specific implementations and streamline path handling. Refactor file operations to enhance type safety and consistency across different environments. Add tests for SQLite interactions and ensure proper handling of database transactions.

* Add release notes for maintenance: Remove usage of 'web' file types

* Refactor filesystem module to use type annotations for exports and improve consistency across methods. Remove deprecated web file handling and enhance encryption functions for better browser compatibility.

* Trigger CI

* Add asyncStorage API file to export Electron index module

* Trigger CI

* Feedback: typo

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-20 18:01:36 +00:00
Pratik Silwal
bf1947a119 Prevent single-slash paths from being parsed as filepaths (#6966)
* fix: prevent single-slash paths from being parsed as filepaths

* add release notes

* [autofix.ci] apply automated fixes

* test: add tests related to filepath

* additonal test from coderabbit

---------

Co-authored-by: Pratik Silwal <pratiksilwal@Pratiks-MacBook-Air.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-20 12:38:29 +00:00
Mats Nilsson
37ad0ed563 Hide selected accounts in the mobile filter (#7030)
When filtering for accounts in the mobile view of reports, hide the
already selected accounts.
2026-02-20 12:34:57 +00:00
Daniel Bates
89d68ea2f8 Add tooltip to imported payee in rule result window (#7031)
* Add tooltip to imported payee column in rule result window

The imported payee column in SimpleTransactionsTable was missing a
title attribute, so truncated text had no tooltip on hover. Other
columns (category, account, notes) already pass title for this purpose.

Fixes #7003

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

* Add release notes for #7031

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

---------

Co-authored-by: Your Name <your-email@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:32:13 +00:00
Kenny McCormick
1ad7b6f781 Add ACTUAL_USER_CREATION_MODE documentation to oauth-auth.md (#6935)
* Add ACTUAL_USER_CREATION_MODE documentation to oauth-auth.md

* [autofix.ci] apply automated fixes

* add note that first external auth user is admin and owner

Added details about admin permissions and server ownership for users authenticating with OpenID/OAuth2.

* improve ACTUAL_USER_CREATION_MODE environment documentation

clarify warning about server owner

* [autofix.ci] apply automated fixes

* move first user admin warning to "after setup" section of OIDC documentation

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-20 02:24:43 +00:00
Matiss Janis Aboltins
fd9ee868a6 Enhance PR template with structured sections (#6989)
* Enhance PR template with description, type of change, and checklist sections

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update PR template to streamline instructions for writing release notes

* Remove unnecessary lines from the PR template to streamline instructions for writing release notes

* Add release notes for PR #6989

* Update category in release notes

Changed category from Enhancements to Maintenance.

* Update PULL_REQUEST_TEMPLATE.md

* Update AGENTS.md and PULL_REQUEST_TEMPLATE.md to clarify PR submission guidelines

* Update 6989.md

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-19 22:46:30 +00:00
Matiss Janis Aboltins
0de44af1de Require authentication for SimpleFIN and Pluggy.ai endpoints (#7034)
* Add authentication middleware to SimpleFIN and Pluggy.ai endpoints

Protect /simplefin/* and /pluggyai/* routes with validateSessionMiddleware
so only authenticated users can access bank account and transaction data.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Release notes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 22:46:10 +00:00
Juulz
cf58712bf1 🎨 High Contrast Light theme for Actual (#7032)
* Update customThemeCatalog.json

* [autofix.ci] apply automated fixes

* Create 7032.md

🎨 High contrast light theme.

* Update customThemeCatalog.json

* Update 7032.md

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-19 21:39:25 +00:00
Dagur Páll Ammendrup
6460af3de4 Set inital focus on category when covering overspending (#7012)
* Set inital focus on category when covering overspending

* Fixup: Make sure that the amount is set

* Unused import

* Fix bug where typing an amount and pressing enter uses previous value

---------

Co-authored-by: Dagur Ammendrup <dagurp@vivaldi.com>
2026-02-19 18:51:07 +00:00
Pratik Silwal
ce890faeeb Fix Net Worth Calculations (#6968)
* fix: computed priorPeriodNetWorth and use it as a baseline for net worth calculations

* add release notes

* correct spelling

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6968

* note

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-19 17:06:21 +00:00
Matt Fiddaman
848b86cd59 ⬆️ recharts (3.4.0 → 3.7.1) (#7022)
* recharts (3.4.1 -> 3.7.0)

* Cell & activeShape deprecation

* note

* fix textAnchor

* remove Cell in BarGraph
2026-02-19 16:28:41 +00:00
Crhistopher Suriel
ec22923f18 feat(currency): Add Dominican Peso (DOP) currency (#7028)
* feat(currency): Add Dominican Peso (DOP) currency

* Add release notes
2026-02-19 14:50:14 +00:00
dependabot[bot]
27402ee2b3 Bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 (#7020)
* Bump @isaacs/brace-expansion from 5.0.0 to 5.0.1

Bumps @isaacs/brace-expansion from 5.0.0 to 5.0.1.

---
updated-dependencies:
- dependency-name: "@isaacs/brace-expansion"
  dependency-version: 5.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* note

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-19 14:48:58 +00:00
Matiss Janis Aboltins
0472211925 [AI] lint: await-thenable, no-floating-promises (#6987)
* [AI] Desktop client, E2E, loot-core, sync-server and tooling updates

Co-authored-by: Cursor <cursoragent@cursor.com>

* Refactor database handling in various modules to use async/await for improved readability and error handling. This includes updates to database opening and closing methods across multiple files, ensuring consistent asynchronous behavior. Additionally, minor adjustments were made to encryption functions to support async operations.

* Refactor sync migration tests to utilize async/await for improved readability. Updated transaction handling to streamline event expectations and cleanup process.

* Refactor various functions to utilize async/await for improved readability and error handling. Updated service stopping, encryption, and file upload/download methods to ensure consistent asynchronous behavior across the application.

* Refactor BudgetFileSelection component to use async/await for onSelect method, enhancing error handling and readability. Update merge tests to utilize async/await for improved clarity in transaction merging expectations.

* Refactor filesystem module to use async/await for init function and related database operations, enhancing error handling and consistency across file interactions. Updated tests to reflect asynchronous behavior in database operations and file writing.

* Fix typo in init function declaration to ensure it returns a Promise<void> instead of Proise<void>.

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6987

* Update tests to use async/await for init function in web filesystem, ensuring consistent asynchronous behavior in database operations.

* Update VRT screenshot for payees filter test to reflect recent changes

* [AI] Fix no-floating-promises lint error in desktop-electron

Wrapped queuedClientWinLogs.map() with Promise.all and void operator to properly handle the array of promises for executing queued logs.

Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>

* Refactor promise handling in global and sync event handlers

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6987

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
2026-02-19 14:22:05 +00:00
Matt Fiddaman
a38104244a ⬆️ @playwright/test (1.57.0 → 1.58.2) (#7021)
* @playwright/test (1.57.0 → 1.58.2)

* note

* disable moving nav bar animations

* vrt
2026-02-19 00:19:54 +00:00
Matiss Janis Aboltins
77b848ca84 [AI] Allow var(--name) in custom theme CSS (no fallbacks) (#7018)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 22:16:25 +00:00
Matt Fiddaman
5179ac7c2d ⬆️ mid month dependency bump (#7013)
* baseline-browser-mapping (^2.9.14 → ^2.9.19)

* minimatch (^10.1.1 → ^10.1.2)

* lage (^2.14.15 → ^2.14.17)

* react-aria-components (^1.14.0 → ^1.15.1)

* @storybook/addon-a11y (^10.2.0 → ^10.2.7)

* p-limit (^7.2.0 → ^7.3.0)

* better-sqlite3 (^12.5.0 → ^12.6.2)

* vitest (^4.0.16 → ^4.0.18)

* @storybook/addon-docs (^10.2.0 → ^10.2.7)

* @storybook/react-vite (^10.2.0 → ^10.2.7)

* eslint-plugin-storybook (^10.2.0 → ^10.2.7)

* storybook (^10.2.0 → ^10.2.7)

* @codemirror/state (^6.5.3 → ^6.5.4)

* @swc/core (^1.15.8 → ^1.15.11)

* @tanstack/react-query (^5.90.19 → ^5.90.20)

* @vitejs/plugin-basic-ssl (^2.1.3 → ^2.1.4)

* @vitejs/plugin-react (^5.1.2 → ^5.1.3)

* i18next (^25.7.4 → ^25.8.4)

* react-aria (^3.45.0 → ^3.46.0)

* react-hotkeys-hook (^5.2.1 → ^5.2.4)

* react-i18next (^16.5.1 → ^16.5.4)

* sass (^1.97.2 → ^1.97.3)

* @easyops-cn/docusaurus-search-local (^0.52.2 → ^0.52.3)

* react (^19.2.3 → ^19.2.4)

* react-dom (^19.2.3 → ^19.2.4)

* component lib peer deps

* eslint-vitest-rule-tester (^3.0.1 → ^3.1.0)

* lru-cache (^11.2.4 → ^11.2.5)

* ua-parser-js (^2.0.7 → ^2.0.9)

* cors (^2.8.5 → ^2.8.6)

* @babel/core (^7.28.5 → ^7.29.0)

* @types/node (^22.19.3 → ^22.19.10)

* react (mixed → 19.2.4)

* @testing-library/react (16.3.0 → 16.3.2)

* react-router (7.12.0 → 7.13.0)

* vite-plugin-node-polyfills (^0.24.0 → ^0.25.0)

* pluggy-sdk (^0.79.0 → ^0.83.0)

* note
2026-02-18 22:09:28 +00:00
youngcw
2bb5a861c1 📖 start of reports dashboard updates (#6976)
* start of reports dashboard updates

* spelling

* Add release notes for PR #6976

* don't need one of these

* consistent naming

* bunny fixes

* images

* image again

* extenion

* fix

* consistent naming

* naming

* fix

* last one

* Ill be done with this someday

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-18 22:07:01 +00:00
Karim Kodera
bc32f4fcde Document graph color variables for custom themes (#7011)
* Document graph color variables for custom themes

Added documentation for graph color variables in custom themes.

* [autofix.ci] apply automated fixes

* Update custom themes documentation for clarity

Clarify the impact of color palettes on custom report widget graphs and format the list of color variables.

* Update packages/docs/docs/experimental/custom-themes.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

* Fix color variable syntax in custom themes documentation

Updated color variable syntax for chart colors in documentation.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-18 18:24:52 +00:00
Joel Jeremy Marquez
848eaadb0f Move redux state to react-query - payees states (#6880)
* Move redux state to react-query - account states

* Fix onbudget and offbudget displaying closed accounts

* Move redux state to react-query - payees states

* Add release notes for PR #6880

* Replace usage of logger in desktop-client with console

* Address feedback on adding default data to usePayees (#6931)

* Initial plan

* Add default data to usePayees usages using inline destructuring

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Fix imports

* Update empty payees list test

* Cleanup and simplify AccountEntity definition to fix `satisfies` syntax

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-18 17:04:26 +00:00
Joel Jeremy Marquez
5ac2947342 Update useAccounts to return react query states (#7009)
* Fix redirect to accounts page when no accounts exists

* Add release notes for PR #7007

* Use isFetching

* Update useAccounts to return react query states (e.g. isPending, isFetching, etc.)

* Add release notes for PR #7009

* Delete upcoming-release-notes/7007.md

* Change category from Enhancements to Maintenance

Refactor `useAccounts` to improve data handling.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-18 05:32:42 +00:00
Joel Jeremy Marquez
da0154a41b Fix redirect to accounts page when no accounts exists (#7007)
* Fix redirect to accounts page when no accounts exists

* Add release notes for PR #7007

* Use isFetching

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-17 22:33:49 +00:00
Joel Jeremy Marquez
180a38890c Improve category server app and react query mutation error handling (#6958)
* Improve category server app and react query mutation error handling

* Add release notes for PR #6958

* Fix test

* Fix throwing async test

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Do not swallow exceptions when batching messages - propagate instead

* Update error type to make 'cause' optional

Make 'cause' property optional in error type.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-17 20:30:57 +00:00
Roy
d3f2f1f7ae Add reports to command bar (#7002)
* Add reports to command bar

* Add release notes
2026-02-17 20:03:44 +00:00
Karim Kodera
c7efb61b84 Add theming to charts and hence allowing custom themes on charts (#6909)
* Add theming to charts and hence allowing custom themes on charts

* Removing additional color scales for charts.

* Fixed return fail over value.
2026-02-17 20:00:21 +00:00
Pieter Ouwerkerk
d605d59d01 Add Tag API docs (#6979)
* Add Tag object type definition for API docs

* Add Tags nav entry to API reference sidebar

* Add Tags methods and examples to API reference
2026-02-17 15:54:25 +00:00
HadiAyache
f7227f4e62 Fix operator precedence grouping for */ and +/- (#6993)
* Fix operator precedence grouping for */ and +/-

* Add release note for #6993

* Fix exponent associativity and add regression test

---------

Co-authored-by: Hadi Ayache <hadiayache@Hadis-Mac-mini.local>
2026-02-17 09:23:15 +00:00
Joel Jeremy Marquez
253530e239 Retrofit dashboard hooks to use react-query (#6957)
* Retrofit useReports to use react-query under the hood

* Add release notes for PR #6951

* Update 6951.md

* Report mutations

* Fix react query cache not being cleared when switching budgets (#6953)

* Fix react query cache not being cleared when switching budgets

* React does not want to export function from src/index

* Release note

* Use react-query is dashboard queries and mutations

* Add release notes for PR #6957

* [autofix.ci] apply automated fixes

* Fix typecheck errors

* Coderabbit feedback

* Make error cause optional

* Rename useDashboardWidgetCopyMenu and update useDashboardWidget to accept object to prevent need to default id to empty string

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-16 22:26:32 +00:00
Julian Dominguez-Schatz
e8f6ceeb98 Address some more low-hanging fruit for ts-strict-ignore (#6992)
* Address some more low-hanging fruit for ts-strict-ignore

* Add release notes

* Fix small issues

* Rabbit
2026-02-16 15:30:27 +00:00
Matiss Janis Aboltins
c031d9aa4f fix(ios): restore status bar color on iOS 26.2 (Safari 26) (#6983)
* fix(ios): restore status bar color on iOS 26.2 (Safari 26)

Safari 26 no longer uses the theme-color meta tag for the status bar
and instead derives the tint from the body background. Set body
background-color in HTML and sync it with the app theme so the status
bar shows purple (or the active theme) instead of white.

Fixes #6946

Co-authored-by: Cursor <cursoragent@cursor.com>

* [autofix.ci] apply automated fixes

* refactor(theme): update useMetaThemeColor to accept theme values directly

Modified the useMetaThemeColor hook to accept theme color values instead of predefined keys. Updated FinancesApp and ManagementApp components to utilize the new implementation, ensuring proper theme color handling based on screen width.

* [autofix.ci] apply automated fixes

* refactor(theme): remove unused body background color in index.html and add tests for useMetaThemeColor hook

Deleted the commented-out body background color in index.html to clean up the code. Added comprehensive tests for the useMetaThemeColor hook to ensure proper handling of theme colors, including support for CSS variables and reactivity to theme changes.

* refactor(theme): improve getPropertyValueFromVarString function in useMetaThemeColor hook

* [autofix.ci] apply automated fixes

* Add release notes for PR #6983

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-15 19:19:50 +00:00
Matiss Janis Aboltins
d4e25f4047 [AI] Introduce type-aware oxlint and disable no-duplicate-type-constituents rule (#6984)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 19:05:57 +00:00
Matiss Janis Aboltins
a7f96a59fa [AI] Update CodeRabbit config for suspect AI generated labels (#6985)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 18:53:08 +00:00
youngcw
8af64ddd5e 📖 Move pluggy.ai doc out of experimental (#6975)
* move pluggy out of experimental

* remove note

* Add release notes for PR #6975

* fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 22:04:18 +00:00
Julian Dominguez-Schatz
26dbb219aa Implement missing logic for limit template type (#6690)
* core: support limit refill templates

* notes: refill templates

* core: apply refill limits during runs

* core: prioritize refill limits

* Patch

* Update release note

* Fix typecheck

* rework.  Tests and template notes still need reworked

* fix parser syntax

* Fix type issue

* Fix after rebase, support merging limit+refill

* PR feedback

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-14 21:52:48 +00:00
Alvin Zhao
c6656a2815 Include category group in transaction export (#6960)
* include category group in transaction export

* Apply suggestion from @yzAlvin

Co-authored-by: Alvin Zhao <yzalvin@duck.com>

---------

Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-14 16:31:27 +00:00
Julian Dominguez-Schatz
6358345286 Fix some low-hanging-fruit @ts-strict-ignore (#6969)
* Fix low-hanging-fruit `@ts-strict-ignore`

* Add release notes

* A few more
2026-02-14 15:58:25 +00:00
Roy
5943ae3df5 Add filter option for category groups (#6834)
* Add filter by category groups

* Add tests

* Add release notes

* [autofix.ci] apply automated fixes

* Fix typecheck findings

* Fix modal

* Address nitpick comment (filterBy)

* Fix e2e tests

* Make group a subfield of category

* Fix test by typing in autocomplete

* Replace testId with a11y lookups

* Apply new type import style rules

* Apply feedback

* Improve typing on array reduce, remove manual type coercion

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-14 15:03:11 +00:00
Matiss Janis Aboltins
7e8a118411 [AI] lint: convert oxlint warnings to errors (#6970)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 13:48:50 +00:00
Joel Jeremy Marquez
465608c76b Move redux state to react-query - account states (#6140)
* Fix typecheck errors

* Move redux state to react-query - account states

* TestProviders

* Add release notes for PR #6140

* Fix lint error

* Fix TestProviders

* Coderabbit feedback

* Cleanup

* [autofix.ci] apply automated fixes

* Fix TestProviders

* Fix onbudget and offbudget displaying closed accounts

* [skip ci] Change category to Maintenance and update migration text

* Replace logger calls in desktop-client to console

* Fix lint errors

* Clear react query on closing of budget file similar to redux resetApp action

* [autofix.ci] apply automated fixes

* Remove sendThrow

* Code review feedback

* [autofix.ci] apply automated fixes

* Fix import

* Fix import

* Coderabbit feedback

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-14 09:32:37 +00:00
Matiss Janis Aboltins
5062fa78a8 Agent instructions for commit messages and PR titles (#6964)
* [AI] Add mandatory [AI] prefix requirement for commit messages and PR titles

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

* [autofix.ci] apply automated fixes

* Add release notes for PR #6964

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 07:54:48 +00:00
Matiss Janis Aboltins
e178396e48 Docs: add Claude Code Pro subscription benefit for core contributors (#6963)
* Add Claude Code Pro subscription benefit for core contributors

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add release notes for PR #6963

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 03:00:36 +00:00
Matiss Janis Aboltins
09d85bbdc5 docs: add Architecture Decision Records page with bank sync credential ADR (#6965)
* docs: add Architecture Decision Records page for controversial decisions

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add release notes for PR #6965

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 01:14:15 +00:00
Joel Jeremy Marquez
a0378c10a9 Move redux state to react-query - tags states (#6941)
* Move redux state to react-query - tags states

* Add release notes for PR #6941

* Cleanup sendThrow

* Cleanup

* Update import

* Fix import

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 19:33:21 +00:00
Adam Stück
ca944baee5 feat: show/hide reconciled transactions on mobile (#6896)
* feat: show/hide reconciled transactions on mobile

Resolves #2969

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-13 17:25:55 +00:00
Joel Jeremy Marquez
cb5237359c Rename loot-core/platform/client/fetch to connection to match server-side package (#6943)
* Rename loot-core/platform/client/fetch package to connection to match the server side package name. Also to avoid confusion with the native fetch package.

* Update connection/init method to not receive any parameter to so browser and default implementation have the same signature

* Add release notes for PR #6943

* Fix names

* Fix imports

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 16:53:51 +00:00
Alexis Vielma
68bb33e5e6 feat: add back button to reports pages (#6702)
* feat: add back button to reports pages

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6702

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 02:35:08 +00:00
J B
cf5fe67e7b API Account Object (#6915)
* api change

* docs

* lint

* release notes

* spelling

* [autofix.ci] apply automated fixes

* spelling

* whoopsie, thanks rabbit

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-13 00:18:49 +00:00
Joel Jeremy Marquez
f8b4e87a67 Update send function to propagate any errors and fix catchErrors to return the error in result when an unknown command/method is sent to the browser server (#6942)
* Fix send not returning error when catchErrors option is enabled and an unknown method error is encountered

* Add release notes for PR #6942

* Fix send to properly propagate errors from the server

* Update release note

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-12 23:47:21 +00:00
Joel Jeremy Marquez
8ae90a7ad1 Retrofit useReports to use react-query under the hood (#6951)
* Retrofit useReports to use react-query under the hood

* Add release notes for PR #6951

* Update 6951.md

* Report mutations

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-12 23:47:05 +00:00
Matiss Janis Aboltins
6f7af102a6 Upgrade oxfmt and oxlint, update .oxfmtrc.json import patterns (#6955)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 23:08:35 +00:00
Piyush Katkar
96a3128305 Fix mobile budget amount inputs when hide decimal places is enabled (#6945) 2026-02-12 22:59:23 +00:00
Joel Jeremy Marquez
003efecc23 Fix react query cache not being cleared when switching budgets (#6953)
* Fix react query cache not being cleared when switching budgets

* React does not want to export function from src/index

* Release note
2026-02-12 20:12:51 +00:00
Michael Clark
155e4df219 🎨 Add remaining component stories to storybook (#6940)
* final storybook stories

* release notes

* spelling mistake
2026-02-12 08:46:24 +00:00
Joel Jeremy Marquez
67d6592333 Add refetchOnSync option to useTransactions to refetch when a server sync event is emitted (#6936)
* Migrate setupTests.js to TypeScript with proper types (#6871)

* Initial plan

* Rename setupTests.js to setupTests.ts and add proper types

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Extract Size type to avoid duplication

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Add release note for setupTests TypeScript migration

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Rename release note file to match PR number 6871

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Delete setupTests PR release note

* Add refetchOnSync to useTransactions to refetch when a server sync event is emitted

* Add release note for useTransactions refetchOnSync feature (#6937)

* Initial plan

* Add release note for PR 6936

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Coderabbit feedback

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-12 00:16:37 +00:00
An, Tran Cong Viet
7fa9fa900b feat(currency): Add Vietnamese Dong (VND) currency (#6902)
* feat(currency): add support for vietnamese dong currency

* release: add upcoming release note

* fix(currency): change the number format for vietnamese currency

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-11 20:04:06 +00:00
jintakhan
9798c26462 feat(currency): Add South Korean Won (#6846)
* Add South Korean Won

* Update currencies.ts

* Add release notes
2026-02-11 17:00:41 +00:00
Joel Jeremy Marquez
37a7d0eccd Retrofit useTransactions to use react-query under the hood (#6757)
* Retrofit useTransactions to use react-query under the hood

* Add release notes for PR #6757

* Update packages/desktop-client/src/transactions/queries.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Disable when there is no query parameter

* Fix typecheck errors

* Remove space

* Update tests

* Coderabbit: Add pageSize to query key

* Use isPending instead of isFetching

* Unexport mockStore

* Revert variables

* Change category from Enhancements to Maintenance

Refactor the useTransactions hook to improve data fetching with react-query.

* Fix lint errors

* Fix lint errors

* Migrate setupTests.js to TypeScript with proper types (#6871)

* Initial plan

* Rename setupTests.js to setupTests.ts and add proper types

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Extract Size type to avoid duplication

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Add release note for setupTests TypeScript migration

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Rename release note file to match PR number 6871

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Update transactionQueries

* Delete setupTests PR release note

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-11 16:58:07 +00:00
Joel Jeremy Marquez
e3e4b13d2b Move redux state to react-query - category states [Part 2 - expose react-query states e.g. isPending, isSuccess, etc] (#6882)
* Move redux state to react-query - category states [Part 2 - expose react-query states]

* Add release notes for PR #6882

* Add default values to useCategories destructuring to prevent undefined crashes (#6884)

* Initial plan

* Add missing defaults to all useCategories usages

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

* Fix lint errors

* Fix rebase mistake

* Change category from Enhancements to Maintenance

Migrate state management for category from Redux to React Query and update related hooks and components.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-11 16:44:32 +00:00
Michael Clark
138ea810d6 🎨 Reorganising Storybook and more component stories (#6924)
* reorg of storybook docs and add some new components

* releaes notes

* Update meta tags for Actual Budget Design System

* Increase sidebar item font size from 14px to 16px
2026-02-11 09:01:11 +00:00
youngcw
07ff514c12 [Goals] fix tracking budget balance carryover for templates (#6922)
* fix tracking budget balance carryover for templates

* Add release notes for PR #6922

* fix note

* fix tests

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-11 01:52:49 +00:00
Gabriel J.
2ca352aaa7 fix(schedules): prevent past missed schedule dates from being marked as upcoming (#6925)
Fixes #6872
2026-02-11 01:04:42 +00:00
Gabriel J.
078da08ad5 Fix/6885 crash when rule has empty date field (#6905)
* Fix crash when rule date field loses focus while empty

Fixes #6885

* Remove ts-strict-ignore and fix types in DateSelect

* Generate release note 6905
2026-02-10 23:05:37 +00:00
samekh248
edcf893a27 Add butterfly custom theme (#6900)
* Added butterfly custom theme

* Added release notes
2026-02-10 23:02:52 +00:00
Juulz
38a72656df [FIX] Update DesktopLinkedNotes so links stay inline (#6858)
* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes so links stay inline

* Update TransactionsTable.tsx

make sure flexDirection is row.

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update NotesTagFormatter.tsx trial

* Update DesktopLinkedNotes.tsx

* Update NotesTagFormatter.tsx

* Update NotesTagFormatter.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

Add role for accessibility

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

Revert to original

* Update DesktopLinkedNotes.tsx

Try to style the bare button to make it work.

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

Reverting to current master

* Update DesktopLinkedNotes.tsx

Add nowrap styling.

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* revert TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update TransactionsTable.tsx

* Update TransactionsTable.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Revert DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

* Update DesktopLinkedNotes.tsx

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-02-10 20:33:39 +00:00
Pieter Ouwerkerk
24f698910a Add Tag API (#6746)
* Add Tag API

* Add Tag API tests

* Add Release Note for #6746

* Make release note more user-facing

* Remove unnecessary type coercion in tagModel.fromExternal

Since APITagEntity picks all properties from TagEntity, the types are
structurally identical and TypeScript can verify compatibility without
manual coercion.

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

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-02-10 17:22:55 +00:00
Jonathon Jongsma
cdaf06abee Remove duplication of CrossoverData type (#6928)
* Remove duplication of CrossoverData type

Move the CrossoverData type definition to crossover-spreadsheet.tsx and
import it from the Crossover and CrossoverCard files instead of having
duplicate definitions in each file.

Signed-off-by: Jonathon Jongsma <jonathon@quotidian.org>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

---------

Signed-off-by: Jonathon Jongsma <jonathon@quotidian.org>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-10 16:06:30 +00:00
Matiss Janis Aboltins
c8aa0cf1d3 Refactor: extract tooltip components and clean up lint suppressions (#6721)
* Refactor: extract tooltip components and clean up lint suppressions

Extract CustomTooltip components from CrossoverGraph and NetWorthGraph
to module level to fix unstable nested components lint warnings. Also
consolidate theme file lint rule into oxlintrc.json and add proper
typing to styles object.

* Add release notes for maintenance updates addressing lint violations

* Remove style prop from CustomTooltip to prevent container layout styles from affecting tooltip

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

* Refactor NetWorthGraph component by extracting TrendTooltip and StackedTooltip into separate functions for improved readability and maintainability. Update tooltip props to include necessary parameters for rendering. Clean up unused code and enhance tooltip styling.

* Refactor NetWorthGraph component to streamline tooltip handling

- Removed unnecessary prop passing for translation function in TrendTooltip.
- Adjusted import statements for better clarity and consistency.
- Cleaned up code to enhance readability and maintainability.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-02-10 15:12:22 +00:00
Matiss Janis Aboltins
84cebed20b Enforce consistent TypeScript type import style (#6805) 2026-02-10 13:33:20 +00:00
Matiss Janis Aboltins
0c3b54ee7d Points: pay for contributions (#6481)
* Add point totals display to all statistics sections in count-points script

* Update funding contributors documentation and add release notes for contributor point counting script

* Refactor contributor points calculation and enhance PR category determination

- Updated point values for PR contributions: reduced points for Features, Enhancements, Bugfix, and Maintenance, and added a new category for Unknown contributions.
- Introduced a new function to retrieve the last commit SHA before a specified date to improve accuracy in reading release notes.
- Modified the getPRCategoryAndPoints function to accept a monthEnd parameter for better context in point assignment.

* Update contributor points values in count-points script to reflect new scoring system

* Add new blog post on funding contributors' next steps

This post outlines plans to expand the contributor compensation system, including broader rewards for project involvement, targeted donations, and a points-based system for feature work. It emphasizes transparency and community feedback in shaping future funding strategies.

* Increase Bugfix points from 2 to 3

* Change points awarded for Features to 2
2026-02-10 12:42:52 +00:00
Michael Clark
eb9f9b3a73 :electron: Flathub PR to be draft on release (#6910)
* make flathub pr draft so that we dont attract attention

* release notes

* grammar
2026-02-09 19:10:37 +00:00
Diego Palacios
5c31aa03ba Fix feedback link for budget analysis report experimental flag (#6914) 2026-02-09 18:28:43 +00:00
xaviuzz
266e7f9cac Fix Ctrl+Enter losing amount value when adding transaction (#6911)
* Fix Ctrl+Enter losing amount value when adding transaction

Fixes #6901

When using Ctrl+Enter to add a transaction immediately after typing
in the amount field, the value wasn't being committed before the
transaction was saved, resulting in a zero amount.

The fix wraps the add-and-close logic in an afterSave() callback
to ensure field values are committed before adding the transaction.

Added regression tests for both debit and credit fields to verify
the fix works correctly.

* Add release notes for PR #6911

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 21:39:25 +00:00
Michael Clark
e951e21fe1 🎨 Storybook docs for block, card, colorpicker, formerror (#6874)
* storybook docs for block, card, colorpicker, formerror

* release notes
2026-02-07 09:40:30 +00:00
Stephen Brown II
1a26253457 Update the Create Linked Account workflow to prompt for Starting Date and Balance (#6629)
* feat: Add optional starting date and balance for bank sync accounts

Adds the ability to specify a custom starting date and balance when
linking new bank sync accounts in the Select Linked Accounts modal.

Addresses: https://discord.com/channels/937901803608096828/1402270361625563186

Changes:
- Frontend: Added inline date and amount input fields in the account
  linking table for new accounts
- Redux: Extended link account actions to accept startingDate and
  startingBalance parameters
- Backend: Updated account linking handlers to pass custom values to
  sync logic
- Sync: Modified syncAccount and processBankSyncDownload to use custom
  starting date/balance for initial sync transactions

Features:
- Only displays starting options when creating new accounts (not upgrades)
- AmountInput with smart sign detection based on account balance
  (negative for credit cards/loans)
- Defaults to 90 days ago for date and 0 for balance
- Mobile-responsive with separate AccountCard layout
- Works across all sync providers: GoCardless, SimpleFIN, Pluggy.ai

The custom starting balance is used directly for the starting balance
transaction, and the custom starting date determines both the sync
start date and the transaction date for the starting balance entry.

* refactor: Extract shared types and components for starting balance inputs

- Create CustomStartingSettings type to replace repeated inline type definitions
- Extract StartingOptionsInput component to consolidate duplicate UI between mobile/desktop views
- Create LinkAccountBasePayload type shared across GoCardless, SimpleFIN, and PluggyAI link functions
- Apply same base type pattern to server-side link account handlers

This simplifies the code introduced for custom starting date/balance when linking bank accounts.

[autofix.ci] apply automated fixes

* allow explicit zero values

* refactor: add type guard for BankSyncError to remove oxlint-disable

- Create isBankSyncError() type guard function with proper type narrowing
- Remove oxlint-disable-next-line comment that suppressed the no-explicit-any rule
- Add JSDoc comments for both isBankSyncError and handleSyncError functions
- Remove redundant type assertion now that type guard narrows correctly

* refactor: address code review nitpicks for SelectLinkedAccountsModal

- Use locale-aware date formatting instead of toISOString()
- Extract isNewAccountOption helper to reduce duplication
- Align AccountCardProps type definition pattern with TableRowProps

* Add placeholder date/balance for already linked accounts

* [autofix.ci] apply automated fixes

* Use StartingBalanceInfo only, and add mobile view

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-06 21:44:30 +00:00
distantvapor
e72f18c5db Add new theme 'You Need A Dark Mode' to catalog (#6891)
* Add new theme 'You Need A Dark Mode' to catalog

* Add 'You Need A Dark Mode' theme to catalog
2026-02-06 20:18:12 +00:00
Christian Speich
5deb2cf790 Add bank sync option to update dates. (#6850)
Signed-off-by: Christian Speich <christian@spei.ch>
2026-02-06 20:16:32 +00:00
Joel Jeremy Marquez
111e01449d Add build-electron to tsconfig excludes (#6883)
* Add build-electron to tsconfig excludes

* Add release notes for PR #6883

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-06 19:20:03 +00:00
Joel Jeremy Marquez
c0bd920c26 Fix react-hooks/exhaustive-deps in DateSelect (#6864)
* Fix react-hooks/exhaustive-deps in DateSelect

* Add release notes for PR #6864

* Fix remaining suppressions

* Change category to Maintenance and fix linting issues

Updated category from Enhancements to Maintenance and fixed linting issues related to react-hooks/exhaustive-deps in DateSelect.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-06 18:03:31 +00:00
Stephen Brown II
b695af66c0 Avoid duplicate category import errors in YNAB5 importer (#6878)
* avoid duplicate category import errors

Add normalizeError helper function

* Add release notes file
2026-02-06 16:15:56 +00:00
Noah
650521f05b Add Catppuccin Themes to custom theme catalog (#6857)
* Add Catppuccin Themes to theme catalog

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-06 16:07:45 +00:00
tabedzki
738a8cda7c Fix date range calculation in BudgetAnalysisCard (#6875)
* fix: corrected date range calculation in BudgetAnalysisCard using calculateTimeRange

* add release note

* fix: ensure correct date formatting in BudgetAnalysisCard for start and end dates

* fix: rename release note file
2026-02-06 15:58:31 +00:00
Stephen Brown II
deadd9aefc Apply tag colors to YNAB flag tags (#6866)
* Apply tag colors to match YNAB flags

* Update tag colors to match YNAB, add description on import

* Tighten types

* Use custom colors

* Use Actual palette equivalents for tag colors

* Nitpick fixes

* Fix nitpick 'fix'

* Handle YNAB flag tag conflicts

* Handle YNAB flag tag conflicts without creating separate color tags

* Simplify

* Reorganize
2026-02-05 22:22:01 +00:00
Joel Jeremy Marquez
16ec636358 Fix react-hooks/exhaustive-deps in ImportTransactionsModal (#6868)
* Fix react-hooks/exhaustive-deps in ImportTransactionsModal

* [autofix.ci] apply automated fixes

* Add release notes for PR #6868

* Update category to Maintenance and fix warnings

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-05 17:09:04 +00:00
Joel Jeremy Marquez
b271de32b6 Fix react-hooks/exhaustive-deps in CustomReport (#6867)
* Fix react-hooks/exhaustive-deps in CustomReport

* Add release notes for PR #6867

* Fix typecheck errors

* [autofix.ci] apply automated fixes

* Change category to Maintenance and update description

* [autofix.ci] apply automated fixes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-05 17:06:54 +00:00
Joel Jeremy Marquez
2fb98156f6 Fix react/exhaustive-deps in PayeeTable (#6863)
* Fix react/exhaustive-deps in Modals

* Fix react/exhaustive-deps in PayeeTable

* Add release notes for PR #6863

* Change category and fix dependency management in PayeeTable

Updated category from 'Enhancements' to 'Maintenance' and fixed dependency management in PayeeTable.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-05 17:05:23 +00:00
Joel Jeremy Marquez
2f86bafd1f Fix react/exhaustive-deps in Modals (#6862)
* Fix react/exhaustive-deps in Modals

* Add release notes for PR #6862

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-05 17:05:09 +00:00
Joel Jeremy Marquez
7f6f4d5def Move redux state to react-query - category states (#5977)
* Move redux state to react query - category states

* Fix typecheck errors

* Fix typecheck errors

* Fix typecheck errors

* Remove t argument

* [autofix.ci] apply automated fixes

* Coderabbot suggestion

* Code review feedback

* Fix type

* Coderabbit

* Delete useCategoryActions

* Fix lint

* Use categories from react query cache

* Fix typecheck error

* Update to use useDeleteCategoryGroupMutation

* Coderabbit feedback

* Break up useCategoryActions

* [autofix.ci] apply automated fixes

* Fix typecheck errors

* Fix typecheck error

* Fix typecheck error

* await nested mutations

* Await deleteCategory

* Rename to sendThrow

* Fix lint errors

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-05 00:42:49 +00:00
Tyler Durr
c57260a504 Remove comma from all instances of month-year strings (#6748)
* Remove comma from all instances of month-year strings, e.g., "January 2026" rather than "January, 2026"

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6748

* All updated calls now include `locale` as an argument

* Update dependencies in `useEffect` to include `locale`

* Reorganized new import

* Remove double colon

* Consistent trailing commas

* Include locale in other dependency arrays

* Reorder imports

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6748

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-05 00:27:00 +00:00
Saahil Jaffer
1452ecfeb7 changes token expiry handling to be automatic sign out (#6798)
* changes token expiry handling to be automatic sign out

* add release notes

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-05 00:01:53 +00:00
Matthias Benaets
264cc9fb0e fix: isConcise state handling for CashFlow report (#6765) 2026-02-04 23:50:22 +00:00
Juulz
554d0b6150 Use consistent color variables on Budget Page. (#6820)
* Change color of budget table scrollbar.

* Update budget sidebar to use budget colors.

* Update fontWeight for 'Category' to match rest of table

* Update to use budget background

* Update ExpenseGroup to use budget color

* Update IncomeGroup to use budget color

* Update SidebarCategory colors

Change drag/drop to buttonPrimaryBackground so it will always be visible in any custom theme.

Background to budget color.

* Update SidebarGroup background to budget color

* Update EnvelopeBudgetComponents

Add 'budgetNumberNeutral' to cheveronDown in budgeted field instead of using default bare button text incases where bare button is set to normal button text and normal button is inverted. remove mobile color variable for shadow

* Update BudgetTotals.tsx

Use tableHeaderText for header.

* Update BudgetSummary.tsx

Use budget colors

* Update MonthPicker.tsx

change selected months at top from tableBorderHover color to buttonPrimaryBackground.

* [autofix.ci] apply automated fixes

* Update EnvelopeBudgetComponents.tsx

* Update BudgetTotals.tsx

revert

* Update SidebarCategory.tsx

* Update TrackingBudgetComponents to use budget colors

* [autofix.ci] apply automated fixes

* Update BudgetSummary to use budget colors

* Update BudgetTotal.tsx

* Update ExpenseProgress.tsx

* Update IncomeProgress.tsx

* Update Saved.tsx

* Use consistent color variables on budget pages.

* Update IncomeProgress.tsx

* Update ExpenseProgress.tsx

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6820

* Update EnvelopeBudgetComponents

Budget total header to follow current/other month style.

* Update EnvelopeBudgetComponents.tsx

* [autofix.ci] apply automated fixes

* Update EnvelopeBudgetComponents.tsx

* Update EnvelopeBudgetComponents.tsx

* [autofix.ci] apply automated fixes

* Update EnvelopeBudgetComponents.tsx

* Revert EnvelopeBudgetComponents.tsx

* [autofix.ci] apply automated fixes

* Update EnvelopeBudgetComponents.tsx

* Update EnvelopeBudgetComponents.tsx

* Update EnvelopeBudgetComponents.tsx

* Revert again :) EnvelopeBudgetComponents.tsx

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-04 23:26:18 +00:00
Joel Jeremy Marquez
11d0b9d824 Update findSortUp and findSortDown parameter to be more generic (#6861)
* findSortUp and findSortDown is also used for CategoryEntity. Updating type to be more generic.

* Generics

* Add release notes for PR #6861

* Adjust category to Maintenance based on review feedback (#6865)

* Initial plan

* Change release notes category to Maintenance

Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: joel-jeremy <20313680+joel-jeremy@users.noreply.github.com>
2026-02-04 22:50:10 +00:00
Stephen Brown II
323c2beb0a Include scheduled transactions in nYNAB imports (#6844)
* Include scheduled transactions in nYNAB imports

* Remove logs and restore schedule name from transaction memo

* Simplify rule actions

* Create schedules with unique names

* Set the note rather than append

* Update ynab5 demo budget and e2e test
2026-02-04 21:40:40 +00:00
Matt Fiddaman
dc5ce6ae96 switch to node alpine docker image (#6840)
* switch to node alpine image

* note
2026-02-04 20:08:17 +00:00
Adam
d8afc6b2be fix(i18n): respect browser preferred languages when supported (#6812)
* fix(i18n): respect browser preferred languages when supported

Instead of relying on the first browser language, the application now
iterates through navigator.languages and selects the first supported
locale, with a fallback to English.

* chore: add the release notes related file

* fix(i18n): check region locales support before falling back to base locale

* fix(i18n): make the unit test aligned with the region locales checking
2026-02-04 20:04:28 +00:00
Joshua Granick
7732fac8b6 Fix sync server migrations (#6346)
* Fix sync-server migrations to use ESM loader

* Add release notes

* Apply CodeRabbit suggestions

* [autofix.ci] apply automated fixes

* Add file extension filter to sync-server migrations import

* [autofix.ci] apply automated fixes

* Ensure migrations occur synchronously

* [autofix.ci] apply automated fixes

* Minor cleanup

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-04 20:00:42 +00:00
Matthias Benaets
6da6f505e6 fix: report widget tooltip z-index (#6849) 2026-02-04 19:38:04 +00:00
Stephen Brown II
4674916d3e Avoid negative zero in budget summary amounts (#6843) 2026-02-04 19:36:55 +00:00
Nam
5388a115e9 Fix translation issue #6828 (#6845)
* Fix translation issue #6828

* Add release note

* [autofix.ci] apply automated fixes

* update release number

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-04 19:36:37 +00:00
Matiss Janis Aboltins
06d31ce035 Improve bug report template with better structure and requirements (#6784)
* Improve bug report template with better structure and requirements

* Fix: Remove empty value fields from textarea inputs in bug report template

* Add release notes for PR #6784

* Update 6784.md

* Update bug report template to request a screenshot of the import screen along with a redacted file version for better issue resolution.

* Update bug report template for import issues

Clarified instructions for reporting import issues.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-04 19:29:21 +00:00
Matiss Janis Aboltins
9585a92cda Add oxlint rule against direct theme imports; fix Login OpenID button styles (#6796)
Co-authored-by: Michael Clark <5285928+MikesGlitch@users.noreply.github.com>
2026-02-04 19:29:20 +00:00
Matiss Janis Aboltins
a0a490c14c Typescript: make arithmetic.ts strict (#6801)
* Make arithmetic.ts strict TypeScript compliant

- Add type definitions for ParserState, Operator, OperatorNode, and AstNode
- Add explicit type annotations to all function parameters
- Fix null/undefined handling in parsePrimary function
- Remove type assertion in makeOperatorParser by using explicit Operator type
- Handle null return from currencyToAmount function
- All functions now have proper return type annotations

* Add test for ignoring leftover characters in evalArithmetic function
2026-02-04 19:29:19 +00:00
Matiss Janis Aboltins
0d6742664b 🔖 (26.2.0) (#6837)
* 🔖 (26.2.0)

* Remove used release notes

* docs pages

* s/flathub/Flathub

* s/coderabbit/CodeRabbit

* allowlist

---------

Co-authored-by: matt-fidd <81489167+matt-fidd@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-02-02 12:12:46 +00:00
Alexis Vielma
37e807f161 fix: resolve issue with banksync table border (#6825) 2026-01-31 18:23:11 +00:00
Matt Fiddaman
c9abc62b3a fix reconciliation when experimental currency support is enabled (#6824)
* fix reconcilation when using currency symbols

* note

* fix tests
2026-01-31 00:34:38 +00:00
Matt Fiddaman
234f257260 fix spacing and borders on bank sync page (#6823)
* fix bank sync tables

* note
2026-01-31 00:26:02 +00:00
Spydi
49d583e4ad docs: add ANZ Plus Bank to community repos (#6785)
* docs: add ANZ Plus Bank to community repos

Added ANZ Plus Bank to the Community Repos page.
Link(s): https://github.com/spydisec/PDFtoOFX/
Short summary: Convert ANZ Plus bank statement PDFs to OFX format for seamless import into Actual Budget and other personal finance applications.
Placement: https://actualbudget.org/docs/community-repos/

* Update packages/docs/docs/community-repos.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix typo in ANZ Plus bank converter description

* chore: update docs spelling allowlist

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-30 23:11:17 +00:00
Stephen Brown II
460cb7b6cd Add workflow to remove 'suspect ai generated' label when 'AI Generated' is added (#6810) 2026-01-30 23:00:15 +00:00
Matt Fiddaman
1eb68c8e19 fix crossover report resizing bug (#6821)
* fix crossover report

* note
2026-01-30 21:27:20 +00:00
Michael Clark
2ec592c2d6 Remove gh pages deploy & reinstate netlify autodeploy (#6822)
* remove gh pages deploys now that netlify have removed prod deploys

* release notes

* thanks wabbit
2026-01-30 19:36:13 +00:00
Copilot
19edbeb5c2 Surface clock drift errors with actionable messages (#6789)
* Initial plan

* Add clock-drift error handling to sync flow

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add release notes for PR #6789

* Refactor error handling in sync flow

- Extracted error handling logic into a separate function for better readability and maintainability.
- Updated the receiveMessages and _sendMessages functions to utilize the new errorHandler.
- Removed redundant error handling code from the _sendMessages function.
- Enhanced error handling for SyncError and ClockDriftError to emit specific events.

* Enhance error handling in receiveMessages function

- Added try-catch block to handle Timestamp.ClockDriftError specifically.
- Emitted a sync error event for clock-drift errors to improve synchronization feedback.
- Maintained existing functionality while improving error reporting.

* Update authorship in release notes and refine clock-drift error messaging for clarity

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-01-29 14:24:25 +00:00
Matiss Janis Aboltins
d8b7e45aaa Fix date picker keyboard navigation regression (#6808) 2026-01-28 21:44:33 +00:00
Michael Clark
f71249f510 🎨 Storybook docs (#6770)
* start to a storybook

* release notes

* commit the release notes

* add test:web back

* adding some scripts to the main package for storybook

* styled it up a bit

* remove unneeded icon

* lint

* remove needless comment

* moving clarifying comment

* fast failing

* feedback

* removing unneeded config
2026-01-28 18:38:02 +00:00
Matiss Janis Aboltins
ebf8e985ad Fix missing bottom navigation bar on mobile reports page (#6803)
* Fix missing bottom navigation bar on mobile reports page

Update route pattern from /reports to /reports/* to match nested routes

* Release notes

* Add release notes for version 6803

* Refactor reports route to include dashboardId parameter
2026-01-28 17:17:22 +00:00
Copilot
690e2d0871 Fix: Accept keyboard apostrophe (U+0027) in arithmetic parser for apostrophe-dot format (#6795)
* Initial plan

* Add test case for apostrophe-dot format bug

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Fix: Support keyboard apostrophe (U+0027) in arithmetic parser for apostrophe-dot format

The arithmetic parser was only recognizing the typographic apostrophe (U+2019)
that Intl.NumberFormat outputs, but not the regular apostrophe (U+0027) that
users type on their keyboards. This caused amounts like "12'345.67" to be
truncated to "12" when users typed them.

The fix adds U+0027 to the regex character class in arithmetic.ts so both
apostrophe characters are accepted.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add explicit character code verification to apostrophe-dot test

Use escape sequences (\u0027 and \u2019) to ensure the test explicitly
uses the correct apostrophe characters, and add assertions to verify
the character codes are as expected.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Fix lint

* Add comprehensive tests for apostrophe-dot format in arithmetic.test.ts

Added three new test cases to verify both apostrophe types work correctly:
1. Tests keyboard apostrophe (U+0027) with explicit character code verification
2. Tests typographic apostrophe (U+2019) with explicit character code verification
3. Tests arithmetic operations with both apostrophe types

This addresses the feedback to add tests for the new logic in arithmetic.ts.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* [autofix.ci] apply automated fixes

* Add release notes for PR #6795

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-28 16:58:33 +00:00
Juulz
91faa2f7f1 [fix] Update BudgetAnalysisGraph to use a darker gray for balance to increase legibility (#6791)
* Update BudgetAnalysisGraph.tsx

Use reportsGray for Balance.

* Fix Budget Analysis Graph balance color variable

Use reportsGray instead of numberNeutral for balance points and line.
2026-01-28 15:18:02 +00:00
Matiss Janis Aboltins
4888d3faed Fix schedule transaction amount color on mobile (#6792)
* Fix schedule transaction amount color on mobile

Change schedule amount color from pageText to pageTextSubdued to match the label color, consistent with desktop version.

Fixes #6781

* Enhance Budget Analysis report UI and functionality

- Improve chart colors to align with Cash Flow report style
- Streamline report view by removing legend items
- Introduce a quick select option for 1 month in the header
- Implement dynamic interval switching based on date range

This update aims to provide a more cohesive and user-friendly experience in the Budget Analysis report.

* Update snapshot images for account page tests and fix color styling in transaction and schedule list items

- Updated snapshot images for account page tests to reflect recent UI changes.
- Adjusted color styling for transaction amounts to ensure consistency with the overall theme.
- Changed schedule amount color back to match the primary text color for better visibility.
2026-01-28 09:10:44 +00:00
Andrii Hrushetskyi
3091320719 Fix: Added Czech CZK and Hungarian HUF currencies (#6500)
* Fix: Added Czech CZK and Hungarian HUF currencies

* Fix: Added translations for prev commit

* Update upcoming-release-notes/6500.md

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

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-01-27 18:49:03 +00:00
Michael Clark
72304c6182 Fix nightly edge not scheduling (#6775)
* fix nightly edge not scheduling

* changing to 11PM UTC

* clarifying comment

* better diff
2026-01-25 10:44:26 +00:00
tabedzki
e4903ca6e3 Feat/add-budget-analysis-report (#6137)
* Add Budget Analysis report with full implementation

Co-authored-by: tabedzki <35670232+tabedzki@users.noreply.github.com>

* Add preset time ranges, intervals, bar chart, hide balance, and display controls

Co-authored-by: tabedzki <35670232+tabedzki@users.noreply.github.com>

* Fix duplicate function declaration syntax error in budget-analysis-spreadsheet.ts

Co-authored-by: tabedzki <35670232+tabedzki@users.noreply.github.com>

* Fix floating point precision error in daily/weekly intervals and replace interval button with dropdown

Co-authored-by: tabedzki <35670232+tabedzki@users.noreply.github.com>

* Make card always display monthly data and match report's chart type

Co-authored-by: tabedzki <35670232+tabedzki@users.noreply.github.com>

* fix: adjust widget placement and presentation

* fix: adjusted the dot presentation

* feat: added svg for line chart/barchart

* fix: added one month to the report

* added the upcoming release notes

* amended the upcoming release notes

* fix: removed unused variables

* formatted using prettier --write

* [autofix.ci] apply automated fixes

* feat: added new feature to the Reports Page in the test budget

* fix: amended the reports.test.ts file to expect Budget Analysis

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* revert: removed the inclusion of the Budget Analysis tool from the test file

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* fix: changed the display to always be monthly since budgets are monthly; removed the 1month view

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix: removed comment

* feat: added experimental feature flag for Budget Analysis tool

* feat: Switched option to use SVG Icons instead of words to shorten horizontal layout

* Removed interval possibilities and removed unnecessary compact variable as indicated by CodeRabbit

* Update packages/desktop-client/src/components/reports/reports/BudgetAnalysisCard.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Added basic documentation for Budget Analysis Report

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* fix: added budget-analysis doc to sidebar

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* feat(reports): improve Budget Analysis report UI and behavior

- Update chart colors to match Cash Flow report style
  (Budgeted: blue, Balance: pageTextLight)
- Remove legend items from report view for cleaner UI
- Add 1 month quick select option to header
- Pass isConcise prop to BudgetAnalysisGraph for proper date formatting
- Add dynamic daily/monthly interval switching based on date range

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* feat(reports): refactor BudgetAnalysisGraph and improve isConcise calculation according to Rabbit AI

* feat(reports): add translation support for Budget Analysis graph labels

* feat(reports): centralize translation for Budget Analysis graph labels

* feat: use the budget values directly from the budget spreadsheet/server

* feat: enhance budget analysis with overspending adjustments and detailed reporting

* style: format code for better readability in BudgetAnalysis and budget-analysis-spreadsheet components

* refactor: remove unused variables

* docs: added in the image to the docs

* fix: reimplement support for conditionsOp

* [autofix.ci] apply automated fixes

* style: simplify budget analysis labels for clarity

* fix: add the on copy function

* feat: tooltip improvements

* feat: enhance budget analysis to track overspending adjustments across months

* fix: removed the absolute value for spent

* feat: update the charts to look closer to the CashFlow report

* fix: correct financial formatting for totalSpent in BudgetAnalysis

* feat: add filterExclude prop to Header and BudgetAnalysis for improved filtering options

* feat: implement privacy mode for Y-axis values in BudgetAnalysisGraph

* feat: change default graph type to Bar in BudgetAnalysis

* feat: remove commented-out filter button code in Header component

* feat: remove commented-out code for filter exclusion in Header component

* fix: update the feedback link to the dedicated issue

* refactor: financial display components to use FinancialText for consistency in Budget Analysis reports

* fix: update the card to also start as bar graph

* docs: update Budget Analysis report to include category filtering information

* style: refactor imports and whitespace

* refactor: simplify inline content structure in BudgetAnalysis component

* [autofix.ci] apply automated fixes

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* fix: removed color descriptors from the chart

* fix: update color themes for Budget Analysis to use custom theme definitions

* [autofix.ci] apply automated fixes

* feat: update Budget Analysis merge md file

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6137

* fix: update budget analysis report image

* fix: white space adjustment in descriptor

* [autofix.ci] apply automated fixes

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-01-24 20:00:10 +00:00
Matt Fiddaman
d768cfa508 update package versions with release action when explicit version passed (#6767)
* update package versions when explicit version passed

* note
2026-01-24 15:30:17 +00:00
Matt Fiddaman
e0ed53c4af make colors on budget page more legibile (#6764)
* use old budget page colours

* running balance

* schedule indicator

* mobile to budget

* note

* template indicator due should match underfunded

* coderabbit

* mobile to budget neutral zero

* matiss feedback

* mobile account balance

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6764

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-24 15:26:02 +00:00
Matiss Janis Aboltins
c3e3a258e0 Mobile: improve performance for transaction list usage (#6755)
* Mobile: add usePullToRefreshOnScrollContainer hook, bypass PTR library, refactor transaction list

* Remove usePullToRefreshOnScrollContainer hook from desktop client

* Enhance PullToRefresh component with optional style prop and refactor TransactionList layout

* Refactor TransactionListItem component: remove unused imports and hooks, update props type to use ListBoxItemRenderProps
2026-01-24 08:39:46 +00:00
Matt Fiddaman
f55a42d817 fix report drag and drop reordering (#6763)
* fix report drag and drop

* note
2026-01-23 19:37:17 +00:00
Mats Nilsson
331aafda30 Create an option for stacked net worth graph (#6603)
This commit adds an option to the net worth graph that makes it into a
stacked line graph.
2026-01-23 16:29:30 +00:00
Matt Fiddaman
be95b9a3d5 fix mobile uncategorised banner (#6758)
* fix mobile uncategorised banner

* cleanup unnecessary nesting

* note
2026-01-23 14:25:59 +00:00
Matt Fiddaman
8eadd09bfc fix monthly spending number colouring (#6756)
* fix monthly spending summary colouring

* note

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6756

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-23 11:11:37 +00:00
Juulz
414aa95fd8 add "To budget" color variables (#6754)
* Add budget color variables to theme

* Add isPositive check for budget amount styling

* Add budget number constants for positive, zero, and negative

* Add budget status constants for positive, zero, and negative

* Add budget number constants for positive, zero, and negative

* Update midnight.ts

* Create 6754.md
2026-01-22 23:10:14 +00:00
Michael Clark
19ed2423d4 Fix edge nightly deploy (#6753)
* fix edge nightly deploy

* fix name

* rename file

* clarifying comment

* bit more clear

* adding release notes to satisfy pipeline

* spelling
2026-01-22 21:47:24 +00:00
Alexis Vielma
4986e433a5 feat: consistent table styling (#6697)
* feat: consistent table styling

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6697

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6697

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6697

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-22 20:43:39 +00:00
Michael Clark
9a3415adab 🚀 Deploy edge every night (#6751)
* deploy edge at midnight

* release ntoes
2026-01-22 19:39:03 +00:00
Jonathon Jongsma
bcfefde4ad Add contributions to crossover report (#6639)
Add the ability to specify expected monthly contributions to the
crossover report. This will allow users to estimate the growth of their
investments more accurately instead of treating all account growth as
exponential investment returns. When the checkbox is unclicked, the
automatically calculated historical return value will be used. When the
checkbox is clicked, it will display two input fields. One for expected
investment growth, and one for expected monthly contributions to your
retirement accounts.

Signed-off-by: Jonathon Jongsma <jonathon@quotidian.org>
2026-01-22 16:52:34 +00:00
dependabot[bot]
c4514b1fe6 Bump lodash from 4.17.21 to 4.17.23 (#6749)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 12:19:01 +00:00
Matt Fiddaman
7a5bfe7c20 use consistent colours on the budget page (#6747)
* fix colouring of non-numerical summary cards

* define new colours

* use new colours on budget page

* use colours on account pages

* note

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6747

* fix mobile colouring

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6747

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-22 11:58:15 +00:00
Juulz
323403b5f7 🎨 Shades of gray custom light theme (#6739)
* Add 'Shades of Gray (light)' theme to catalog

* Update color format in customThemeCatalog.json

* [autofix.ci] apply automated fixes

* Add release notes for light custom theme enhancement

🎨 Light custom theme featuring shades of gray and a few colors.

* Reorder color values in customThemeCatalog.json

* Change color value 

Updated a color value in the custom theme catalog.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-21 20:55:19 +00:00
Matt Fiddaman
a35fdf4d18 make colours on reports page consistent (#6672)
* add colour definitions

* chart theme definitions

* replace with new colour variables

* clean up chart-theme

* merge fixes

* note

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6672

* coderabbit

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-21 18:06:00 +00:00
Matiss Janis Aboltins
d0a72f10b6 Fix reports to correctly render categories with identical names (#6733)
* Fix reports to correctly render categories with identical names

Use category id instead of name as the data key for chart lookups to prevent
collisions when multiple categories share the same display name.

* Enhance CustomTooltip in LineGraph and StackedBarGraph to utilize legend data for improved display names. This change introduces a new LegendEntity type and updates the tooltip rendering logic to map data keys to their corresponding names, ensuring clarity when displaying chart data.
2026-01-21 18:01:23 +00:00
J B
b651238ad2 Enhance Average Goal Template to allow adjusting the budgetted amount… (#6711)
* Enhance Average Goal Template to allow adjusting the budgetted amount from the average by a percent or fixed amount

* fix typos

* ensure check is for undefined, so zero doesn't cause edge cases

* typo in enhancement message

* scale by cents for fixed amount, fixup tests for this as well

* Add support for fixed values in Schedule template

* [autofix.ci] apply automated fixes

* Changed to 'fixed' from 'amount'. Added unparse logic for Average, also fixed Schedule unparse

* move to generic for syntax from specific number

* consider currency preferences when calculating fixed modifications

* lint

* [autofix.ci] apply automated fixes

* timeframe -> period

* percentage -> value

* pass currency for calculation

* lint

* import from proper local dir

* [autofix.ci] apply automated fixes

* script block around {number} to prevent mdx conflict

* match order of ops between schedule and average for percent adjustment

* diversify example

* Link to schedule>adjustments from average

* Removed example column in favor of extra context
Slight rewording to avoid 'average' overload

* rabbit nitpicks

* number

* period

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
2026-01-21 17:31:46 +00:00
Michael Clark
8d1f0cf1de :electron: Update flathub screenshots to meet quality guidelines (#6611)
* update desktop app screenshots to meet quality guidelines

* better comply with flathub quality guidelines

* screenshot updates
2026-01-21 09:12:53 +00:00
Copilot
b50c45c9c9 Remove broken bank identifier spreadsheet link from GoCardless README (#6731)
* Initial plan

* Remove broken Google Doc link from GoCardless README

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add release notes for PR #6731

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-20 21:16:44 +00:00
Matiss Janis Aboltins
8f2369d5b8 Fix desktop notifications positioning (#6729)
* Fix desktop notifications container width

Add explicit width (400px) to the notifications container on desktop
to fix the positioning issue introduced in #6551.

Fixes #6718

* Add release notes for PR #6729

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-20 21:14:39 +00:00
Matiss Janis Aboltins
713ac88fee Mobile: navigate to config page when switching budget file (#6728)
Change the "Switch budget file" menu option on mobile to navigate back
to the config/budget selection page instead of opening a modal. This
makes the behavior consistent with the "Switch file" button in settings.

Remove the now-unused BudgetFileSelectionModal component.
2026-01-20 21:14:24 +00:00
Juulz
bcbcd6ad9f 🎨 Add new theme 'Theme from 1970' to customThemeCatalog (#6730)
* Add new theme 'Theme from 1970' to customThemeCatalog

* Add release notes for 1970-inspired light theme

Added release notes for a new custom light theme inspired by 1970 colors.
2026-01-20 20:57:06 +00:00
Matiss Janis Aboltins
c9a0ffa91c Replace theme screenshots with color palette preview (#6722)
* Replace theme screenshots with color palette preview

- Replace screenshot images in theme installer with 3x2 color palette grid
- Add colors array to catalog themes in customThemeCatalog.json
- Update CatalogTheme type to include optional colors property
- Remove getThemeScreenshotUrl function
- Extract ColorPalette component to separate file
- Update documentation to reflect color palette instead of screenshots
- Update tests to verify color palettes instead of images
- Extract actual theme colors from GitHub repositories

* Update ColorPalette component to use data-swatch attribute for color swatches in ThemeInstaller tests

* Update packages/desktop-client/src/data/customThemeCatalog.json

Co-authored-by: Michael Clark <5285928+MikesGlitch@users.noreply.github.com>

* Update color palettes for Simple Dark and Okabe Ito themes

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

---------

Co-authored-by: Michael Clark <5285928+MikesGlitch@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-01-20 18:00:34 +00:00
Matiss Janis Aboltins
6a9df6562c Fix AuthSettings - hide when no server configured and show warning when offline (#6723)
* Fix AuthSettings - hide if no server configured and show warning if offline

* Add release notes for auth settings fix

* Refactor AuthSettings component to always display OpenID label hint and remove offline condition check in tests
2026-01-20 18:00:07 +00:00
Matiss Janis Aboltins
d4144f4b9c lint: enable oxfmt on docs package (#6720) 2026-01-19 22:37:56 +00:00
Matiss Janis Aboltins
176336e7f3 Upgrade oxfmt/oxlint and migrate import sorting to oxfmt (#6719)
* Upgrade oxfmt/oxlint and migrate import sorting to oxfmt

Upgrade oxfmt from 0.22.0 to 0.26.0 and oxlint from 1.38.0 to 1.41.0.

Move import sorting configuration from oxlint (perfectionist/sort-imports)
to oxfmt's new experimentalSortImports feature, which provides native
import sorting support in the formatter.

* Add release notes for maintenance updates including oxlint and oxfmt upgrades
2026-01-19 21:46:24 +00:00
Matt Fiddaman
de0f4e9440 ⬆️ upgrade react deps (#6717)
* react & react-dom

* react-virtualized-auto-sizer v2

* react-grid-layout v2

* note

* Update table.tsx

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6717

---------

Co-authored-by: Joel Jeremy Marquez <joeljeremy.marquez@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-19 18:53:15 +00:00
Matiss Janis Aboltins
65dee4c627 lint: a11y issue fixes (#6679)
* Fix accessibility issues: use semantic HTML and correct tab indices

- Replace View with semantic h1 in ModalHeader
- Fix tabIndex from 1 to 0 in Menu component
- Remove disabled oxlint accessibility rules
- Update components to use proper semantic HTML elements

* Refactor button elements to semantic HTML in Autocomplete and CategoryAutocomplete components

- Replace button elements with div/View while maintaining role="button" for accessibility.
- Update styles and props accordingly to ensure consistent behavior.
- Adjust onClick types in Item and SecondaryItem components for better type safety.

* Add release notes for upcoming maintenance updates addressing various accessibility issues

* Refactor autocomplete components to improve text alignment and button semantics

- Added textAlign: 'left' style to AccountItem and PayeeItem for consistent text alignment.
- Removed type="button" from CategoryItem to streamline button semantics.
- Updated ItemContent to use the Button component instead of a button element, enhancing accessibility and consistency.

* Refactor budget and report components to improve text alignment

- Removed font: 'inherit' style from EnvelopeBudgetComponents and TrackingBudgetComponents for cleaner styling.
- Updated ActionableGridListItem and ReportCard components to replace font: 'inherit' with textAlign: 'left' for consistent text alignment.

* Update ActionableGridListItem to include font inheritance for improved styling consistency

* Refactor button elements to use the Button component for consistency and improved semantics

- Updated various components (EnvelopeBudgetComponents, IncomeCategoryMonth, CategoryMonth, ActionableGridListItem, ReportCard, DesktopLinkedNotes) to replace native button elements with the custom Button component.
- Adjusted styles and event handlers to align with the new Button implementation, ensuring consistent behavior and accessibility across the application.

* Update Button and ActionableGridListItem styles for consistency

- Set a fixed borderRadius of 4 for the Button component, ensuring uniformity across variants.
- Adjusted ActionableGridListItem to have a borderRadius of 0 for a cleaner design.

* Update CategoryAutocomplete to include button type attribute for improved semantics

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6679

* Update VRT screenshot for Payees search functionality

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-19 18:19:13 +00:00
Copilot
9376217c5e Fix mobile Calendar crash from missing DisplayPayeeProvider (#6698)
* Initial plan

* Fix mobile Calendar fatal error by correcting height constraint

The Virtualizer component in TransactionList requires its parent to have a defined height. Changed the View wrapper from height: '100%' to flex: 1 to properly distribute space within the flex container, accounting for the chevron button.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Add DisplayPayeeProvider to fix mobile Calendar error

The TransactionListItem component uses useDisplayPayee hook which requires DisplayPayeeProvider context. Wrapped TransactionListMobile with DisplayPayeeProvider to fix "useDisplayPayee must be used within a DisplayPayeeContextProvider" error on mobile.

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Change flex to height for View component

* Add release notes for PR #6698

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-19 18:05:02 +00:00
Matiss Janis Aboltins
e6e108ffbd Add error styling to ThemeInstaller component (#6689)
* Add error styling to ThemeInstaller component

* Add upcoming release notes for theme enhancements, including improved error handling and expanded theme catalog.

* Update upcoming-release-notes/6689.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update ThemeInstaller to set selected catalog theme on click

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-19 18:04:45 +00:00
Matt Fiddaman
517b1b4a81 fix "show completed schedules" toggle being persisted (#6716)
* make mobile consistent with desktop

* remove old option and modal

* note

* remove some useMemos

* coderabbit
2026-01-19 17:37:42 +00:00
Matt Fiddaman
cdae09e554 fix HyperFormula custom functions and add FIXED formula (#6645)
* fix custom HyperFormula functions

* add FIXED formula

* note

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-19 17:00:40 +00:00
Juulz
59233c4786 🎨 Add Simple Dark and Okabe Ito custom themes to catalog (#6696)
* Update customThemeCatalog.json

Add Simple Dark and Okabe Ito themes

* Add Simple Dark and Okabe Ito themes to theme catalog

* Update customThemeCatalog.json
2026-01-19 16:54:45 +00:00
Matt Fiddaman
502babd310 add separator between PR body and bundle stats (#6715)
* add seperator line before bundle stats post

* note

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-19 16:39:36 +00:00
Matt Fiddaman
3c302b3af9 apply tabular number styles more consistently (#6661)
* reports/reports

* reports/graphs

* account balance

* budgets

* goals

* reports stragglers

* rules

* schedules

* table

* bank sync modal

* spreadsheet

* inputs

* cover menu

* note

* agent configs, and coderabbit review rule

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6661

* fix test

* rename to FinancialText

* lint

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6661

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-19 16:39:16 +00:00
Matt Fiddaman
4c2d3e6998 ⬆️ bump Electron versions (#6703)
* bump electron

* note
2026-01-19 11:19:47 +00:00
Julian Dominguez-Schatz
e79d91b000 Change 'Bugfix' -> 'Bugfixes' in release notes (#6707)
* Change 'Bugfix' -> 'Bugfixes'

* Add release notes for PR #6707

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-18 22:03:39 +00:00
Matiss Janis Aboltins
2457a0b454 Add blog post: Next Steps for Funding Contributors (#6673)
* Add blog post: Next Steps for Funding Contributors

* Add release notes for PR #6673

* Delete upcoming-release-notes/6673.md

* Update packages/docs/blog/2026-01-17-next-steps-for-funding-contributors.md

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-01-18 21:37:22 +00:00
Julian Dominguez-Schatz
84a1d12dae Remove duplicate spelling check (#6691)
* Remove duplicate spelling check

* Correct Flathub store capitalization in release notes

* Fix typo in Flathub store automation entry

* Add release notes for PR #6691

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-18 19:27:49 +00:00
Copilot
b50d48a31a Fix sidebar link highlighting for nested routes (#6669)
* Initial plan

* Add release notes for PR #6669

* Fix Reports link highlighting in sidebar for nested routes

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Simplify by hardcoding end={false} in InternalLink

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6669

* Fix All accounts link to use exact matching

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6669

* Merge master and accept incoming screenshot changes

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Remove screenshot changes from current branch

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Revert all remaining screenshots to master versions

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Rename 'end' prop to 'isExactPathMatch' for clarity

Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6669

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: MatissJanis <886567+MatissJanis@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
2026-01-18 18:11:07 +00:00
Matiss Janis Aboltins
9289932af9 Fix flaky mobile payees VRT test by waiting for navbar animation (#6699)
* Fix flaky mobile payees VRT test by waiting for navbar animation to complete

Add wait timeout after dragNavbarDown() to ensure react-spring animation finishes before screenshots are taken.

* Add release notes for reducing payees e2e test flakiness

* Fix release note formatting: use sentence case and add period

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-01-18 18:10:35 +00:00
Matt Fiddaman
fe624f0158 post bundle size comparison comment in the PR body instead (#6677)
* post size-compare comment in pr body instead of comment

* note
2026-01-18 18:07:48 +00:00
Julian Dominguez-Schatz
7443886856 Run yarn dedupe on the repository (#6694)
* Run yarn dedupe

* Add release notes
2026-01-18 16:42:07 +00:00
Matthias Benaets
58b1420c60 fix: limit custom css textarea to resize vertically (#6695) 2026-01-18 13:54:57 +00:00
Michael Clark
7035b32f26 🎨 Custom theme: Black Gold (#6688)
* black gold theme

* release notes

* release note typo

* release note
2026-01-18 09:42:00 +00:00
Matiss Janis Aboltins
ee0e7ed3e0 Refactor theme catalog fetching to use custom hook (#6681)
* Refactor theme catalog fetching to use custom hook

- Move catalog fetching logic from ThemeInstaller to useThemeCatalog hook
- Fetch catalog asynchronously from GitHub instead of direct import
- Update tests to mock useThemeCatalog hook for faster test execution
- Remove catalog validation and translation dependencies from hook

* Remove redundant visibility checks for 'Demo Theme' button in ThemeInstaller tests and add assertion to verify presence of images.

* Refactor ThemeInstaller component to improve error handling and loading state display

- Change conditional rendering for catalog error to use ternary operator for clarity
- Simplify loading state display logic within the theme catalog view
- Ensure consistent styling and structure for theme items in the catalog

* Initialize loading state in useThemeCatalog hook to true

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-17 10:06:16 +00:00
Matiss Janis Aboltins
b6452f930b lint: add perfectionist/sort-named-imports rule (#6680)
* Apply import sorting with perfectionist/sort-named-imports rule

- Add perfectionist/sort-named-imports oxlint rule
- Sort named imports: value imports before type imports
- Update component-library and desktop-client files to match new rule

* Add release notes for linting updates on named imports
2026-01-16 23:41:45 +00:00
Joel Jeremy Marquez
bf814a6873 Update category schedule indicator to honor the upcoming days length setting (#6559)
* Update category schedule indicator to honor the upcoming days length setting

* [autofix.ci] apply automated fixes

* Update filter

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-16 17:40:05 +00:00
Matt Fiddaman
81c5dd347f fix inaccurate net payment/deposit calculations (#6675)
* fix net calculations

* note
2026-01-16 17:22:16 +00:00
Matt Fiddaman
cd87d5a899 give contributor points for marking issues as duplicates (#6659)
* reward closing issues as duplicates

* note

* remove debugging
2026-01-16 15:19:57 +00:00
Asherah Connor
f5e1d5eab4 Only reverse import preview transactions if they appear to be sorted by date ascending (#6543)
* Sort import preview transactions by (parsed) date, descending

* Update VRT screenshots

Auto-generated by VRT workflow

PR: #6543

---------

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-16 14:50:54 +00:00
Matiss Janis Aboltins
6bf119786c lint: re-enable some react rules (#6667)
* Remove hooks disable in electron fixtures

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

* Add release notes for PR #6667

* Re-enable exhaustive-deps for low-risk hooks

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

* Reduce exhaustive-deps overrides

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

* Format useQuery hook

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

* Fix exhaustive-deps warnings in hooks

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

* Format useQuery dependencies

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

* Document dynamic hook dependencies

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

* Use react exhaustive-deps disables

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

* Adjust exhaustive-deps disables

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

* Refactor React hooks to address exhaustive-deps linting issues

- Updated multiple components to use `useEffectEvent` for better handling of dependencies.
- Adjusted dependencies in various hooks to improve code quality and maintainability.
- Removed unnecessary `useCallback` and `useRef` usages where applicable.
- Consolidated linting rules for React hooks to enhance consistency across the codebase.

* Refactor useEffect hooks to improve dependency management

- Updated ManageRules component to correctly return the init function in useEffect.
- Adjusted Modals component to disable exhaustive-deps linting for specific dependencies, enhancing clarity and maintainability.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-16 08:45:29 +00:00
Juulz
14d4c7748d add missing theme variable for custom theming (#6665)
* Update BudgetName color to use sidebarBudgetName 

New theme variable sidebarBudgetName

* Update dark.ts to add sidebarBudgetName variable

* Update development.ts to add sidebarBudgetName variable

* Update light.ts to add sidebarBudgetName variable

* Update midnight.ts to add sidebarBudgetName variable

* Update dark.ts

* Update development.ts

* Update light.ts

* Update midnight.ts

* Update theme.ts

* Create 6665.md

Add `sidebarBudgetName` color variable
2026-01-16 00:19:30 +00:00
Matiss Janis Aboltins
edf61a477a Add code review guidelines for LLM agents (#6670)
* Add code review guidelines and update documentation

* Add release notes for PR #6670

* LLM: add code review guidelines to upcoming release notes

* Update review_status in .coderabbit.yaml to false

* Update CODE_REVIEW_GUIDELINES.md

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-15 21:13:42 +00:00
Matiss Janis Aboltins
0724f7eaef Add GitHub workflow to welcome fork PR contributors (#6657)
* lint: patch some no-empty-function violations

* docs: update ESLint rules and remove unused loadOwner function in UserAccess component

* Add GitHub workflow to welcome fork PR contributors

* Add release notes for PR #6657

* Update category for upcoming release notes

Changed category from Features to Maintenance.

* Update fork-pr-welcome.yml

Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-01-15 20:34:31 +00:00
Matiss Janis Aboltins
d01d0eacb8 Add custom themes installation feature (#6612)
* Add custom themes feature with GitHub installation support

- Add ThemeInstaller component for installing themes from GitHub
- Implement custom theme validation and CSS parsing
- Add support for installed custom themes in preferences
- Add CustomThemeStyle component with CSS validation
- Update theme system to support custom overlay backgrounds
- Add comprehensive tests for theme installation and validation
- Add documentation and release notes for custom themes feature

* Update custom theme catalog: remove several themes and add 'Miami Beach' from a new repository.

* Enhance CSS validation in custom themes

- Refactor `validatePropertyValue` to implement an allowlist approach for CSS property values, rejecting complex constructs and functions except for rgb/rgba/hsl/hsla.
- Add comprehensive tests for various invalid CSS scenarios, including function calls and at-rules.
- Improve error messages for better clarity on validation failures.
- Ensure property name validation checks for format and allowed characters.

* Update custom theme catalog: rename theme from 'Miami Beach' to 'Shades of Coffee'.

* Remove 'forceReload' feature flag and related code from the application settings and feature flag definitions.

* Enhance ThemeInstaller component to support installed themes

- Add `installedTheme` prop to `ThemeInstaller` for managing custom themes.
- Implement logic to populate the text box with installed custom theme CSS when reopening.
- Update tests to verify behavior for installed themes with and without repositories.
- Improve CSS validation to allow additional CSS keywords and ensure proper structure.
2026-01-15 18:04:35 +00:00
lif
bb70074f35 fix: disable oneOf and notOneOf operations for Notes filter (#6567)
* fix: disable oneOf and notOneOf operations for Notes filter

The Notes field is a free-text field and should not support "one of" /
"not one of" filter operations, as these require predefined values.
Attempting to use these operations with the Notes filter caused the app
to crash with a TypeError.

This change:
- Adds 'oneOf' and 'notOneOf' to disallowedOps for the notes field
- Updates the TypeScript type definitions to reflect this constraint
- Updates existing tests to use imported_payee field for oneOf tests
- Adds unit tests for the isValidOp and getValidOps functions

Fixes #6325

Signed-off-by: majiayu000 <1835304752@qq.com>

* move note

---------

Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Matt Fiddaman <github@m.fiddaman.uk>
2026-01-15 00:56:22 +00:00
dependabot[bot]
5860b95c9c [WIP] chore(deps): bump undici from 7.16.0 to 7.18.2 (#6658)
* chore(deps): bump undici from 7.16.0 to 7.18.2

Bumps [undici](https://github.com/nodejs/undici) from 7.16.0 to 7.18.2.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.16.0...v7.18.2)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.18.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Add release notes for PR #6658

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-14 21:53:46 +00:00
Bradley Nelson
a58bac6de6 Add session token authentication to API init() (#6540)
* add session token authentication to API init()

Allow users to authenticate using a session token directly instead of
username/password when initializing the API. This is useful for
scenarios where a token has already been obtained through other means.

Changes:
- Refactor InitConfig to use union types for type-safe auth method selection
- Add sessionToken option that validates token on init
- Use TypeScript `never` types to make auth methods mutually exclusive

* add release notes for session token authentication
2026-01-14 21:24:33 +00:00
Juulz
d01f38d480 Copy all old colors from colors.ts to palette.ts (#6627)
* Update color palette with old color values

* Update color palette with new values

* Replace old color imports with old color definitions

* Update color exports to use consistent casing

Done in bulk in MS-Word

* Fix export statements for color constants

* Clean up palette.ts by removing old import comment

Removed commented-out import statement for old colors.

* Add oldColor hex color definitions to palette.ts

Copy hex color definitions from colors.ts into palette.ts.

* Delete packages/desktop-client/src/style/colors.ts

no longer needed

* Disable colors export due to file deletion

* [autofix.ci] apply automated fixes

* Remove restricted import comment from index.ts

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-14 20:48:04 +00:00
Matt Fiddaman
4b1f127910 update baseline-browser-mapping (#6647)
* update baseline-browser-mapping

* note
2026-01-14 20:40:56 +00:00
1595 changed files with 48391 additions and 18064 deletions

View File

@@ -3,7 +3,7 @@ issue_enrichment:
enabled: false
reviews:
request_changes_workflow: true
review_status: true
review_status: false
high_level_summary: false
finishing_touches:
docstrings:
@@ -12,19 +12,7 @@ reviews:
docstrings:
mode: off
enabled: false
custom_checks:
- mode: error
name: 'settings'
instructions: 'Every addition of a new setting toggle must be thoroughly evaluated against the core design principles of Actual. The settings screen is reserved for essential and foundational options only — do not introduce settings for minor UI adjustments such as sizes, paddings, colors, or margins. Prioritize preserving a simple and uncluttered user experience. Users proposing new settings must confirm in a reply to the Coderabbit comment that they have reviewed and ensured alignment with these principles. Excessive or granular UI options increase code complexity and risk confusing users, and are generally not permitted.'
- mode: error
name: 'linting'
instructions: 'Do not allow any oxlint-disable lines.'
- mode: error
name: 'typecheck'
instructions: 'Do not allow creating new components or utilities with the @ts-strict-ignore comment.'
labeling_instructions:
- label: 'suspect ai generated'
instructions: 'This issue or PR is suspected to be generated by AI.'
- label: 'API'
instructions: 'This issue or PR updates the API in `packages/api`.'
- label: 'documentation'

View File

@@ -0,0 +1,74 @@
---
description: Rules for AI-generated commits and pull requests
globs:
alwaysApply: true
---
# PR and Commit Rules for AI Agents
Canonical source: `.github/agents/pr-and-commit-rules.md`
## Commit Rules
### [AI] Prefix Requirement
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
### Git Safety Rules
- **Never** update git config
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
- **Never** force push to `main`/`master`
- **Never** commit unless explicitly asked by the user
## Pre-Commit Quality Checklist
Before committing, ensure all of the following:
- [ ] Commit message is prefixed with `[AI]`
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] User-facing strings are translated
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
## Pull Request Rules
### [AI] Prefix Requirement
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
### Labels
Add the **"AI generated"** label to all AI-created pull requests.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese** (简体中文).
## Quick-Reference Workflow
1. Make your changes
2. Run `yarn typecheck` — fix any errors
3. Run `yarn lint:fix` — fix any remaining lint errors
4. Run relevant tests (`yarn test` for all, or workspace-specific)
5. Stage files and commit with `[AI]` prefix — do not skip hooks
6. When creating a PR:
- Use `[AI]` prefix in the title
- Add the `"AI generated"` label
- Leave the PR template blank (do not fill it in)

View File

@@ -8,35 +8,66 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
⚠️ **CRITICAL:** Bug reports without clear, step-by-step reproduction instructions will be closed. We cannot investigate or fix bugs without being able to reproduce them. Please take the time to provide detailed reproduction steps.
- type: markdown
attributes:
value: |
**IMPORTANT:** we use GitHub Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
**Bank-sync problems (SimpleFin / GoCardless)?** Reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) first and open an issue only if the community deems the issue to be a legitimate bug in Actual.
- type: checkboxes
id: existing-issue
attributes:
label: 'Verified issue does not already exist?'
description: 'Please search to see if an issue already exists for the issue you encountered.'
options:
- label: 'I have searched and found no existing issue'
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen? If you're reporting an issue with imports, please attach a (redacted) version of the file you're having trouble importing. You may need to zip it before uploading.
placeholder: Tell us what you see!
value: 'A bug happened!'
description: |
Describe the bug clearly and concisely. Include:
- What you were trying to do
- What you expected to happen
- What actually happened instead
- Any error messages (copy/paste the exact text)
If you're reporting an issue with imports, please include a (redacted) version of the file, and a screenshot of the import screen. You may need to zip it before uploading.
placeholder: |
I was trying to [action] when [context].
Expected: [expected behavior]
Actual: [actual behavior]
Error message: [if any]
validations:
required: true
- type: markdown
attributes:
value: |
## Reproduction Steps
**REQUIRED:** Without clear reproduction steps, we cannot investigate or fix the bug. Please provide detailed, step-by-step instructions that anyone can follow to reproduce the issue.
- type: textarea
id: reproduction
attributes:
label: How can we reproduce the issue?
description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file.
value: 'How can we reproduce the issue?'
description: |
**This field is mandatory and must be filled out completely.**
Provide numbered, step-by-step instructions that allow us to reproduce the bug. Include:
- Specific actions you took (e.g., "Click on the Budget tab", "Enter $100 in the amount field")
- What you expected to happen
- What actually happened instead
Example format:
1. Navigate to [specific page/section]
2. Click on [specific button/link]
3. Enter [specific data] in [specific field]
4. Click [action]
5. Observe [expected vs actual behavior]
If the issue involves importing data, please attach a (redacted) sample file. You may need to zip it before uploading.
placeholder: |
1. Go to [specific location]
2. Click [specific element]
3. Enter [specific data]
4. Click [action]
5. Expected: [what should happen]
Actual: [what actually happens]
validations:
required: true
- type: markdown

View File

@@ -1 +1,21 @@
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
## Description
<!-- What does this PR do? Why is it needed? Please give context on the "why?": why do we need this change? What problem is it solving for you?-->
## Related issue(s)
<!-- e.g. Fixes #123, Relates to #456 -->
## Testing
<!-- What did you test? How can we reproduce the issue you are fixing or how can we test the feature you built? -->
## Checklist
- [ ] Release notes added (see link above)
- [ ] No obvious regressions in affected areas
- [ ] Self-review has been performed - I understand what each change in the code does and why it is needed
<!--- actual-bot-sections --->

View File

@@ -74,4 +74,4 @@ async function checkReleaseNotesExists() {
}
}
checkReleaseNotesExists();
void checkReleaseNotesExists();

View File

@@ -74,4 +74,4 @@ async function commentOnPR() {
}
}
commentOnPR();
void commentOnPR();

View File

@@ -94,4 +94,4 @@ ${summaryData.summary}
}
}
createReleaseNotesFile();
void createReleaseNotesFile();

View File

@@ -33,11 +33,11 @@ try {
{
role: 'system',
content:
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfix", or "Maintenance". No other text or explanation.',
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfixes", or "Maintenance". No other text or explanation.',
},
{
role: 'user',
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfix: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfixes: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
},
],
max_tokens: 10,
@@ -86,7 +86,7 @@ try {
// Validate the category response
const validCategories = [
'Features',
'Bugfix',
'Bugfixes',
'Enhancements',
'Maintenance',
];

View File

@@ -37,12 +37,14 @@ async function getPRDetails() {
console.log('- PR Author:', pr.user.login);
console.log('- PR Title:', pr.title);
console.log('- Base Branch:', pr.base.ref);
console.log('- Head Branch:', pr.head.ref);
const result = {
number: pr.number,
author: pr.user.login,
title: pr.title,
baseBranch: pr.base.ref,
headBranch: pr.head.ref,
};
setOutput('result', JSON.stringify(result));

View File

@@ -2,9 +2,11 @@ Abanca
ABNAMRO
ABNANL
Activo
actualrc
AESUDEF
ALZEY
Anglais
ANZ
aql
AUR
Authentik
@@ -30,6 +32,7 @@ CAGLPTPL
Caixa
CAMT
cashflow
Catppuccin
Cetelem
cimode
Citi
@@ -40,13 +43,13 @@ COBADEFF
CODEOWNERS
COEP
commerzbank
COOP
Copiar
COUNTA
COUNTBLANK
countif
CREGBEBB
crt
CZK
Danske
datadir
DATEDIF
@@ -68,7 +71,6 @@ Fineco
Finicity
Fintro
Finverse
flathub
Flathub
FORTUNEO
FTNOFRP
@@ -83,6 +85,7 @@ HABAL
Hampel
HELADEF
HLOOKUP
HUF
IFERROR
IFNA
INDUSTRIEL
@@ -109,6 +112,7 @@ Keycloak
Khurozov
KORT
Kreditbank
KRW
lage
LHV
LHVBEE
@@ -116,12 +120,14 @@ LKR
MAXA
mbank
mdc
metainfo
modals
Moldovan
murmurhash
NETWORKDAYS
nginx
OIDC
Okabe
overbudgeted
overbudgeting
oxc
@@ -134,7 +140,6 @@ prefs
Primoco
Priotecs
proactively
pwa
Qatari
QNTOFRP
QONTO
@@ -172,6 +177,7 @@ touchscreen
triaging
UAH
ubuntu
undici
userinfo
Userscripts
UZS

View File

@@ -79,3 +79,6 @@
# allowlist specific non-English words with non-ASCII characters
\b(Länsförsäkringar|München|Złoty)\b
# allowlist specific proper nouns
\b(CodeRabbit)\b

View File

@@ -15,7 +15,7 @@ runs:
using: composite
steps:
- name: Install node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: Install yarn
@@ -27,7 +27,7 @@ runs:
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
shell: bash
- name: Cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
id: cache
with:
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
@@ -36,7 +36,7 @@ runs:
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
shell: bash
- name: Cache Lage
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ${{ format('{0}/.lage', inputs.working-directory) }}
key: lage-${{ runner.os }}-${{ github.sha }}
@@ -48,7 +48,7 @@ runs:
shell: bash
if: steps.cache.outputs.cache-hit != 'true'
- name: Download translations
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: actualbudget/translations
path: ${{ inputs.working-directory }}/packages/desktop-client/locale

70
.github/agents/pr-and-commit-rules.md vendored Normal file
View File

@@ -0,0 +1,70 @@
# PR and Commit Rules for AI Agents
This is the single source of truth for all commit and pull request rules that AI agents must follow when working with Actual Budget.
## Commit Rules
### [AI] Prefix Requirement
**ALL commit messages MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
- `Add support for new transaction categories` (MISSING PREFIX - NOT ALLOWED)
### Git Safety Rules
- **Never** update git config
- **Never** run destructive git operations (force push, hard reset) unless the user explicitly requests it
- **Never** skip hooks (`--no-verify`, `--no-gpg-sign`)
- **Never** force push to `main`/`master`
- **Never** commit unless explicitly asked by the user
## Pre-Commit Quality Checklist
Before committing, ensure all of the following:
- [ ] Commit message is prefixed with `[AI]`
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
- [ ] User-facing strings are translated
- [ ] Code style conventions followed (see `AGENTS.md` for full style guide)
## Pull Request Rules
### [AI] Prefix Requirement
**ALL pull request titles MUST be prefixed with `[AI]`.** This is a mandatory requirement with no exceptions.
**Examples:**
- `[AI] Fix type error in account validation`
- `[AI] Add support for new transaction categories`
- `Fix type error in account validation` (MISSING PREFIX - NOT ALLOWED)
### Labels
Add the **"AI generated"** label to all AI-created pull requests. This helps maintainers understand the nature of the contribution.
### PR Template: Do Not Fill In
- **NEVER fill in the PR template** (`.github/PULL_REQUEST_TEMPLATE.md`). Leave all blank spaces and placeholder comments as-is. Humans are expected to fill in the Description, Related issue(s), Testing, and Checklist sections.
- **Exception**: If a human **explicitly asks** you to fill out the PR template, then fill it out **in Chinese**, using Chinese characters (简体中文) for all content you add.
## Quick-Reference Workflow
Follow these steps when committing and creating PRs:
1. Make your changes
2. Run `yarn typecheck` — fix any errors
3. Run `yarn lint:fix` — fix any remaining lint errors
4. Run relevant tests (`yarn test` for all, or workspace-specific)
5. Stage files and commit with `[AI]` prefix — do not skip hooks
6. When creating a PR:
- Use `[AI]` prefix in the title
- Add the `"AI generated"` label
- Leave the PR template blank (do not fill it in)

View File

@@ -8,6 +8,13 @@ const CONFIG = {
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
PR_CONTRIBUTION_POINTS: [
{ categories: ['Features'], points: 2 },
{ categories: ['Enhancements'], points: 2 },
{ categories: ['Bugfixes', 'Bugfix'], points: 3 },
{ categories: ['Maintenance'], points: 2 },
{ categories: ['Unknown'], points: 2 },
],
// Point tiers for code changes (non-docs)
CODE_PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
@@ -31,6 +38,122 @@ const CONFIG = {
DOCS_FILES_PATTERN: 'packages/docs/**/*',
};
/**
* Parse category from release notes file content.
* @param {string} content - The content of the release notes file.
* @returns {string|null} The category or null if not found.
*/
function parseReleaseNotesCategory(content) {
if (!content) return null;
// Extract YAML front matter
const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!frontMatterMatch) return null;
// Extract category from front matter
const categoryMatch = frontMatterMatch[1].match(/^category:\s*(.+)$/m);
if (!categoryMatch) return null;
return categoryMatch[1].trim();
}
/**
* Get the last commit SHA on or before a given date.
* @param {Octokit} octokit - The Octokit instance.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {Date} beforeDate - The date to find the last commit before.
* @returns {Promise<string|null>} The commit SHA or null if not found.
*/
async function getLastCommitBeforeDate(octokit, owner, repo, beforeDate) {
try {
// Get the default branch from the repository
const { data: repoData } = await octokit.repos.get({ owner, repo });
const defaultBranch = repoData.default_branch;
const { data: commits } = await octokit.repos.listCommits({
owner,
repo,
sha: defaultBranch,
until: beforeDate.toISOString(),
per_page: 1,
});
if (commits.length > 0) {
return commits[0].sha;
}
} catch {
// If error occurs, return null to fall back to default branch
}
return null;
}
/**
* Get the category and points for a PR by reading its release notes file.
* @param {Octokit} octokit - The Octokit instance.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {number} prNumber - PR number.
* @param {Date} monthEnd - The end date of the month to use as base revision.
* @returns {Promise<Object>} Object with category and points, or null if error.
*/
async function getPRCategoryAndPoints(
octokit,
owner,
repo,
prNumber,
monthEnd,
) {
const releaseNotesPath = `upcoming-release-notes/${prNumber}.md`;
try {
// Get the last commit of the month to use as base revision
const commitSha = await getLastCommitBeforeDate(
octokit,
owner,
repo,
monthEnd,
);
// Try to read the release notes file from the last commit of the month
const { data: fileContent } = await octokit.repos.getContent({
owner,
repo,
path: releaseNotesPath,
ref: commitSha || undefined, // Use commit SHA if available, otherwise default branch
});
if (fileContent.content) {
// Decode base64 content
const content = Buffer.from(fileContent.content, 'base64').toString(
'utf-8',
);
const category = parseReleaseNotesCategory(content);
const tier = CONFIG.PR_CONTRIBUTION_POINTS.find(e =>
e.categories.includes(category),
);
if (tier) {
return {
category,
points: tier.points,
};
}
}
} catch {
// Do nothing
}
const unknownTier = CONFIG.PR_CONTRIBUTION_POINTS.find(e =>
e.categories.includes('Unknown'),
);
return {
category: 'Unknown',
points: unknownTier.points,
};
}
/**
* Get the start and end dates for the last month.
* @returns {Object} An object containing the start and end dates.
@@ -89,6 +212,7 @@ async function countContributorPoints() {
{
codeReviews: [], // Will store objects with PR number and points for main repo changes
docsReviews: [], // Will store objects with PR number and points for docs changes
prContributions: [], // Will store objects with PR number, category, and points for PR author contributions
labelRemovals: [],
issueClosings: [],
points: 0,
@@ -202,6 +326,28 @@ async function countContributorPoints() {
mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR;
}
} else {
// Award points to PR author if they are a core maintainer
const prAuthor = pr.user?.login;
if (prAuthor && orgMemberLogins.has(prAuthor)) {
const categoryAndPoints = await getPRCategoryAndPoints(
octokit,
owner,
repo,
pr.number,
until,
);
if (categoryAndPoints) {
const authorStats = stats.get(prAuthor);
authorStats.prContributions.push({
pr: pr.number.toString(),
category: categoryAndPoints.category,
points: categoryAndPoints.points,
});
authorStats.points += categoryAndPoints.points;
}
}
const uniqueReviewers = new Set();
reviews.data.forEach(review => {
if (
@@ -278,7 +424,7 @@ async function countContributorPoints() {
if (
event.event === 'closed' &&
event.state_reason === 'not_planned'
['not_planned', 'duplicate'].includes(event.state_reason)
) {
const closer = event.actor.login;
const userStats = stats.get(closer);
@@ -293,7 +439,7 @@ async function countContributorPoints() {
// Print all statistics
printStats(
'Code Review Statistics',
stats => stats.codeReviews.length,
stats => stats.codeReviews.reduce((sum, r) => sum + r.points, 0),
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
@@ -308,7 +454,7 @@ async function countContributorPoints() {
printStats(
'Docs Review Statistics',
stats => stats.docsReviews.length,
stats => stats.docsReviews.reduce((sum, r) => sum + r.points, 0),
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
@@ -316,16 +462,27 @@ async function countContributorPoints() {
.join(', ')})`,
);
printStats(
'PR Contribution Statistics',
stats => stats.prContributions.reduce((sum, r) => sum + r.points, 0),
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.prContributions.map(r => `#${r.pr} (${r.points}pts - ${r.category})`)
.join(', ')})`,
);
printStats(
'"Needs Triage" Label Removal Statistics',
stats => stats.labelRemovals.length,
stats => stats.labelRemovals.length * CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
);
printStats(
'Issue Closing Statistics',
stats => stats.issueClosings.length,
stats =>
stats.issueClosings.length * CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
);

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -41,21 +41,12 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Check if PR targets master branch
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
id: check-base-branch
run: |
BASE_BRANCH=$(echo '${{ steps.pr-details.outputs.result }}' | jq -r '.baseBranch')
echo "Base branch: $BASE_BRANCH"
if [ "$BASE_BRANCH" = "master" ]; then
echo "targets_master=true" >> $GITHUB_OUTPUT
else
echo "targets_master=false" >> $GITHUB_OUTPUT
echo "PR does not target master branch, skipping release notes generation"
fi
- name: Check if release notes file already exists
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null' && steps.check-base-branch.outputs.targets_master == 'true'
if: >-
steps.check-first-comment.outputs.result == 'true' &&
steps.pr-details.outputs.result != 'null' &&
fromJSON(steps.pr-details.outputs.result).baseBranch == 'master' &&
!startsWith(fromJSON(steps.pr-details.outputs.result).headBranch, 'release/')
id: check-release-notes-exists
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
env:

View File

@@ -15,11 +15,11 @@ jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Format code
run: yarn lint:fix
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
- uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8 # v1.3.3

View File

@@ -22,7 +22,7 @@ jobs:
api:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -34,12 +34,12 @@ jobs:
- name: Prepare bundle stats artifact
run: cp packages/api/app/stats.json api-stats.json
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-api
path: packages/api/actual-api.tgz
- name: Upload API bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: api-build-stats
path: api-stats.json
@@ -47,7 +47,7 @@ jobs:
crdt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -57,7 +57,7 @@ jobs:
- name: Create package tgz
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-crdt
path: packages/crdt/actual-crdt.tgz
@@ -65,23 +65,23 @@ jobs:
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: yarn build:browser
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-web
path: packages/desktop-client/build
- name: Upload Build Stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: build-stats
path: packages/desktop-client/build-stats
server:
cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -89,10 +89,35 @@ jobs:
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Build Server
run: yarn workspace @actual-app/sync-server build
- name: Build CLI
run: yarn build:cli
- name: Create package tgz
run: cd packages/cli && yarn pack && mv package.tgz actual-cli.tgz
- name: Prepare bundle stats artifact
run: cp packages/cli/dist/stats.json cli-stats.json
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: actual-cli
path: packages/cli/actual-cli.tgz
- name: Upload CLI bundle stats
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: cli-build-stats
path: cli-stats.json
server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Build Server
run: yarn workspace @actual-app/sync-server build
- name: Upload Build
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: sync-server
path: packages/sync-server/build

View File

@@ -12,10 +12,20 @@ concurrency:
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
jobs:
constraints:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Check dependency version consistency
run: yarn constraints
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -25,7 +35,7 @@ jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -35,7 +45,7 @@ jobs:
validate-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -47,7 +57,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -59,9 +69,10 @@ jobs:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
node-version: 22
download-translations: 'false'
- name: Check migrations
run: node ./.github/actions/check-migrations.js
run: yarn workspace @actual-app/ci-actions tsx bin/check-migrations.ts

View File

@@ -22,14 +22,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
languages: javascript
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
category: '/language:javascript'

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:

View File

@@ -36,17 +36,17 @@ jobs:
matrix:
os: [ubuntu, alpine]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
# Push to both Docker Hub and Github Container Registry
images: ${{ env.IMAGES }}
@@ -54,14 +54,14 @@ jobs:
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
if: github.event_name != 'pull_request' && !github.event.repository.fork
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@@ -76,7 +76,7 @@ jobs:
run: yarn build:server
- name: Build image for testing
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: false
@@ -87,13 +87,13 @@ jobs:
- name: Test that the docker image boots
run: |
docker run --detach --network=host actualbudget/actual-server-testing
sleep 5
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
sleep 10
curl --fail -sS -LI -w '%{http_code}\n' --retry 20 --retry-delay 1 --retry-connrefused localhost:5006
# This will use the cache from the earlier build step and not rebuild the image
# https://docs.docker.com/build/ci/github-actions/test-before-push/
- name: Build and push images
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

View File

@@ -28,17 +28,17 @@ jobs:
name: Build Docker image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
# Push to both Docker Hub and Github Container Registry
images: ${{ env.IMAGES }}
@@ -48,7 +48,7 @@ jobs:
- name: Docker meta for Alpine image
id: alpine-meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ env.IMAGES }}
# Automatically update :latest
@@ -58,13 +58,13 @@ jobs:
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -78,7 +78,7 @@ jobs:
run: yarn build:server
- name: Build and push ubuntu image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: true
@@ -87,7 +87,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
- name: Build and push alpine image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
push: true

View File

@@ -1,33 +0,0 @@
name: Release Docs to Github Pages
# Release docs on every push to master
on:
push:
branches:
- master
paths:
- 'packages/docs/**'
- '.github/workflows/docs-spelling.yml'
- '.github/actions/docs-spelling/**'
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy Docs
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Docusaurus Deploy
run: |
GIT_USER=MikesGlitch \
GIT_PASS=${{ secrets.DOCS_GITHUB_PAGES_DEPLOY }} \
GIT_USER_NAME=github-actions[bot] \
GIT_USER_EMAIL=github-actions[bot]@users.noreply.github.com \
yarn deploy:docs

View File

@@ -30,9 +30,9 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -41,7 +41,7 @@ jobs:
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Run E2E Tests
run: yarn e2e --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: desktop-client-test-results-shard-${{ matrix.shard }}
@@ -53,9 +53,9 @@ jobs:
name: Functional Desktop App
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
@@ -65,7 +65,7 @@ jobs:
- name: Run Desktop app E2E Tests
run: |
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: desktop-app-test-results
@@ -81,16 +81,16 @@ jobs:
matrix:
shard: [1, 2, 3, 4, 5]
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Run VRT Tests
run: yarn vrt --shard=${{ matrix.shard }}/5
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: always()
with:
name: vrt-blob-report-${{ matrix.shard }}
@@ -104,13 +104,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Download all blob reports
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: packages/desktop-client/all-blob-reports
pattern: vrt-blob-report-*
@@ -118,7 +118,7 @@ jobs:
- name: Merge reports
id: merge-reports
run: yarn workspace @actual-app/web run playwright merge-reports --reporter html ./all-blob-reports
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
id: playwright-report-vrt
with:
name: html-report--attempt-${{ github.run_attempt }}
@@ -134,7 +134,7 @@ jobs:
echo "${{ steps.playwright-report-vrt.outputs.artifact-url }}" > vrt-metadata/artifact-url.txt
- name: Upload VRT metadata
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vrt-comment-metadata
path: vrt-metadata/

View File

@@ -18,7 +18,7 @@ jobs:
if: github.event.workflow_run.event == 'pull_request'
steps:
- name: Download VRT metadata
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -53,7 +53,7 @@ jobs:
- name: Comment on PR with VRT report link
if: steps.metadata.outputs.should_comment == 'true'
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
number: ${{ steps.metadata.outputs.pr_number }}
header: vrt-comment

View File

@@ -29,7 +29,7 @@ jobs:
- macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
- if: ${{ ! startsWith(matrix.os, 'windows') }}
@@ -74,7 +74,7 @@ jobs:
if: ${{ ! startsWith(matrix.os, 'macos') }}
run: ./bin/package-electron
- name: Upload Build
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}
path: |
@@ -85,13 +85,13 @@ jobs:
packages/desktop-electron/dist/*.flatpak
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}-appx
path: |
packages/desktop-electron/dist/*.appx
- name: Add to new release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
draft: true
body: |
@@ -126,7 +126,7 @@ jobs:
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
- name: Download Microsoft Store artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: actual-electron-windows-latest-appx
@@ -156,52 +156,3 @@ jobs:
-NoStatus `
-AutoCommit `
-Force
publish-flathub:
needs: build
runs-on: ubuntu-22.04
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
steps:
- name: Download Linux artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: actual-electron-ubuntu-22.04
- name: Calculate AppImage SHA256
id: appimage_sha256
run: |
APPIMAGE_X64_SHA256=$(sha256sum Actual-linux-x86_64.AppImage | awk '{ print $1 }')
APPIMAGE_ARM64_SHA256=$(sha256sum Actual-linux-arm64.AppImage | awk '{ print $1 }')
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
- name: Checkout Flathub repo
uses: actions/checkout@v6
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
- name: Update manifest with new SHA256
run: |
# Replace x86_64 entry
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_X64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${{ needs.build.outputs.version }}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
# Replace arm64 entry
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_ARM64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${{ needs.build.outputs.version }}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
cat com.actualbudget.actual.yml
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
branch: 'release/${{ needs.build.outputs.version }}'
title: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ needs.build.outputs.version }}.
:link: [View release notes](https://actualbudget.org/blog/release-${{ needs.build.outputs.version }})
reviewers: 'jfdoming,MatissJanis,youngcw' # The core team that have accepted the collaborator access to the Flathub repo

View File

@@ -33,7 +33,7 @@ jobs:
- macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
- if: ${{ ! startsWith(matrix.os, 'windows') }}
@@ -42,6 +42,8 @@ jobs:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install setuptools
- name: Set up environment
uses: ./.github/actions/setup
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
@@ -56,65 +58,63 @@ jobs:
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Electron
run: ./bin/package-electron
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-x86_64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-arm64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
- name: Upload Linux x64 flatpak
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-x86_64.flatpak
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.flatpak
- name: Upload Windows x32 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-ia32.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
- name: Upload Windows x64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-x64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-x64.exe
- name: Upload Windows arm64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-arm64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
- name: Upload Mac x64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-x64.dmg
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
- name: Upload Mac arm64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-arm64.dmg
if-no-files-found: ignore
@@ -122,7 +122,7 @@ jobs:
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

48
.github/workflows/fork-pr-welcome.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Fork PR Welcome
##########################################################################################
# WARNING! This workflow uses the 'pull_request_target' event. That means that it will #
# always run in the context of the main actualbudget/actual repo, even if the PR is from #
# a fork. This is necessary to get access to a GitHub token that can post a comment on #
# the PR. Be VERY CAREFUL about adding things to this workflow, since forks can inject #
# arbitrary code into their branch, and can pollute the artifacts we download. Arbitrary #
# code execution in this workflow could lead to a compromise of the main repo. #
##########################################################################################
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests #
##########################################################################################
on:
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
jobs:
welcome:
name: Post Welcome Message
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Post welcome comment
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}
header: fork-pr-welcome
hide_and_recreate: true
hide_classify: OUTDATED
message: |
<!-- fork-pr-welcome -->
👋 Hello contributor!
We would love to review your PR! Before we can do that, please make sure:
- ✅ All CI checks pass
- ✅ The PR is moved from draft to open (if applicable)
- ✅ The "[WIP]" prefix is removed from the PR title
- ✅ All CodeRabbit code review comments are resolved (if you disagree with anything - reply to the bot with your reasoning so we can read through it). The bot will eventually approve the PR.
We do this to reduce the TOIL the core contributor team has to go through for each PR and to allow for speedy reviews and merges.
For more information, please see our [Contributing Guide](https://actualbudget.org/docs/contributing/).

View File

@@ -17,9 +17,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.inputs.ref }}
- name: Set up environment
uses: ./.github/actions/setup
with:
download-translations: 'false'
- name: Bump package versions
id: bump_package_versions
shell: bash
@@ -35,9 +39,12 @@ jobs:
pkg="${packages[$key]}"
if [[ -n "${{ github.event.inputs.version }}" ]]; then
version="${{ github.event.inputs.version }}"
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--version "${{ github.event.inputs.version }}" \
--update)
else
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
version=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts \
--package-json "./packages/$pkg/package.json" \
--type auto \
--update)
@@ -48,7 +55,7 @@ jobs:
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
- name: Create PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.ACTIONS_UPDATE_TOKEN }}
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'

View File

@@ -12,7 +12,7 @@ jobs:
if: github.repository == 'actualbudget/actual'
steps:
- name: Check out main repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: actual
- name: Set up environment
@@ -44,7 +44,7 @@ jobs:
push \
actualbudget/actual
- name: Check out updated translations
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
repository: actualbudget/translations

View File

@@ -24,8 +24,8 @@ jobs:
runs-on: ubuntu-latest
steps:
# This is not a security concern because we have approved & merged the PR
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
- name: Handle feature requests

View File

@@ -0,0 +1,37 @@
# When the "unfreeze" label is added to a PR, add that PR to Merge Freeze's unblocked list
# so it can be merged during a freeze. Uses pull_request_target so the workflow runs in
# the base repo and has access to MERGEFREEZE_ACCESS_TOKEN for fork PRs; it does not
# checkout or run any PR code. Requires MERGEFREEZE_ACCESS_TOKEN repo secret
# (project-specific token from Merge Freeze Web API panel for actualbudget/actual / master).
# See: https://docs.mergefreeze.com/web-api#post-freeze-status
name: Merge Freeze add PR to unblocked list
on:
pull_request_target:
types: [labeled]
jobs:
unfreeze:
if: ${{ github.event.label.name == 'unfreeze' }}
runs-on: ubuntu-latest
permissions: {}
concurrency:
group: merge-freeze-unfreeze-${{ github.ref }}-labels
cancel-in-progress: false
steps:
- name: POST to Merge Freeze add PR to unblocked list
env:
MERGEFREEZE_ACCESS_TOKEN: ${{ secrets.MERGEFREEZE_ACCESS_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
USER_NAME: ${{ github.actor }}
run: |
set -e
if [ -z "$MERGEFREEZE_ACCESS_TOKEN" ]; then
echo "::error::MERGEFREEZE_ACCESS_TOKEN secret is not set"
exit 1
fi
url="https://www.mergefreeze.com/api/branches/actualbudget/actual/master/?access_token=${MERGEFREEZE_ACCESS_TOKEN}"
payload=$(jq -n --arg user_name "$USER_NAME" --argjson pr "$PR_NUMBER" '{frozen: true, user_name: $user_name, unblocked_prs: [$pr]}')
curl -sf -X POST "$url" -H "Content-Type: application/json" -d "$payload"
echo "Merge Freeze updated: PR #$PR_NUMBER added to unblocked list."

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup

126
.github/workflows/publish-flathub.yml vendored Normal file
View File

@@ -0,0 +1,126 @@
name: Publish Flathub
defaults:
run:
shell: bash
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g. v25.3.0)'
required: true
type: string
concurrency:
group: publish-flathub
cancel-in-progress: false
jobs:
publish-flathub:
runs-on: ubuntu-22.04
steps:
- name: Resolve version
id: resolve_version
env:
EVENT_NAME: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_TAG: ${{ inputs.tag }}
run: |
if [[ "$EVENT_NAME" == "release" ]]; then
TAG="$RELEASE_TAG"
else
TAG="$INPUT_TAG"
fi
if [[ -z "$TAG" ]]; then
echo "::error::No tag provided"
exit 1
fi
# Validate tag format (v-prefixed semver, e.g. v25.3.0 or v1.2.3-beta.1)
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid tag format: $TAG (expected v-prefixed semver, e.g. v25.3.0)"
exit 1
fi
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved tag=$TAG version=$VERSION"
- name: Verify release assets exist
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ steps.resolve_version.outputs.tag }}"
echo "Checking release assets for tag $TAG..."
ASSETS=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.assets[].name')
echo "Found assets:"
echo "$ASSETS"
if ! echo "$ASSETS" | grep -qx "Actual-linux-x86_64.AppImage"; then
echo "::error::Missing asset: Actual-linux-x86_64.AppImage"
exit 1
fi
if ! echo "$ASSETS" | grep -qx "Actual-linux-arm64.AppImage"; then
echo "::error::Missing asset: Actual-linux-arm64.AppImage"
exit 1
fi
echo "All required AppImage assets found."
- name: Calculate AppImage SHA256 (streamed)
run: |
VERSION="${{ steps.resolve_version.outputs.version }}"
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
echo "Streaming x86_64 AppImage to compute SHA256..."
APPIMAGE_X64_SHA256=$(curl -fSL "${BASE_URL}/Actual-linux-x86_64.AppImage" | sha256sum | awk '{ print $1 }')
echo "x86_64 SHA256: $APPIMAGE_X64_SHA256"
echo "Streaming arm64 AppImage to compute SHA256..."
APPIMAGE_ARM64_SHA256=$(curl -fSL "${BASE_URL}/Actual-linux-arm64.AppImage" | sha256sum | awk '{ print $1 }')
echo "arm64 SHA256: $APPIMAGE_ARM64_SHA256"
echo "APPIMAGE_X64_SHA256=$APPIMAGE_X64_SHA256" >> "$GITHUB_ENV"
echo "APPIMAGE_ARM64_SHA256=$APPIMAGE_ARM64_SHA256" >> "$GITHUB_ENV"
- name: Checkout Flathub repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: flathub/com.actualbudget.actual
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
- name: Update manifest with new version
run: |
VERSION="${{ steps.resolve_version.outputs.version }}"
# Replace x86_64 entry
sed -i "/x86_64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_X64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/x86_64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-x86_64.AppImage|" com.actualbudget.actual.yml
# Replace arm64 entry
sed -i "/arm64.AppImage/{n;s|sha256:.*|sha256: ${{ env.APPIMAGE_ARM64_SHA256 }}|}" com.actualbudget.actual.yml
sed -i "/arm64.AppImage/s|url:.*|url: https://github.com/actualbudget/actual/releases/download/v${VERSION}/Actual-linux-arm64.AppImage|" com.actualbudget.actual.yml
echo "Updated manifest:"
cat com.actualbudget.actual.yml
- name: Create PR in Flathub repo
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
branch: 'release/${{ steps.resolve_version.outputs.version }}'
title: 'Update Actual flatpak to version ${{ steps.resolve_version.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ steps.resolve_version.outputs.version }}.
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.resolve_version.outputs.version }})
reviewers: 'jfdoming,MatissJanis,youngcw'

View File

@@ -28,7 +28,7 @@ jobs:
runs-on: ${{ matrix.os }}
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- if: ${{ startsWith(matrix.os, 'windows') }}
run: pip.exe install setuptools
@@ -39,6 +39,9 @@ jobs:
source .venv/bin/activate
python3 -m pip install setuptools
- name: Set up environment
uses: ./.github/actions/setup
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
name: Setup Flatpak dependencies
run: |
@@ -53,16 +56,14 @@ jobs:
METAINFO_FILE="packages/desktop-electron/extra-resources/linux/com.actualbudget.actual.metainfo.xml"
TODAY=$(date +%Y-%m-%d)
VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
sed -i "s/%RELEASE_VERSION%/$VERSION/g; s/%RELEASE_DATE%/$TODAY/g" "$METAINFO_FILE"
flatpak run --command=flatpak-builder-lint org.flatpak.Builder appstream "$METAINFO_FILE"
- name: Set up environment
uses: ./.github/actions/setup
- name: Update package versions
run: |
# Get new nightly version
NEW_DESKTOP_APP_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-electron/package.json --type nightly)
NEW_DESKTOP_APP_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-electron/package.json --type nightly)
# Set package version
npm version $NEW_DESKTOP_APP_VERSION --no-git-tag-version --workspace=desktop-electron --no-workspaces-update
@@ -82,49 +83,49 @@ jobs:
run: ./bin/package-electron
- name: Upload Linux x64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-x86_64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-x86_64.AppImage
- name: Upload Linux arm64 AppImage
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-linux-arm64.AppImage
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-linux-arm64.AppImage
- name: Upload Windows x32 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-ia32.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-ia32.exe
- name: Upload Windows x64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-x64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-x64.exe
- name: Upload Windows arm64 exe
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-windows-arm64.exe
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-windows-arm64.exe
- name: Upload Mac x64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-x64.dmg
if-no-files-found: ignore
path: packages/desktop-electron/dist/Actual-mac-x64.dmg
- name: Upload Mac arm64 dmg
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Actual-mac-arm64.dmg
if-no-files-found: ignore
@@ -132,7 +133,7 @@ jobs:
- name: Upload Windows Store Build
if: ${{ startsWith(matrix.os, 'windows') }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: actual-electron-${{ matrix.os }}-appx
path: |

View File

@@ -1,6 +1,6 @@
name: Publish nightly npm packages
# Nightly npm packages are built daily
# Nightly npm packages are built daily at midnight UTC
on:
schedule:
- cron: '0 0 * * *'
@@ -12,7 +12,7 @@ jobs:
name: Build and pack npm packages
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
@@ -20,19 +20,27 @@ jobs:
- name: Update package versions
run: |
# Get new nightly versions
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
NEW_CORE_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/loot-core/package.json --type nightly)
NEW_WEB_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/api/package.json --type nightly)
NEW_CLI_VERSION=$(yarn workspace @actual-app/ci-actions tsx bin/get-next-package-version.ts --package-json ./packages/cli/package.json --type nightly)
# Set package versions
npm version $NEW_CORE_VERSION --no-git-tag-version --workspace=@actual-app/core --no-workspaces-update
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
npm version $NEW_CLI_VERSION --no-git-tag-version --workspace=@actual-app/cli --no-workspaces-update
- name: Yarn install
run: |
yarn install
- name: Pack the core package
run: |
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
- name: Build Server & Web
run: yarn build:server
@@ -48,14 +56,23 @@ jobs:
run: |
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
- name: Build CLI
run: yarn workspace @actual-app/cli build
- name: Pack the cli package
run: |
yarn workspace @actual-app/cli pack --filename @actual-app/cli.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: npm-packages
path: |
packages/loot-core/@actual-app/core.tgz
packages/desktop-client/@actual-app/web.tgz
packages/sync-server/@actual-app/sync-server.tgz
packages/api/@actual-app/api.tgz
packages/cli/@actual-app/cli.tgz
publish:
runs-on: ubuntu-latest
@@ -66,16 +83,22 @@ jobs:
packages: write
steps:
- name: Download the artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-packages
- name: Setup node and npm registry
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Publish Core
run: |
npm publish loot-core/@actual-app/core.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public --tag nightly
@@ -93,3 +116,9 @@ jobs:
npm publish api/@actual-app/api.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish CLI
run: |
npm publish cli/@actual-app/cli.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -11,11 +11,15 @@ jobs:
runs-on: ubuntu-latest
name: Build and pack npm packages
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up environment
uses: ./.github/actions/setup
- name: Pack the core package
run: |
yarn workspace @actual-app/core pack --filename @actual-app/core.tgz
- name: Build Web
run: yarn build:server
@@ -31,14 +35,23 @@ jobs:
run: |
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
- name: Build CLI
run: yarn workspace @actual-app/cli build
- name: Pack the cli package
run: |
yarn workspace @actual-app/cli pack --filename @actual-app/cli.tgz
- name: Upload package artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: npm-packages
path: |
packages/loot-core/@actual-app/core.tgz
packages/desktop-client/@actual-app/web.tgz
packages/sync-server/@actual-app/sync-server.tgz
packages/api/@actual-app/api.tgz
packages/cli/@actual-app/cli.tgz
publish:
runs-on: ubuntu-latest
@@ -49,16 +62,22 @@ jobs:
packages: write
steps:
- name: Download the artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: npm-packages
- name: Setup node and npm registry
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- name: Publish Core
run: |
npm publish loot-core/@actual-app/core.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Web
run: |
npm publish desktop-client/@actual-app/web.tgz --access public
@@ -76,3 +95,9 @@ jobs:
npm publish api/@actual-app/api.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish CLI
run: |
npm publish cli/@actual-app/cli.tgz --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Get changed files

View File

@@ -35,7 +35,7 @@ jobs:
contents: read
steps:
- name: Checkout base branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
- name: Set up environment
@@ -57,6 +57,13 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: api
ref: ${{github.base_ref}}
- name: Wait for ${{github.base_ref}} CLI build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: master-cli-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli
ref: ${{github.base_ref}}
- name: Wait for PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
@@ -72,15 +79,22 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: api
ref: ${{github.event.pull_request.head.sha}}
- name: Wait for CLI PR build to succeed
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
id: wait-for-cli-build
with:
token: ${{ secrets.GITHUB_TOKEN }}
checkName: cli
ref: ${{github.event.pull_request.head.sha}}
- name: Report build failure
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure'
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure' || steps.wait-for-cli-build.outputs.conclusion == 'failure'
run: |
echo "Build failed on PR branch or ${{github.base_ref}}"
exit 1
- name: Download web build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
id: pr-web-build
with:
branch: ${{github.base_ref}}
@@ -89,7 +103,7 @@ jobs:
name: build-stats
path: base
- name: Download API build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
id: pr-api-build
with:
branch: ${{github.base_ref}}
@@ -98,7 +112,7 @@ jobs:
name: api-build-stats
path: base
- name: Download build stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
@@ -107,7 +121,7 @@ jobs:
path: head
allow_forks: true
- name: Download API stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@1f8785ff7a5130826f848e7f72725c85d241860f # v18
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
@@ -115,6 +129,23 @@ jobs:
name: api-build-stats
path: head
allow_forks: true
- name: Download CLI build artifact from ${{github.base_ref}}
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
with:
branch: ${{github.base_ref}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: cli-build-stats
path: base
- name: Download CLI stats from PR
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
with:
pr: ${{github.event.pull_request.number}}
workflow: build.yml
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
name: cli-build-stats
path: head
allow_forks: true
- name: Strip content hashes from stats files
run: |
if [ -f ./head/web-stats.json ]; then
@@ -136,10 +167,13 @@ jobs:
--base desktop-client=./base/web-stats.json \
--base loot-core=./base/loot-core-stats.json \
--base api=./base/api-stats.json \
--base cli=./base/cli-stats.json \
--head desktop-client=./head/web-stats.json \
--head loot-core=./head/loot-core-stats.json \
--head api=./head/api-stats.json \
--identifier combined > bundle-stats-comment.md
--head cli=./head/cli-stats.json \
--identifier combined \
--format pr-body > bundle-stats-comment.md
- name: Post combined bundle stats comment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -148,4 +182,5 @@ jobs:
run: |
node packages/ci-actions/bin/update-bundle-stats-comment.mjs \
--comment-file bundle-stats-comment.md \
--identifier '<!--- bundlestats-action-comment key:combined --->'
--identifier combined \
--target pr-body

View File

@@ -8,7 +8,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
@@ -18,7 +18,7 @@ jobs:
stale-wip:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
days-before-stale: 7
@@ -29,7 +29,7 @@ jobs:
stale-needs-info:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
stale-issue-label: 'needs info'
days-before-stale: -1

View File

@@ -19,7 +19,7 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download patch artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -27,7 +27,7 @@ jobs:
path: /tmp/artifacts
- name: Download metadata artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -54,7 +54,7 @@ jobs:
- name: Checkout fork branch
if: steps.metadata.outputs.pr_number != ''
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ steps.metadata.outputs.head_repo }}
ref: ${{ steps.metadata.outputs.head_ref }}

View File

@@ -44,7 +44,7 @@ jobs:
github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.57.0-jammy
image: mcr.microsoft.com/playwright:v1.58.2-jammy
steps:
- name: Get PR details
id: pr
@@ -60,7 +60,7 @@ jobs:
core.setOutput('head_ref', pr.head.ref);
core.setOutput('head_repo', pr.head.repo.full_name);
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.pr.outputs.head_sha }}
@@ -113,7 +113,7 @@ jobs:
- name: Upload patch artifact
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vrt-patch-${{ github.event.issue.number }}
path: vrt-update.patch
@@ -129,7 +129,7 @@ jobs:
- name: Upload PR metadata
if: steps.create-patch.outputs.has_changes == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: vrt-metadata-${{ github.event.issue.number }}
path: pr-metadata/

12
.gitignore vendored
View File

@@ -33,7 +33,9 @@ packages/desktop-electron/dist
packages/desktop-electron/loot-core
packages/desktop-client/service-worker
packages/plugins-service/dist
packages/component-library/dist
packages/loot-core/lib-dist
**/.tsbuildinfo
packages/sync-server/coverage
bundle.desktop.js
bundle.desktop.js.map
@@ -76,3 +78,13 @@ build/
# Lage cache
.lage/
*storybook.log
storybook-static
# cli config when testing locally
.actualrc.json
.actualrc
.actualrc.yaml
.actualrc.yml
actual.config.js

13
.husky/post-checkout Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
# Run yarn install when switching branches (if yarn.lock changed)
# $3 is 1 for branch checkout, 0 for file checkout
if [ "$3" != "1" ]; then
exit 0
fi
# Check if yarn.lock changed between the old and new HEAD
if git diff --name-only "$1" "$2" | grep -q "^yarn.lock$"; then
echo "yarn.lock changed — running yarn install..."
yarn install
fi

7
.husky/post-merge Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Run yarn install after pulling/merging (if yarn.lock changed)
if git diff --name-only ORIG_HEAD HEAD | grep -q "^yarn.lock$"; then
echo "yarn.lock changed — running yarn install..."
yarn install
fi

View File

@@ -4,7 +4,31 @@
"trailingComma": "all",
"arrowParens": "avoid",
"printWidth": 80,
"ignorePatterns": [
"packages/docs/*" // TOOD: fixme; temporary
]
"experimentalSortImports": {
"groups": [
"react",
"builtin",
"external",
"loot-core",
["parent", "subpath"],
"sibling",
"index",
"desktop-client"
],
"customGroups": [
{
"groupName": "react",
"elementNamePattern": ["react", "react-dom/*", "react-*"]
},
{
"groupName": "loot-core",
"elementNamePattern": ["loot-core/**", "@actual-app/core/**"]
},
{
"groupName": "desktop-client",
"elementNamePattern": ["@desktop-client/**"]
}
],
"newlinesBetween": true
}
}

View File

@@ -18,104 +18,74 @@
"FS": "readonly"
},
"rules": {
// TODO fix all these and re-enable
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/prefer-tag-over-role": "off",
"jsx-a11y/tabindex-no-positive": "off",
// Import sorting
// TODO replace once oxfmt supports this: https://github.com/oxc-project/oxc/issues/17076
"perfectionist/sort-imports": [
"warn",
"perfectionist/sort-named-imports": [
"error",
{
"groups": [
"react",
"builtin",
"external",
"loot-core",
"parent",
"sibling",
"index",
"desktop-client"
],
"customGroups": [
{
"groupName": "react",
"elementNamePattern": "^react(-.*)?$"
},
{
"groupName": "loot-core",
"elementNamePattern": "^loot-core"
},
{
"groupName": "desktop-client",
"elementNamePattern": "^@desktop-client"
}
],
"newlinesBetween": "always"
"groups": ["value-import", "type-import"]
}
],
// Actual rules
"actual/typography": "warn",
"actual/typography": "error",
"actual/no-untranslated-strings": "error",
"actual/prefer-trans-over-t": "error",
"actual/prefer-if-statement": "warn",
"actual/prefer-if-statement": "error",
"actual/prefer-logger-over-console": "error",
"actual/object-shorthand-properties": "warn",
"actual/prefer-const": "warn",
"actual/no-anchor-tag": "warn",
"actual/no-react-default-import": "warn",
"actual/object-shorthand-properties": "error",
"actual/prefer-const": "error",
"actual/no-anchor-tag": "error",
"actual/no-react-default-import": "error",
// JSX A11y rules
"jsx-a11y/no-autofocus": [
"warn",
"error",
{
"ignoreNonDOM": true
}
],
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/alt-text": "error",
"jsx-a11y/anchor-has-content": "error",
"jsx-a11y/anchor-is-valid": [
"warn",
"error",
{
"aspects": ["noHref", "invalidHref"]
}
],
"jsx-a11y/aria-activedescendant-has-tabindex": "warn",
"jsx-a11y/aria-props": "warn",
"jsx-a11y/aria-proptypes": "warn",
"jsx-a11y/aria-activedescendant-has-tabindex": "error",
"jsx-a11y/aria-props": "error",
"jsx-a11y/aria-proptypes": "error",
"jsx-a11y/aria-role": [
"warn",
"error",
{
"ignoreNonDOM": true
}
],
"jsx-a11y/aria-unsupported-elements": "warn",
"jsx-a11y/heading-has-content": "warn",
"jsx-a11y/iframe-has-title": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"jsx-a11y/no-access-key": "warn",
"jsx-a11y/no-distracting-elements": "warn",
"jsx-a11y/no-redundant-roles": "warn",
"jsx-a11y/role-has-required-aria-props": "warn",
"jsx-a11y/role-supports-aria-props": "warn",
"jsx-a11y/scope": "warn",
"jsx-a11y/aria-unsupported-elements": "error",
"jsx-a11y/heading-has-content": "error",
"jsx-a11y/iframe-has-title": "error",
"jsx-a11y/img-redundant-alt": "error",
"jsx-a11y/no-access-key": "error",
"jsx-a11y/no-distracting-elements": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/role-supports-aria-props": "error",
"jsx-a11y/scope": "error",
// Typescript rules
"typescript/ban-ts-comment": ["warn"],
"typescript/consistent-type-definitions": ["warn", "type"],
"typescript/ban-ts-comment": ["error"],
"typescript/consistent-type-definitions": ["error", "type"],
"typescript/consistent-type-imports": [
"warn",
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/no-implied-eval": "warn",
"typescript/no-explicit-any": "warn",
"typescript/no-implied-eval": "error",
"typescript/no-explicit-any": "error",
"typescript/no-restricted-types": [
"warn",
"error",
{
"types": {
// forbid FC as superfluous
@@ -128,141 +98,156 @@
}
}
],
"typescript/no-var-requires": "warn",
"typescript/no-var-requires": "error",
// we want to allow unions such as "{ name: DbAccount['name'] | DbPayee['name'] }"
"typescript/no-duplicate-type-constituents": "off",
// we want to allow unions such as "string | 'network' | 'file-key-mismatch'"
"typescript/no-redundant-type-constituents": "off",
"typescript/await-thenable": "error",
"typescript/no-floating-promises": "error",
"typescript/require-array-sort-compare": "error",
"typescript/unbound-method": "error",
"typescript/no-for-in-array": "error",
"typescript/restrict-template-expressions": "error",
"typescript/no-misused-spread": "warn", // TODO: enable this
"typescript/no-base-to-string": "warn", // TODO: enable this
"typescript/no-unsafe-unary-minus": "warn", // TODO: enable this
"typescript/no-unsafe-type-assertion": "warn", // TODO: enable this
// Import rules
"import/consistent-type-specifier-style": "error",
"import/first": "error",
"import/no-amd": "error",
"import/no-default-export": "warn",
"import/no-default-export": "error",
"import/no-webpack-loader-syntax": "error",
"import/no-useless-path-segments": "warn",
"import/no-unresolved": "warn",
"import/no-unused-modules": "warn",
"import/no-useless-path-segments": "error",
"import/no-unresolved": "error",
"import/no-unused-modules": "error",
"import/no-duplicates": [
"warn",
"error",
{
"prefer-inline": true
"prefer-inline": false
}
],
// React rules
"react/exhaustive-deps": [
"warn",
"error",
{
"additionalHooks": "(useQuery|useEffectAfterMount)"
"additionalHooks": "(^useQuery$|^useEffectAfterMount$)"
}
],
"react/jsx-curly-brace-presence": "warn",
"react/jsx-curly-brace-presence": "error",
"react/jsx-filename-extension": [
"warn",
"error",
{
"extensions": [".jsx", ".tsx"],
"allow": "as-needed"
}
],
"react/jsx-no-comment-textnodes": "warn",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-target-blank": "warn",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-undef": "error",
"react/jsx-no-useless-fragment": "warn",
"react/jsx-no-useless-fragment": "error",
"react/jsx-pascal-case": [
"warn",
"error",
{
"allowAllCaps": true,
"ignore": []
}
],
"react/no-danger-with-children": "warn",
"react/no-direct-mutation-state": "warn",
"react/no-is-mounted": "warn",
"react/no-unstable-nested-components": "warn",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-is-mounted": "error",
"react/no-unstable-nested-components": "error",
"react/require-render-return": "error",
"react/rules-of-hooks": "error",
"react/self-closing-comp": "warn",
"react/style-prop-object": "warn",
"react/jsx-boolean-value": "warn",
"react/self-closing-comp": "error",
"react/style-prop-object": "error",
"react/jsx-boolean-value": "error",
// ESLint rules
"eslint/array-callback-return": "warn",
"eslint/curly": ["warn", "multi-line", "consistent"],
"eslint/array-callback-return": "error",
"eslint/curly": ["error", "multi-line", "consistent"],
"eslint/default-case": [
"warn",
"error",
{
"commentPattern": "^no default$"
}
],
"eslint/eqeqeq": ["warn", "smart"],
"eslint/no-array-constructor": "warn",
"eslint/no-caller": "warn",
"eslint/no-cond-assign": ["warn", "except-parens"],
"eslint/no-const-assign": "warn",
"eslint/no-control-regex": "warn",
"eslint/no-delete-var": "warn",
"eslint/no-dupe-class-members": "warn",
"eslint/no-dupe-keys": "warn",
"eslint/no-duplicate-case": "warn",
"eslint/no-empty-character-class": "warn",
"eslint/no-empty-function": "warn",
"eslint/no-empty-pattern": "warn",
"eslint/no-eval": "warn",
"eslint/no-ex-assign": "warn",
"eslint/no-extend-native": "warn",
"eslint/no-extra-bind": "warn",
"eslint/no-extra-label": "warn",
"eslint/no-fallthrough": "warn",
"eslint/no-func-assign": "warn",
"eslint/no-invalid-regexp": "warn",
"eslint/no-iterator": "warn",
"eslint/no-label-var": "warn",
"eslint/no-var": "warn",
"eslint/eqeqeq": ["error", "smart"],
"eslint/no-array-constructor": "error",
"eslint/no-caller": "error",
"eslint/no-cond-assign": ["error", "except-parens"],
"eslint/no-const-assign": "error",
"eslint/no-control-regex": "error",
"eslint/no-delete-var": "error",
"eslint/no-dupe-class-members": "error",
"eslint/no-dupe-keys": "error",
"eslint/no-duplicate-case": "error",
"eslint/no-empty-character-class": "error",
"eslint/no-empty-function": "error",
"eslint/no-empty-pattern": "error",
"eslint/no-eval": "error",
"eslint/no-ex-assign": "error",
"eslint/no-extend-native": "error",
"eslint/no-extra-bind": "error",
"eslint/no-extra-label": "error",
"eslint/no-fallthrough": "error",
"eslint/no-func-assign": "error",
"eslint/no-invalid-regexp": "error",
"eslint/no-iterator": "error",
"eslint/no-label-var": "error",
"eslint/no-var": "error",
"eslint/no-labels": [
"warn",
"error",
{
"allowLoop": true,
"allowSwitch": false
}
],
"eslint/no-new-func": "warn",
"eslint/no-script-url": "warn",
"eslint/no-self-assign": "warn",
"eslint/no-self-compare": "warn",
"eslint/no-sequences": "warn",
"eslint/no-shadow-restricted-names": "warn",
"eslint/no-sparse-arrays": "warn",
"eslint/no-template-curly-in-string": "warn",
"eslint/no-this-before-super": "warn",
"eslint/no-throw-literal": "warn",
"eslint/no-unreachable": "warn",
"eslint/no-obj-calls": "warn",
"eslint/no-new-wrappers": "warn",
"eslint/no-unsafe-negation": "warn",
"eslint/no-multi-str": "warn",
"eslint/no-global-assign": "warn",
"eslint/no-lone-blocks": "warn",
"eslint/no-unused-labels": "warn",
"eslint/no-object-constructor": "warn",
"eslint/no-new-native-nonconstructor": "warn",
"eslint/no-redeclare": "warn",
"eslint/no-useless-computed-key": "warn",
"eslint/no-useless-concat": "warn",
"eslint/no-useless-escape": "warn",
"eslint/require-yield": "warn",
"eslint/getter-return": "warn",
"eslint/unicode-bom": ["warn", "never"],
"eslint/no-use-isnan": "warn",
"eslint/valid-typeof": "warn",
"eslint/no-new-func": "error",
"eslint/no-script-url": "error",
"eslint/no-self-assign": "error",
"eslint/no-self-compare": "error",
"eslint/no-sequences": "error",
"eslint/no-shadow-restricted-names": "error",
"eslint/no-sparse-arrays": "error",
"eslint/no-template-curly-in-string": "error",
"eslint/no-this-before-super": "error",
"eslint/no-throw-literal": "error",
"eslint/no-unreachable": "error",
"eslint/no-obj-calls": "error",
"eslint/no-new-wrappers": "error",
"eslint/no-unsafe-negation": "error",
"eslint/no-multi-str": "error",
"eslint/no-global-assign": "error",
"eslint/no-lone-blocks": "error",
"eslint/no-unused-labels": "error",
"eslint/no-object-constructor": "error",
"eslint/no-new-native-nonconstructor": "error",
"eslint/no-redeclare": "error",
"eslint/no-useless-computed-key": "error",
"eslint/no-useless-concat": "error",
"eslint/no-useless-escape": "error",
"eslint/require-yield": "error",
"eslint/getter-return": "error",
"eslint/unicode-bom": ["error", "never"],
"eslint/no-use-isnan": "error",
"eslint/valid-typeof": "error",
"eslint/no-useless-rename": [
"warn",
"error",
{
"ignoreDestructuring": false,
"ignoreImport": false,
"ignoreExport": false
}
],
"eslint/no-with": "warn",
"eslint/no-regex-spaces": "warn",
"eslint/no-with": "error",
"eslint/no-regex-spaces": "error",
"eslint/no-restricted-globals": [
"warn",
"error",
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
"addEventListener",
@@ -324,7 +309,7 @@
"top"
],
"eslint/no-restricted-imports": [
"warn",
"error",
{
"paths": [
{
@@ -363,6 +348,10 @@
"importNames": ["colors"],
"message": "Please use themes instead of colors"
},
{
"group": ["**/style/themes/*"],
"message": "Please do not import theme files directly"
},
{
"group": ["@actual-app/web/**/*"],
"message": "Please do not import `@actual-app/web` in `loot-core`"
@@ -370,61 +359,15 @@
]
}
],
"eslint/no-useless-constructor": "warn",
"eslint/no-undef": "warn",
"eslint/no-unused-expressions": "warn"
"eslint/no-useless-constructor": "error",
"eslint/no-undef": "error",
"eslint/no-unused-expressions": "error"
},
"overrides": [
{
// TODO: fix the issues in these files
"files": [
"packages/component-library/src/Menu.tsx",
"packages/desktop-client/src/components/accounts/Account.jsx",
"packages/desktop-client/src/components/accounts/MobileAccount.jsx",
"packages/desktop-client/src/components/accounts/MobileAccounts.jsx",
"packages/desktop-client/src/components/budget/BudgetCategories.jsx",
"packages/desktop-client/src/components/budget/BudgetSummaries.tsx",
"packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx",
"packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx",
"packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx",
"packages/desktop-client/src/components/budget/index.tsx",
"packages/desktop-client/src/components/budget/MobileBudget.tsx",
"packages/desktop-client/src/components/FinancesApp.tsx",
"packages/desktop-client/src/components/GlobalKeys.ts",
"packages/desktop-client/src/components/LoggedInUser.tsx",
"packages/desktop-client/src/components/manager/ManagementApp.jsx",
"packages/desktop-client/src/components/manager/subscribe/common.tsx",
"packages/desktop-client/src/components/ManageRules.tsx",
"packages/desktop-client/src/components/mobile/MobileAmountInput.jsx",
"packages/desktop-client/src/components/mobile/MobileNavTabs.tsx",
"packages/desktop-client/src/components/Modals.tsx",
"packages/desktop-client/src/components/modals/EditRule.jsx",
"packages/desktop-client/src/components/modals/ImportTransactions.jsx",
"packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx",
"packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx",
"packages/desktop-client/src/components/Notifications.tsx",
"packages/desktop-client/src/components/payees/ManagePayees.jsx",
"packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx",
"packages/desktop-client/src/components/payees/PayeeTable.tsx",
"packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx",
"packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx",
"packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.jsx",
"packages/desktop-client/src/components/reports/reports/CustomReport.tsx",
"packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx",
"packages/desktop-client/src/components/reports/SaveReportName.tsx",
"packages/desktop-client/src/components/reports/useReport.ts",
"packages/desktop-client/src/components/schedules/ScheduleDetails.jsx",
"packages/desktop-client/src/components/schedules/ScheduleEditModal.tsx",
"packages/desktop-client/src/components/schedules/SchedulesTable.tsx",
"packages/desktop-client/src/components/select/DateSelect.tsx",
"packages/desktop-client/src/components/sidebar/Tools.tsx",
"packages/desktop-client/src/components/sort.tsx",
"packages/desktop-client/src/hooks/useEffectAfterMount.ts",
"packages/desktop-client/src/hooks/useQuery.ts"
],
"files": ["packages/desktop-electron/**/*"],
"rules": {
"react/exhaustive-deps": "off"
"react/rules-of-hooks": "off"
}
},
{
@@ -461,6 +404,12 @@
"typescript-paths/absolute-import": ["error", { "enableAlias": false }]
}
},
{
"files": ["packages/desktop-client/src/style/themes/*"],
"rules": {
"eslint/no-restricted-imports": "off"
}
},
// TODO: enable these
{
"files": [

View File

@@ -42,6 +42,12 @@ yarn start:desktop
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
- Tests run once and exit by default (using `vitest --run`)
### ⚠️ CRITICAL REQUIREMENT: AI-Generated Commit Messages and PR Titles
**ALL commit messages and PR titles MUST be prefixed with `[AI]`.** No exceptions.
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for the full specification, including git safety rules, pre-commit checklist, and PR workflow.
### Task Orchestration with Lage
The project uses **[lage](https://microsoft.github.io/lage/)** (a task runner for JavaScript monorepos) to efficiently run tests and other tasks across multiple workspaces:
@@ -78,7 +84,7 @@ The core application logic that runs on any platform.
```bash
# Run all loot-core tests
yarn workspace loot-core run test
yarn workspace @actual-app/core run test
# Or run tests across all packages using lage
yarn test
@@ -213,7 +219,7 @@ yarn test
yarn test:debug
# Run tests for a specific package
yarn workspace loot-core run test
yarn workspace @actual-app/core run test
```
**E2E Tests (Playwright)**
@@ -259,6 +265,10 @@ Always run `yarn typecheck` before committing.
- Generate i18n files: `yarn generate:i18n`
- Custom ESLint rules enforce translation usage
### 5. Financial Number Typography
- Wrap standalone financial numbers with `FinancialText` or apply `styles.tnum` directly if wrapping is not possible
## Code Style & Conventions
### TypeScript Guidelines
@@ -288,6 +298,7 @@ Always run `yarn typecheck` before committing.
**React Patterns:**
- The project uses **React Compiler** (`babel-plugin-react-compiler`) in the desktop-client. The compiler auto-memoizes component bodies, so you can omit manual `useCallback`, `useMemo`, and `React.memo` when adding or refactoring code; prefer inline callbacks and values unless a stable identity is required by a non-compiled dependency.
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
- Don't use `React.*` patterns - use named imports instead
- Use `<Link>` instead of `<a>` tags
@@ -334,11 +345,7 @@ Always maintain newlines between import groups.
**Git Commands:**
- Never update git config
- Never run destructive git operations (force push, hard reset) unless explicitly requested
- Never skip hooks (--no-verify, --no-gpg-sign)
- Never force push to main/master
- Never commit unless explicitly asked
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete git safety rules, commit message requirements, and PR workflow.
## File Structure Patterns
@@ -501,7 +508,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
2. Reinstall dependencies: `yarn install`
3. Check Node.js version (requires >=20)
3. Check Node.js version (requires >=22)
4. Check Yarn version (requires ^4.9.1)
## Testing Patterns
@@ -537,6 +544,7 @@ Icons in `packages/component-library/src/icons/` are auto-generated. Don't manua
Before committing changes, ensure:
- [ ] Commit and PR rules followed (see [PR and Commit Rules](.github/agents/pr-and-commit-rules.md))
- [ ] `yarn typecheck` passes
- [ ] `yarn lint:fix` has been run
- [ ] Relevant tests pass
@@ -549,9 +557,11 @@ Before committing changes, ensure:
## Pull Request Guidelines
When creating pull requests:
See [PR and Commit Rules](.github/agents/pr-and-commit-rules.md) for complete PR creation rules, including title prefix requirements, labeling, and PR template handling.
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
## Code Review Guidelines
When performing code reviews (especially for LLM agents): **see [CODE_REVIEW_GUIDELINES.md](./CODE_REVIEW_GUIDELINES.md)** for specific guidelines.
## Performance Considerations
@@ -578,7 +588,7 @@ yarn install:server
## Environment Requirements
- **Node.js**: >=20
- **Node.js**: >=22
- **Yarn**: ^4.9.1 (managed by packageManager field)
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
@@ -591,3 +601,40 @@ The codebase is actively being migrated:
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
When working with older code, follow the newer patterns described in this guide.
## Cursor Cloud specific instructions
### Services overview
| Service | Command | Port | Required |
| ------------------- | ----------------------- | ---- | ----------------------------- |
| Web Frontend (Vite) | `yarn start` | 3001 | Yes |
| Sync Server | `yarn start:server-dev` | 5006 | Optional (sync features only) |
All storage is **SQLite** (file-based via `better-sqlite3`). No external databases or services are needed.
### Running the app
- `yarn start` builds the plugins-service worker, loot-core browser backend, and starts the Vite dev server on port **3001**.
- `yarn start:server-dev` starts both the sync server (port 5006) and the web frontend together.
- The Vite HMR dev server serves many unbundled modules. In constrained environments, the browser may hit `ERR_INSUFFICIENT_RESOURCES`. If that happens, use `yarn build:browser` followed by serving the built output from `packages/desktop-client/build/` with proper COOP/COEP headers (`Cross-Origin-Opener-Policy: same-origin`, `Cross-Origin-Embedder-Policy: require-corp`).
### Lint, test, typecheck
Standard commands documented in `package.json` scripts and the Quick Start section above:
- `yarn lint` / `yarn lint:fix` (uses oxlint + oxfmt)
- `yarn test` (lage across all workspaces)
- `yarn typecheck` (tsgo + lage typecheck)
### Testing and previewing the app
When running the app for manual testing or demos, use **"View demo"** on the initial setup screen (after selecting "Don't use a server"). This creates a test budget pre-populated with realistic sample data (accounts, transactions, categories, and budgeted amounts), which is far more useful than starting with an empty budget.
### Gotchas
- The `engines` field requires **Node.js >=22** and **Yarn ^4.9.1**. The `.nvmrc` specifies `v22/*`.
- Pre-commit hook runs `lint-staged` (oxfmt + oxlint) via Husky. Run `yarn prepare` once after install to set up hooks.
- Lage caches test results in `.lage/`. If tests behave unexpectedly, clear with `rm -rf .lage`.
- Native modules (`better-sqlite3`, `bcrypt`) require build tools (`gcc`, `make`, `python3`). These are pre-installed in the Cloud VM.
- All yarn commands must be run from the repository root, never from child workspaces.

2
CLAUDE.md Normal file
View File

@@ -0,0 +1,2 @@
@AGENTS.md
@.github/agents/pr-and-commit-rules.md

94
CODE_REVIEW_GUIDELINES.md Normal file
View File

@@ -0,0 +1,94 @@
# CODE_REVIEW_GUIDELINES.md - Guidelines for LLM Agents Performing Code Reviews
This document provides specific guidelines for LLM agents performing code reviews on the Actual Budget codebase. These guidelines help maintain code quality, consistency, and follow the project's design principles.
## Settings Proliferation
**Do NOT add new settings for every little UI tweak.**
Actual Budget follows a design philosophy that prioritizes simplicity and avoids settings bloat. Before introducing code that adds new settings:
- Consider if the UI tweak can be achieved through existing theme/design tokens
- Evaluate whether the setting provides meaningful value to users
- Check if the change aligns with Actual's design guidelines
- Prefer hardcoded values or theme-based solutions over adding user-facing settings
## TypeScript Strict Mode Suppressions
**Do NOT approve code that adds new `@ts-strict-ignore` comments.**
The project uses strict TypeScript checking via `typescript-strict-plugin`. Adding `@ts-strict-ignore` comments undermines type safety. Instead, review should encourage:
- Fixing the underlying type issue
- Using proper type definitions
- Refactoring code to satisfy strict type checking
- Only in exceptional cases, document why strict checking cannot be applied and seek alternative solutions
## Linter Suppressions
**Do NOT approve code that adds new `eslint-disable` or `oxlint-disable` comments.**
Linter rules are in place for good reasons. Instead of suppressing them:
- Fix the underlying issue
- If the rule is incorrectly flagging valid code, consider if the code can be refactored
- Only approve suppressions if there's a documented, exceptional reason
## Type Assertions
**Prefer `x satisfies SomeType` over `x as SomeType` for type coercions.**
The `satisfies` operator provides better type safety by:
- Ensuring the value actually satisfies the type (narrowing)
- Preserving the actual type information for better inference
- Catching type mismatches at compile time
**Exception:** If you truly need to assert a type that TypeScript cannot verify (e.g., runtime type guards), use `as` but require a comment explaining why it's safe.
## Avoiding `any` and `unknown`
**Flag code that uses `any` or `unknown` unless absolutely necessary.**
The use of `any` or `unknown` should be rare and well-justified. Before approving:
- Require explicit justification for why the type cannot be determined
- Suggest using proper type definitions or generics
- Consider if the type can be narrowed or properly inferred
- Look for existing type definitions in `packages/loot-core/src/types/`
Only approve `any` or `unknown` if there's a documented, exceptional reason (e.g., interop with untyped external libraries, gradual migration).
## Internationalization (i18n)
**All user-facing strings must be translated.**
The project has custom ESLint rules (`actual/no-untranslated-strings`) that enforce i18n usage, but reviewers should actively flag untranslated strings:
- Use `Trans` component instead of `t()` function when possible
- All text visible to users must use i18n functions
- Flag hardcoded strings that should be translated
## Test Mocking
**Minimize mocked dependencies; prefer real implementations.**
When reviewing tests, encourage the use of real implementations over mocks:
- Prefer real dependencies, utilities, and data structures
- Only mock when the real implementation is impractical (e.g., external APIs, file system in unit tests)
- Ensure mocks accurately represent real behavior
Over-mocking makes tests brittle and less reliable. Real implementations provide better confidence that code works correctly.
## Financial Number Typography
Standalone financial numbers should have tabular number styles applied.
- Standalone financial numbers should be wrapped with `FinancialText` or `styles.tnum` should be applied directly if wrapping is not possible
## Related Documentation
- See [AGENTS.md](./AGENTS.md) for general development guidelines
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines
- Community documentation: [https://actualbudget.org/docs/contributing/](https://actualbudget.org/docs/contributing/)

View File

@@ -17,7 +17,7 @@ packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace plugins-service build
yarn workspace loot-core build:browser
yarn workspace @actual-app/core build:browser
yarn workspace @actual-app/web build:browser
echo "packages/desktop-client/build"

View File

@@ -51,14 +51,17 @@ fi
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace plugins-service build
yarn workspace loot-core build:node
yarn workspace @actual-app/core build:node
yarn workspace @actual-app/web build --mode=desktop # electron specific build
# required for running the sync-server server
yarn workspace loot-core build:browser
yarn workspace @actual-app/core build:browser
yarn workspace @actual-app/web build:browser
yarn workspace @actual-app/sync-server build
# Emit @actual-app/core declarations so desktop-electron (which includes typings/window.ts) can build
yarn workspace @actual-app/core exec tsgo -p tsconfig.json
yarn workspace desktop-electron update-client
(

23
bin/package-mobile Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/bash -e
ROOT=`dirname $0`
cd "$ROOT/.."
echo "Building web assets for mobile..."
# Build the browser version (same assets used by the Capacitor wrapper)
./bin/package-browser
echo "Syncing Capacitor iOS project..."
cd packages/mobile
npx cap sync ios
echo ""
echo "iOS project ready at: packages/mobile/ios/App/App.xcworkspace"
echo ""
echo "To build for the App Store:"
echo " 1. Open the project: yarn mobile:open"
echo " 2. Select your signing team in Xcode"
echo " 3. Archive: Product > Archive"
echo " 4. Distribute via App Store Connect"

View File

@@ -37,7 +37,7 @@ async function run() {
choices: [
{ title: '✨ Features', value: 'Features' },
{ title: '👍 Enhancements', value: 'Enhancements' },
{ title: '🐛 Bugfix', value: 'Bugfix' },
{ title: '🐛 Bugfixes', value: 'Bugfixes' },
{ title: '⚙️ Maintenance', value: 'Maintenance' },
],
},
@@ -178,4 +178,4 @@ async function execAsync(cmd: string, errorLog?: string): Promise<string> {
});
}
run();
void run();

View File

@@ -1,29 +0,0 @@
#!/bin/sh
# Run browser tests (vitest browser mode) in Docker for consistent screenshot quality
# Browser tests generate screenshots that vary by environment (fonts, rendering, etc.)
# Running in Docker ensures consistent results across different machines
#
# Usage:
# yarn test:browser # Run all browser tests
# yarn test:browser AuthSettings.browser # Run specific test file
# yarn test:browser --update # Update snapshots
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
yarn
fi
TEST_ARGS=""
# Loop through all arguments
while [ $# -gt 0 ]; do
TEST_ARGS="$TEST_ARGS $1"
shift
done
echo "Running browser tests in Docker for consistent screenshot quality..."
echo "Test args: $TEST_ARGS"
echo ""
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash \
-c "yarn workspace @actual-app/web test --project=browser $TEST_ARGS"

View File

@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
echo "E2E_START_URL: $E2E_START_URL"
echo "VRT_ARGS: $VRT_ARGS"
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.57.0-jammy /bin/bash \
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.58.2-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -1,6 +1,10 @@
/** @type {import('lage').ConfigOptions} */
module.exports = {
pipeline: {
typecheck: {
type: 'npmScript',
dependsOn: ['^typecheck'],
},
test: {
type: 'npmScript',
options: {
@@ -13,6 +17,7 @@ module.exports = {
},
build: {
type: 'npmScript',
dependsOn: ['^build'],
cache: true,
options: {
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],

View File

@@ -25,26 +25,32 @@
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
"start:docs": "yarn workspace docs start",
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
"start:desktop-node": "yarn workspace loot-core watch:node",
"start:desktop-node": "yarn workspace @actual-app/core watch:node",
"start:desktop-client": "yarn workspace @actual-app/web watch",
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
"start:desktop-electron": "yarn workspace desktop-electron watch",
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
"start:service-plugins": "yarn workspace plugins-service watch",
"start:browser-backend": "yarn workspace loot-core watch:browser",
"start:browser-backend": "yarn workspace @actual-app/core watch:browser",
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
"build:browser-backend": "yarn workspace loot-core build:browser",
"start:storybook": "yarn workspace @actual-app/components start:storybook",
"build": "lage build",
"build:browser-backend": "yarn workspace @actual-app/core build:browser",
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
"build:browser": "./bin/package-browser",
"build:desktop": "./bin/package-electron",
"build:mobile": "yarn build:browser && yarn workspace @actual-app/mobile sync",
"mobile:open": "yarn workspace @actual-app/mobile open",
"mobile:run:ios": "yarn workspace @actual-app/mobile run:ios",
"build:plugins-service": "yarn workspace plugins-service build",
"build:api": "yarn workspace @actual-app/api build",
"build:cli": "yarn build --scope=@actual-app/cli",
"build:docs": "yarn workspace docs build",
"build:storybook": "yarn workspace @actual-app/components build:storybook",
"deploy:docs": "yarn workspace docs deploy",
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
"test": "lage test --continue",
"test:browser": "./bin/run-browser-tests",
"test:debug": "lage test --no-cache --continue",
"e2e": "yarn workspace @actual-app/web run e2e",
"e2e:desktop": "yarn build:desktop --skip-exe-build --skip-translations && yarn workspace desktop-electron e2e",
@@ -52,39 +58,44 @@
"vrt": "yarn workspace @actual-app/web run vrt",
"vrt:docker": "./bin/run-vrt",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
"rebuild-node": "yarn workspace loot-core rebuild",
"lint": "oxfmt --check . && oxlint --deny-warnings",
"lint:fix": "oxfmt . && oxlint --deny-warnings --fix",
"rebuild-node": "yarn workspace @actual-app/core rebuild",
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
"typecheck": "yarn tsc --incremental && tsc-strict",
"constraints": "yarn constraints",
"typecheck": "tsgo -p tsconfig.root.json --noEmit && lage typecheck",
"jq": "./node_modules/node-jq/bin/jq",
"prepare": "husky"
},
"devDependencies": {
"@octokit/rest": "^22.0.1",
"@types/node": "^22.19.3",
"@types/node": "^22.19.15",
"@types/prompts": "^2.4.9",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"@yarnpkg/types": "^4.0.1",
"baseline-browser-mapping": "^2.10.0",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-plugin-perfectionist": "^4.15.1",
"eslint": "^9.39.3",
"eslint-plugin-perfectionist": "^5.6.0",
"eslint-plugin-typescript-paths": "^0.0.33",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"lage": "^2.14.15",
"lint-staged": "^16.2.7",
"minimatch": "^10.1.1",
"lage": "^2.14.19",
"lint-staged": "^16.3.2",
"minimatch": "^10.2.4",
"node-jq": "^6.3.1",
"npm-run-all": "^4.1.5",
"oxfmt": "^0.22.0",
"oxlint": "^1.38.0",
"p-limit": "^7.2.0",
"oxfmt": "^0.32.0",
"oxlint": "^1.51.0",
"oxlint-tsgolint": "^0.13.0",
"p-limit": "^7.3.0",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-strict-plugin": "^2.4.4"
"typescript": "^5.9.3"
},
"resolutions": {
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
"rollup": "4.40.1",
"socks": ">=2.8.3"
},
@@ -93,7 +104,7 @@
"oxfmt --no-error-on-unmatched-pattern"
],
"*.{js,mjs,jsx,ts,tsx}": [
"oxlint --deny-warnings --fix"
"oxlint --fix --type-aware --quiet"
]
},
"browserslist": [

View File

@@ -1,4 +1,7 @@
class Query {
/** @type {import('loot-core/shared/query').QueryState} */
state;
constructor(state) {
this.state = {
filterExpressions: state.filterExpressions || [],

View File

@@ -3,26 +3,18 @@ import type {
RequestInit as FetchInit,
} from 'node-fetch';
// loot-core types
import type { InitConfig } from 'loot-core/server/main';
import { init as initLootCore } from '@actual-app/core/server/main';
import type { InitConfig, lib } from '@actual-app/core/server/main';
// oxlint-disable-next-line typescript/ban-ts-comment
// @ts-ignore: bundle not available until we build it
import * as bundle from './app/bundle.api.js';
import * as injected from './injected';
import { validateNodeVersion } from './validateNodeVersion';
let actualApp: null | typeof bundle.lib;
export const internal = bundle.lib;
export * from './methods';
export * as utils from './utils';
export async function init(config: InitConfig = {}) {
if (actualApp) {
return;
}
/** @deprecated Please use return value of `init` instead */
export let internal: typeof lib | null = null;
export async function init(config: InitConfig = {}) {
validateNodeVersion();
if (!globalThis.fetch) {
@@ -33,21 +25,19 @@ export async function init(config: InitConfig = {}) {
};
}
await bundle.init(config);
actualApp = bundle.lib;
injected.override(bundle.lib.send);
return bundle.lib;
internal = await initLootCore(config);
return internal;
}
export async function shutdown() {
if (actualApp) {
if (internal) {
try {
await actualApp.send('sync');
await internal.send('sync');
} catch {
// most likely that no budget is loaded, so the sync failed
}
await actualApp.send('close-budget');
actualApp = null;
await internal.send('close-budget');
internal = null;
}
}

View File

@@ -1,7 +0,0 @@
// TODO: comment on why it works this way
export let send;
export function override(sendImplementation) {
send = sendImplementation;
}

View File

@@ -1,10 +1,29 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { type RuleEntity } from 'loot-core/types/models';
import { vi } from 'vitest';
import type { RuleEntity } from '@actual-app/core/types/models';
import * as api from './index';
// In tests we run from source; loot-core's API fs uses __dirname (for the built dist/).
// Mock the fs so path constants point at loot-core package root where migrations live.
vi.mock(
'../loot-core/src/platform/server/fs/index.api',
async importOriginal => {
const actual = (await importOriginal()) as Record<string, unknown>;
const pathMod = await import('path');
const lootCoreRoot = pathMod.join(__dirname, '..', 'loot-core');
return {
...actual,
migrationsPath: pathMod.join(lootCoreRoot, 'migrations'),
bundledDatabasePath: pathMod.join(lootCoreRoot, 'default-db.sqlite'),
demoBudgetPath: pathMod.join(lootCoreRoot, 'demo-budget'),
};
},
);
const budgetName = 'test-budget';
global.IS_TESTING = true;
@@ -356,6 +375,143 @@ describe('API CRUD operations', () => {
);
});
// apis: createTag, getTags, updateTag, deleteTag
test('Tags: successfully complete tag operations', async () => {
// Create tags
const tagId1 = await api.createTag({ tag: 'test-tag1', color: '#ff0000' });
const tagId2 = await api.createTag({
tag: 'test-tag2',
description: 'A test tag',
});
let tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId1,
tag: 'test-tag1',
color: '#ff0000',
}),
expect.objectContaining({
id: tagId2,
tag: 'test-tag2',
description: 'A test tag',
}),
]),
);
// Update tag
await api.updateTag(tagId1, { tag: 'updated-tag', color: '#00ff00' });
tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId1,
tag: 'updated-tag',
color: '#00ff00',
}),
]),
);
// Delete tag
await api.deleteTag(tagId2);
tags = await api.getTags();
expect(tags).not.toEqual(
expect.arrayContaining([expect.objectContaining({ id: tagId2 })]),
);
});
test('Tags: create tag with minimal fields', async () => {
const tagId = await api.createTag({ tag: 'minimal-tag' });
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'minimal-tag',
color: null,
description: null,
}),
]),
);
});
test('Tags: update single field only', async () => {
const tagId = await api.createTag({ tag: 'original', color: '#ff0000' });
// Update only color, tag and description should remain unchanged
await api.updateTag(tagId, { color: '#00ff00' });
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'original',
color: '#00ff00',
description: null,
}),
]),
);
});
test('Tags: handle null values correctly', async () => {
const tagId = await api.createTag({
tag: 'with-nulls',
color: null,
description: null,
});
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
color: null,
description: null,
}),
]),
);
});
test('Tags: clear optional field', async () => {
const tagId = await api.createTag({
tag: 'clearable',
color: '#ff0000',
description: 'will be cleared',
});
// Clear color by setting to null
await api.updateTag(tagId, { color: null });
let tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'clearable',
color: null,
description: 'will be cleared',
}),
]),
);
// Clear description by setting to null
await api.updateTag(tagId, { description: null });
tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'clearable',
color: null,
description: null,
}),
]),
);
});
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
test('Rules: successfully update rules', async () => {
await api.createPayee({ name: 'test-payee' });
@@ -740,6 +896,73 @@ describe('API CRUD operations', () => {
);
expect(transactions[0].notes).toBeNull();
});
test('Transactions: reimportDeleted=false prevents reimporting deleted transactions', async () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
// Import a transaction
const result1 = await api.importTransactions(accountId, [
{
date: '2023-11-03',
imported_id: 'reimport-test-1',
amount: 100,
account: accountId,
},
]);
expect(result1.added).toHaveLength(1);
// Delete the transaction
await api.deleteTransaction(result1.added[0]);
// Reimport the same transaction with reimportDeleted=false
const result2 = await api.importTransactions(
accountId,
[
{
date: '2023-11-03',
imported_id: 'reimport-test-1',
amount: 100,
account: accountId,
},
],
{ reimportDeleted: false },
);
// Should match the deleted transaction and not create a new one
expect(result2.added).toHaveLength(0);
expect(result2.updated).toHaveLength(0);
});
test('Transactions: reimportDeleted=true reimports deleted transactions', async () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
// Import a transaction
const result1 = await api.importTransactions(accountId, [
{
date: '2023-11-03',
imported_id: 'reimport-test-2',
amount: 200,
account: accountId,
},
]);
expect(result1.added).toHaveLength(1);
// Delete the transaction
await api.deleteTransaction(result1.added[0]);
// Reimport the same transaction relying on reimportDeleted=true default
const result2 = await api.importTransactions(accountId, [
{
date: '2023-11-03',
imported_id: 'reimport-test-2',
amount: 200,
account: accountId,
},
]);
// Should create a new transaction since deleted ones are ignored
expect(result2.added).toHaveLength(1);
});
});
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule

View File

@@ -5,16 +5,17 @@ import type {
APIFileEntity,
APIPayeeEntity,
APIScheduleEntity,
} from 'loot-core/server/api-models';
import type { Query } from 'loot-core/shared/query';
import type { Handlers } from 'loot-core/types/handlers';
APITagEntity,
} from '@actual-app/core/server/api-models';
import { lib } from '@actual-app/core/server/main';
import type { Query } from '@actual-app/core/shared/query';
import type { ImportTransactionsOpts } from '@actual-app/core/types/api-handlers';
import type { Handlers } from '@actual-app/core/types/handlers';
import type {
ImportTransactionEntity,
RuleEntity,
TransactionEntity,
} from 'loot-core/types/models';
import * as injected from './injected';
} from '@actual-app/core/types/models';
export { q } from './app/query';
@@ -22,7 +23,7 @@ function send<K extends keyof Handlers, T extends Handlers[K]>(
name: K,
args?: Parameters<T>[0],
): Promise<Awaited<ReturnType<T>>> {
return injected.send(name, args);
return lib.send(name, args);
}
export async function runImport(
@@ -125,11 +126,6 @@ export function addTransactions(
});
}
export type ImportTransactionsOpts = {
defaultCleared?: boolean;
dryRun?: boolean;
};
export function importTransactions(
accountId: APIAccountEntity['id'],
transactions: ImportTransactionEntity[],
@@ -274,6 +270,25 @@ export function deletePayee(id: APIPayeeEntity['id']) {
return send('api/payee-delete', { id });
}
export function getTags() {
return send('api/tags-get');
}
export function createTag(tag: Omit<APITagEntity, 'id'>) {
return send('api/tag-create', { tag });
}
export function updateTag(
id: APITagEntity['id'],
fields: Partial<Omit<APITagEntity, 'id'>>,
) {
return send('api/tag-update', { id, fields });
}
export function deleteTag(id: APITagEntity['id']) {
return send('api/tag-delete', { id });
}
export function mergePayees(
targetId: APIPayeeEntity['id'],
mergeIds: APIPayeeEntity['id'][],

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "26.1.0",
"version": "26.3.0",
"description": "An API for Actual",
"license": "MIT",
"files": [
@@ -9,27 +9,41 @@
],
"main": "dist/index.js",
"types": "@types/index.d.ts",
"exports": {
".": {
"development": "./index.ts",
"default": "./dist/index.js"
}
},
"publishConfig": {
"exports": {
".": {
"types": "./@types/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"build:app": "yarn workspace loot-core build:api",
"build:crdt": "yarn workspace @actual-app/crdt build",
"build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json",
"build:migrations": "cp migrations/*.sql dist/migrations",
"build:default-db": "cp default-db.sqlite dist/",
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
"test": "yarn run clean && yarn run build:app && yarn run build:crdt && vitest --run",
"clean": "rm -rf dist @types"
"build": "vite build",
"test": "vitest --run",
"typecheck": "tsgo -b && tsc-strict"
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^12.5.0",
"@actual-app/core": "workspace:*",
"@actual-app/crdt": "workspace:*",
"better-sqlite3": "^12.6.2",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^13.0.0"
},
"devDependencies": {
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"rollup-plugin-visualizer": "^6.0.11",
"typescript-strict-plugin": "^2.4.4",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-peggy-loader": "^2.0.1",
"vitest": "^4.1.0"
},
"engines": {
"node": ">=20"

View File

@@ -1,19 +1,22 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
// Using ES2021 because that's the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
"module": "CommonJS",
"moduleResolution": "node10",
"module": "es2022",
"moduleResolution": "bundler",
"noEmit": false,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": ".",
"declarationDir": "@types",
"paths": {
"loot-core/*": ["./@types/loot-core/src/*"]
}
"tsBuildInfoFile": "dist/.tsbuildinfo",
"plugins": [{ "name": "typescript-strict-plugin", "paths": ["."] }]
},
"references": [{ "path": "../crdt" }, { "path": "../loot-core" }],
"include": ["."],
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts", "*.config.ts"]
}

2
packages/api/typings.ts Normal file
View File

@@ -0,0 +1,2 @@
declare module 'hyperformula/i18n/languages/enUS';
declare module '*.pegjs';

1
packages/api/typings/pegjs.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '*.pegjs';

View File

@@ -1,6 +1,4 @@
// oxlint-disable-next-line typescript/ban-ts-comment
// @ts-ignore: bundle not available until we build it
import * as bundle from './app/bundle.api.js';
import { lib } from '@actual-app/core/server/main';
export const amountToInteger = bundle.lib.amountToInteger;
export const integerToAmount = bundle.lib.integerToAmount;
export const amountToInteger = lib.amountToInteger;
export const integerToAmount = lib.integerToAmount;

View File

@@ -0,0 +1,93 @@
import fs from 'fs';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import peggyLoader from 'vite-plugin-peggy-loader';
const lootCoreRoot = path.resolve(__dirname, '../loot-core');
const distDir = path.resolve(__dirname, 'dist');
const typesDir = path.resolve(__dirname, '@types');
function cleanOutputDirs() {
return {
name: 'clean-output-dirs',
buildStart() {
if (fs.existsSync(distDir)) fs.rmSync(distDir, { recursive: true });
if (fs.existsSync(typesDir)) fs.rmSync(typesDir, { recursive: true });
},
};
}
function copyMigrationsAndDefaultDb() {
return {
name: 'copy-migrations-and-default-db',
closeBundle() {
const migrationsSrc = path.join(lootCoreRoot, 'migrations');
const defaultDbPath = path.join(lootCoreRoot, 'default-db.sqlite');
if (!fs.existsSync(migrationsSrc)) {
throw new Error(`migrations directory not found at ${migrationsSrc}`);
}
const migrationsStat = fs.statSync(migrationsSrc);
if (!migrationsStat.isDirectory()) {
throw new Error(`migrations path is not a directory: ${migrationsSrc}`);
}
const migrationsDest = path.join(distDir, 'migrations');
fs.mkdirSync(migrationsDest, { recursive: true });
for (const name of fs.readdirSync(migrationsSrc)) {
if (name.endsWith('.sql') || name.endsWith('.js')) {
fs.copyFileSync(
path.join(migrationsSrc, name),
path.join(migrationsDest, name),
);
}
}
if (!fs.existsSync(defaultDbPath)) {
throw new Error(`default-db.sqlite not found at ${defaultDbPath}`);
}
fs.copyFileSync(defaultDbPath, path.join(distDir, 'default-db.sqlite'));
},
};
}
export default defineConfig({
ssr: { noExternal: true, external: ['better-sqlite3'] },
build: {
ssr: true,
target: 'node20',
outDir: distDir,
emptyOutDir: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, 'index.ts'),
formats: ['cjs'],
fileName: () => 'index.js',
},
},
plugins: [
cleanOutputDirs(),
peggyLoader(),
dts({
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
outDir: path.resolve(__dirname, '@types'),
rollupTypes: true,
}),
copyMigrationsAndDefaultDb(),
visualizer({ template: 'raw-data', filename: 'app/stats.json' }),
],
resolve: {
extensions: ['.api.ts', '.js', '.ts', '.tsx', '.json'],
},
test: {
globals: true,
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
// print only console.error
return type === 'stderr';
},
maxWorkers: 2,
},
});

View File

@@ -1,10 +0,0 @@
export default {
test: {
globals: true,
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
// print only console.error
return type === 'stderr';
},
maxWorkers: 2,
},
};

1
packages/ci-actions/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist/*

View File

@@ -174,6 +174,7 @@ function parseArgs(argv) {
return {
sections,
identifier: getSingleValue(args, 'identifier') ?? 'bundle-stats',
format: getSingleValue(args, 'format') ?? 'pr-body',
};
}
@@ -463,6 +464,12 @@ const TOTAL_HEADERS = makeHeader([
'Total bundle size',
'% Changed',
]);
const SUMMARY_HEADERS = makeHeader([
'Bundle',
'Files count',
'Total bundle size',
'% Changed',
]);
const TABLE_HEADERS = makeHeader(['Asset', 'File Size', '% Changed']);
const CHUNK_TABLE_HEADERS = makeHeader(['File', 'Δ', 'Size']);
@@ -596,6 +603,24 @@ function printTotalAssetTable(statsDiff) {
return `**Total**\n${TOTAL_HEADERS}\n${printAssetTableRow(statsDiff.total)}`;
}
function printSummaryTable(sections) {
if (sections.length === 0) {
return `${SUMMARY_HEADERS}\nNo bundle stats were generated.`;
}
const rows = sections.map(section => {
const total = section.statsDiff.total;
return [
section.name,
total.name,
toFileSizeDiffCell(total),
conditionalPercentage(total.diffPercentage),
].join(' | ');
});
return `${SUMMARY_HEADERS}\n${rows.join('\n')}`;
}
function renderSection(title, statsDiff, chunkModuleDiff) {
const { total, ...groups } = statsDiff;
const parts = [`#### ${title}`, '', printTotalAssetTable({ total })];
@@ -615,8 +640,30 @@ function renderSection(title, statsDiff, chunkModuleDiff) {
return parts.join('\n');
}
function renderSections(sections) {
return sections
.map(section =>
renderSection(section.name, section.statsDiff, section.chunkDiff),
)
.join('\n\n---\n\n');
}
function getIdentifierMarkers(key) {
const label = 'bundlestats-action-comment';
return {
start: `<!--- ${label} key:${key} start --->`,
end: `<!--- ${label} key:${key} end --->`,
};
}
async function main() {
const args = parseArgs(process.argv);
const allowedFormats = new Set(['comment', 'pr-body']);
if (!allowedFormats.has(args.format)) {
throw new Error(
`Invalid format "${args.format}". Use "comment" or "pr-body".`,
);
}
console.error(
`[bundle-stats] Found ${args.sections.length} sections to process`,
@@ -654,22 +701,29 @@ async function main() {
});
}
const identifier = `<!--- bundlestats-action-comment key:${args.identifier} --->`;
const markers = getIdentifierMarkers(args.identifier);
const sectionsContent = renderSections(sections);
const summaryTable = printSummaryTable(sections);
const comment = [
const detailedBody = ['### Bundle Stats', '', sectionsContent].join('\n');
const commentBody = [markers.start, detailedBody, '', markers.end, ''].join(
'\n',
);
const prBody = [
markers.start,
'### Bundle Stats',
'',
sections
.map(section =>
renderSection(section.name, section.statsDiff, section.chunkDiff),
)
.join('\n\n---\n\n'),
summaryTable,
'',
identifier,
`<details>\n<summary>View detailed bundle stats</summary>\n\n${sectionsContent}\n</details>`,
'',
markers.end,
'',
].join('\n');
process.stdout.write(comment);
process.stdout.write(args.format === 'comment' ? commentBody : prBody);
}
main().catch(error => {

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env node
// overview:
// 1. Identify the migrations in packages/loot-core/migrations/* on `master` and HEAD
// 2. Make sure that any new migrations on HEAD are dated after the latest migration on `master`.
const { spawnSync } = require('child_process');
const path = require('path');
import { spawnSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';
const migrationsDir = path.join(
__dirname,
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'..',
'packages',
@@ -16,7 +16,7 @@ const migrationsDir = path.join(
'migrations',
);
function readMigrations(ref) {
function readMigrations(ref: string) {
const { stdout } = spawnSync('git', [
'ls-tree',
'--name-only',

View File

@@ -2,13 +2,13 @@
// This script is used in GitHub Actions to get the next version based on the current package.json version.
// It supports three types of versioning: nightly, hotfix, and monthly.
import fs from 'node:fs';
import { parseArgs } from 'node:util';
import { getNextVersion } from '../src/versions/get-next-package-version.js';
const args = process.argv;
import {
getNextVersion,
isValidVersionType,
} from '../src/versions/get-next-package-version';
const options = {
'package-json': {
@@ -19,41 +19,63 @@ const options = {
type: 'string', // nightly, hotfix, monthly, auto
short: 't',
},
version: {
type: 'string',
short: 'v',
},
update: {
type: 'boolean',
short: 'u',
default: false,
},
};
} as const;
function fail(message: string): never {
console.error(message);
process.exit(1);
}
const { values } = parseArgs({
args,
options,
allowPositionals: true,
});
if (!values['package-json']) {
console.error(
const packageJsonPath = values['package-json'];
if (!packageJsonPath) {
fail(
'Please specify the path to package.json using --package-json or -p option.',
);
process.exit(1);
}
try {
const packageJsonPath = values['package-json'];
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (!('version' in packageJson) || typeof packageJson.version !== 'string') {
fail('The specified package.json does not contain a valid version field.');
}
const currentVersion = packageJson.version;
const explicitVersion = values.version;
let newVersion;
try {
newVersion = getNextVersion({
currentVersion,
type: values.type,
currentDate: new Date(),
});
} catch (e) {
console.error(e.message);
process.exit(1);
if (explicitVersion) {
newVersion = explicitVersion;
} else {
const type = values.type;
if (!type || !isValidVersionType(type)) {
fail('Please specify the release type using --type or -t.');
}
try {
newVersion = getNextVersion({
currentVersion,
type,
currentDate: new Date(),
});
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
}
process.stdout.write(newVersion);
@@ -67,6 +89,5 @@ try {
);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
fail(`Error: ${error instanceof Error ? error.message : String(error)}`);
}

8
packages/ci-actions/bin/tsx Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
cd ../../
script="$1"
shift
exec node --import=extensionless/register --experimental-strip-types packages/ci-actions/"$script" "$@"

View File

@@ -14,10 +14,14 @@ import process from 'node:process';
import { Octokit } from '@octokit/rest';
const BOT_BOUNDARY_MARKER = '<!--- actual-bot-sections --->';
const BOT_BOUNDARY_TEXT = `${BOT_BOUNDARY_MARKER}\n<hr />`;
function parseArgs(argv) {
const args = {
commentFile: null,
identifier: null,
target: 'comment',
};
for (let i = 2; i < argv.length; i += 2) {
@@ -41,6 +45,9 @@ function parseArgs(argv) {
case '--identifier':
args.identifier = value;
break;
case '--target':
args.target = value;
break;
default:
throw new Error(`Unknown argument "${key}".`);
}
@@ -54,6 +61,12 @@ function parseArgs(argv) {
throw new Error('Missing required argument "--identifier".');
}
if (!['comment', 'pr-body'].includes(args.target)) {
throw new Error(
`Invalid value "${args.target}" for "--target". Use "comment" or "pr-body".`,
);
}
return args;
}
@@ -110,20 +123,123 @@ function isGitHubActionsBot(comment) {
return comment.user?.login === 'github-actions[bot]';
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getIdentifierMarkers(identifier) {
if (identifier.includes('<!---')) {
return {
start: identifier,
end: null,
};
}
const label = 'bundlestats-action-comment';
return {
start: `<!--- ${label} key:${identifier} start --->`,
end: `<!--- ${label} key:${identifier} end --->`,
};
}
function upsertBlock(existingBody, block, markers) {
const body = existingBody ?? '';
if (markers.end) {
const pattern = new RegExp(
`${escapeRegExp(markers.start)}[\\s\\S]*?${escapeRegExp(markers.end)}`,
'm',
);
if (pattern.test(body)) {
return body.replace(pattern, block.trim());
}
}
if (body.trim().length === 0) {
return block.trim();
}
const separator = body.endsWith('\n') ? '\n' : '\n\n';
const boundary = body.includes(BOT_BOUNDARY_MARKER)
? ''
: `${BOT_BOUNDARY_TEXT}\n\n`;
return `${body}${separator}${boundary}${block.trim()}`;
}
async function updatePullRequestBody(
octokit,
owner,
repo,
pullNumber,
block,
markers,
) {
const { data } = await octokit.rest.pulls.get({
owner,
repo,
pull_number: pullNumber,
});
const nextBody = upsertBlock(data.body ?? '', block, markers);
await octokit.rest.pulls.update({
owner,
repo,
pull_number: pullNumber,
body: nextBody,
});
}
async function deleteExistingComment(
octokit,
owner,
repo,
issueNumber,
markers,
) {
const comments = await listComments(octokit, owner, repo, issueNumber);
const existingComment = comments.find(
comment =>
isGitHubActionsBot(comment) && comment.body?.includes(markers.start),
);
if (existingComment) {
await octokit.rest.issues.deleteComment({
owner,
repo,
comment_id: existingComment.id,
});
}
}
async function main() {
const { commentFile, identifier } = parseArgs(process.argv);
const { commentFile, identifier, target } = parseArgs(process.argv);
const commentBody = await loadCommentBody(commentFile);
const token = assertGitHubToken();
const { owner, repo } = getRepoInfo();
const issueNumber = getPullRequestNumber();
const markers = getIdentifierMarkers(identifier);
const octokit = new Octokit({ auth: token });
const comments = await listComments(octokit, owner, repo, issueNumber);
if (target === 'pr-body') {
await updatePullRequestBody(
octokit,
owner,
repo,
issueNumber,
commentBody,
markers,
);
await deleteExistingComment(octokit, owner, repo, issueNumber, markers);
console.log('Updated pull request body with bundle stats.');
return;
}
const comments = await listComments(octokit, owner, repo, issueNumber);
const existingComment = comments.find(
comment =>
isGitHubActionsBot(comment) && comment.body?.includes(identifier),
isGitHubActionsBot(comment) && comment.body?.includes(markers.start),
);
if (existingComment) {
@@ -134,15 +250,16 @@ async function main() {
body: commentBody,
});
console.log('Updated existing bundle stats comment.');
} else {
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: commentBody,
});
console.log('Created new bundle stats comment.');
return;
}
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: commentBody,
});
console.log('Created new bundle stats comment.');
}
main().catch(error => {

View File

@@ -3,9 +3,18 @@
"private": true,
"type": "module",
"scripts": {
"test": "vitest --run"
"tsx": "bin/tsx",
"test": "vitest --run",
"typecheck": "tsgo -b"
},
"devDependencies": {
"vitest": "^4.0.16"
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"extensionless": "^2.0.6",
"vitest": "^4.1.0"
},
"extensionless": {
"lookFor": [
"ts"
]
}
}

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import { getNextVersion } from './get-next-package-version';
@@ -77,7 +77,7 @@ describe('getNextVersion (lib)', () => {
expect(() =>
getNextVersion({
currentVersion: '25.8.4',
type: 'unknown',
type: 'unknown' as never,
currentDate: new Date('2025-08-10'),
}),
).toThrow(/Invalid type/);

View File

@@ -1,35 +1,69 @@
function parseVersion(version) {
export const versionTypeArray = [
'auto',
'hotfix',
'monthly',
'nightly',
] as const;
export type VersionType = (typeof versionTypeArray)[number];
type ParsedVersion = {
versionYear: number;
versionMonth: number;
versionHotfix: number;
};
type GetNextVersionOptions = {
currentVersion: string;
type: VersionType;
currentDate?: Date;
};
function parseVersion(version: string): ParsedVersion {
const [y, m, p] = version.split('.');
return {
versionYear: parseInt(y, 10),
versionMonth: parseInt(m, 10),
versionHotfix: parseInt(p, 10),
versionYear: Number.parseInt(y, 10),
versionMonth: Number.parseInt(m, 10),
versionHotfix: Number.parseInt(p, 10),
};
}
function computeNextMonth(versionYear, versionMonth) {
// Create date and add 1 month
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
function computeNextMonth(versionYear: number, versionMonth: number) {
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1);
const nextVersionMonthDate = new Date(
versionDate.getFullYear(),
versionDate.getMonth() + 1,
1,
);
// Format back to YY.M format
const fullYear = nextVersionMonthDate.getFullYear();
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1;
return { nextVersionYear, nextVersionMonth };
}
// Determine logical type from 'auto' based on the current date and version
function resolveType(type, currentDate, versionYear, versionMonth) {
if (type !== 'auto') return type;
export function isValidVersionType(value: string): value is VersionType {
return versionTypeArray.includes(value as VersionType);
}
function resolveType(
type: VersionType,
currentDate: Date,
versionYear: number,
versionMonth: number,
) {
if (type !== 'auto') {
return type;
}
const inPatchMonth =
currentDate.getFullYear() === 2000 + versionYear &&
currentDate.getMonth() + 1 === versionMonth;
if (inPatchMonth && currentDate.getDate() <= 25) return 'hotfix';
if (inPatchMonth && currentDate.getDate() <= 25) {
return 'hotfix';
}
return 'monthly';
}
@@ -37,7 +71,7 @@ export function getNextVersion({
currentVersion,
type,
currentDate = new Date(),
}) {
}: GetNextVersionOptions) {
const { versionYear, versionMonth, versionHotfix } =
parseVersion(currentVersion);
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
@@ -51,11 +85,10 @@ export function getNextVersion({
versionMonth,
);
// Format date stamp once for nightly
const currentDateString = currentDate
.toISOString()
.split('T')[0]
.replaceAll('-', '');
.replace(/-/g, '');
switch (resolvedType) {
case 'nightly':
@@ -66,7 +99,7 @@ export function getNextVersion({
return `${nextVersionYear}.${nextVersionMonth}.0`;
default:
throw new Error(
'Invalid type specified. Use "auto", "nightly", "hotfix", or "monthly".',
`Invalid type ${String(resolvedType satisfies never)} specified. Use "auto", "nightly", "hotfix", or "monthly".`,
);
}
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": [],
"module": "es2022",
"moduleResolution": "bundler",
"skipLibCheck": true,
"strict": true,
"types": ["node"],
"outDir": "dist",
"rootDir": ".",
"composite": true
},
"include": ["src/**/*", "bin/**/*"],
"exclude": ["node_modules"]
}

7
packages/cli/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
dist
coverage
.actualrc.json
.actualrc
.actualrc.yaml
.actualrc.yml
actual.config.js

155
packages/cli/README.md Normal file
View File

@@ -0,0 +1,155 @@
# @actual-app/cli
> **WARNING:** This CLI is experimental.
Command-line interface for [Actual Budget](https://actualbudget.org). Query and modify your budget data from the terminal — accounts, transactions, categories, payees, rules, schedules, and more.
> **Note:** This CLI connects to a running [Actual sync server](https://actualbudget.org/docs/install/). It does not operate on local budget files directly.
## Installation
```bash
npm install -g @actual-app/cli
```
Requires Node.js >= 22.
## Quick Start
```bash
# Set connection details
export ACTUAL_SERVER_URL=http://localhost:5006
export ACTUAL_PASSWORD=your-password
export ACTUAL_SYNC_ID=your-sync-id # Found in Settings → Advanced → Sync ID
# List your accounts
actual accounts list
# Check a balance
actual accounts balance <account-id>
# View this month's budget
actual budgets month 2026-03
```
## Configuration
Configuration is resolved in this order (highest priority first):
1. **CLI flags** (`--server-url`, `--password`, etc.)
2. **Environment variables**
3. **Config file** (via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig))
4. **Defaults** (`dataDir` defaults to `~/.actual-cli/data`)
### Environment Variables
| Variable | Description |
| ---------------------- | --------------------------------------------- |
| `ACTUAL_SERVER_URL` | URL of the Actual sync server (required) |
| `ACTUAL_PASSWORD` | Server password (required unless using token) |
| `ACTUAL_SESSION_TOKEN` | Session token (alternative to password) |
| `ACTUAL_SYNC_ID` | Budget Sync ID (required for most commands) |
| `ACTUAL_DATA_DIR` | Local directory for cached budget data |
### Config File
Create an `.actualrc.json` (or `.actualrc`, `.actualrc.yaml`, `actual.config.js`):
```json
{
"serverUrl": "http://localhost:5006",
"password": "your-password",
"syncId": "1cfdbb80-6274-49bf-b0c2-737235a4c81f"
}
```
**Security:** Do not store plaintext passwords in config files (e.g. `.actualrc.json`, `.actualrc`, `.actualrc.yaml`, `actual.config.js`). Add these files to `.gitignore` if they contain secrets. Prefer the `ACTUAL_SESSION_TOKEN` environment variable instead of the `password` field. See [Environment Variables](#environment-variables) for using a session token.
### Global Flags
| Flag | Description |
| ------------------------- | ----------------------------------------------- |
| `--server-url <url>` | Server URL |
| `--password <pw>` | Server password |
| `--session-token <token>` | Session token |
| `--sync-id <id>` | Budget Sync ID |
| `--data-dir <path>` | Data directory |
| `--format <format>` | Output format: `json` (default), `table`, `csv` |
| `--verbose` | Show informational messages |
## Commands
| Command | Description |
| ----------------- | ------------------------------ |
| `accounts` | Manage accounts |
| `budgets` | Manage budgets and allocations |
| `categories` | Manage categories |
| `category-groups` | Manage category groups |
| `transactions` | Manage transactions |
| `payees` | Manage payees |
| `tags` | Manage tags |
| `rules` | Manage transaction rules |
| `schedules` | Manage scheduled transactions |
| `query` | Run an ActualQL query |
| `server` | Server utilities and lookups |
Run `actual <command> --help` for subcommands and options.
### Examples
```bash
# List all accounts (as a table)
actual accounts list --format table
# Find an entity ID by name
actual server get-id --type accounts --name "Checking"
# Add a transaction (amount in integer cents: -2500 = -$25.00)
actual transactions add --account <id> \
--data '[{"date":"2026-03-14","amount":-2500,"payee_name":"Coffee Shop"}]'
# Export transactions to CSV
actual transactions list --account <id> \
--start 2026-01-01 --end 2026-12-31 --format csv > transactions.csv
# Set budget amount ($500 = 50000 cents)
actual budgets set-amount --month 2026-03 --category <id> --amount 50000
# Run an ActualQL query
actual query run --table transactions \
--select "date,amount,payee" --filter '{"amount":{"$lt":0}}' --limit 10
```
### Amount Convention
All monetary amounts are **integer cents**:
| CLI Value | Dollar Amount |
| --------- | ------------- |
| `5000` | $50.00 |
| `-12350` | -$123.50 |
## Running Locally (Development)
If you're working on the CLI within the monorepo:
```bash
# 1. Build the CLI
yarn build:cli
# 2. Start a local sync server (in a separate terminal)
yarn start:server-dev
# 3. Open http://localhost:5006 in your browser, create a budget,
# then find the Sync ID in Settings → Advanced → Sync ID
# 4. Run the CLI directly from the build output
ACTUAL_SERVER_URL=http://localhost:5006 \
ACTUAL_PASSWORD=your-password \
ACTUAL_SYNC_ID=your-sync-id \
node packages/cli/dist/cli.js accounts list
# Or use a shorthand alias for convenience
alias actual-dev="node $(pwd)/packages/cli/dist/cli.js"
actual-dev budgets list
```

35
packages/cli/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "@actual-app/cli",
"version": "26.3.0",
"description": "CLI for Actual Budget",
"license": "MIT",
"bin": {
"actual": "./dist/cli.js",
"actual-cli": "./dist/cli.js"
},
"files": [
"dist"
],
"type": "module",
"scripts": {
"build": "vite build",
"test": "vitest --run",
"typecheck": "tsgo -b"
},
"dependencies": {
"@actual-app/api": "workspace:*",
"cli-table3": "^0.6.5",
"commander": "^13.0.0",
"cosmiconfig": "^9.0.0"
},
"devDependencies": {
"@types/node": "^22.19.15",
"@typescript/native-preview": "^7.0.0-dev.20260309.1",
"rollup-plugin-visualizer": "^6.0.11",
"vite": "^8.0.0",
"vitest": "^4.1.0"
},
"engines": {
"node": ">=22"
}
}

View File

@@ -0,0 +1,259 @@
import * as api from '@actual-app/api';
import { Command } from 'commander';
import { printOutput } from '../output';
import { registerAccountsCommand } from './accounts';
vi.mock('@actual-app/api', () => ({
getAccounts: vi.fn().mockResolvedValue([]),
createAccount: vi.fn().mockResolvedValue('new-id'),
updateAccount: vi.fn().mockResolvedValue(undefined),
closeAccount: vi.fn().mockResolvedValue(undefined),
reopenAccount: vi.fn().mockResolvedValue(undefined),
deleteAccount: vi.fn().mockResolvedValue(undefined),
getAccountBalance: vi.fn().mockResolvedValue(10000),
}));
vi.mock('../connection', () => ({
withConnection: vi.fn((_opts, fn) => fn()),
}));
vi.mock('../output', () => ({
printOutput: vi.fn(),
}));
function createProgram(): Command {
const program = new Command();
program.option('--format <format>');
program.option('--server-url <url>');
program.option('--password <pw>');
program.option('--session-token <token>');
program.option('--sync-id <id>');
program.option('--data-dir <dir>');
program.option('--verbose');
program.exitOverride();
registerAccountsCommand(program);
return program;
}
async function run(args: string[]) {
const program = createProgram();
await program.parseAsync(['node', 'test', ...args]);
}
describe('accounts commands', () => {
let stderrSpy: ReturnType<typeof vi.spyOn>;
let stdoutSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
stdoutSpy = vi
.spyOn(process.stdout, 'write')
.mockImplementation(() => true);
});
afterEach(() => {
stderrSpy.mockRestore();
stdoutSpy.mockRestore();
});
describe('list', () => {
it('calls api.getAccounts and prints result', async () => {
const accounts = [{ id: '1', name: 'Checking' }];
vi.mocked(api.getAccounts).mockResolvedValue(accounts);
await run(['accounts', 'list']);
expect(api.getAccounts).toHaveBeenCalled();
expect(printOutput).toHaveBeenCalledWith(accounts, undefined);
});
it('passes format option to printOutput', async () => {
vi.mocked(api.getAccounts).mockResolvedValue([]);
await run(['--format', 'csv', 'accounts', 'list']);
expect(printOutput).toHaveBeenCalledWith([], 'csv');
});
});
describe('create', () => {
it('passes name and defaults to api.createAccount', async () => {
await run(['accounts', 'create', '--name', 'Savings']);
expect(api.createAccount).toHaveBeenCalledWith(
{ name: 'Savings', offbudget: false },
0,
);
expect(printOutput).toHaveBeenCalledWith({ id: 'new-id' }, undefined);
});
it('passes offbudget and balance options', async () => {
await run([
'accounts',
'create',
'--name',
'Investments',
'--offbudget',
'--balance',
'50000',
]);
expect(api.createAccount).toHaveBeenCalledWith(
{ name: 'Investments', offbudget: true },
50000,
);
});
});
describe('update', () => {
it('passes fields to api.updateAccount', async () => {
await run(['accounts', 'update', 'acct-1', '--name', 'NewName']);
expect(api.updateAccount).toHaveBeenCalledWith('acct-1', {
name: 'NewName',
});
expect(printOutput).toHaveBeenCalledWith(
{ success: true, id: 'acct-1' },
undefined,
);
});
it('passes offbudget true', async () => {
await run([
'accounts',
'update',
'acct-1',
'--name',
'X',
'--offbudget',
'true',
]);
expect(api.updateAccount).toHaveBeenCalledWith('acct-1', {
name: 'X',
offbudget: true,
});
});
it('passes offbudget false', async () => {
await run([
'accounts',
'update',
'acct-1',
'--name',
'X',
'--offbudget',
'false',
]);
expect(api.updateAccount).toHaveBeenCalledWith('acct-1', {
name: 'X',
offbudget: false,
});
});
it('rejects invalid offbudget value', async () => {
await expect(
run(['accounts', 'update', 'acct-1', '--offbudget', 'yes']),
).rejects.toThrow(
'Invalid --offbudget: "yes". Expected "true" or "false".',
);
});
it('rejects empty name', async () => {
await expect(
run(['accounts', 'update', 'acct-1', '--name', ' ']),
).rejects.toThrow('Invalid --name: must be a non-empty string.');
});
it('rejects update with no fields', async () => {
await expect(run(['accounts', 'update', 'acct-1'])).rejects.toThrow(
'No update fields provided. Use --name or --offbudget.',
);
});
});
describe('close', () => {
it('passes transfer options to api.closeAccount', async () => {
await run([
'accounts',
'close',
'acct-1',
'--transfer-account',
'acct-2',
]);
expect(api.closeAccount).toHaveBeenCalledWith(
'acct-1',
'acct-2',
undefined,
);
});
it('passes transfer category', async () => {
await run([
'accounts',
'close',
'acct-1',
'--transfer-category',
'cat-1',
]);
expect(api.closeAccount).toHaveBeenCalledWith(
'acct-1',
undefined,
'cat-1',
);
});
});
describe('reopen', () => {
it('calls api.reopenAccount', async () => {
await run(['accounts', 'reopen', 'acct-1']);
expect(api.reopenAccount).toHaveBeenCalledWith('acct-1');
expect(printOutput).toHaveBeenCalledWith(
{ success: true, id: 'acct-1' },
undefined,
);
});
});
describe('delete', () => {
it('calls api.deleteAccount', async () => {
await run(['accounts', 'delete', 'acct-1']);
expect(api.deleteAccount).toHaveBeenCalledWith('acct-1');
expect(printOutput).toHaveBeenCalledWith(
{ success: true, id: 'acct-1' },
undefined,
);
});
});
describe('balance', () => {
it('calls api.getAccountBalance without cutoff', async () => {
await run(['accounts', 'balance', 'acct-1']);
expect(api.getAccountBalance).toHaveBeenCalledWith('acct-1', undefined);
expect(printOutput).toHaveBeenCalledWith(
{ id: 'acct-1', balance: 10000 },
undefined,
);
});
it('calls api.getAccountBalance with cutoff date', async () => {
await run(['accounts', 'balance', 'acct-1', '--cutoff', '2025-01-15']);
expect(api.getAccountBalance).toHaveBeenCalledWith(
'acct-1',
new Date('2025-01-15'),
);
});
});
});

View File

@@ -0,0 +1,135 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag, parseIntFlag } from '../utils';
export function registerAccountsCommand(program: Command) {
const accounts = program.command('accounts').description('Manage accounts');
accounts
.command('list')
.description('List all accounts')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getAccounts();
printOutput(result, opts.format);
});
});
accounts
.command('create')
.description('Create a new account')
.requiredOption('--name <name>', 'Account name')
.option('--offbudget', 'Create as off-budget account', false)
.option('--balance <amount>', 'Initial balance in cents', '0')
.action(async cmdOpts => {
const balance = parseIntFlag(cmdOpts.balance, '--balance');
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createAccount(
{ name: cmdOpts.name, offbudget: cmdOpts.offbudget },
balance,
);
printOutput({ id }, opts.format);
});
});
accounts
.command('update <id>')
.description('Update an account')
.option('--name <name>', 'New account name')
.option('--offbudget <bool>', 'Set off-budget status')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
const fields: Record<string, unknown> = {};
if (cmdOpts.name !== undefined) {
const trimmed = cmdOpts.name.trim();
if (trimmed === '') {
throw new Error('Invalid --name: must be a non-empty string.');
}
fields.name = trimmed;
}
if (cmdOpts.offbudget !== undefined) {
fields.offbudget = parseBoolFlag(cmdOpts.offbudget, '--offbudget');
}
if (Object.keys(fields).length === 0) {
throw new Error(
'No update fields provided. Use --name or --offbudget.',
);
}
await withConnection(opts, async () => {
await api.updateAccount(id, fields);
printOutput({ success: true, id }, opts.format);
});
});
accounts
.command('close <id>')
.description('Close an account')
.option(
'--transfer-account <id>',
'Transfer remaining balance to this account',
)
.option(
'--transfer-category <id>',
'Transfer remaining balance to this category',
)
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.closeAccount(
id,
cmdOpts.transferAccount,
cmdOpts.transferCategory,
);
printOutput({ success: true, id }, opts.format);
});
});
accounts
.command('reopen <id>')
.description('Reopen a closed account')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.reopenAccount(id);
printOutput({ success: true, id }, opts.format);
});
});
accounts
.command('delete <id>')
.description('Delete an account')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteAccount(id);
printOutput({ success: true, id }, opts.format);
});
});
accounts
.command('balance <id>')
.description('Get account balance')
.option('--cutoff <date>', 'Cutoff date (YYYY-MM-DD)')
.action(async (id: string, cmdOpts) => {
let cutoff: Date | undefined;
if (cmdOpts.cutoff) {
const cutoffDate = new Date(cmdOpts.cutoff);
if (Number.isNaN(cutoffDate.getTime())) {
throw new Error(
'Invalid cutoff date: expected a valid date (e.g. YYYY-MM-DD).',
);
}
cutoff = cutoffDate;
}
const opts = program.opts();
await withConnection(opts, async () => {
const balance = await api.getAccountBalance(id, cutoff);
printOutput({ id, balance }, opts.format);
});
});
}

View File

@@ -0,0 +1,135 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { resolveConfig } from '../config';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag, parseIntFlag } from '../utils';
export function registerBudgetsCommand(program: Command) {
const budgets = program.command('budgets').description('Manage budgets');
budgets
.command('list')
.description('List all available budgets')
.action(async () => {
const opts = program.opts();
await withConnection(
opts,
async () => {
const result = await api.getBudgets();
printOutput(result, opts.format);
},
{ loadBudget: false },
);
});
budgets
.command('download <syncId>')
.description('Download a budget by sync ID')
.option('--encryption-password <password>', 'Encryption password')
.action(async (syncId: string, cmdOpts) => {
const opts = program.opts();
const config = await resolveConfig(opts);
const password = config.encryptionPassword ?? cmdOpts.encryptionPassword;
await withConnection(
opts,
async () => {
await api.downloadBudget(syncId, {
password,
});
printOutput({ success: true, syncId }, opts.format);
},
{ loadBudget: false },
);
});
budgets
.command('sync')
.description('Sync the current budget')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.sync();
printOutput({ success: true }, opts.format);
});
});
budgets
.command('months')
.description('List available budget months')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getBudgetMonths();
printOutput(result, opts.format);
});
});
budgets
.command('month <month>')
.description('Get budget data for a specific month (YYYY-MM)')
.action(async (month: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getBudgetMonth(month);
printOutput(result, opts.format);
});
});
budgets
.command('set-amount')
.description('Set budget amount for a category in a month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--category <id>', 'Category ID')
.requiredOption('--amount <amount>', 'Amount in cents')
.action(async cmdOpts => {
const amount = parseIntFlag(cmdOpts.amount, '--amount');
const opts = program.opts();
await withConnection(opts, async () => {
await api.setBudgetAmount(cmdOpts.month, cmdOpts.category, amount);
printOutput({ success: true }, opts.format);
});
});
budgets
.command('set-carryover')
.description('Enable/disable carryover for a category')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--category <id>', 'Category ID')
.requiredOption('--flag <bool>', 'Enable (true) or disable (false)')
.action(async cmdOpts => {
const flag = parseBoolFlag(cmdOpts.flag, '--flag');
const opts = program.opts();
await withConnection(opts, async () => {
await api.setBudgetCarryover(cmdOpts.month, cmdOpts.category, flag);
printOutput({ success: true }, opts.format);
});
});
budgets
.command('hold-next-month')
.description('Hold budget amount for next month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.requiredOption('--amount <amount>', 'Amount in cents')
.action(async cmdOpts => {
const parsedAmount = parseIntFlag(cmdOpts.amount, '--amount');
const opts = program.opts();
await withConnection(opts, async () => {
await api.holdBudgetForNextMonth(cmdOpts.month, parsedAmount);
printOutput({ success: true }, opts.format);
});
});
budgets
.command('reset-hold')
.description('Reset budget hold for a month')
.requiredOption('--month <month>', 'Budget month (YYYY-MM)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.resetBudgetHold(cmdOpts.month);
printOutput({ success: true }, opts.format);
});
});
}

View File

@@ -0,0 +1,75 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag } from '../utils';
export function registerCategoriesCommand(program: Command) {
const categories = program
.command('categories')
.description('Manage categories');
categories
.command('list')
.description('List all categories')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getCategories();
printOutput(result, opts.format);
});
});
categories
.command('create')
.description('Create a new category')
.requiredOption('--name <name>', 'Category name')
.requiredOption('--group-id <id>', 'Category group ID')
.option('--is-income', 'Mark as income category', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createCategory({
name: cmdOpts.name,
group_id: cmdOpts.groupId,
is_income: cmdOpts.isIncome,
hidden: false,
});
printOutput({ id }, opts.format);
});
});
categories
.command('update <id>')
.description('Update a category')
.option('--name <name>', 'New category name')
.option('--hidden <bool>', 'Set hidden status')
.action(async (id: string, cmdOpts) => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name !== undefined) fields.name = cmdOpts.name;
if (cmdOpts.hidden !== undefined) {
fields.hidden = parseBoolFlag(cmdOpts.hidden, '--hidden');
}
if (Object.keys(fields).length === 0) {
throw new Error('No update fields provided. Use --name or --hidden.');
}
const opts = program.opts();
await withConnection(opts, async () => {
await api.updateCategory(id, fields);
printOutput({ success: true, id }, opts.format);
});
});
categories
.command('delete <id>')
.description('Delete a category')
.option('--transfer-to <id>', 'Transfer transactions to this category')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteCategory(id, cmdOpts.transferTo);
printOutput({ success: true, id }, opts.format);
});
});
}

View File

@@ -0,0 +1,73 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
import { parseBoolFlag } from '../utils';
export function registerCategoryGroupsCommand(program: Command) {
const groups = program
.command('category-groups')
.description('Manage category groups');
groups
.command('list')
.description('List all category groups')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getCategoryGroups();
printOutput(result, opts.format);
});
});
groups
.command('create')
.description('Create a new category group')
.requiredOption('--name <name>', 'Group name')
.option('--is-income', 'Mark as income group', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createCategoryGroup({
name: cmdOpts.name,
is_income: cmdOpts.isIncome,
hidden: false,
});
printOutput({ id }, opts.format);
});
});
groups
.command('update <id>')
.description('Update a category group')
.option('--name <name>', 'New group name')
.option('--hidden <bool>', 'Set hidden status')
.action(async (id: string, cmdOpts) => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name !== undefined) fields.name = cmdOpts.name;
if (cmdOpts.hidden !== undefined) {
fields.hidden = parseBoolFlag(cmdOpts.hidden, '--hidden');
}
if (Object.keys(fields).length === 0) {
throw new Error('No update fields provided. Use --name or --hidden.');
}
const opts = program.opts();
await withConnection(opts, async () => {
await api.updateCategoryGroup(id, fields);
printOutput({ success: true, id }, opts.format);
});
});
groups
.command('delete <id>')
.description('Delete a category group')
.option('--transfer-to <id>', 'Transfer transactions to this category ID')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteCategoryGroup(id, cmdOpts.transferTo);
printOutput({ success: true, id }, opts.format);
});
});
}

View File

@@ -0,0 +1,95 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
export function registerPayeesCommand(program: Command) {
const payees = program.command('payees').description('Manage payees');
payees
.command('list')
.description('List all payees')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getPayees();
printOutput(result, opts.format);
});
});
payees
.command('common')
.description('List frequently used payees')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getCommonPayees();
printOutput(result, opts.format);
});
});
payees
.command('create')
.description('Create a new payee')
.requiredOption('--name <name>', 'Payee name')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createPayee({ name: cmdOpts.name });
printOutput({ id }, opts.format);
});
});
payees
.command('update <id>')
.description('Update a payee')
.option('--name <name>', 'New payee name')
.action(async (id: string, cmdOpts) => {
const fields: Record<string, unknown> = {};
if (cmdOpts.name) fields.name = cmdOpts.name;
if (Object.keys(fields).length === 0) {
throw new Error(
'No fields to update. Use --name to specify a new name.',
);
}
const opts = program.opts();
await withConnection(opts, async () => {
await api.updatePayee(id, fields);
printOutput({ success: true, id }, opts.format);
});
});
payees
.command('delete <id>')
.description('Delete a payee')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deletePayee(id);
printOutput({ success: true, id }, opts.format);
});
});
payees
.command('merge')
.description('Merge payees into a target payee')
.requiredOption('--target <id>', 'Target payee ID')
.requiredOption('--ids <ids>', 'Comma-separated payee IDs to merge')
.action(async (cmdOpts: { target: string; ids: string }) => {
const mergeIds = cmdOpts.ids
.split(',')
.map(id => id.trim())
.filter(id => id.length > 0);
if (mergeIds.length === 0) {
throw new Error(
'No valid payee IDs provided in --ids. Provide comma-separated IDs.',
);
}
const opts = program.opts();
await withConnection(opts, async () => {
await api.mergePayees(cmdOpts.target, mergeIds);
printOutput({ success: true }, opts.format);
});
});
}

View File

@@ -0,0 +1,346 @@
import * as api from '@actual-app/api';
import { Command } from 'commander';
import { printOutput } from '../output';
import { parseOrderBy, registerQueryCommand } from './query';
vi.mock('@actual-app/api', () => {
const queryObj = {
select: vi.fn().mockReturnThis(),
filter: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
offset: vi.fn().mockReturnThis(),
groupBy: vi.fn().mockReturnThis(),
calculate: vi.fn().mockReturnThis(),
};
return {
q: vi.fn().mockReturnValue(queryObj),
aqlQuery: vi.fn().mockResolvedValue({ data: [] }),
};
});
vi.mock('../connection', () => ({
withConnection: vi.fn((_opts, fn) => fn()),
}));
vi.mock('../output', () => ({
printOutput: vi.fn(),
}));
function createProgram(): Command {
const program = new Command();
program.option('--format <format>');
program.option('--server-url <url>');
program.option('--password <pw>');
program.option('--session-token <token>');
program.option('--sync-id <id>');
program.option('--data-dir <dir>');
program.option('--verbose');
program.exitOverride();
registerQueryCommand(program);
return program;
}
async function run(args: string[]) {
const program = createProgram();
await program.parseAsync(['node', 'test', ...args]);
}
function getQueryObj() {
return vi.mocked(api.q).mock.results[0]?.value;
}
describe('parseOrderBy', () => {
it('parses plain field names', () => {
expect(parseOrderBy('date')).toEqual(['date']);
});
it('parses field:desc', () => {
expect(parseOrderBy('date:desc')).toEqual([{ date: 'desc' }]);
});
it('parses field:asc', () => {
expect(parseOrderBy('amount:asc')).toEqual([{ amount: 'asc' }]);
});
it('parses multiple mixed fields', () => {
expect(parseOrderBy('date:desc,amount:asc,id')).toEqual([
{ date: 'desc' },
{ amount: 'asc' },
'id',
]);
});
it('throws on invalid direction', () => {
expect(() => parseOrderBy('date:backwards')).toThrow(
'Invalid order direction "backwards"',
);
});
it('throws on empty field', () => {
expect(() => parseOrderBy('date,,amount')).toThrow('empty field');
});
});
describe('query commands', () => {
let stderrSpy: ReturnType<typeof vi.spyOn>;
let stdoutSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
stdoutSpy = vi
.spyOn(process.stdout, 'write')
.mockImplementation(() => true);
});
afterEach(() => {
stderrSpy.mockRestore();
stdoutSpy.mockRestore();
});
describe('run', () => {
it('builds a basic query from flags', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--select',
'date,amount',
'--limit',
'5',
]);
expect(api.q).toHaveBeenCalledWith('transactions');
const qObj = getQueryObj();
expect(qObj.select).toHaveBeenCalledWith(['date', 'amount']);
expect(qObj.limit).toHaveBeenCalledWith(5);
});
it('rejects unknown table name', async () => {
await expect(
run(['query', 'run', '--table', 'nonexistent']),
).rejects.toThrow('Unknown table "nonexistent"');
});
it('parses order-by with desc direction', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--order-by',
'date:desc,amount:asc',
]);
const qObj = getQueryObj();
expect(qObj.orderBy).toHaveBeenCalledWith([
{ date: 'desc' },
{ amount: 'asc' },
]);
});
it('passes --filter as JSON', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--filter',
'{"amount":{"$lt":0}}',
]);
const qObj = getQueryObj();
expect(qObj.filter).toHaveBeenCalledWith({ amount: { $lt: 0 } });
});
});
describe('--last flag', () => {
it('sets default table, select, orderBy, and limit', async () => {
await run(['query', 'run', '--last', '10']);
expect(api.q).toHaveBeenCalledWith('transactions');
const qObj = getQueryObj();
expect(qObj.select).toHaveBeenCalledWith([
'date',
'account.name',
'payee.name',
'category.name',
'amount',
'notes',
]);
expect(qObj.orderBy).toHaveBeenCalledWith([{ date: 'desc' }]);
expect(qObj.limit).toHaveBeenCalledWith(10);
});
it('allows explicit --select override', async () => {
await run(['query', 'run', '--last', '5', '--select', 'date,amount']);
const qObj = getQueryObj();
expect(qObj.select).toHaveBeenCalledWith(['date', 'amount']);
});
it('allows explicit --order-by override', async () => {
await run(['query', 'run', '--last', '5', '--order-by', 'amount:asc']);
const qObj = getQueryObj();
expect(qObj.orderBy).toHaveBeenCalledWith([{ amount: 'asc' }]);
});
it('allows --table transactions explicitly', async () => {
await run(['query', 'run', '--last', '5', '--table', 'transactions']);
expect(api.q).toHaveBeenCalledWith('transactions');
});
it('errors if --table is not transactions', async () => {
await expect(
run(['query', 'run', '--last', '5', '--table', 'accounts']),
).rejects.toThrow('--last implies --table transactions');
});
it('errors if --limit is also set', async () => {
await expect(
run(['query', 'run', '--last', '5', '--limit', '10']),
).rejects.toThrow('--last and --limit are mutually exclusive');
});
});
describe('--count flag', () => {
it('uses calculate with $count', async () => {
vi.mocked(api.aqlQuery).mockResolvedValueOnce({ data: 42 });
await run(['query', 'run', '--table', 'transactions', '--count']);
const qObj = getQueryObj();
expect(qObj.calculate).toHaveBeenCalledWith({ $count: '*' });
expect(printOutput).toHaveBeenCalledWith({ count: 42 }, undefined);
});
it('errors if --select is also set', async () => {
await expect(
run([
'query',
'run',
'--table',
'transactions',
'--count',
'--select',
'date',
]),
).rejects.toThrow('--count and --select are mutually exclusive');
});
});
describe('--where alias', () => {
it('works the same as --filter', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--where',
'{"amount":{"$gt":0}}',
]);
const qObj = getQueryObj();
expect(qObj.filter).toHaveBeenCalledWith({ amount: { $gt: 0 } });
});
it('errors if both --where and --filter are provided', async () => {
await expect(
run([
'query',
'run',
'--table',
'transactions',
'--where',
'{}',
'--filter',
'{}',
]),
).rejects.toThrow('--where and --filter are mutually exclusive');
});
});
describe('--offset flag', () => {
it('passes offset through to query', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--offset',
'20',
'--limit',
'10',
]);
const qObj = getQueryObj();
expect(qObj.offset).toHaveBeenCalledWith(20);
expect(qObj.limit).toHaveBeenCalledWith(10);
});
});
describe('--group-by flag', () => {
it('passes group-by through to query', async () => {
await run([
'query',
'run',
'--table',
'transactions',
'--group-by',
'category.name',
'--select',
'category.name,amount',
]);
const qObj = getQueryObj();
expect(qObj.groupBy).toHaveBeenCalledWith(['category.name']);
});
});
describe('tables subcommand', () => {
it('lists available tables', async () => {
await run(['query', 'tables']);
expect(printOutput).toHaveBeenCalledWith(
expect.arrayContaining([
{ name: 'transactions' },
{ name: 'accounts' },
{ name: 'categories' },
{ name: 'payees' },
]),
undefined,
);
});
});
describe('fields subcommand', () => {
it('lists fields for a known table', async () => {
await run(['query', 'fields', 'accounts']);
const output = vi.mocked(printOutput).mock.calls[0][0] as Array<{
name: string;
type: string;
}>;
expect(output).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'id', type: 'id' }),
expect.objectContaining({ name: 'name', type: 'string' }),
]),
);
});
it('errors on unknown table', async () => {
await expect(run(['query', 'fields', 'unknown'])).rejects.toThrow(
'Unknown table "unknown"',
);
});
});
});

View File

@@ -0,0 +1,344 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
import { parseIntFlag } from '../utils';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
/**
* Parse order-by strings like "date:desc,amount:asc,id" into
* AQL orderBy format: [{ date: 'desc' }, { amount: 'asc' }, 'id']
*/
export function parseOrderBy(
input: string,
): Array<string | Record<string, string>> {
return input.split(',').map(part => {
const trimmed = part.trim();
if (!trimmed) {
throw new Error('--order-by contains an empty field');
}
const colonIndex = trimmed.indexOf(':');
if (colonIndex === -1) {
return trimmed;
}
const field = trimmed.slice(0, colonIndex).trim();
if (!field) {
throw new Error(
`Invalid order field in "${trimmed}". Field name cannot be empty.`,
);
}
const direction = trimmed.slice(colonIndex + 1);
if (direction !== 'asc' && direction !== 'desc') {
throw new Error(
`Invalid order direction "${direction}" for field "${field}". Expected "asc" or "desc".`,
);
}
return { [field]: direction };
});
}
// TODO: Import schema from API once it exposes table/field metadata
const TABLE_SCHEMA: Record<
string,
Record<string, { type: string; ref?: string }>
> = {
transactions: {
id: { type: 'id' },
account: { type: 'id', ref: 'accounts' },
date: { type: 'date' },
amount: { type: 'integer' },
payee: { type: 'id', ref: 'payees' },
category: { type: 'id', ref: 'categories' },
notes: { type: 'string' },
imported_id: { type: 'string' },
transfer_id: { type: 'id' },
cleared: { type: 'boolean' },
reconciled: { type: 'boolean' },
starting_balance_flag: { type: 'boolean' },
imported_payee: { type: 'string' },
is_parent: { type: 'boolean' },
is_child: { type: 'boolean' },
parent_id: { type: 'id' },
sort_order: { type: 'float' },
schedule: { type: 'id', ref: 'schedules' },
'account.name': { type: 'string', ref: 'accounts' },
'payee.name': { type: 'string', ref: 'payees' },
'category.name': { type: 'string', ref: 'categories' },
'category.group.name': { type: 'string', ref: 'category_groups' },
},
accounts: {
id: { type: 'id' },
name: { type: 'string' },
offbudget: { type: 'boolean' },
closed: { type: 'boolean' },
sort_order: { type: 'float' },
},
categories: {
id: { type: 'id' },
name: { type: 'string' },
is_income: { type: 'boolean' },
group_id: { type: 'id', ref: 'category_groups' },
sort_order: { type: 'float' },
hidden: { type: 'boolean' },
'group.name': { type: 'string', ref: 'category_groups' },
},
payees: {
id: { type: 'id' },
name: { type: 'string' },
transfer_acct: { type: 'id', ref: 'accounts' },
},
rules: {
id: { type: 'id' },
stage: { type: 'string' },
conditions_op: { type: 'string' },
conditions: { type: 'json' },
actions: { type: 'json' },
},
schedules: {
id: { type: 'id' },
name: { type: 'string' },
rule: { type: 'id', ref: 'rules' },
next_date: { type: 'date' },
completed: { type: 'boolean' },
},
};
const AVAILABLE_TABLES = Object.keys(TABLE_SCHEMA).join(', ');
const LAST_DEFAULT_SELECT = [
'date',
'account.name',
'payee.name',
'category.name',
'amount',
'notes',
];
function buildQueryFromFile(
parsed: Record<string, unknown>,
fallbackTable: string | undefined,
) {
const table = typeof parsed.table === 'string' ? parsed.table : fallbackTable;
if (!table) {
throw new Error(
'--table is required when the input file lacks a "table" field',
);
}
let queryObj = api.q(table);
if (Array.isArray(parsed.select)) queryObj = queryObj.select(parsed.select);
if (isRecord(parsed.filter)) queryObj = queryObj.filter(parsed.filter);
if (Array.isArray(parsed.orderBy)) {
queryObj = queryObj.orderBy(parsed.orderBy);
}
if (typeof parsed.limit === 'number') queryObj = queryObj.limit(parsed.limit);
if (typeof parsed.offset === 'number') {
queryObj = queryObj.offset(parsed.offset);
}
if (Array.isArray(parsed.groupBy)) {
queryObj = queryObj.groupBy(parsed.groupBy);
}
return queryObj;
}
function buildQueryFromFlags(cmdOpts: Record<string, string | undefined>) {
const last = cmdOpts.last ? parseIntFlag(cmdOpts.last, '--last') : undefined;
if (last !== undefined) {
if (cmdOpts.table && cmdOpts.table !== 'transactions') {
throw new Error(
'--last implies --table transactions. Cannot use with --table ' +
cmdOpts.table,
);
}
if (cmdOpts.limit) {
throw new Error('--last and --limit are mutually exclusive');
}
}
const table =
cmdOpts.table ?? (last !== undefined ? 'transactions' : undefined);
if (!table) {
throw new Error('--table is required (or use --file or --last)');
}
if (!(table in TABLE_SCHEMA)) {
throw new Error(
`Unknown table "${table}". Available tables: ${AVAILABLE_TABLES}`,
);
}
if (cmdOpts.where && cmdOpts.filter) {
throw new Error('--where and --filter are mutually exclusive');
}
if (cmdOpts.count && cmdOpts.select) {
throw new Error('--count and --select are mutually exclusive');
}
let queryObj = api.q(table);
if (cmdOpts.count) {
queryObj = queryObj.calculate({ $count: '*' });
} else if (cmdOpts.select) {
queryObj = queryObj.select(cmdOpts.select.split(','));
} else if (last !== undefined) {
queryObj = queryObj.select(LAST_DEFAULT_SELECT);
}
const filterStr = cmdOpts.filter ?? cmdOpts.where;
if (filterStr) {
queryObj = queryObj.filter(JSON.parse(filterStr));
}
const orderByStr =
cmdOpts.orderBy ??
(last !== undefined && !cmdOpts.count ? 'date:desc' : undefined);
if (orderByStr) {
queryObj = queryObj.orderBy(parseOrderBy(orderByStr));
}
const limitVal =
last ??
(cmdOpts.limit ? parseIntFlag(cmdOpts.limit, '--limit') : undefined);
if (limitVal !== undefined) {
queryObj = queryObj.limit(limitVal);
}
if (cmdOpts.offset) {
queryObj = queryObj.offset(parseIntFlag(cmdOpts.offset, '--offset'));
}
if (cmdOpts.groupBy) {
queryObj = queryObj.groupBy(cmdOpts.groupBy.split(','));
}
return queryObj;
}
const RUN_EXAMPLES = `
Examples:
# Show last 5 transactions (shortcut)
actual query run --last 5
# Transactions ordered by date descending
actual query run --table transactions --select "date,amount,payee.name" --order-by "date:desc" --limit 10
# Filter with JSON (negative amounts = expenses)
actual query run --table transactions --filter '{"amount":{"$lt":0}}' --limit 5
# Count transactions
actual query run --table transactions --count
# Group by category (use --file for aggregate expressions)
echo '{"table":"transactions","groupBy":["category.name"],"select":["category.name",{"amount":{"$sum":"$amount"}}]}' | actual query run --file -
# Pagination
actual query run --table transactions --order-by "date:desc" --limit 10 --offset 20
# Use --where (alias for --filter)
actual query run --table transactions --where '{"payee.name":"Grocery Store"}' --limit 5
# Read query from a JSON file
actual query run --file query.json
# Pipe query from stdin
echo '{"table":"transactions","limit":5}' | actual query run --file -
Available tables: ${AVAILABLE_TABLES}
Use "actual query tables" and "actual query fields <table>" for schema info.
Common filter operators: $eq, $ne, $lt, $lte, $gt, $gte, $like, $and, $or
See ActualQL docs for full reference: https://actualbudget.org/docs/api/actual-ql/`;
export function registerQueryCommand(program: Command) {
const query = program
.command('query')
.description('Run AQL (Actual Query Language) queries');
query
.command('run')
.description('Execute an AQL query')
.option(
'--table <table>',
'Table to query (use "actual query tables" to list available tables)',
)
.option('--select <fields>', 'Comma-separated fields to select')
.option('--filter <json>', 'Filter as JSON (e.g. \'{"amount":{"$lt":0}}\')')
.option(
'--where <json>',
'Alias for --filter (cannot be used together with --filter)',
)
.option(
'--order-by <fields>',
'Fields with optional direction: field1:desc,field2 (default: asc)',
)
.option('--limit <n>', 'Limit number of results')
.option('--offset <n>', 'Skip first N results (for pagination)')
.option(
'--last <n>',
'Show last N transactions (implies --table transactions, --order-by date:desc)',
)
.option('--count', 'Count matching rows instead of returning them')
.option(
'--group-by <fields>',
'Comma-separated fields to group by (use with aggregate selects)',
)
.option(
'--file <path>',
'Read full query object from JSON file (use - for stdin)',
)
.addHelpText('after', RUN_EXAMPLES)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const parsed = cmdOpts.file ? readJsonInput(cmdOpts) : undefined;
if (parsed !== undefined && !isRecord(parsed)) {
throw new Error('Query file must contain a JSON object');
}
const queryObj = parsed
? buildQueryFromFile(parsed, cmdOpts.table)
: buildQueryFromFlags(cmdOpts);
const result = await api.aqlQuery(queryObj);
if (cmdOpts.count) {
printOutput({ count: result.data }, opts.format);
} else {
printOutput(result, opts.format);
}
});
});
query
.command('tables')
.description('List available tables for querying')
.action(() => {
const opts = program.opts();
const tables = Object.keys(TABLE_SCHEMA).map(name => ({ name }));
printOutput(tables, opts.format);
});
query
.command('fields <table>')
.description('List fields for a given table')
.action((table: string) => {
const opts = program.opts();
const schema = TABLE_SCHEMA[table];
if (!schema) {
throw new Error(
`Unknown table "${table}". Available tables: ${Object.keys(TABLE_SCHEMA).join(', ')}`,
);
}
const fields = Object.entries(schema).map(([name, info]) => ({
name,
type: info.type,
...(info.ref ? { ref: info.ref } : {}),
}));
printOutput(fields, opts.format);
});
}

View File

@@ -0,0 +1,77 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
export function registerRulesCommand(program: Command) {
const rules = program
.command('rules')
.description('Manage transaction rules');
rules
.command('list')
.description('List all rules')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getRules();
printOutput(result, opts.format);
});
});
rules
.command('payee-rules <payeeId>')
.description('List rules for a specific payee')
.action(async (payeeId: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getPayeeRules(payeeId);
printOutput(result, opts.format);
});
});
rules
.command('create')
.description('Create a new rule')
.option('--data <json>', 'Rule definition as JSON')
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const rule = readJsonInput(cmdOpts) as Parameters<
typeof api.createRule
>[0];
const id = await api.createRule(rule);
printOutput({ id }, opts.format);
});
});
rules
.command('update')
.description('Update a rule')
.option('--data <json>', 'Rule data as JSON (must include id)')
.option('--file <path>', 'Read rule from JSON file (use - for stdin)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const rule = readJsonInput(cmdOpts) as Parameters<
typeof api.updateRule
>[0];
await api.updateRule(rule);
printOutput({ success: true }, opts.format);
});
});
rules
.command('delete <id>')
.description('Delete a rule')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteRule(id);
printOutput({ success: true, id }, opts.format);
});
});
}

View File

@@ -0,0 +1,67 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
export function registerSchedulesCommand(program: Command) {
const schedules = program
.command('schedules')
.description('Manage scheduled transactions');
schedules
.command('list')
.description('List all schedules')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getSchedules();
printOutput(result, opts.format);
});
});
schedules
.command('create')
.description('Create a new schedule')
.option('--data <json>', 'Schedule definition as JSON')
.option('--file <path>', 'Read schedule from JSON file (use - for stdin)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const schedule = readJsonInput(cmdOpts) as Parameters<
typeof api.createSchedule
>[0];
const id = await api.createSchedule(schedule);
printOutput({ id }, opts.format);
});
});
schedules
.command('update <id>')
.description('Update a schedule')
.option('--data <json>', 'Fields to update as JSON')
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
.option('--reset-next-date', 'Reset next occurrence date', false)
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields = readJsonInput(cmdOpts) as Parameters<
typeof api.updateSchedule
>[1];
await api.updateSchedule(id, fields, cmdOpts.resetNextDate);
printOutput({ success: true, id }, opts.format);
});
});
schedules
.command('delete <id>')
.description('Delete a schedule')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteSchedule(id);
printOutput({ success: true, id }, opts.format);
});
});
}

View File

@@ -0,0 +1,60 @@
import * as api from '@actual-app/api';
import { Option } from 'commander';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
export function registerServerCommand(program: Command) {
const server = program.command('server').description('Server utilities');
server
.command('version')
.description('Get server version')
.action(async () => {
const opts = program.opts();
await withConnection(
opts,
async () => {
const version = await api.getServerVersion();
printOutput({ version }, opts.format);
},
{ loadBudget: false },
);
});
server
.command('get-id')
.description('Get entity ID by name')
.addOption(
new Option('--type <type>', 'Entity type')
.choices(['accounts', 'categories', 'payees', 'schedules'])
.makeOptionMandatory(),
)
.requiredOption('--name <name>', 'Entity name')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.getIDByName(cmdOpts.type, cmdOpts.name);
printOutput(
{ id, type: cmdOpts.type, name: cmdOpts.name },
opts.format,
);
});
});
server
.command('bank-sync')
.description('Run bank synchronization')
.option('--account <id>', 'Specific account ID to sync')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const args = cmdOpts.account
? { accountId: cmdOpts.account }
: undefined;
await api.runBankSync(args);
printOutput({ success: true }, opts.format);
});
});
}

View File

@@ -0,0 +1,74 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { printOutput } from '../output';
export function registerTagsCommand(program: Command) {
const tags = program.command('tags').description('Manage tags');
tags
.command('list')
.description('List all tags')
.action(async () => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getTags();
printOutput(result, opts.format);
});
});
tags
.command('create')
.description('Create a new tag')
.requiredOption('--tag <tag>', 'Tag name')
.option('--color <color>', 'Tag color')
.option('--description <description>', 'Tag description')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const id = await api.createTag({
tag: cmdOpts.tag,
color: cmdOpts.color,
description: cmdOpts.description,
});
printOutput({ id }, opts.format);
});
});
tags
.command('update <id>')
.description('Update a tag')
.option('--tag <tag>', 'New tag name')
.option('--color <color>', 'New tag color')
.option('--description <description>', 'New tag description')
.action(async (id: string, cmdOpts) => {
const fields: Record<string, unknown> = {};
if (cmdOpts.tag !== undefined) fields.tag = cmdOpts.tag;
if (cmdOpts.color !== undefined) fields.color = cmdOpts.color;
if (cmdOpts.description !== undefined) {
fields.description = cmdOpts.description;
}
if (Object.keys(fields).length === 0) {
throw new Error(
'At least one of --tag, --color, or --description is required',
);
}
const opts = program.opts();
await withConnection(opts, async () => {
await api.updateTag(id, fields);
printOutput({ success: true, id }, opts.format);
});
});
tags
.command('delete <id>')
.description('Delete a tag')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteTag(id);
printOutput({ success: true, id }, opts.format);
});
});
}

View File

@@ -0,0 +1,114 @@
import * as api from '@actual-app/api';
import type { Command } from 'commander';
import { withConnection } from '../connection';
import { readJsonInput } from '../input';
import { printOutput } from '../output';
export function registerTransactionsCommand(program: Command) {
const transactions = program
.command('transactions')
.description('Manage transactions');
transactions
.command('list')
.description('List transactions for an account')
.requiredOption('--account <id>', 'Account ID')
.requiredOption('--start <date>', 'Start date (YYYY-MM-DD)')
.requiredOption('--end <date>', 'End date (YYYY-MM-DD)')
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const result = await api.getTransactions(
cmdOpts.account,
cmdOpts.start,
cmdOpts.end,
);
printOutput(result, opts.format);
});
});
transactions
.command('add')
.description('Add transactions to an account')
.requiredOption('--account <id>', 'Account ID')
.option('--data <json>', 'Transaction data as JSON array')
.option(
'--file <path>',
'Read transaction data from JSON file (use - for stdin)',
)
.option('--learn-categories', 'Learn category assignments', false)
.option('--run-transfers', 'Process transfers', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const transactions = readJsonInput(cmdOpts) as Parameters<
typeof api.addTransactions
>[1];
const result = await api.addTransactions(
cmdOpts.account,
transactions,
{
learnCategories: cmdOpts.learnCategories,
runTransfers: cmdOpts.runTransfers,
},
);
printOutput(result, opts.format);
});
});
transactions
.command('import')
.description('Import transactions to an account')
.requiredOption('--account <id>', 'Account ID')
.option('--data <json>', 'Transaction data as JSON array')
.option(
'--file <path>',
'Read transaction data from JSON file (use - for stdin)',
)
.option('--dry-run', 'Preview without importing', false)
.action(async cmdOpts => {
const opts = program.opts();
await withConnection(opts, async () => {
const transactions = readJsonInput(cmdOpts) as Parameters<
typeof api.importTransactions
>[1];
const result = await api.importTransactions(
cmdOpts.account,
transactions,
{
defaultCleared: true,
dryRun: cmdOpts.dryRun,
},
);
printOutput(result, opts.format);
});
});
transactions
.command('update <id>')
.description('Update a transaction')
.option('--data <json>', 'Fields to update as JSON')
.option('--file <path>', 'Read fields from JSON file (use - for stdin)')
.action(async (id: string, cmdOpts) => {
const opts = program.opts();
await withConnection(opts, async () => {
const fields = readJsonInput(cmdOpts) as Parameters<
typeof api.updateTransaction
>[1];
await api.updateTransaction(id, fields);
printOutput({ success: true, id }, opts.format);
});
});
transactions
.command('delete <id>')
.description('Delete a transaction')
.action(async (id: string) => {
const opts = program.opts();
await withConnection(opts, async () => {
await api.deleteTransaction(id);
printOutput({ success: true, id }, opts.format);
});
});
}

View File

@@ -0,0 +1,185 @@
import { homedir } from 'os';
import { join } from 'path';
import { resolveConfig } from './config';
const mockSearch = vi.fn().mockResolvedValue(null);
vi.mock('cosmiconfig', () => ({
cosmiconfig: () => ({
search: (...args: unknown[]) => mockSearch(...args),
}),
}));
function mockConfigFile(config: Record<string, unknown> | null) {
if (config) {
mockSearch.mockResolvedValue({ config, isEmpty: false });
} else {
mockSearch.mockResolvedValue(null);
}
}
describe('resolveConfig', () => {
const savedEnv: Record<string, string | undefined> = {};
const envKeys = [
'ACTUAL_SERVER_URL',
'ACTUAL_PASSWORD',
'ACTUAL_SESSION_TOKEN',
'ACTUAL_SYNC_ID',
'ACTUAL_DATA_DIR',
'ACTUAL_ENCRYPTION_PASSWORD',
];
beforeEach(() => {
for (const key of envKeys) {
savedEnv[key] = process.env[key];
delete process.env[key];
}
mockConfigFile(null);
});
afterEach(() => {
for (const key of envKeys) {
if (savedEnv[key] !== undefined) {
process.env[key] = savedEnv[key];
} else {
delete process.env[key];
}
}
});
describe('priority chain', () => {
it('CLI opts take highest priority', async () => {
process.env.ACTUAL_SERVER_URL = 'http://env';
process.env.ACTUAL_PASSWORD = 'envpw';
process.env.ACTUAL_ENCRYPTION_PASSWORD = 'env-enc';
mockConfigFile({
serverUrl: 'http://file',
password: 'filepw',
encryptionPassword: 'file-enc',
});
const config = await resolveConfig({
serverUrl: 'http://cli',
password: 'clipw',
encryptionPassword: 'cli-enc',
});
expect(config.serverUrl).toBe('http://cli');
expect(config.password).toBe('clipw');
expect(config.encryptionPassword).toBe('cli-enc');
});
it('env vars override file config', async () => {
process.env.ACTUAL_SERVER_URL = 'http://env';
process.env.ACTUAL_PASSWORD = 'envpw';
process.env.ACTUAL_ENCRYPTION_PASSWORD = 'env-enc';
mockConfigFile({
serverUrl: 'http://file',
password: 'filepw',
encryptionPassword: 'file-enc',
});
const config = await resolveConfig({});
expect(config.serverUrl).toBe('http://env');
expect(config.password).toBe('envpw');
expect(config.encryptionPassword).toBe('env-enc');
});
it('file config is used when no CLI opts or env vars', async () => {
mockConfigFile({
serverUrl: 'http://file',
password: 'filepw',
syncId: 'budget-1',
encryptionPassword: 'file-enc',
});
const config = await resolveConfig({});
expect(config.serverUrl).toBe('http://file');
expect(config.password).toBe('filepw');
expect(config.syncId).toBe('budget-1');
expect(config.encryptionPassword).toBe('file-enc');
});
});
describe('defaults', () => {
it('dataDir defaults to ~/.actual-cli/data', async () => {
const config = await resolveConfig({
serverUrl: 'http://test',
password: 'pw',
});
expect(config.dataDir).toBe(join(homedir(), '.actual-cli', 'data'));
});
it('CLI opt overrides default dataDir', async () => {
const config = await resolveConfig({
serverUrl: 'http://test',
password: 'pw',
dataDir: '/custom/dir',
});
expect(config.dataDir).toBe('/custom/dir');
});
});
describe('validation', () => {
it('throws when serverUrl is missing', async () => {
await expect(resolveConfig({ password: 'pw' })).rejects.toThrow(
'Server URL is required',
);
});
it('throws when neither password nor sessionToken provided', async () => {
await expect(resolveConfig({ serverUrl: 'http://test' })).rejects.toThrow(
'Authentication required',
);
});
it('accepts sessionToken without password', async () => {
const config = await resolveConfig({
serverUrl: 'http://test',
sessionToken: 'tok',
});
expect(config.sessionToken).toBe('tok');
expect(config.password).toBeUndefined();
});
it('accepts password without sessionToken', async () => {
const config = await resolveConfig({
serverUrl: 'http://test',
password: 'pw',
});
expect(config.password).toBe('pw');
expect(config.sessionToken).toBeUndefined();
});
});
describe('cosmiconfig handling', () => {
it('handles null result (no config file found)', async () => {
mockConfigFile(null);
const config = await resolveConfig({
serverUrl: 'http://test',
password: 'pw',
});
expect(config.serverUrl).toBe('http://test');
});
it('handles isEmpty result', async () => {
mockSearch.mockResolvedValue({ config: {}, isEmpty: true });
const config = await resolveConfig({
serverUrl: 'http://test',
password: 'pw',
});
expect(config.serverUrl).toBe('http://test');
});
});
});

141
packages/cli/src/config.ts Normal file
View File

@@ -0,0 +1,141 @@
import { homedir } from 'os';
import { join } from 'path';
import { cosmiconfig } from 'cosmiconfig';
export type CliConfig = {
serverUrl: string;
password?: string;
sessionToken?: string;
syncId?: string;
dataDir: string;
encryptionPassword?: string;
};
export type CliGlobalOpts = {
serverUrl?: string;
password?: string;
sessionToken?: string;
syncId?: string;
dataDir?: string;
encryptionPassword?: string;
format?: 'json' | 'table' | 'csv';
verbose?: boolean;
};
type ConfigFileContent = {
serverUrl?: string;
password?: string;
sessionToken?: string;
syncId?: string;
dataDir?: string;
encryptionPassword?: string;
};
const configFileKeys: readonly string[] = [
'serverUrl',
'password',
'sessionToken',
'syncId',
'dataDir',
'encryptionPassword',
];
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function validateConfigFileContent(value: unknown): ConfigFileContent {
if (!isRecord(value)) {
throw new Error(
'Invalid config file: expected an object with keys: ' +
configFileKeys.join(', '),
);
}
for (const key of Object.keys(value)) {
if (!configFileKeys.includes(key)) {
throw new Error(`Invalid config file: unknown key "${key}"`);
}
if (value[key] !== undefined && typeof value[key] !== 'string') {
throw new Error(
`Invalid config file: key "${key}" must be a string, got ${typeof value[key]}`,
);
}
}
return value as ConfigFileContent;
}
async function loadConfigFile(): Promise<ConfigFileContent> {
const explorer = cosmiconfig('actual', {
searchPlaces: [
'package.json',
'.actualrc',
'.actualrc.json',
'.actualrc.yaml',
'.actualrc.yml',
'actual.config.json',
'actual.config.yaml',
'actual.config.yml',
],
});
const result = await explorer.search();
if (result && !result.isEmpty) {
return validateConfigFileContent(result.config);
}
return {};
}
export async function resolveConfig(
cliOpts: CliGlobalOpts,
): Promise<CliConfig> {
const fileConfig = await loadConfigFile();
const serverUrl =
cliOpts.serverUrl ??
process.env.ACTUAL_SERVER_URL ??
fileConfig.serverUrl ??
'';
const password =
cliOpts.password ?? process.env.ACTUAL_PASSWORD ?? fileConfig.password;
const sessionToken =
cliOpts.sessionToken ??
process.env.ACTUAL_SESSION_TOKEN ??
fileConfig.sessionToken;
const syncId =
cliOpts.syncId ?? process.env.ACTUAL_SYNC_ID ?? fileConfig.syncId;
const dataDir =
cliOpts.dataDir ??
process.env.ACTUAL_DATA_DIR ??
fileConfig.dataDir ??
join(homedir(), '.actual-cli', 'data');
const encryptionPassword =
cliOpts.encryptionPassword ??
process.env.ACTUAL_ENCRYPTION_PASSWORD ??
fileConfig.encryptionPassword;
if (!serverUrl) {
throw new Error(
'Server URL is required. Set --server-url, ACTUAL_SERVER_URL env var, or serverUrl in config file.',
);
}
if (!password && !sessionToken) {
throw new Error(
'Authentication required. Set --password/--session-token, ACTUAL_PASSWORD/ACTUAL_SESSION_TOKEN env var, or password/sessionToken in config file.',
);
}
return {
serverUrl,
password,
sessionToken,
syncId,
dataDir,
encryptionPassword,
};
}

View File

@@ -0,0 +1,134 @@
import * as api from '@actual-app/api';
import { resolveConfig } from './config';
import { withConnection } from './connection';
vi.mock('@actual-app/api', () => ({
init: vi.fn().mockResolvedValue(undefined),
downloadBudget: vi.fn().mockResolvedValue(undefined),
shutdown: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('./config', () => ({
resolveConfig: vi.fn(),
}));
function setConfig(overrides: Record<string, unknown> = {}) {
vi.mocked(resolveConfig).mockResolvedValue({
serverUrl: 'http://test',
password: 'pw',
dataDir: '/tmp/data',
syncId: 'budget-1',
...overrides,
});
}
describe('withConnection', () => {
let stderrSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
stderrSpy = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
setConfig();
});
afterEach(() => {
stderrSpy.mockRestore();
});
it('calls api.init with password when no sessionToken', async () => {
setConfig({ password: 'pw', sessionToken: undefined });
await withConnection({}, async () => 'ok');
expect(api.init).toHaveBeenCalledWith({
serverURL: 'http://test',
password: 'pw',
dataDir: '/tmp/data',
verbose: undefined,
});
});
it('calls api.init with sessionToken when present', async () => {
setConfig({ sessionToken: 'tok', password: undefined });
await withConnection({}, async () => 'ok');
expect(api.init).toHaveBeenCalledWith({
serverURL: 'http://test',
sessionToken: 'tok',
dataDir: '/tmp/data',
verbose: undefined,
});
});
it('calls api.downloadBudget when syncId is set', async () => {
setConfig({ syncId: 'budget-1' });
await withConnection({}, async () => 'ok');
expect(api.downloadBudget).toHaveBeenCalledWith('budget-1', {
password: undefined,
});
});
it('throws when loadBudget is true but syncId is not set', async () => {
setConfig({ syncId: undefined });
await expect(withConnection({}, async () => 'ok')).rejects.toThrow(
'Sync ID is required',
);
});
it('skips budget download when loadBudget is false and syncId is not set', async () => {
setConfig({ syncId: undefined });
await withConnection({}, async () => 'ok', { loadBudget: false });
expect(api.downloadBudget).not.toHaveBeenCalled();
});
it('does not call api.downloadBudget when loadBudget is false', async () => {
setConfig({ syncId: 'budget-1' });
await withConnection({}, async () => 'ok', { loadBudget: false });
expect(api.downloadBudget).not.toHaveBeenCalled();
});
it('returns callback result', async () => {
const result = await withConnection({}, async () => 42);
expect(result).toBe(42);
});
it('calls api.shutdown in finally block on success', async () => {
await withConnection({}, async () => 'ok');
expect(api.shutdown).toHaveBeenCalled();
});
it('calls api.shutdown in finally block on error', async () => {
await expect(
withConnection({}, async () => {
throw new Error('boom');
}),
).rejects.toThrow('boom');
expect(api.shutdown).toHaveBeenCalled();
});
it('does not write to stderr by default', async () => {
await withConnection({}, async () => 'ok');
expect(stderrSpy).not.toHaveBeenCalled();
});
it('writes info to stderr when verbose', async () => {
await withConnection({ verbose: true }, async () => 'ok');
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining('Connecting to'),
);
});
});

View File

@@ -0,0 +1,65 @@
import { mkdirSync } from 'fs';
import * as api from '@actual-app/api';
import { resolveConfig } from './config';
import type { CliGlobalOpts } from './config';
function info(message: string, verbose?: boolean) {
if (verbose) {
process.stderr.write(message + '\n');
}
}
type ConnectionOptions = {
loadBudget?: boolean;
};
export async function withConnection<T>(
globalOpts: CliGlobalOpts,
fn: () => Promise<T>,
options: ConnectionOptions = {},
): Promise<T> {
const { loadBudget = true } = options;
const config = await resolveConfig(globalOpts);
mkdirSync(config.dataDir, { recursive: true });
info(`Connecting to ${config.serverUrl}...`, globalOpts.verbose);
if (config.sessionToken) {
await api.init({
serverURL: config.serverUrl,
dataDir: config.dataDir,
sessionToken: config.sessionToken,
verbose: globalOpts.verbose,
});
} else if (config.password) {
await api.init({
serverURL: config.serverUrl,
dataDir: config.dataDir,
password: config.password,
verbose: globalOpts.verbose,
});
} else {
throw new Error(
'Authentication required. Provide --password or --session-token, or set ACTUAL_PASSWORD / ACTUAL_SESSION_TOKEN.',
);
}
try {
if (loadBudget && config.syncId) {
info(`Downloading budget ${config.syncId}...`, globalOpts.verbose);
await api.downloadBudget(config.syncId, {
password: config.encryptionPassword,
});
} else if (loadBudget && !config.syncId) {
throw new Error(
'Sync ID is required for this command. Set --sync-id or ACTUAL_SYNC_ID.',
);
}
return await fn();
} finally {
await api.shutdown();
}
}

Some files were not shown because too many files have changed in this diff Show More