desktop-client no longer imports absurd-sql directly — that plumbing
moved into loot-core's backend-worker module as part of the browser
worker consolidation. The dep was left over; removing it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `yarn constraints` flagged jsdom ^29.0.2 in api vs ^27.4.0 in
desktop-client. Align to ^27.4.0 — api's browser-facade test only
uses a minimal jsdom env, both versions satisfy its needs.
- `yarn typecheck` under tsc-strict needed a declaration for
absurd-sql/dist/indexeddb-main-thread; added a one-line .d.ts-style
shim under packages/loot-core/typings/, matching the existing
pattern used for vite-plugin-peggy-loader.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
loot-core's JS migrations use `#`-subpath imports that only resolve
inside loot-core's package boundary. Once those files live in
`node_modules/@actual-app/api/dist/data/migrations/`, Vite's dev-server
import-analysis tries to resolve them and errors. Consumer workaround
was a bespoke middleware in their vite.config.ts — a leaky abstraction
for a package that should just work on import.
Fix it inside the api package:
- Build-time rename: copyMigrationsAndDefaultDb now writes each .js
migration under dist/data/migrations/ with an extra `.data` suffix
and records the suffix in dist/data-file-index.txt. dist/migrations/
(flat, used by Node consumers) stays untouched.
- Runtime fetch wrap: browser-worker.ts installs a small pre-hook at
module load that rewrites URLs to match — .js → .js.data on the
request side, strips the suffix from data-file-index.txt responses —
so loot-core's migration runner still sees files at /migrations/foo.js
in the virtual FS.
Consumer-side vite.config.ts is now just COOP/COEP + optimizeDeps.exclude;
no dev-server plumbing needed. Verified end-to-end via the playground:
init → download → 22 accounts → 2 transactions → done, with zero config
hacks in the consumer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Persist custom CSS overrides as a standalone global pref
Moves the custom CSS override out of the InstalledTheme JSON blob into
a dedicated customCssOverride global pref so that overrides survive
switching themes, clearing installed themes, or toggling auto/light/dark
mode. Includes a one-time migration that lifts the legacy overrideCss
field out of installedCustomLightTheme / installedCustomDarkTheme JSON.
- Add customCssOverride global pref (loot-core types + server defaults)
- Inject the override as a trailing style layer in CustomThemeStyle so
it layers on top of any installed custom theme
- Drop overrideCss from the InstalledTheme type; extractLegacyOverride
+ migrateLegacyOverride handle the one-time lift with whitespace trim
- Run the migration from CustomThemeStyle with an idempotent effect that
re-runs safely once prefs hydrate
- Bind the ThemeInstaller textarea directly to the new pref
- Add a "Custom CSS is active" indicator button next to the theme
selector that opens the installer for editing the override without
flipping auto mode to light
- Pre-switch out of auto when the user picks "Custom theme" from the
main selector, so the flag that used to distinguish entry points goes
away and handleInstall collapses to a pure slot dispatch
- Tests: hermetic Themes settings tests, expanded customThemes unit
tests covering extraction/migration/trim edge cases, updated
ThemeInstaller tests for the new pref binding
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Address review feedback for custom CSS override installer
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] Defer auto-mode theme switch and guard stale installer callbacks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [AI] sync-server: use workspace reference for @actual-app/crdt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update build script in sync-server package to use TypeScript's build mode
* Package electron
* [AI] crdt: add conditional exports so Node can load the built bundle
Before this change the root exports entry pointed at `./src/index.ts`, so
any pure-Node consumer (notably the sync server that Electron forks as a
utility process) failed to import `@actual-app/crdt` — Node can't execute
TypeScript source directly. Sync-server had been masking this by pulling
`@actual-app/crdt@npm:2.1.0` where `publishConfig.exports` resolves to
`./dist/index.js`; once sync-server switched to `workspace:*`, the
Functional Desktop App CI job timed out waiting for the sync server to
boot.
Switch to conditional exports in the same shape `@actual-app/api` already
uses:
- `types` → `./dist/index.d.ts` for TypeScript tooling
- `development` → `./src/index.ts` for Vite/Vitest (HMR, fast feedback)
- `default` → `./dist/index.js` for Node runtime
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tsgo -b (used by both project-reference builds and the typecheck
script) emits .js per source file into outDir. Since the api's outDir
is dist/ and tsconfig had noEmit: false + declaration: true, every
`yarn typecheck` overwrote the Vite-built browser.js / worker.js with
per-file TS compilations, breaking downstream consumers until the next
Vite rebuild.
Adding emitDeclarationOnly: true to tsconfig keeps the composite /
declaration wiring intact (required for project references) but
suppresses JS emission. build:node still passes --emitDeclarationOnly
on the CLI so the intent is explicit there too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consumers no longer copy default-db.sqlite, migrations, sql-wasm.wasm,
or data-file-index.txt into their static assets directory. The api's
dist/ now contains everything loot-core's browser fs asks for — the
existing files plus a new data-file-index.txt manifest and a data/
mirror directory (hard-linked to avoid duplicating bytes).
At init time the main-thread facade derives the directory portion of
its own bundle URL (via string manipulation to dodge Vite's asset
plugin) and hands it to the worker as __assetsBaseUrl. The worker
sets process.env.PUBLIC_URL to that URL before calling loot-core's
init(config), so populateDefaultFilesystem and sql.js locateFile all
resolve against @actual-app/api/dist/ wherever the consumer's bundler
placed it.
Playground shrinks accordingly: no more public/ directory,
copy-assets.sh script, or predev hook. `yarn dev` now does just
`vite` — matching the zero-setup `api.init()` story.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add 'Last 30 days' date range option to custom reports
Add a rolling 30-day date range to the Live mode date filter in custom
reports. The option appears between 'Last month' and 'Last 3 months'
and is available for Daily, Weekly, and Monthly intervals.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* [autofix.ci] apply automated fixes
* Fix release note filename and author to match PR #7217
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: youngcw <calebyoung94@gmail.com>
Extracts the WorkerBridge class + the Worker/SharedWorker transport
selector out of packages/desktop-client/src/browser-preload.js and into
a new loot-core module at packages/loot-core/src/platform/client/
browser-preload/. desktop-client's preload shrinks to a thin shell that
only wires its PWA service worker, package.json version, SharedWorker
factory, and the global.Actual shim — everything else is a one-line
startBrowserBackend({ ... }) call into loot-core.
The api package still uses the lighter-weight createBackendWorker entry
in the sibling loot-core module; both packages now consume the worker-
bootstrap primitives from loot-core rather than duplicating them.
Verified end-to-end in the browser via playwright — the api playground
still loads, downloads the budget, and renders accounts+transactions
identically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the last piece of bespoke browser wiring in @actual-app/api so
consumers call api.init({...}) with no worker construction of their own.
Under the hood, the api package now:
- Spawns its Web Worker itself via `new Worker(new URL('./worker.js',
import.meta.url), { type: 'module' })`. Consumer bundlers resolve the
sibling asset at their own build time.
- Speaks loot-core's existing {id, name, args} / {type:'reply', id,
result} backend protocol, including the {type:'connect'} handshake
— same protocol desktop-client's browser-preload.js already feeds.
- Delegates sqlite bootstrap to loot-core's public init(config) via a
worker-registered `api-browser/init` handler; server-side dispatch is
handled by the existing packages/loot-core/src/platform/server/
connection layer, so no custom {op, payload} shape remains.
The absurd-sql main-thread plumbing (initSQLBackend + __absurd:* filter)
is now a single function in loot-core:
`packages/loot-core/src/platform/client/backend-worker/createBackendWorker`,
consumed by both desktop-client's browser-preload.js and the api's
browser rpc.ts.
Test split moves accordingly: browser-facade.test.ts swaps in a
Worker mock that speaks the new protocol (id, name, args / reply handshake)
and confirms init forwards config via api-browser/init.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
absurd-sql uses Atomics.wait for sync sqlite access, which only works
inside a Web Worker. Rather than forcing every consumer to wire up
their own worker + RPC glue, ship two artifacts:
- dist/browser.js: tiny main-thread facade (~10 KB). Reuses
packages/api/methods.ts verbatim by aliasing
@actual-app/core/server/main to browser/lib-stub.ts at build time;
every lib.send call posts to the worker.
- dist/worker.js: the full loot-core + sql.js + absurd-sql stack
(~3.6 MB) running in a Web Worker.
Consumer wiring:
const worker = new Worker(
new URL('@actual-app/api/dist/worker.js', import.meta.url),
{ type: 'module' },
);
await api.init({ worker, dataDir: '/documents', serverURL, password });
await api.getAccounts();
Same named imports as Node/Electron — the worker is the only
browser-specific wiring. Keeping the URL construction in consumer
code lets their bundler (Vite, Webpack, ...) handle worker.js as an
asset without forcing us onto a single bundler convention.
Tests split accordingly: Node runs the full CRUD roundtrip against
real loot-core; jsdom runs a facade test that verifies init
validation, postMessage payload shapes, and error propagation via
a mock Worker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Node-side build plugin to copy @jlongster/sql.js' sql-wasm.wasm
into dist/ alongside default-db.sqlite and migrations/. Browser
consumers can now point a static handler at dist/ without reaching
into nested node_modules of a transitive dep.
Also introduces vite-plugin-node-polyfills for the browser build
(process / Buffer / stream / path / crypto / zlib / fs / assert),
with process.env.* values substituted at build time. Splits the
browser vitest config out of vite.browser.config.mts so node polyfills
don't shadow real Node fs in test setup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds setup.browser.ts with fake-indexeddb, a fetch polyfill that points
sql.js WASM and loot-core's data-file-index fetches at on-disk files,
and wires the browser Vite config to use jsdom. The shared integration
spec now gates the full CRUD roundtrip behind __API_FULL_SUITE__ (set
only in Node) because absurd-sql's worker + SharedArrayBuffer
requirement is not met under jsdom; the browser smoke test verifies
that init returns a usable handle. Full-flow browser coverage moves
to the playground app in the next phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root *.test.ts glob no longer matches once the tests live in test/.
Widens the pattern to **/*.test.ts and adds test/setup.*.ts so the
integration setup keeps the same latitude the existing tests had with
global state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small end-to-end integration test that bootstraps a budget via
the internal create-budget handler, writes an account and a couple of
transactions through the public API, then reads them back. The spec is
environment-agnostic and will be rerun under jsdom + fake-indexeddb in
a follow-up task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves methods.test.ts and its snapshots into test/, and extracts the
loot-core fs mock and IS_TESTING flag into a dedicated setup.node.ts
wired up via vite.config.mts. Prepares for a sibling setup.browser.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds build:browser and test:browser scripts alongside the existing
Node-targeted ones, and a new "browser" condition in the package
exports so bundlers auto-pick the browser build. Also adds
npm-run-all and fake-indexeddb dev dependencies used by the browser
build/test pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task-by-task TDD plan covering the browser entry and Vite build, dual
Node+browser unit tests, release notes, and the out-of-repo playground
for manual verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the approach for adding a browser build to @actual-app/api, the
unit-test setup that runs the same integration spec under both Node and
browser environments, and an out-of-repo playground app that hand-verifies
the browser build against a real Actual sync server.
Continuation of the work started in #7247 (closed stale).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Emit bundle stats from the crdt package
The crdt package was the only published library without a stats.json
artifact. Migrate its build to Vite (mirroring the api/cli setup), wire
in rollup-plugin-visualizer to emit dist/stats.json, and upload it from
the CRDT CI job. Declarations are still produced by tsgo via
--emitDeclarationOnly.
https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ
* Add release notes for PR #7537
* [AI] crdt: drop redundant rm -rf dist from build script
Vite's build.emptyOutDir: true already clears the output directory
before writing, so the leading rm -rf dist is unnecessary.
https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ
* [AI] Include crdt in the size-compare bundle stats table
Wait for the crdt build check on both the base branch and the PR,
download the crdt-build-stats artifact for each, and pass it to
bundle-stats-comment.mjs so the summary table rendered on the PR
includes a row for the crdt package alongside desktop-client,
loot-core, api, and cli.
https://claude.ai/code/session_01CDVAGLGu49q5YMHsRLkYLQ
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* Apply suggested fix to packages/desktop-client/src/components/modals/EditFieldModal.tsx from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Apply suggested fix to packages/desktop-client/src/components/modals/EditFieldModal.tsx from Copilot Autofix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* note
* fix typo in note
* remove useless conditions
---------
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* [AI] Fix regex checkbox label contrast in notes modal (#7514)
Apply theme.menuAutoCompleteText color to the "Use Regular Expressions"
checkbox label in the find-and-replace tab so it contrasts with the
menuAutoCompleteBackground modal background.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rename release note file to match PR number
The CI expects the release note file to be named after the PR number
(7515), not the issue number (7514).
---------
Co-authored-by: liuren.lcy <liuren.lcy@antgroup.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* [AI] Refactor IS_GENERIC_BROWSER env var to --mode=browser
Replace the IS_GENERIC_BROWSER environment variable with Vite's built-in
--mode=browser flag to distinguish browser builds from Electron builds.
This aligns with the existing --mode=desktop pattern used for Electron
production builds.
Also fix build-shims.js to derive NODE_ENV from import.meta.env.DEV
instead of import.meta.env.MODE, so custom modes don't leak into
process.env.NODE_ENV.
https://claude.ai/code/session_014HvkpR59Ke4eUoiUzsUruv
* Add release notes for PR #7466
* [AI] Fix COOP/COEP headers not set with --mode=browser
The server.headers config was gated on mode === 'development', which
excluded --mode=browser. Since server.headers only applies during
vite serve (not builds), always set the COOP/COEP headers. These are
required for SharedArrayBuffer support used by the SQLite backend.
https://claude.ai/code/session_014HvkpR59Ke4eUoiUzsUruv
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>