* [AI] Tighten VRT per-pixel threshold so faint overlays fail
Playwright's default `threshold: 0.2` translates to a pixelmatch YIQ
delta cutoff of ~1408, which silently swallows low-alpha overlays. PR
#7841 striped the transactions table with rgba(..., .15); the resulting
per-pixel deltas (~270 light, ~320 dark) fell below the cutoff, so VRT
reported 0 diff pixels and passed despite a clearly visible change.
Drop `threshold` to 0.05 (cutoff ~88) so faint tints are flagged while
keeping headroom for anti-aliasing noise.
* [AI] Apply VRT threshold to electron config and drop redundant local override
- Remove `maxDiffPixels: 5` from `toMatchThemeScreenshots` — it duplicates
the global config and obscured the fact that per-call options would
override the global threshold if added there too.
- Mirror the threshold in desktop-electron's playwright config so its
VRT screenshots are subject to the same sensitivity.
- Tighten the threshold comment: drop the PR/task reference per
AGENTS.md and the worked-example numbers; keep the formula and the
low-alpha rationale.
* Add release notes for PR #7864
* Revert "[AI] Apply VRT threshold to electron config and drop redundant local override"
This reverts commit ec789703ea.
* Revert "[AI] Tighten VRT per-pixel threshold so faint overlays fail"
This reverts commit 334954bb33.
* [AI] Remove stale release note for reverted threshold change
* Revert "[AI] Remove stale release note for reverted threshold change"
This reverts commit 90f97d95f3.
* Reapply "[AI] Tighten VRT per-pixel threshold so faint overlays fail"
This reverts commit 98d0f8b3e7.
* Reapply "[AI] Apply VRT threshold to electron config and drop redundant local override"
This reverts commit 23f94362ec.
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7864
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Neutralize CSV formula-injection in CLI output and transaction export
Prefix cells starting with =, +, -, @, tab, or CR with a single quote so
that user-controlled strings (payee/account/category/tag names, notes,
etc.) cannot evaluate as formulas when the CSV is opened in Excel,
LibreOffice Calc, or Google Sheets. Numeric values are left unprefixed.
- packages/cli/src/output.ts: harden escapeCsv used by --format csv
- packages/loot-core/src/server/transactions/export/export-to-csv.ts:
pass a cast.string option to csv-stringify
* [AI] Simplify CSV formula-injection guard
- Drop the FormattedCell type; check typeof value === 'number' inline in
the CSV path instead of threading an isNumeric flag through formatCellValue
- Shorten verbose comments and drop standards-body references
* [AI] Remove @ts-strict-ignore from export-to-csv test
* [AI] Add release notes for CSV formula-injection fix
* [AI] Quote CSV cells containing carriage returns
Per RFC 4180, bare \r inside an unquoted field can be interpreted as
a record terminator. Extend escapeCsv to also quote on \r so neutralized
formula payloads (e.g. "'\rHELLO") are emitted as a single cell.
* [autofix.ci] apply automated fixes
* [AI] Rephrase release note in user-facing language
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* [AI] Convert theme color TS modules to CSS files with CSS variables
Replace `palette.ts` and `themes/{light,dark,midnight}.ts` with plain CSS
files that declare the same variables directly via `:root` blocks. The
palette becomes `--palette-*` variables; each theme defines `--color-*`
variables referencing those (or other `--color-*` for intra-theme aliases).
`theme.tsx` and Storybook's `preview.tsx` now import each CSS file as a
string via Vite's `?inline` query and render two `<style>` tags (palette +
active theme) instead of building the variable declarations at runtime
from a TS object.
Auto/dark-preference selection, `prefers-color-scheme` listening, the
custom-theme baseTheme override, and the `CustomThemeStyle` override layer
are all preserved.
* [AI] Move theme CSS files to component-library
Move palette.css and themes/{light,dark,midnight}.css from desktop-client's
style/ directory into component-library at src/themes/, and add explicit
package.json exports for each. desktop-client's theme.tsx now imports them
via `@actual-app/components/themes/*.css?inline`, and Storybook's
preview.tsx uses a normal relative path inside its own package instead of
reaching across the monorepo boundary.
* [AI] Minimize theme.tsx rename churn
Keep the original variable names — `themes[*].colors`, `themeColors` state,
`lightColors` / `darkColors` locals, and `getBaseThemeColors` — even though
they now hold CSS strings rather than color objects. Limits the diff to the
import changes, the state type, and the render path.
* [AI] Add release note for theme CSS conversion
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] Require admin for GET /secret/:name in OpenID mode
The GET handler only verified an authenticated session, while the
sibling POST handler enforced an admin gate when the active auth
method is openid. A non-admin BASIC user in an OpenID multi-user
deployment could enumerate which admin-managed bank-sync secrets
were configured by probing 204 vs 404 responses.
Factor the auth-method + admin check into a shared helper used by
both POST and GET, and restrict the GET :name parameter to the
known SecretName enum so unrelated probing returns 404 up front.
* [AI] Simplify secrets auth guard after review
- Use existing getActiveLoginMethod() helper instead of duplicating
the SELECT inline; drop the redundant try/catch.
- Validate the secret name against the SecretName enum before doing
the auth-method DB query so bogus probes fail fast.
- Switch the enum membership check from hasOwnProperty.call to the
more idiomatic in operator.
- Tighten the function-header comment to a single WHY line.
- Drop the dead else-branch from the test helper.
* [AI] Move response building back into the secrets handlers
Reshape the helper as canManageSecrets(userId) - a pure predicate over
the user. Each handler now owns its own 403 response so request/response
plumbing stays inside the route handlers.
* [AI] Undo testSecretName -> validSecretName rename
Reuse the original testSecretName / testSecretValue constants; only
the value of testSecretName changes to a real SecretName so the GET
handler's enum check accepts it.
* [AI] Add release note for #7862
* [AI] Validate POST secret name and tighten GET auth ordering
- POST /secret/ now rejects names not in the SecretName enum with 400.
- GET /secret/:name runs the admin check before the enum check so
non-admins in OpenID mode get a uniform 403 regardless of whether
the requested name is valid.
- Add tests for both: admin POST success in OpenID mode, and POST 400
for unknown secret names.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* warning when only a balance cap is used
* add tooltip for short descriptions
* fix parsing issue with template 0 up to templates
* preselect value to make deletion easier
* note
* fix balance cap note
* fix tests
* remove recurring
* Goal/Automation wording
* [AI] Fix template injection in setup action's Lage cache step
The 'Ensure Lage cache directory exists' step expanded
${{ inputs.working-directory }} directly into the shell command via
format(), which zizmor flags as a code-injection risk. Pass the input
through an env var and reference it with shell expansion instead.
* [AI] Add release note for template injection fix
* [AI] Rename release note to match PR #7858
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] Replace superfluous actions flagged by zizmor
Address zizmor's `superfluous-actions` audit by replacing actions whose
functionality is already provided by the runner's pre-installed `gh` CLI:
- `actions-ecosystem/action-add-labels` -> `gh issue edit --add-label`
- `peter-evans/create-or-update-comment` -> `gh issue comment`
- `softprops/action-gh-release` -> `gh release create` / `gh release upload`
For the Electron release workflow, the create step is race-safe across
the three matrix OS jobs that share the same draft release.
* [AI] Simplify electron release upload script
- Drop the `gh release view` existence check; `gh release create ... || true`
already handles the matrix-job race against the same draft release.
- Use `extglob` to exclude `Actual-windows.exe` inline instead of looping
over `.exe` separately.
* Add release notes for PR #7852
* [AI] Narrow error suppression on gh release create
Only swallow the "already_exists" error from the parallel-matrix race;
propagate any other failure (auth, network, API) instead of masking it.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Keep the mobile page header mounted across navigation to avoid flashing
On mobile, navigating between pages unmounted and remounted the page header
(including its background), causing a visible flash while the next page
rendered. Mobile pages now publish their header content through a context to a
single persistent `MobilePageHeaderSlot` rendered by the app shell, so only the
header content swaps while its background stays put.
* [AI] Render the persistent mobile page header via a portal
Replaces the context/state + useLayoutEffect plumbing for the persistent
mobile header with a portal into the `MobilePageHeaderSlot` DOM node, so
header content reconciles in place without re-rendering the provider on every
page render.
* Add release notes for PR #7842
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] feat: add balance forecast backend
* [AI] feat: add balance forecast report UI
* [AI] feat: gate balance forecast behind an experimental flag
* [AI] Include account-less schedules in balance forecast via explicit flag
- Add includeAccountlessSchedules to forecast/generate and normalize
schedules without an account into FORECAST_UNASSIGNED_ACCOUNT_ID
- When enabled, append synthetic bucket and rule stub; skip transfer legs
for unassigned schedules
- Balance forecast UI sets the flag when widget meta has no account filter
- Add loot-core tests for include vs exclude behavior
* [AI] Improve balance forecast chart refresh UX
Keep forecast charts stable during refetches and let the Y-axis scale to forecast data so balance changes remain visible.
* [AI] Document balance forecast report
Add experimental user documentation and navigation links for the new balance forecast report.
* [AI] Link balance forecast experimental flag to feedback issue #7669
* docs: add PR release notes
* [AI] chore: rerun CI
* [AI] fix: match no transactions when "has tags" input lacks `#` (closes#7797)
The SQL-side `hasTags` filter extracts `#tag` patterns from the user
input and `$and`s them together. When the input has no `#` (e.g. user
types `foo` instead of `#foo`), the extraction returns an empty array
and the resulting `$and: []` matches every transaction.
Mirror the empty-`oneOf` behaviour and return the match-nothing
sentinel (`{ id: null }`) in that case.
* [AI] Add release notes entry for #7808
---------
Co-authored-by: MaksZhukov <maks_zhukov_97@users.noreply.github.com>
* [AI] Fix flaky openid /config test from cross-worker auth race
Vitest runs sync-server test files in parallel workers that share
account.sqlite. Other files (e.g. app-account.test.js) insert 'openid'
auth rows, and auth.method is a PRIMARY KEY, so a concurrent INSERT in
app-openid.test.ts can hit UNIQUE constraint failed: auth.method.
Use INSERT OR REPLACE in the helper and clear the auth table in
beforeEach for a clean start.
* Add release notes for PR #7847
* Change category from Bugfixes to Maintenance
Fix OpenID authentication test flakiness by ensuring test isolation with INSERT OR REPLACE.
* [AI] Disable file parallelism for sync-server tests
The previous fix only patched insertOpenIdAuth in app-openid.test.ts,
but app-account.test.js's insertAuthRow helper also does plain
INSERT INTO auth ... 'openid' ... (lines 197, 203, 210, 229, 245).
With maxWorkers: 2 and a shared account.sqlite, either file's INSERT
can race the other's and hit UNIQUE constraint failed: auth.method.
Disable cross-file parallelism so test files run sequentially against
the shared DB. Within-file tests still run sequentially by default.
Test suite goes from ~20s to ~36s; trades some speed for stability.
* [AI] Revert openid test changes, reword release note
The fileParallelism: false change in vitest.config.ts already prevents
the auth.method UNIQUE-constraint race across files, so the INSERT OR
REPLACE and extra beforeEach cleanup in app-openid.test.ts are no longer
needed. Revert that file back to its original state and reword the
release note to describe the actual fix.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Stabilize size-compare job by pinning downloads to run_id
The compare job in .github/workflows/size-compare.yml was flaky because
fountainhead/action-wait-for-check matched a check by name from any run
on the branch, while dawidd6/action-download-artifact with branch:/pr:
filters and workflow_conclusion: '' resolved to the latest run regardless
of completion. When a new master build started in the seconds between
waiting and downloading, the action picked up the in-progress run and
failed with "artifact not found".
Replaces the eight wait-for-check steps with one actions/github-script
step that polls listWorkflowRuns for a successful build.yml run on
master and the PR head SHA in parallel via Promise.all, then pins all
eight downloads to those run_ids.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add release notes for PR #7780
* Change category to Maintenance in release notes
Updated category from 'Enhancements' to 'Maintenance'.
* [AI] Clean up comment to remove reference to previous implementation
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Matiss Janis Aboltins <MatissJanis@users.noreply.github.com>
* [AI] Fix npm provenance for @actual-app/crdt and bump to 3.0.1
Add the missing repository field to packages/crdt/package.json so the npm
provenance bundle can validate the source against
https://github.com/actualbudget/actual. Without it, publishing fails with
"Error verifying sigstore provenance bundle: repository.url is \"\"".
* Add release notes for PR #7845
* [AI] Revert @actual-app/crdt version back to 3.0.0
* Fix metadata formatting in package.json for crdt
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Replace any-typed Modal in undo state with structural type
loot-core can't import @actual-app/web's Modal union, so the undo MRU
typed openModal as `any`. The undo system only stores the value and
reads `.name`, so a minimal structural shape `{ name: string; options?: unknown }`
is enough. desktop-client's full Modal still assigns to it, and the one
reader (global-events.ts) re-narrows back to Modal when handing the
value to replaceModal().
* Add release notes for PR #7813
* Update 7813.md
* [AI] Add TODO on Modal cast for future type consolidation
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Share cleanup-group helpers and let storeTemplates write cleanup_def
- Extract resolveCleanupGroup and tombstoneOrphanCleanupGroups out of
cleanup-template-notes.ts into a new cleanup-groups.ts so the
upcoming UI-driven create flow can reuse the resurrect-aware lookup.
- Let storeTemplates accept an optional cleanup array per category
(omitted = leave as-is, [] = clear, non-empty = replace), and run the
orphan tombstone sweep whenever cleanup_def is touched so groups
removed from the UI don't linger.
- Register budget/store-note-cleanups so the UI can migrate a single
category's notes on demand, and budget/create-cleanup-group so it can
create groups inline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Add UI editor for end-of-month cleanup automations
- New cleanup row in the BudgetAutomations sidebar with read-only
summary; selecting it opens an editor with a Global scope card and
an optional named-group scope (single group per category for now,
since multi-group ordering depends on category sort).
- Each scope card has independent "send leftover" / "take a share"
toggles plus a weight; group scopes additionally support
"only enough to cover overspending".
- Group picker is a typeahead that creates groups inline via
budget/create-cleanup-group.
- useCategoryCleanup migrates notes to cleanup_def at modal-open for
unmigrated categories; useCleanupGroups streams the live list.
- Un-migrate flow renders cleanup_def back to #cleanup note lines and
drops rows whose group can't be resolved, so users never see UUIDs
in their notes.
- Sidebar/automation-button "has automations" probes also check
cleanup_def so cleanup-only categories still get the indicator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* note
* review pass 1
* bring automation logic in line with cleanup logic
* review pass 3
* coderabbit pass 1
* wording suggestions
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Require @actual-app/crdt version bump and auto-publish
Adds two workflows:
- crdt-version-check: fails PRs that modify files in packages/crdt/
without bumping the version in packages/crdt/package.json.
- publish-crdt: publishes @actual-app/crdt to npm when the version in
packages/crdt/package.json changes on master, tagging the release as
crdt-v<version>.
* [AI] Skip git tagging in @actual-app/crdt publish workflow
Remove the tag-and-push step and the now-unused version output;
downgrade contents permission to read.
* [AI] Simplify crdt version-bump workflows
- Drop the redundant explicit base-branch fetch (fetch-depth: 0 already
retrieves all remote branches).
- Remove the unreachable "no changes" guard; the pull_request paths
filter already scopes the workflow to packages/crdt changes.
- Replace the embedded Node semver comparison with `sort -V`.
- Read versions with `jq` instead of inline Node.
* [AI] Add release notes for crdt publish workflows
* [AI] Restrict GITHUB_TOKEN permissions in crdt workflows
Add top-level `permissions: contents: read` to both crdt workflows so
the implicit jobs no longer inherit overly broad permissions (flagged by
zizmor).
---------
Co-authored-by: Claude <noreply@anthropic.com>
* [AI] crdt: typecheck test files and clean up lint issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Replace google-protobuf with @bufbuild/protobuf
Swap the google-protobuf + ts-protoc-gen + protoc-gen-js toolchain for
@bufbuild/protobuf + @bufbuild/protoc-gen-es. The generator now emits a
single pure-TS sync_pb.ts (no .js sidecar, no globalThis.proto hack)
and a thin wrapper in proto/compat.ts preserves the SyncProtoBuf /
SyncRequest / etc. API so call sites stay unchanged. Removes the
loot-core CommonJS require polyfill that only existed to service
google-protobuf.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] Align @bufbuild/protobuf version ranges with installed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: drop the SyncProtoBuf compat layer
The proto/compat.ts wrapper was introduced alongside the bufbuild
migration to avoid touching call sites. With bufbuild messages already
exposing fields as plain mutable properties, the wrapper was just
boilerplate hiding direct reads and writes — and it had drifted (e.g.
setMessagesList was called in a test but never defined).
Delete compat.ts and migrate the six call sites in loot-core and
sync-server to use @bufbuild/protobuf directly. The crdt package now
re-exports the sync_pb types/schemas and the three bufbuild runtime
helpers (create, fromBinary, toBinary) so consumers keep a single
import source.
Also switch sync-server's @actual-app/crdt dependency from the pinned
"2.1.0" to "workspace:*", matching api/loot-core — the npm pin was
pulling the stale published copy instead of the workspace source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] CI: drive sync-server build through lage so crdt deps are built
Before: the server job ran `yarn workspace @actual-app/sync-server build`
directly, which invokes tsgo without first emitting the workspace
dependencies' declarations. That worked when sync-server pinned crdt to
the published npm version (declarations bundled in the tarball), but
with `workspace:*` it fails with TS6305 because packages/crdt/dist/*.d.ts
hasn't been built yet.
Switch the CI command to `yarn build --to=@actual-app/sync-server`.
Lage respects the `dependsOn: ['^build']` pipeline and builds
@actual-app/crdt (and the other transitive deps) before sync-server.
Using --to rather than --scope keeps the build set minimal; --scope
would also include dependents like desktop-electron.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] sync-server: build project references via tsgo -b
The build script ran plain `tsgo`, which doesn't compile referenced
projects. With @actual-app/crdt now a `workspace:*` dep (no bundled
declarations from the npm tarball), the sync-server build fails with
TS6305 because packages/crdt/dist/index.d.ts doesn't exist yet.
Switch to `tsgo -b` so the sync-server build is self-contained: it
emits crdt's declarations into packages/crdt/dist on demand. This
mirrors what the sync-server `typecheck` script already does and fixes
all callers (`build:server`, docker-edge, publish workflows, the
direct `yarn workspace @actual-app/sync-server build` invocation in
build.yml) without needing per-workflow lage orchestration.
Revert the build.yml workaround added in the previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] sync-server: build @actual-app/crdt before tsgo
The previous tsgo -b approach emitted crdt's .d.ts via the project
reference but never produced dist/index.js — tsgo respects crdt's
tsconfig which has emitDeclarationOnly: true, and the actual JS
runtime is emitted by Vite in crdt's build script. So sync-server
compiled cleanly but crashed at runtime when forked by desktop-electron
(require('@actual-app/crdt') resolved to a package whose main pointed
at a nonexistent file, surfaced in e2e as the onboarding screen never
leaving the "Configure your server" state).
Unlike packages/api (which uses Vite with noExternal: true and bundles
crdt's source inline), sync-server uses plain tsgo compilation and
keeps its deps external — so crdt must be built ahead of time and be
resolvable via node_modules at runtime.
Chain `yarn workspace @actual-app/crdt build` before tsgo so every
caller of sync-server's build (build:server, docker-edge, publish
workflows, direct invocations in CI) gets a complete crdt dist. Revert
tsgo -b back to plain tsgo since crdt's build step now emits both the
JS and the declarations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: expose dist/ via conditional exports so Node can load it
The package's `exports` field pointed straight at `./src/index.ts`,
which works for TS tooling and bundlers (vite with noExternal, vitest)
but breaks at plain-Node runtime — Node can't execute `.ts` files and
resolves dependent `./crdt` as a directory import, failing with
ERR_UNSUPPORTED_DIR_IMPORT.
That was invisible before because sync-server pinned
`@actual-app/crdt@2.1.0` and ran against the published npm tarball
(whose `publishConfig.exports` had already been promoted to the main
`exports` by yarn pack). Switching sync-server to `workspace:*` made
the raw workspace exports win at runtime: the compiled server imported
crdt when desktop-electron forked it, Node hit the `.ts` entry, the
utility process crashed before emitting `server-started`, and the
onboarding flow stalled on "Configure your server".
Switch to the same conditional-exports pattern packages/api already
uses: types → dist/index.d.ts, development → src/index.ts (for vitest
runs that enable the `development` condition), default → dist/index.js
(Node runtime and any other consumer). `publishConfig.exports` still
collapses this to just types + default for the npm tarball.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: split exports per consumer (browser source, node dist)
Previous commit's conditional exports routed everything non-development
to ./dist/index.js. That broke the web build: rolldown runs with
conditions ['electron-renderer', 'module', 'browser', 'default'] — no
match for development, falls through to the dist entry, which isn't
built by bin/package-browser, and fails to resolve @actual-app/crdt
when bundling loot-core's server/undo.ts.
Split the entries so each consumer lands on the right artifact:
types → ./dist/index.d.ts (TypeScript, project references)
development → ./src/index.ts (vitest — both configs include it)
browser → ./src/index.ts (web rolldown bundles the source)
node → ./dist/index.js (sync-server forked by Node at
runtime — the failure that kicked
off this whole saga)
default → ./src/index.ts (fallback for bundlers like api's
vite build with conditions=['api'])
Verified: node resolves to dist, yarn build:browser succeeds from a
clean crdt/, sync-server build produces both dist/index.js and
build/app.js, loot-core (552) + sync-server (386) tests pass, full
typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] address review feedback on crdt/sync-server
- generate-proto: add `set -euo pipefail` so a protoc failure exits the
script non-zero instead of silently running oxfmt on whatever is in
src/proto/ from the previous run.
- sync.proto SyncRequest: field numbers jumped from 3 to 5; declare
`reserved 4;` so the slot can't be silently reused for a new field
with an incompatible type. Regenerated sync_pb.ts — the reservation
shows up in the encoded file descriptor.
- sync-simple.js: SQLite stores is_encrypted as a 0/1 integer and
better-sqlite3 hands it back as a number, but the bufbuild
MessageEnvelope schema types isEncrypted as bool. Coerce to boolean
when constructing the envelope so the JS value matches the field
type before toBinary runs.
Skipped the suggested `types` → ./src/index.ts swap in crdt's exports:
packages/api uses the same `types` → dist pattern and TypeScript's
bundler resolution already falls through when dist/*.d.ts doesn't yet
exist (verified — loot-core typecheck passes with packages/crdt/dist
removed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] address review feedback on encoder/app-sync test
- encoder.ts: prefs.getPrefs().encryptKeyId is `string | undefined`
(MetadataPrefs is a Partial<>). The bufbuild SyncRequestSchema's
keyId field is a non-optional proto3 string. Current code worked by
accident — passing undefined into `create(Schema, init)` falls back
to the schema default '' — but relied on bufbuild's undef-handling
and would break if someone dropped @ts-strict-ignore. Normalize to
'' explicitly.
- app-sync.test.ts: add a short WHY comment next to
`syncRequest.since = ''` in "returns 422 if since is not provided".
The test's intent (missing since) only matches the handler's
`requestPb.since || null` falsy-check because proto3 strips '' on
the wire and decodes it back to ''. Not obvious without the comment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] crdt: load source directly in dev, only use dist when published
Local exports point at src/index.ts so consumers (sync-server in
particular) never load a stale Vite bundle. publishConfig keeps the
dist/ mapping for npm consumers. Switched the Vite output to ESM and
added "type": "module" so the published bundle stays consistent.
Sync-server's existing extension-resolution loader is extended to
handle directory imports and is now registered at runtime via
--import ./register-loader.mjs, matching how tests already load it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] desktop-electron: register sync-server loader on the embedded fork
The Electron app starts the sync server via utilityProcess.fork, which
bypasses sync-server's `start` script. With crdt now loaded from
source, the fork needs the same `--import register-loader.mjs` that
the standalone server uses; otherwise it crashes on the extensionless
`from './crdt'` directory import. Adds the loader files to
sync-server's published `files` so they actually ship with the
packaged app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] sync-server: bootstrap entry that registers the loader for utilityProcess
Electron's utilityProcess.fork accepts execArgv but silently ignores
--import (verified with a minimal repro: the flag shows up in
process.execArgv but the preload module never executes), so the
previous attempt was a no-op and the embedded sync-server still
crashed on crdt's ESM directory imports. Add packages/sync-server/start.mjs
that statically imports register-loader.mjs and then dynamic-imports
build/app.js, so the loader is in place before the app's module graph
resolves. desktop-electron now points utilityProcess.fork at start.mjs
and drops the ineffective --import flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [AI] fix: allow clearing pre-assigned category on new transactions
Add a "Nothing" button to the category autocomplete modal that allows
users to clear a pre-assigned category when adding or editing
transactions. Previously, when a payee had a pre-assigned category,
there was no way to remove it and leave the transaction uncategorized.
Closes#7390
* [AI] docs: add release notes for PR #7521
* [AI] chore: re-trigger CI for flaky test
The test failure in methods.test.ts (Budgets: successfully update budgets)
is a pre-existing flaky test caused by a race condition in
advanceSchedulesService. The async schedule service fires via
void runMutator() after a sync event, but the database can be closed
before the query completes. This is unrelated to the PR changes which
only touch desktop-client UI code.
* chore: retrigger CI (flaky api test)
* fix type issue, better text
* more type fixes
* actually fixed?
---------
Co-authored-by: youngcw <calebyoung94@gmail.com>
* moved the bank sync indicator to the right side of the text in mobile accounts view
* release notes
* moved spacing to the left again but made it smaller
* removed react from imports
* compressed space further
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7611
---------
Co-authored-by: Alec Bakholdin <alecbakholdin.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Add T keyboard shortcut for Make transfer
* [AI] Add release notes for #7750
* [AI] Switch Make transfer shortcut from T to R and document it
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7750
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7750
* Update VRT screenshots
Auto-generated by VRT workflow
PR: #7750
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* [AI] Fix flaky upload-user-file test
The "uploads and updates an existing file successfully" test wrote the
old file content using the async callback form of fs.writeFile without
awaiting it. That write could land after the upload endpoint had already
written the new content, leaving the file with stale content and failing
the assertion. Use fs.writeFileSync so the setup completes before the
request is sent.
* [AI] Increase api test timeouts to fix flaky budget-load test
methods.test.ts loads a budget file and runs all DB migrations in each
test/hook. On busy CI runners this regularly approaches the default 5s
limit, and when it exceeds it the in-flight loadBudget keeps running after
teardown closes the database, producing a cascade of unhandled rejections
("database connection is not open", "no such table: v_schedules",
"Cannot read properties of undefined (reading 'timestamp')") that fail the
suite. Bump testTimeout/hookTimeout to 20s for the api package.
* [AI] Add release note for flaky test fixes
---------
Co-authored-by: Claude <noreply@anthropic.com>