Commit Graph

3105 Commits

Author SHA1 Message Date
Sure Admin (bot)
1e800e2f93 Include uncategorized spending in budget UI (#2877) 2026-08-01 07:50:09 +02:00
Sure Admin (bot)
2bdaa7a171 Bump version after v0.7.3 release (#2879) 2026-08-01 04:46:24 +02:00
dependabot[bot]
708819b1b4 chore(deps): bump msgpack from 1.8.0 to 1.8.2 (#2866)
Bumps [msgpack](https://github.com/msgpack/msgpack-ruby) from 1.8.0 to 1.8.2.
- [Changelog](https://github.com/msgpack/msgpack-ruby/blob/master/ChangeLog)
- [Commits](https://github.com/msgpack/msgpack-ruby/compare/v1.8.0...v1.8.2)

---
updated-dependencies:
- dependency-name: msgpack
  dependency-version: 1.8.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-31 23:38:46 +02:00
Will Wilson
b6cd8437a5 fix(oidc): honor http issuer scheme during OIDC discovery (#2853)
* fix(oidc): honor http issuer scheme during OIDC discovery

Self-hosted IdPs served over plain HTTP (no SSL) failed OIDC login with
"Failed to open TCP connection to <issuer>:443 (Connection refused)".

The openid_connect/swd gems hardcode discovery to HTTPS: SWD.url_builder
defaults to URI::HTTPS, and OpenIDConnect::Discovery::Provider::Config::Resource
drops the issuer's scheme, rebuilding the .well-known URL from host/port only.
So an http:// issuer is upgraded to https:443 and never connects. (This is why
the in-app "Test connection" passes -- it uses Faraday against the raw issuer
URL and never goes through the gem.)

Patch Config::Resource to remember the issuer's scheme and build the discovery
endpoint with URI::HTTP or URI::HTTPS accordingly. Per-request, no global mutable
state, so mixed http/https providers coexist. Only discovery needs patching: the
endpoints it returns are absolute and rack-oauth2 preserves an existing scheme,
so the token/userinfo/jwks calls follow over http automatically.

Verified against openid_connect 2.3.1 / swd 2.0.3.

Fixes #2844

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

* fix(oidc): scheme-aware port and discovery cache key

Address automated review feedback on the OIDC http-discovery patch:

- Preserve explicitly configured non-default ports (http on 443, https on 80)
  by omitting only the scheme's own default port instead of both 80 and 443.
- Override the discovery cache_key to include scheme/port/path (the gem keyed
  on host alone), so an http:// issuer can't reuse an https:// issuer's cached
  metadata on the same host. Latent today (default SWD cache is a no-op) but
  removed to keep the override self-consistent.

Adds regression tests for both cases.

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

* test(oidc): cover port and path as cache-key components

Extend the discovery cache-key test to assert scheme, port, and path each
produce a distinct key on the same host, per review feedback.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 03:24:28 +02:00
Sure Admin (bot)
078c49b35b Stabilize net worth breakdown series test dates (#2863) 2026-07-31 03:19:33 +02:00
Maximus Barbare
4c1bc774e5 Fix SnapTrade account setup and reconnection (#2858)
* Infer SnapTrade account types from categories

* Allow choosing SnapTrade account types

* Reauthorize SnapTrade connections needing attention

* Clear stale SnapTrade reconnect state

* Always schedule SnapTrade reconnect syncs

* Recognize SnapTrade credit card accounts

* Recognize SnapTrade crypto account types

* Schedule SnapTrade syncs after active imports

* Fix SnapTrade account type CI tests

* Normalize SnapTrade card account types
2026-07-31 02:20:03 +02:00
pro3958
8e6035c62c Wire @kraken_items into the accounts index (#2770)
app/views/accounts/index.html.erb never referenced @kraken_items.
Kraken support was added upstream but left out of the accounts index
in two places: the top-level empty-state condition that decides
whether to render the "empty" partial, and the provider render
section. AccountsController#index also did not assign @kraken_items
at all.

As a result, a family whose only connections are Kraken items saw the
blank empty state instead of their Kraken accounts.

Assign @kraken_items in the controller with the same eager-loading
shape used for the other crypto providers, add @kraken_items.empty?
to the empty-state condition, and render the items in the provider
section in the correct order.

Fixes #2577

Co-authored-by: agentloop <agentloop@localhost>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-07-30 09:12:42 +02:00
pro3958
785f9a19c5 fix(accounts): skip digest on sidebar fragment cache (#2776)
The account sidebar fragment renders DS::* view components. Rails' ERB
dependency tracker parses `render DS::Foo.new` as a dynamic render
dependency named DS and inflects it into the nonexistent partial "Ds/D".
When the cache helper computes the template digest, ActionView::Digestor
fails to resolve that partial and logs "Couldn't find template for
digesting: Ds/D" on every sidebar cache miss.

The fragment's cache key is already manually versioned and invalidated
via account_sidebar_tabs_cache_key ("account_sidebar_tabs_v2",
invalidate_on_data_updates: true), so automatic template digesting adds
nothing. Pass skip_digest: true to the cache block so the digestor never
runs for this fragment. Rendered output is unchanged.

Fixes #2516

Co-authored-by: agentloop <agentloop@localhost>
2026-07-30 07:25:21 +02:00
pro3958
8a441582f9 fix(layout): persist sidebar collapse preference across reloads (#2764)
The app-layout Stimulus controller read this.userIdValue when saving
sidebar state but never declared a static values block, so Stimulus
did not bind data-app-layout-user-id-value. this.userIdValue was
undefined, and every toggle PATCHed /users/undefined, which could not
update show_sidebar or show_ai_sidebar. The preference was never
stored, so the layout reset on the next load.

Declare static values = { userId: String }. String is correct for
both UUID and integer ids since the value is only interpolated into
the request URL. The layout already renders the attribute and
UsersController#user_params already permits both fields, so the
update now completes.

Fixes #2473

Co-authored-by: agentloop <agentloop@localhost>
2026-07-30 07:20:32 +02:00
pro3958
150e659846 fix(tags): return 404 for missing or cross-family tag deletion (#2703)
Tag::DeletionsController looked up the tag and its replacement with
find_by, so a missing id or a tag belonging to another family left
@tag nil. create then called @tag.replace_and_destroy!, raising
NoMethodError and returning a 500 instead of a 404.

Use find in set_tag and set_replacement_tag so an out-of-scope id
raises ActiveRecord::RecordNotFound, which is rendered as 404. The
replacement lookup keeps a presence guard so deleting without a
replacement still works. This mirrors Category::DeletionsController.

Fixes #2469

Co-authored-by: agentloop <agentloop@localhost>
2026-07-30 07:15:50 +02:00
ghost
213bb0d6e8 design-system(mobile): polish Dashboard with Sure tokens (#2457)
* feat(mobile): add SureSpacing + SureTypography scale tokens

Introduce hand-authored spacing and type-scale constants mirroring the
Tailwind defaults the web design system relies on, so widgets reference a
named step instead of a raw numeric EdgeInsets/SizedBox/fontSize.

- SureSpacing: xs..huge mapping to Tailwind space-1..space-8 (4..32px).
- SureTypography: xs..xxl mapping to Tailwind text-xs..text-2xl font sizes.

Both are hand-written rather than generated from sure.tokens.json because
spacing and the type ramp come from Tailwind's built-in scale, not the
canonical token file (consistent with the tracker's guidance).

Adopt them in the existing primitives (card padding, button metrics +
gap, chip/segmented/list-group gaps and padding, text-field padding +
label gap). All migrations are value-preserving — each token equals the
literal it replaces — so there is no layout change; off-scale one-offs
(control heights, hairlines, deliberate 14px field padding) stay literal.

flutter analyze: no new issues; full suite (166) green.

* design-system(mobile): polish Dashboard with Sure tokens

Align the dashboard with the Sure design system (no behavior changes).

NetWorthCard: the hero card adopts the canonical Sure card chrome — container
fill, hairline borderSecondary, radiusLg, and the subtle DS shadow (mirroring
SureCard/AccountCard) instead of Material surfaceContainerHighest/outline with
an ad-hoc radius and no elevation. Dividers, the Net Worth label/value, the
Outdated badge, asset/liability totals, and the currency-breakdown sheet all
resolve from the active SureColors palette (brightness-aware).

dashboard_screen.dart: empty/error states use SureButton + palette colors; the
account-type group header badge uses surfaceInset/textSecondary + SureTypography;
the sync success banner and sync/refresh snackbars use palette.success/
palette.destructive; spacing moves onto the SureSpacing scale.

Adds net_worth_card_test.dart asserting the hero card chrome resolves Sure
tokens in light and dark.

Builds on the SureSpacing/SureTypography scale tokens (#2438).

* fix(mobile): readable foreground on tokenized dashboard snackbars + keyed chrome test

Address review feedback on #2457:

- Snackbar contrast: the success/error snackbars switched their background to
  palette.success/palette.destructive but kept a white icon + default white
  text. In dark theme palette.success is a bright green (#32D583), so white was
  low-contrast. Set the icon and text foreground to palette.textInverse, which
  flips with the theme (#FFFFFF light / #171717 dark) and stays readable on both
  semantic fills.
- Test robustness: key the NetWorthCard chrome Container ('netWorthCardChrome')
  and look it up with find.byKey instead of the fragile first-descendant
  Container match.

* fix(mobile): SureButton owns leading-icon foreground via IconTheme

Address review feedback (jjmata): call sites shouldn't hardcode the button's
foreground on leading icons. Wrap SureButton's content in an IconTheme set to
the variant foreground, so leading icons (e.g. SureIcon) inherit it
automatically — mirroring how Material's ElevatedButton.icon propagates icon
color. Icons that pass an explicit color still win.

Drop the now-redundant `color: palette.textInverse` from the dashboard
empty/error-state button icons; they follow the button variant automatically.

Add a SureButton test asserting a leading icon inherits the variant foreground
(textInverse for primary, textPrimary for outline) via the ambient IconTheme.

* Fix net worth card mask merge regression

* Provide privacy state in net worth card tests

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-07-30 04:12:30 +02:00
ghost
a21ab1946e perf(accounts): preload transfer, category, and split-parent associations on show (#2441)
* perf(accounts): preload transfer, category, and split-parent associations on show

AccountsController#show iterated over paginated entries and called
transaction.transfer (two queries via transfer_as_inflow || transfer_as_outflow),
transaction.category, and transaction.merchant individually per row, and fell
back to entry.split_parent? (child_entries.exists? per entry) because
@split_parent_entry_ids was never set.

Fix by:
- Batch-preloading transfer_as_inflow, transfer_as_outflow, category, and merchant
  on transaction entryables after pagination using Associations::Preloader (same
  API already used in accounts/index/_account_groups.erb).
- Setting @split_parent_entry_ids with a single IN query after pagination,
  matching the identical pattern already in TransactionsController#index.

Resolves Sentry issues SURE-APP-PN (60 users), SURE-APP-XE (32 users),
SURE-APP-26 (51 users) and related slow-DB reports on AccountsController#show.

* docs(accounts): note the show preload is intentionally page-scoped

Address review feedback (jjmata): add a comment clarifying that the transfer/
category/merchant preload and the split-parent lookup operate on the current
page (@entries) by design — only this page is rendered, so a child entry whose
split parent is on another page deliberately won't resolve it. Comment-only; no
behavior change.
2026-07-30 03:42:50 +02:00
sentry[bot]
32eef8e2ea Fix(exchange_rate): Handle RecordInvalid in find_or_fetch_rate race condition (#2734)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-07-30 03:05:14 +02:00
Guillem Arias Fauste
cedf28a5a9 fix(accounts): make the sync toolbar and toast agree, fix toast overlap (#2813)
* fix(accounts): make the sync toolbar and toast agree, fix toast overlap

The Accounts page's own sync toolbar (refresh icon, "Cancel sync") and the
global sync-complete toast were three inconsistently-styled, disconnected
pieces of UI representing one action, and the toast overlapped the page's
own header instead of sitting near it.

- "Cancel sync" was hand-rolled markup instead of a DS::Button, unlike its
  sibling refresh icon right next to it — now both are DS::Button (:ghost).
- The refresh icon just went `disabled` with no visible "working" state —
  now shows a spinning loader-circle while a family sync is in progress,
  matching the pattern already used in provider_sync_summary.html.erb.
- The toolbar was plain server-rendered HTML with no way to know a sync
  finished, so it stayed stuck showing "still syncing" indefinitely next to
  a toast now saying otherwise. Family::SyncCompleteEvent now broadcasts a
  second replace target for the toolbar alongside the existing toast
  replace, so both resolve together.
- The notification tray was a <body>-level fixed overlay centered on the
  full viewport, but every layout that renders it has a sidebar of some
  kind — so it never actually centered on the visible content pane, and
  landed on top of the settings-layout header. It now renders in-flow at
  the top of each layout's own content region (opt-in via
  notification_tray_inline, since the simpler single-column layouts don't
  have this mismatch and are unaffected).
- Added the Catalan sync_toast/cancel_sync strings that were missing
  entirely, which is why the toast/toolbar showed English text on an
  otherwise-Catalan page.

Verified live in a real browser via a new system test covering the idle,
syncing, and cancel-flash states, plus a model test on the new broadcast
target.

* fix(accounts): keep the tray a floating overlay, sidebar-aware instead

Codex on this PR: with the tray as first-child-of-scrollable-main, a
notification delivered while scrolled down is inserted above the
viewport and stays unseen — breaking the sync toast's manual-refresh
path specifically, since sync_toast_controller.js suppresses
auto-refresh while a form is focused and relies on the toast being
visible to offer that manual refresh.

Reverts the tray to a position: fixed overlay for every layout (so it
can't be scrolled out of view), and fixes the actual bug that made it
overlap the accounts toolbar in the first place — a ResizeObserver on
<main> centers it on the real content pane instead of the viewport,
for the two layouts with a sidebar (application, settings passed via
sidebar_aware:). The five single-column layouts are untouched; for
those, viewport-center already is content-pane-center.

One trap worth flagging: this app renders turbo_refreshes_with
method: :morph, and idiomorph resets any inline style a client script
set that isn't in the freshly-fetched HTML — including the JS-set
`left`. data-turbo-permanent looked like the fix but isn't: it invokes
idiomorph's node-identity matching (same id preserved across ANY
morphed page), which broke navigation once the id existed on
structurally different layouts (app vs settings) — a real, reproduced
bug, caught by the system test before it shipped. Went with the
narrower turbo:before-morph-attribute event instead, which blocks only
the `style` attribute on this one element, with no node-identity
system involved.

Rewrote the system test's positioning assertion to match: it now
asserts the tray centers on <main> rather than sitting above the page
header, since a fixed overlay was never going to satisfy the latter by
construction.

Verified: full bin/rails test (6023 runs, 0 failures), rubocop, erb_lint,
brakeman (0 warnings) all clean. Live-verified in a browser across both
sidebar-aware layouts and a simple layout, including the full
cancel-sync -> morph -> re-render cycle.

* fix(accounts): use declarative Stimulus action for morph-attribute guard

Replace the manual addEventListener/removeEventListener pair for
turbo:before-morph-attribute with a data-action, per the repo's
declarative-actions convention. Same element, same listener — just no
manual lifecycle management.
2026-07-30 02:57:16 +02:00
Guillem Arias Fauste
07a3413250 fix(ds): keep DS::Menu/Popover panels anchored across Turbo morphs (#2812)
* fix(ds): keep DS::Menu/Popover panels anchored across Turbo morphs

The app refreshes pages via Turbo morph (`turbo_refreshes_with method:
:morph`), and same-page account actions (disable, exclude, set-default,
etc.) trigger one. Two bugs in the shared floating-ui controllers surface
as a result:

- The panel's `position: fixed` only ever existed as a JS-applied inline
  style. Idiomorph resets every menu/popover's `style` attribute to match
  the server-rendered markup (which has none), silently stripping
  `position: fixed` from every panel on the page. The next dropdown/
  popover opened before floating-ui's async recompute lands briefly
  renders in normal flex flow, shoving its own trigger sideways and
  making computePosition anchor to that phantom position instead of the
  real button. Fix: make `position: fixed` part of the static markup so
  it can never be stripped.

- `this.show` was a plain instance property. Because the morph preserves
  the Stimulus controller in place (stable-id turbo frame), `this.show`
  doesn't reset when idiomorph re-closes the content element, so it can
  desync from the DOM and swallow the next click. Fix: derive `show` from
  the content element's own class instead of tracking it separately.

Reproduced and verified against the actual Turbo/Stimulus/floating-ui
pipeline in an isolated harness before and after the fix.

* test(ds): add regression coverage for menu/popover reopen-after-morph

Simulates what idiomorph does to an open panel on a same-page Turbo
morph — resets the content element's class back to the always-hidden
server-rendered markup and strips the JS-applied inline style, without
going through toggle()/close(). Verified against the pre-fix controllers
that this fails without the DOM-derived `show` getter.
2026-07-30 02:54:42 +02:00
Otter
1c6d018b6a Fix transactions posting a day early when booked around midnight. (#2744)
* Fix 2668

* Fix CodeRabbit nitpick

* Fix Akahu parsing

* Anchor provider transaction date parsing to family timezone

* Coderabbit suggestion for Date/DateTime order

* Remove duplicate condition in wise

* Safe unless column_exists + pass family to date parse to family components
2026-07-30 02:29:13 +02:00
PrplHaz4
16d2bc0ce9 Don’t re-create pending SimpleFIN transactions when pending sync is disabled (#2835)
* fix(simplefin): skip pending entries in processor when pending is disabled

When SIMPLEFIN_INCLUDE_PENDING/syncs_include_pending is off, pending rows
already stored in raw_transactions_payload were still (re)created as
entries on every sync - including ones the user manually deleted - because
the setting only affected the API request, not reprocessing of the stored
payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(simplefin): add rake task to prune stale pending rows from payload store

raw_transactions_payload accumulates transactions across syncs and is
never pruned, so pending rows fetched before pending inclusion was
disabled keep getting re-imported. This one-time maintenance task removes
them (dry-run by default; scope by item_id/account_id).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(simplefin): address PR #2835 review feedback on pending detection

Fix epoch-zero pending check treating non-numeric posted strings (e.g.
"unavailable") as pending via String#to_i coercion; compare against
explicit zero representations instead, matching posted_date. Dedupe the
prune_pending rake task's copy of this logic by delegating to a new
public SimplefinEntry::Processor.pending? class method. Also close a
test gap where SIMPLEFIN_INCLUDE_PENDING env var precedence over the
Setting wasn't actually exercised.

* test(simplefin): cover pending-guard precedence and add rake task tests

Add the missing mirror case for pending_enabled? precedence (env var
disabling pending over a permissive Setting) and add test coverage for
the prune_pending rake task, which previously had none: dry_run safety
default, correct pruning via the shared Processor.pending? predicate
(including the malformed-posted regression), and that it never touches
Entry/Transaction rows.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 01:13:02 +02:00
Bishal Shrestha
5ee3275f98 feat: show counterpart account in transfer transaction list row (#2643)
* Show counterpart account in transfer transaction list row

* feat: show transfer counterpart account with access and nil guards

* - Gate counterpart name behind accessible_accounts check.
- Add nested transfer includes to TransactionsController and
AccountsController to prevent N+1 queries.
- Use precomputed @accessible_account_ids Set for O(1) lookups.

* test: add view tests for transfer counterpart rendering

Cover outflow arrow, inflow arrow, and unmatched transfer fallback
using ActionView::TestCase following existing merged_badge pattern.

* Fix transfer eager loading for polymorphic entryables

* Keep accessible_account_ids as Array to fix mock test expectations
2026-07-30 01:11:56 +02:00
Sure Admin (bot)
3c1c87a9d9 Update Rails for Active Storage security advisory (#2849)
Bump Rails and Active Storage from 8.1.3 to 8.1.3.1 to address GHSA-xr9x-r78c-5hrm.
2026-07-30 00:45:43 +02:00
dependabot[bot]
15b5daa298 chore(deps): bump oauth2 from 2.0.18 to 2.0.22 (#2846)
Bumps [oauth2](https://github.com/ruby-oauth/oauth2) from 2.0.18 to 2.0.22.
- [Release notes](https://github.com/ruby-oauth/oauth2/releases)
- [Changelog](https://github.com/ruby-oauth/oauth2/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ruby-oauth/oauth2/compare/v2.0.18...v2.0.22)

---
updated-dependencies:
- dependency-name: oauth2
  dependency-version: 2.0.22
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-29 22:44:03 +02:00
Oscar
0fa1fbd650 add redbark setup guide to hosting docs (#2817)
- new docs/hosting/redbark.md covering account setup, api keys, linking and sync behaviour
- listed redbark in the onboarding guide's provider integrations
2026-07-28 06:42:57 +02:00
Juan José Mata
c9a28e1aa4 Bump versions 2026-07-25 23:27:33 -07:00
dependabot[bot]
d79925da02 chore(deps-dev): bump vite from 5.4.21 to 6.4.3 in /desktop (#2810)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.21 to 6.4.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.3
  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-07-26 08:12:41 +02:00
Max Barbare
700ad34eb1 feat: Introduce macOS app v0.1.0 (#2762)
* feat(desktop): scaffold Tauri 2 macOS shell with empty window

* feat(desktop): server store, URL normalization, and health-check helpers

* feat(desktop): IPC commands for server list/add/remove/health + active-server state

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* feat(desktop): native onboarding server picker with health check and remembered servers

* feat(desktop): vibrancy background, overlay titlebar, and inset traffic lights

* feat(desktop): native menu bar with standard shortcuts and menu events

* feat(desktop): webview→Rust bridge with native notifications

* fix(desktop): gate bridge injection on PageLoadEvent::Finished

Prevents double-injecting the bridge IIFE (once on Started, once on
Finished), which was duplicating every native notification.

* feat(desktop): Dock badge driven by webview attention count

* feat(desktop): launch-at-login autostart commands

* feat(desktop): sure:// deep link scheme with parse tests and navigation

* feat(desktop): preferences window with server switcher and launch-at-login

* docs(desktop): README for dev, release, signing/notarization, and deferred widget

* fix(desktop): remove dead New Window menu item

* fix(desktop): correct login route to /sessions/new

Rails uses `resources :sessions` (plural), so the login page is
/sessions/new, not the /session/new the plan assumed. Fixes an
immediate 404 when connecting to a server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* feat(desktop): Sure-styled onboarding, draggable titlebar, and app content offset

- Restyle onboarding + prefs to match Sure's auth page: solid surface
  background, centered logomark, .form-field-style inputs, inverse primary
  button; theme-aware via prefers-color-scheme (design-system tokens).
- Add a draggable titlebar strip on bundled pages and inject one into the
  remote page so the window drags from the top everywhere.
- Inject a top offset on the logged-in app-layout root so the sidebar logo
  clears the macOS traffic lights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): de-dupe login navigation to prevent CSRF token/session race

connect() navigated to /sessions/new directly AND via the
active-server-changed event, which is also handled by a second listener
injected into the page by bridge.js. One connect fired multiple concurrent
GET /sessions/new requests, each minting a fresh session + CSRF token; the
form shown and the _sure_session finally stored could come from different
GETs, so the login POST failed 'Can't verify CSRF token authenticity'
intermittently. Route all navigation through a single window-level guard so
only the first request per server wins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): enable window drag permission; offset only the icon rail

- Add core:window:allow-start-dragging (+ show/set-focus, event emit/listen) to
  capabilities so data-tauri-drag-region actually drags the window on macOS.
- Offset only the 84px left icon rail (logomark) to clear the traffic lights
  instead of pushing the entire app-layout down; keep main content full-height.
- Drag strip z-index lowered below Sure's sticky headers so its controls stay
  clickable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* feat(desktop): persist active server and resume session on launch

- Persist the active server to the Keychain in set_active_server; active_server
  falls back to it so a relaunch knows where to go.
- On launch, auto-resume straight to the last server instead of showing the
  picker every time.
- Navigate to the server root (not /sessions/new): Rails serves the dashboard
  when the session cookie is still valid, or redirects to login when not — so a
  persisted session no longer forces a re-login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* feat: desktop SSO via system browser with PKCE code exchange

Passkeys/WebAuthn don't work in an embedded WKWebView, so SSO now runs in
the system browser and hands a session back to the app securely.

Server (Rails):
- GET /auth/desktop/:provider — stashes a PKCE S256 challenge, hands off to
  OmniAuth (reusing the mobile auto-submit form). Passkeys work (real browser).
- openid_connect — for a linked identity in a desktop flow, mints a single-use,
  2-min, PKCE-bound one-time code and redirects to sure://sso/callback?code=...
  (unlinked identities are sent back with an error).
- GET /sessions/desktop_exchange — verifies the code + PKCE verifier
  (secure_compare), single-use (cache delete), then create_session_for; MFA is
  enforced at exchange time. Sets the normal web session cookie in the webview.
- Tests: happy path + single-use, wrong-verifier rejection, missing challenge.

Desktop (Tauri):
- start_sso command: generates PKCE, opens the browser, stores the verifier.
- sure://sso/callback deep link -> webview navigates to desktop_exchange with
  the verifier (never sent through the deep link, so an intercepted code is
  useless).
- bridge.ts intercepts SSO provider form submits and routes them to start_sso;
  password login stays in the webview.
- remote.json capability: minimal IPC (drag, event bridge, prefs window,
  start_sso) for the remote Sure origin — no fs/shell/http.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): correct remote IPC capability + handle menu in Rust + drag fallback

Root cause of prefs/switch-server/SSO/drag doing nothing on the logged-in
page: the remote-bridge capability's remote.urls ('https://*') did not match
the server origin, so all IPC (event listen, invoke, drag command) was denied.
Per Tauri v2, window.__TAURI__ is injected on remote pages only with
withGlobalTauri (set) AND a matching remote.urls; patterns need a path
wildcard.

- remote.json: urls -> https://*/**, http://*/** (+ bare host) so any server
  origin matches.
- menu.rs: Preferences and Switch Server now show the prefs window directly in
  Rust (no dependency on remote-page IPC); Switch Server moved from Window to
  the App menu.
- bridge.ts: drops the menu-event listeners (Rust owns them), adds a
  startDragging mousedown fallback for the drag strip, logs diagnostics, and
  reports start_sso success/failure to the console.
- main.ts: drops the now-unused menu listeners.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): SSO via event (remote can't invoke commands), disk-backed server store

Diagnostics confirmed window.__TAURI__ + IPC work on the remote page, but a
remote origin cannot invoke custom commands ('start_sso not allowed. Plugin
not found'). Events are permitted, so SSO now goes through an event.

- SSO: bridge emits 'sure://start-sso'; Rust listens and runs begin_sso
  (opens the system browser). start_sso command kept for local use.
- servers: mirror the server list + active server to a JSON file in
  Application Support as a fallback — Keychain items don't persist for
  unsigned builds, which was wiping the saved server on relaunch.
- remote.json: add notification:default (Sure's PWA was requesting it and
  erroring).
- menu: log whether the prefs window is present when Preferences/Switch
  Server fire, to diagnose the no-op.
- main: log the persisted active server on boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): drag the top band on every page via mousedown, not a z-indexed strip

The fixed drag strip sat below Sure's sticky headers (z-10) so it worked only
on pages without a top header. Replace it with a document-level mousedown in the
top ~34px that starts a window drag unless the target is an interactive element
— so dragging works on all pages, Sure's titlebar controls stay clickable, and
main content isn't pushed down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* ci(desktop): tag-triggered GitHub Actions release for universal unsigned .dmg

- .github/workflows/desktop-release.yml: on a 'desktop-v*' tag, build the
  universal (Apple Silicon + Intel) .dmg on a macOS runner via tauri-action and
  publish it to a GitHub Release with unsigned-install instructions.
- README: universal build command, the tag-based release process, and the
  Gatekeeper 'Open Anyway' / xattr steps for end users.
- Drop the unused iOS/Android icon sets (macOS build only needs icon.icns).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* ci(desktop): rolling desktop-latest build on desktop/ changes, not manual tags

Replaces the manual desktop-v* tag release with a path-filtered workflow that
builds only when desktop/ changes on main and publishes to a single rolling
'desktop-latest' prerelease with a stable Sure.dmg filename — one permanent
download URL, and the file changes only when the desktop code does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* ci(desktop): tag-driven versioned releases; tag is the single source of version

Revert to manual version tags (desktop-v*) for explicit version control, but
derive the app/.dmg version from the tag so package.json + tauri.conf.json are
synced automatically in CI — no manual version-file edits. Each tag produces its
own versioned GitHub Release with the universal unsigned .dmg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* ci(desktop): release entirely from GitHub via workflow_dispatch version input

Make the GitHub Action the single tool to version + deploy the desktop app:
Run workflow -> enter a version -> it syncs the version, builds the universal
unsigned .dmg, and creates the desktop-v<version> tag + Release. Refuses to
re-release an existing version; marks pre-release versions accordingly. Tag
push (desktop-v*) still works as a secondary trigger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* ci(desktop): publish releases with make_latest:false so they don't hijack the repo's Latest badge

Desktop is a secondary artifact, not the main product. Build with tauri-action,
then publish via action-gh-release with make_latest:false so the repo's 'Latest
release' badge stays on the main app's v* release. Separate desktop-v* tag
namespace already keeps it out of the v* publish workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): address PR review feedback (security + correctness)

Security:
- workflow: pass workflow_dispatch version via env (no shell injection); pin all
  third-party actions to commit SHAs.
- SSO: gate deep-link navigation and begin_sso to servers the user has saved
  (is_known_server), so a rogue page/deep link can't drive them.
- desktop_exchange is now POST (verifier in body, not URL/logs); CSRF skipped
  since the single-use PKCE code is the protection.
- desktop_sso_start validates the code_challenge is a 43-char base64url digest.
- desktop_exchange claims the one-time code atomically (delete-and-check) to
  close the read/delete TOCTOU.
- failure: return desktop SSO errors to the app via sure://sso/callback?error.

Correctness / stability:
- prefs window hides on close instead of being destroyed, so the menu can
  reopen it.
- servers.rs: on-disk store is authoritative (file-first read), atomic writes
  (temp + rename).
- main.ts/prefs.ts: try/catch around add/set/remove/active_server and boot; add
  a shared serverErrorMessage helper (no duplicated substring checks).
- vite.config.ts: derive dir from import.meta.url (ESM has no __dirname).
- bridge.ts: coalesce MutationObserver scans to one per frame.
- README: notarization example uses the universal .dmg name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): scope remote IPC to server origins at runtime, drop wildcard capability

Resolves the remaining security finding: the static remote.json granted Tauri
IPC to any http(s) origin (https://*). Remove it and instead add a capability
scoped to each server's exact origin at runtime (CapabilityBuilder +
add_capability), granting only the minimal permissions the bridge needs, for
saved/active servers on startup and for the target in set_active_server. No
origin outside the user's configured servers can access IPC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): add runtime per-origin IPC capability (grant_server_capability)

Implements the runtime-scoped capability that replaces the removed wildcard
remote.json: CapabilityBuilder scoped to each server's exact origin, added via
add_capability for saved/active servers at startup and in set_active_server.
(Split from the previous commit, which only recorded the remote.json removal.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* ci(desktop): harden release workflow (no shared caches, environment gate)

Address two release-workflow security findings:
- Remove cache: npm and the swatinem/rust-cache step so a poisoned Actions
  cache written by another workflow can't flow into a published .dmg (P0).
- Add 'environment: release' to the build job so publishing can require manual
  approval and scope secrets to release runs (P1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nno38ujctqiqSoY8eFRhaf

* fix(desktop): remove transparency code and fix relaunch behavior

* fix(desktop): fix PR review findings; adjust app notarization path, use Sure theme tokens instead of hardcoding values

* ci(desktop): switch release from independant versioning to using Sure's publishing workflow, releasing and versioning with every main app release

* fix(desktop): restrict CSP as much as possible while maintaining functionality; allow bundled scripts, Tauri IPC, inline styles; deny wildcards

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 07:44:06 +02:00
Oscar
c6a240a183 feat(redbark): add australian bank sync (redbark) (#2794)
* add redbark provider integration

- per family api key provider, built like the lunchflow integration
- syncs accounts, balances and transactions from api.redbark.com
- account setup flow, settings panel, locales and routes
- tests and fixtures

* harden redbark integration based on prior provider pr feedback

- use DebugLogEntry.capture for sync/import/unlink failures
- retry 429s and 5xxs with backoff, raise on page cap instead of truncating
- keep raw response bodies out of logs and errors
- not null constraints on account columns, migration base 7.2
- persist ignored flag for skipped accounts so they stop nagging setup
- validate api key on every save, re-arm status on key rotation
- destroy aborts if unlink fails, atomic account create and link
- require_admin on mutating actions, see_other on error redirects
- single grouped query for item account counts
- i18n default connection name, blank password field value
- controller and provider tests

* fix issues found in second review sweep

- add missing syncable scope, without it every family sync raises
- kick off a sync on connection create and on key rotation
- setup dialog fetches accounts inline for fresh connections and shows api errors
- skip balance write when no balance has been fetched yet, never anchor a false zero
- exclude stale and non banking accounts from the batched balances call, per account fallback if the batch is rejected
- detect the server row ceiling and empty pages instead of silently truncating history
- user sync start date only governs the initial backfill, incremental after that
- fetch connections before the per account loop so auth errors propagate once
- drop untemplated index/show/new/edit routes and dead preload/link_accounts actions
- stable dom id on the settings panel so repeat turbo replaces keep working

* skip brokerage connections, found in live testing

- the transactions endpoint 400s for brokerage connections, they belong to /v1/trades
- only import accounts from banking and documents connections
- guard transaction fetches for any legacy linked non banking account

* address review feedback

- treat the truncation header as a pagination signal: split the date window and refetch instead of failing the account
- prune stale pending rows from the snapshot so settled pendings cant come back as duplicates
- block linking a sure account that already has another provider feed
- count setup failures separately from skips and surface an error instead of "all skipped"
- add not nulls on redbark_items name and api key
- enqueue the destroy job after the flag commits, not inside the transaction
- swap bg-gray-400 for bg-surface-inset, drop amounts from info logs, remove i18n default fallbacks
- tests for window splitting, pending pruning and encrypted payload round trip

* fix issues from convention review

- benign skips (unlinked account, blank id, unparseable rows) no longer count as failures, tracked separately so a clean batch reports success
- currency parsing goes through extract_currency so hash shaped payloads resolve instead of falling to the default
- merchant ids use truncated sha256 instead of md5
- debug log entries for import failures and account sync scheduling failures

* bound the raw transactions snapshot to the fetch window

- trim raw_transactions_payload to the current fetch window on merge, same as brex
- keep rows without a parseable date, drop settled pendings as before
- surface skipped rows in the aggregate debug log entry with imported/skipped counts
2026-07-26 07:40:25 +02:00
minion1227
e5608ca6d9 fix(settings): allow clearing an encrypted provider API key (#2544)
* fix(settings): allow clearing an encrypted provider API key

`update_encrypted_setting` skipped the write whenever the submitted value
was blank, so clearing the field (which auto-submits an empty value) never
removed the stored key — the masked "********" placeholder just reappeared
on re-render. This affected every encrypted provider key (Twelve Data,
Tiingo, EODHD, Alpha Vantage, Tinkoff).

Treat "********" as "leave unchanged" (the untouched masked placeholder)
but persist nil for an explicit blank submission, so a key can be removed
from the UI.

Closes #2465

* fix(settings): clear the remaining encrypted provider tokens on blank

Route openai_access_token, anthropic_access_token, and
external_assistant_token through update_encrypted_setting so blanking
any of them clears the stored value instead of silently retaining it —
same fix as the securities keys, completing the scope of #2465.

Add regression tests for clearing each token and for the OpenAI masked
placeholder, and reset the newly-touched keys in teardown.

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

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-07-26 07:31:25 +02:00
Juan José Mata
375dd060dc feat(insights): gate the insights feed behind preview features (#2788)
* feat(insights): gate the insights feed behind preview features

Insights shipped to everyone in #2550. Make it opt-in via Settings →
Preferences until it's proven, so users who haven't enabled preview
features see nothing and cost nothing.

Entry points gated:
- InsightsController — require_preview_features! covers all four actions,
  including the refresh action that enqueues the job
- Dashboard — the insights_feed section is omitted from the section list
  rather than left in it hidden, so the saved-order lookup and the
  insights_feed unshift special-case never fire; the feed query is skipped
- Top bar — the lightbulb entry and its unread COUNT, which previously ran
  on every page render

The job is gated too, departing from the guide's default that background
jobs keep running. That default fits a job like SweepExpiredGoalPledgesJob,
which only walks records opted-in users created and is naturally inert.
GenerateInsightsJob instead manufactures data for every family nightly —
seven generators over the income statement and balance sheet, plus paid LLM
narration — so it would have kept spending on families who can't see the
result. The fan-out filters with Family.with_preview_features (one indexed
jsonb containment query, not load-and-iterate), and generate_for_family
re-checks above the advisory lock so a gated family skips the broadcast too.

Adds Family#preview_features_enabled? and the matching scopes, keeping the
predicate name identical on User and Family so the guide's GA-removal grep
finds every call site. Verified the SQL scope and the Ruby predicate agree
for true / false / "yes" / nil. Documents the job-gating pattern in
docs/llm-guides/gating-a-preview-feature.md, which previously said the gate
does nothing for jobs.

Existing insight rows are left alone: invisible without the flag, and the
next nightly run refreshes facts and expires anything stale if a family
opts in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6ZRA6cgCRM4UdFKct3wm4

* perf(insights): use EXISTS for the family preview rollup

Family#preview_features_enabled? is asked once per family by the nightly
job; the block form loaded and instantiated every member to answer a
boolean. Delegate to the scope instead.

The predicate now shares an implementation with the scope, so the
truthy-non-boolean test asserts against User#preview_features_enabled? —
the predicate the UI actually gates on — to keep the cross-check
meaningful rather than tautological.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6ZRA6cgCRM4UdFKct3wm4

* docs(insights): fix guide/code drift and stale cron description

Review follow-ups from @gariasf:

- The guide's family-rollup snippet still showed the block form after the
  EXISTS commit changed it. It mattered more than normal doc drift: the
  paragraph below calls GenerateInsightsJob "the reference implementation",
  so the next person writing a gated job would have copied the form
  family.rb's comment explicitly rejects.
- schedule.yml still described the job as running for "all families" — the
  string someone reads while debugging why a family got no insights.
- Document that the shared predicate name is per-user on User but
  "anyone in the household" on Family, and prohibit gating UI on the family
  form: Current.family.preview_features_enabled? reads naturally and would
  show the feature to a user who explicitly opted out. Noted in both the
  model and the guide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6ZRA6cgCRM4UdFKct3wm4

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillem Arias Fauste <accounts@gariasf.com>
2026-07-26 07:18:23 +02:00
kai392
6e2cbb727a fix(ai): prevent Anthropic chat crash on no-argument tool calls (#2755)
* fix: prevent Anthropic chat crash on no-argument tool calls

When the model streams a tool_use block with no arguments (e.g.
get_categories), the accumulated input arrives as an empty string.
The Anthropic chat parser passed that empty string straight through as
function_args, and Assistant::FunctionToolCaller then called
JSON.parse("") — which raises "unexpected end of input", surfaces as a
Provider::Anthropic::Error, and kills the whole assistant turn.

Fix at the source by normalizing empty/nil tool input to an empty JSON
object in the parser, plus a defensive guard in FunctionToolCaller so
any provider that emits blank arguments cannot crash a turn.

Fixes #2722

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

* test: assert nil-args result as Hash to match jsonb function_result

Addresses Codex P1 / CodeRabbit review: EchoFunction returns the parsed
params Hash and ToolCall::Function stores it in a jsonb column, so the
nil-arguments test must assert the Hash directly instead of JSON.parse.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 04:47:15 +02:00
RealDiligent
d61ee3abea fix: omit SimpleFin pending param when pending transactions are disabled (#2796)
The SimpleFIN protocol only defines pending=1 (include pending); pending
transactions are excluded by default when the param is absent. Bridges
presence-check the param, so the pending=0 we sent when the 'Include
pending transactions' setting (or SIMPLEFIN_INCLUDE_PENDING=0) was
disabled behaved exactly like pending=1, making the setting a no-op —
pending transactions kept being downloaded, causing pending/posted
duplicates and churn.

Omit the pending query param entirely unless pending is enabled, per
the spec.

Fixes #2440

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 01:56:42 +02:00
RealDiligent
98bf563458 fix: prevent AddAmountToTransfers migration aborting on legacy transfer data (#2795)
The backfill computed amount = outflow_entry.amount - source_fee_amount
and then added a check constraint requiring amount >= 0. Historical rows
that predate the modern sign convention (negative outflow amounts) or
carry fees larger than the entry amount produce a negative principal, so
the constraint aborts the migration with PG::CheckViolation — blocking
db:prepare and the entire upgrade for affected self-hosters.

Normalize with ABS and clamp at zero in the backfill. Rows that already
satisfied the old expression are unchanged (ABS is a no-op for positive
amounts); only previously-failing rows now migrate instead of killing
the upgrade.

Fixes #2653

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 01:54:49 +02:00
Erkan Doğan
1fb5202495 fix(indexa_capital): correct cash activity amount-sign convention (#2793) (#2801)
Fixes #2793.

## Problem

IndexaCapitalAccount::ActivitiesProcessor#normalize_cash_amount used the inverted amount-sign convention relative to Sure's core conventions.

- Outflows (WITHDRAWAL, TRANSFER_OUT, FEE, TAX) were stored as negative amounts.
- Inflows (CONTRIBUTION, TRANSFER_IN, DIVIDEND, DIV, INTEREST) were stored as positive amounts.

Sure requires asset account inflows to be stored as negative amounts (-amount.abs) and outflows as positive amounts (amount.abs).

## Fix

Flip signs in IndexaCapitalAccount::ActivitiesProcessor#normalize_cash_amount to align with Sure's sign convention and sibling processors (SnaptradeAccount::ActivitiesProcessor).

## Test

Added unit tests in test/models/indexa_capital_account/activities_processor_test.rb covering cash inflows, outflows, transfers, fee, label mappings, and empty payloads. All tests pass (39/39 for indexa_capital_account).

Co-authored-by: erkdgn <erkdgn@users.noreply.github.com>
2026-07-26 01:47:53 +02:00
William Wei Ming
34dd5fbc62 update transactions_controller (#1953)
* update transactions_controller

* fix FEEDBACK - Remove DISTINCT that breaks tag-filtered index queries

* fix FEEBACK from jjmata

* Ignore Brakeman EOLRails warning for Rails 7.2
Restore fingerprint-scoped ignore lost during merge from main.

* update transactions_controller

* fix FEEDBACK - Remove DISTINCT that breaks tag-filtered index queries

* fix FEEBACK from jjmata

* Ignore Brakeman EOLRails warning for Rails 7.2
Restore fingerprint-scoped ignore lost during merge from main.

* resolve review - Add .distinct to the tag filtering subquery

* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* Drop obsolete Rails EOL note and Brakeman EOLRails ignore

The EOLRails ignore and the accompanying migration-rule note were only
needed while the app ran Rails 7.2.3.1, whose support window closed
2026-08-09. Main has since moved to Rails 8.1.3, so the check no longer
warns and both changes are dead weight that only widen this PR's diff.

Keeps the PR focused on the transactions controller query optimization.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 01:42:18 +02:00
Erkan Doğan
71106f63b0 fix(merchants): preserve user-selected merchant colors on save (#2760) (#2802)
* fix(merchants): preserve user-selected merchant colors on save (#2760)

Fixes #2760.

## Problem

FamilyMerchant#set_default_color callback ran on before_validation and unconditionally executed self.color = COLORS.sample.

As a result:
- Any user-selected color or color passed via API/import was discarded and replaced with a random palette color.
- Renaming or updating any merchant field re-assigned a new random color on every save.

## Fix

Guard set_default_color so it only assigns a random palette color when color is blank (self.color = COLORS.sample if color.blank?).

## Test

- Added test/models/family_merchant_test.rb testing color preservation on creation and update, as well as default color fallback when blank.
- Updated test/controllers/family_merchants_controller_test.rb to assert persisted color on create and update.
- Verified in dev container: 9 runs, 22 assertions, 0 failures, 0 errors.
- RuboCop: 0 offenses.

* fix(merchants): validate hex format and fallback to default color if invalid

Add hex format validation (/\A#[0-9A-Fa-f]{6}\z/) to FamilyMerchant#color (matching Category and Tag models) and ensure set_default_color replaces invalid hex values (e.g. from API/CSV imports) with a default palette color before saving.

Responds to Codex review feedback on PR #2802.

---------

Co-authored-by: erkdgn <erkdgn@users.noreply.github.com>
2026-07-26 01:38:31 +02:00
Antoine 🚀🏃‍♂️
6b6ae0ca31 feat(accounts): add Gains / ROI chart view for investment accounts (#2660)
* feat(accounts): add Gains / ROI chart view for investment accounts

Adds a fourth chart view to the account details page showing the
historical unrealized gains series (market value - cost basis per
holding, summed daily with LOCF and FX conversion), following the
same ChartSeriesBuilder/Series pipeline as the existing views.

Holdings without a usable cost basis (nil, or unlocked zero from
providers) contribute zero gain, matching Holding#avg_cost semantics.

* fix(accounts): carry cost basis forward over gap-filled holdings in gains series

Gap-filled holding rows (weekends, price-history gaps) are persisted
without cost_basis even though the position and basis are unchanged,
which zeroed the gains series on those dates. Look up the basis from
the latest snapshot that has a usable one instead of reading it from
the current row, so already-persisted gap-filled rows are handled too.

* docs(accounts): add method docstrings for gains chart view code

Satisfies the pre-merge docstring coverage check on methods added or
touched by the Gains / ROI feature.

* fix(accounts): sign converted amount like main indicator in gains view

Extracts the gains sign-prefix logic into a shared signed_format helper
so the family-currency converted amount shown on foreign-currency
accounts matches the main indicator (+€79.53 / +$85.00), and adds
component tests covering the positive, negative, non-gains and
foreign-currency formatting paths.

* test(accounts): cover FX conversion path in gains series

The gains_series tests only used USD holdings against a USD target,
leaving the exchange_rates LATERAL join untested. Adds a case with EUR
holdings converted to USD, including LOCF rate carry-forward.

---------

Co-authored-by: Antoine GUYON <agy@ibanfirst.com>
2026-07-25 20:14:56 +02:00
pro3958
12b040e47e fix(akahu): pull full history on initial Akahu sync (#2779)
* fix(akahu): pull full history on initial Akahu sync

Akahu-linked accounts only pulled ~90 days of history on their first
sync. Provider::Akahu#fetch_all already walks the full range via
Akahu's cursor pagination, so the paging logic was not the limit.
AkahuItem::Importer#determine_sync_start_date clamped the initial
window to 90.days.ago when an account had no stored transactions and
no configured sync_start_date, and Akahu's transactions endpoint only
returns data from the requested start onward, so that fallback capped
the first import at 90 days.

Request a 5.years lookback on the first sync instead. Incremental
syncs still continue from last_synced_at - 7.days, and an explicitly
configured sync_start_date still takes precedence.

Fixes #2609

* fix(akahu): omit start date on initial sync to pull full history

determine_sync_start_date still clamped the first import to INITIAL_SYNC_LOOKBACK.ago
(5 years), truncating Akahu apps/accounts that can access more history. Akahu's
account-transactions endpoint defaults to the entire accessible range when start/end
are omitted, so the no-config/no-stored-transactions case now returns nil (no start
date). Subsequent syncs still use the incremental last_synced_at - 7.days path.

Removes the now-unused INITIAL_SYNC_LOOKBACK constant and updates the test to assert
the initial sync omits the start date.

---------

Co-authored-by: agentloop <agentloop@localhost>
Co-authored-by: pro3958 <pro3958@users.noreply.github.com>
2026-07-25 20:11:25 +02:00
Guillem Arias Fauste
9fd959da0b fix(dashboard): add hover state to section header controls (#2798)
* fix(dashboard): add hover state to section header controls

The collapse chevron, widget-size settings, drag handle and cashflow
expand button signalled hover with a colour shift alone
(`text-secondary` → `text-primary`). On 14–16px glyphs that reads as
almost nothing, so there was no feedback for which of two adjacent
controls was about to be clicked.

Adopt the icon-control recipe already used for the top bar in
`layouts/application.html.erb`: add `hover:bg-container-inset-hover`
plus a radius alongside the existing colour shift. The token resolves to
gray-100 / gray-700, so light and dark are both covered and no design
system change is needed.

Replaces the ad-hoc `p-0.5` / `w-5 h-5` sizing with a uniform 24px
centred box, so the four controls present the same hover target and the
same tint area. 24px matches the header's `text-base` line-height, so
the row height is unchanged.

The drag handle moves from `hidden lg:block` to `hidden lg:flex` rather
than gaining a bare `flex`, which would have overridden `hidden` at the
mobile breakpoint depending on utility order.

* fix(dashboard): drop hover fill from the drag handle

The grip is a grab surface, not a button — clicking it does nothing. A
filled hover state reads as "clickable" and promises a click that never
lands, so it keeps cursor-grab plus the colour shift and skips the
background the genuinely clickable controls use.

Stays on the same 24px box so the icons remain aligned with the
widget-size control beside it.
2026-07-25 18:59:43 +02:00
Erkan Doğan
32ab402d38 fix(snaptrade): sign bare TRANSFER activities by provider direction (#2792)
SnapTrade delivers payroll-deducted 401k contributions as type "TRANSFER"
rather than "CONTRIBUTION", which normalize_cash_amount did not handle.
The value fell through to the pass-through branch and was stored positive,
violating the convention that inflows to an asset account are negative.

That stored sign drove both reported symptoms. Entry#classification reads a
positive amount as an expense, so the row rendered -$1,320.75 while the
brokerage showed +$1,320.75. Balance::ReverseCalculator reads it as a value
decrease, so walking backward from the provider-anchored current balance
produced a steadily declining history despite a correct present-day total.

TRANSFER_IN and TRANSFER_OUT encode direction in the type and can force the
sign with .abs. A bare TRANSFER does not, so the provider's sign is the only
directional signal available and is inverted into Sure's convention instead
of being passed through. Scoped to TRANSFER specifically: CASH_TYPES is
unreferenced, so every non-trade type reaches this method and broadening the
else branch would silently flip SPLIT, MERGER, JOURNAL and others.

Fixes #2756

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:54:37 +02:00
github-actions[bot]
f3afcf955a Bump version to next iteration after v0.7.3-alpha.5 release (#2791)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 07:18:12 +02:00
kianrafiee
40b18e8484 feat(reports): add net worth chart with breakdown tooltip (#2716)
* Add monthly net worth chart with group breakdown to Reports

Adds a net worth trend chart to the Reports > Net Worth section,
rendered in the same design scheme as the dashboard chart. The chart
shows one data point per month across the period selected at the top
of the Reports page, and its hover tooltip breaks the hovered month
down into per-account-group balances (Cash, Investments, Credit
Cards, Loans, etc.) under Assets and Liabilities headings with
section totals.

- BalanceSheet::NetWorthBreakdownSeriesBuilder builds the monthly
  series by running Balance::ChartSeriesBuilder per account group
  (grouped by accountable type), with liabilities reported as
  positive magnitudes and all-zero groups omitted; cached with the
  same invalidation pattern as the existing net worth series
- net_worth_chart Stimulus controller extends the existing
  time_series_chart controller, overriding only data normalization
  and the tooltip template
- Reports controller passes the series through the existing
  net_worth_metrics hash; tooltip headings reuse existing locale keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review: month-over-month tooltip deltas, Stimulus value labels

- Recompute each chart point's trend from the previous monthly point
  instead of inheriting the raw series trend, which at a monthly
  interval compared the underlying balance row's own start/end and so
  reflected only the last balance update before the sample date
  (chatgpt-codex-connector). The first point has no prior month and
  renders the standard flat state.
- Pass tooltip section labels to the Stimulus controller as declared
  values (data-*-value attributes) per coding guidelines (coderabbit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 05:59:32 +02:00
sure-admin
3d40d93bba fix(schema): restore enable banking account columns 2026-07-25 03:36:33 +00:00
Guillem Arias Fauste
9d4b4bc27c feat(jobs): reap imports and exports stuck by lost background jobs (#2681)
* feat(jobs): reap imports and exports stuck by lost background jobs

A hard-killed worker (OOM, SIGKILL during deploy) loses its in-flight
Sidekiq job permanently. Sync is the only model with a stale sweep;
everything else wedges in a non-terminal status forever:

- Import stuck "importing"/"reverting" (e.g. #2274)
- ImportSession stuck "importing" — unrecoverable, publish_later
  refuses to re-publish while importing
- FamilyExport stuck "pending"/"processing" — exports index polls
  every 3s indefinitely
- PdfImport's AI-processing claim held forever (ProcessPdfJob's
  reclaim only runs if the job is redelivered)
- Provider activities_fetch_pending flags stranded when a
  self-rescheduling fetch chain loses a link

SyncCleanerJob (existing hourly cron) now sweeps all of them, each
isolated so one failure doesn't block the rest, and records every
reaped record as a DebugLogEntry with the family attached.

Idempotency guards so a stray or redelivered job cannot corrupt a
record the reaper (or a user retry) has since moved on:

- Import#publish skips complete/reverting/revert_failed imports —
  a redelivered ImportJob after completion double-applies data for
  import types without row dedup. Failed imports deliberately stay
  publishable: their transaction rolled back, so a re-run is a retry.
- FamilyDataExportJob refuses terminal exports but still allows
  processing ones through, since graceful-shutdown redelivery is what
  completes them.

Reaper thresholds (6h imports, 2h exports) key off updated_at and
dwarf legitimate runtimes, so live jobs are not swept in practice.

* fix(jobs): make the reapers race-safe and commit-aware

Review feedback on #2681 (jjmata, Codex, CodeRabbit):

- Every sweep now mutates under record.with_lock with a staleness
  re-check, mirroring the guard Sync#perform gained in #2680 — a job
  finishing between the sweep query and the write can no longer be
  clobbered mid-flight
- Import.clean distinguishes which side of import!'s single transaction
  the worker died on: rows attached means the data committed and only
  the status write was lost, so the record is finalized as complete
  (marking it failed invited a re-publish that double-imports types
  without row dedup, e.g. TradeImport); no rows means a clean rollback
  and the failed/try-again path stays
- PdfImport.clean applies the same split: no rows → the AI-extraction
  claim died, reclaim to pending; rows → the publish died post-commit,
  finalize complete instead of letting the same extracted rows be
  published twice. Stuck PdfImport reverts (previously unswept by
  either clean) now go to revert_failed like every other import
- ImportSession.clean reconciles chunks whose import! committed but
  never got the complete-status write before failing the session, so
  re-publish skips them instead of duplicating their rows via
  SureImport's split path
- Import#publish redelivery skip is captured via DebugLogEntry instead
  of Rails.logger so support can see it in /settings/debug
- The interrupted-error copy is i18n-backed (imports.errors.interrupted)

* fix(jobs): round-2 review feedback on the reapers

- Import.clean excludes session-owned chunks (import_session_id: nil) —
  SyncCleanerJob runs it before ImportSession.clean, so it could
  finalize or fail a SureImport chunk outside the session flow that
  owns its lifecycle. Regression test added (CodeRabbit major)
- family.sync_later moved outside the row-lock transaction in both
  reap paths — Rails doesn't defer enqueues to after-commit by default,
  so Sidekiq could pick the job up before the status write was visible
- Per-record rescue in Import.clean / PdfImport.clean so one bad record
  doesn't abort the rest of the hourly sweep
- ImportSession.clean uses the importing? enum predicate;
  reconcile_committed_chunks! iterates with each (default-ordered
  association made find_each warn, and chunk counts are tiny)

* fix(jobs): address round-3 reaper review (isolation + commit signal)

Per-record rescue isolation for the three sweeps that lacked it —
FamilyExport.clean, ImportSession.clean, and SyncCleanerJob's
activity-flag loop — mirroring Import.clean/PdfImport.clean. One bad
record (validation error, DB blip) no longer aborts the rest of that
sweep for the hour; the activity-flag guard is per-record so a failure
also stops skipping the models that follow.

data_committed? now covers Category/Rule/Merchant imports, whose
records hang off the family rather than the import (no entries or
accounts), via the new Import#committed_by_named_records? helper. A
committed one is reaped to complete instead of a retryable failed;
nameless RuleImport rows carry no stable key, so a nameless-only file
has no commit signal and stays retryable.

* fix(schema): drop duplicate enable_banking_accounts columns from merge

The merge of main into this branch re-appended product, credit_limit,
and identification_hashes after updated_at, so schema.rb declared each
twice and db:schema:load raised "you can't define an already defined
column 'product'". Removed the duplicate declarations (kept the new
treat_balance_as_available_credit column); the test database loads
again.
2026-07-25 05:04:30 +02:00
Anthony
e3a7107271 Feature/dashboard Add "Money In / Out" dashboard widget with monthly bar chart (#2594)
* Add "Money In / Out" dashboard widget

Adds a new dashboard section showing a monthly bar chart of cash
activity alongside a summary card (net balance, income, expenses),
with per-widget month navigation and account filtering.

- IncomeStatement#totals_for computes income/expense totals for an
  arbitrary period, optionally scoped to a set of account ids
- New bar_chart_controller.js (D3) renders the monthly bars
- Income/expense rows link through to filtered transactions

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

* Fix tooltip/dropdown positioning and split money flow chart by income/expense

The widget's @container wrapper established a new containing block for
position:fixed descendants, so the month-picker and account-filter
menus (floating-ui, strategy: fixed) and the D3 tooltip (container-
relative coordinates) rendered away from their trigger/bar. Drop
@container in favor of regular viewport breakpoints for this
full-width widget, and position the tooltip with page-relative
coordinates like the other chart controllers.

Also replace the single combined bar per month with a grouped
expense/income pair (red/green, with a legend) so each month's
inflow and outflow are visible independently.

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

* Cap and scroll the money flow widget's month picker

Add an optional max_height to DS::Menu (opt-in, backward compatible)
so a long item list scrolls inside a fixed-height panel instead of
overflowing the viewport. Use it for the money flow widget's 12-month
picker.

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

* Use design system tokens for income/expense indicators in money flow widget

Swap the four bg-green-500/bg-red-500 dot indicators (legend + income/expense row links) in the money flow widget for bg-success/bg-destructive, matching the "functional tokens only" rule. The same partial already uses
text-success/text-destructive for the balance figure, and bg-success/ bg-destructive are already established elsewhere (DS::Alert, budget_categories/_budget_category.html.erb).

* Fix future-month 500 and pending-transaction link mismatch in money flow widget

Two CI review findings on the money flow widget:

- A future month passed via ?money_flow_month= (e.g. a bookmarked/hand-edited URL) made end_date earlier than month_start once capped at Date.current, which Period.custom rejects, causing a 500. money_flow_month_param now clamps future months to the current month, same as it already does for malformed input.

- The income/expense row links passed type/date/account filters but no status, so Transaction::Search included pending transactions even though the displayed totals (IncomeStatement#totals_for) exclude them via excluding_pending. Add status: ["confirmed"] to both links so the linked list matches the card total.

Also strengthens the widget's controller tests: asserts the highlighted bar's actual income/expense values instead of just its presence, and adds a regression test proving an account id outside the current user's accessible accounts is dropped (falls back to the unfiltered state) rather than leaking or erroring.

* Fix account filter eligibility and SVG dark mode fill in money flow widget

Two more CI review findings on the money flow widget:

The account filter iterated over all visible/accessible accounts, a broader set than IncomeStatement actually counts (accounts excluded from reports, tax-advantaged accounts like 401k/IRA, or shared accounts not included in the user's finances). Selecting one of these silently computed to zero while its drill-down link could still list its transactions. Add IncomeStatement#eligible_accounts, mirroring the same criteria already applied in the totals SQL, and use it for both the checkbox list and the account_ids intersection.

The D3 axis tick text elements used text-primary/text-secondary, which set CSS color, not SVG fill, so labels rendered with the default black fill and were unreadable in dark mode. Add fill-current, matching the pattern already used in sankey_chart_controller.js.

Adds controller/model tests for eligible_accounts (excluded from the account filter, ignored when passed as a filter id, excluded from totals) and verifies the dark-mode fill fix visually.

* Extract duplicated bar-chart JSON parsing into a test helper

The css_select("[data-controller='bar-chart']").first + JSON.parse(chart["data-bar-chart-data-value"]) pattern was repeated across four money flow widget tests in pages_controller_test.rb. Extract it into a private money_flow_bars helper and use it everywhere instead.

* Preserve eligible account scope in money flow drill-down links when unfiltered

The income/expense drill-down links used money_flow_data[:account_ids] directly, which is nil in the widget's default unfiltered state, so .compact dropped the account filter entirely from the link. TransactionsController treats an absent account_ids as all accessible accounts, a broader set than IncomeStatement#eligible_accounts (which excludes tax-advantaged, excluded-from-reports, and non-finance shared accounts). Users with any such account could click a displayed total and see transactions that were never counted in it.

Use selected_account_ids (already computed as money_flow_data[:account_ids] || accounts.map(&:id)) for both links instead, so they always pin to the same eligible accounts backing the total, filtered or not. Left the month-picker link on money_flow_data[:account_ids] so navigating months while unfiltered doesn't bloat the URL with every eligible account id.

Adds a regression test confirming the default (unfiltered) links include account_ids and exclude an ineligible account's id.

* Refine Money In/Out dashboard widget

- 6-month window (was 3), half-width default with responsive stack, income-first
  bars, 2px floor + faded in-progress (partial) month
- Expense series/figures neutral (gray/text-primary) — app reserves red for
  negative/overspend; neutral zero balance
- Account filter: outline DS::Button + list-filter icon trigger with in-panel
  search (DS::SearchInput + list-filter), matching the app's filter convention
- i18n search_accounts (en + fr); bump money_flow bar-count test 3 -> 6

* Clarify Money In/Out scope: month label, 6-month caption, filter "All"

Addresses confusion between the widget's own month picker and the dashboard's
global period, and between the picked month and the 6-month chart span.

- Card now headed with the selected month (e.g. "July 2026") so it's clear the
  totals below are that month's, driven by the picker
- Chart legend row captioned "Last N months" so the trailing window is explicit
- Info tooltip by the month picker: the widget scopes to the month you pick
  here, independent of the dashboard's top-level period
- Account filter trigger reads "All accounts" when nothing is filtered out,
  instead of "Filter accounts (N)"
- i18n (en + fr) for the new strings

* Match tooltip expense dot to the gray bar palette

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Guillem Arias <accounts@gariasf.com>
2026-07-25 04:59:29 +02:00
jdcdp
849578a84a Add support for trading212 integration in investments sync (#2513)
* Add support for trading212 integration in investments sync

* Respect Trading 212 history rate limits

* Replace hex colors with design tokens

* Changed withdrawal to withdraw

* Address PR review feedback for Trading 212 integration

- Replace all Rails.logger calls with DebugLogEntry.capture across
  syncer, importer, processor, provider, controller, and unlinking
- Fix destroy action to surface unlink errors via DebugLogEntry
- Replace hardcoded 'GBP' currency fallback with Current.family.currency
- Replace hardcoded English status strings in Syncer with i18n
- Add config/initializers/trading212.rb with DEBUG_RAW ENV toggle
- Add comprehensive test coverage: item, account, data_helpers,
  holdings_processor, activities_processor, importer, syncer,
  and controller tests
- Rewrite syncer and controller tests to match actual interfaces

* Fix tests and bugs found during local test execution

- Fix standard_ticker: empty string caused nil.upcase via [].first
- Fix parse_date: DateTime < Date, so when Date caught DateTime first
- Fix ActivitiesProcessor/HoldingsProcessor: missing instruments_map
  in DataHelpers caused NameError when processing dividends
- Fix test: security fixture ticker collision with test data
- Fix test: sync_status_summary i18n matching in assertions
- Fix test: trading212_provider ConfigurationError test path
- Fix test: controller invalid params needs Turbo-Frame header
- Fix test: syncer test uses stubs instead of strict expects

116 tests, 257 assertions, 0 failures, 0 errors

* Keep raw provider response bodies out of exception messages.

* Fix Secrets leak into page HTML, prevents the API key/secret from appearing in the HTML source while keeping the "leave blank to keep existing" UX.

* Address review findings: env gate, sync test, uniqueness test, destroy flow

- Gate TRADING212_DEBUG_RAW behind Rails.env.local? so staging/production
  cannot accidentally enable raw payload logging
- Assert SyncJob enqueue in sync controller test, not just redirect
- Fix cross-item uniqueness test to actually use two different items
- Remove rescue in destroy so unlink failures stop the flow (matching
  Brex/Akahu pattern) instead of proceeding to destroy_later silently

* Add Trading212 tables to db/schema.rb for CI test database

CI runs db:test:prepare which loads db/schema.rb, not migrations.
Without these table definitions, fixture loading fails with
PG::UndefinedTable: relation "trading212_accounts" does not exist.

* Removed lint complaint

* Register Trading212 in ProviderConnectionStatus::PROVIDERS

Fixes CI failure: test_provider_registry_covers_syncable_family_provider_item_associations

* Fixed bad merge

---------

Signed-off-by: jdcdp <47483528+jdcdp@users.noreply.github.com>
Co-authored-by: jdcdp <jdcdp@cdm4.net>
2026-07-25 04:54:30 +02:00
kianrafiee
d267288a45 Add RentCast & Realie integration for automatic property data and valuations (#2727)
* Add RentCast and Realie AVM providers for property valuation

Adds Automated Valuation Model (AVM) provider support so self-hosted
users can create property accounts from a US address lookup instead of
entering details manually:

- Provider::Rentcast and Provider::Realie clients, each fetching the
  property record (type, year built, square footage) and value estimate
  in a single API request, registered under a new :property_valuations
  registry concept
- API key fields (encrypted, ENV-overridable) with monthly usage display
  in Settings > Self-Hosting under "Property Valuation Providers"
- New property flow: when a provider key is configured, the method
  selector offers "Add via RentCast/Realie" alongside manual entry;
  the lookup form reuses the manual flow's localized address fields
  and creates the account active with fetched attributes, valuation as
  balance, and the address saved
- SyncPropertyValuationsJob refreshes linked property valuations once a
  day (never hourly) via config/schedule.yml; each provider enforces its
  monthly request cap (RentCast 50, Realie 25) with a calendar-month
  counter shared between creation lookups and refreshes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Allow ENV override of AVM provider monthly request caps

The RentCast (50/month) and Realie (25/month) budgets default to the
free tier limits but can now be raised for paid plans via
RENTCAST_MAX_REQUESTS_PER_MONTH / REALIE_MAX_REQUESTS_PER_MONTH,
mirroring the AlphaVantage and Tiingo request limit overrides. The
settings descriptions and usage display reflect the effective cap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review feedback: durable request counters, safer refresh job

- Move monthly AVM request counters from Rails.cache to a new
  provider_request_counts table with atomic upsert increments, so the
  hard budget caps survive cache eviction, restarts, and Redis flushes
- SyncPropertyValuationsJob: only mark a property synced when the
  balance update succeeds (wrapped in a transaction), process stalest
  valuations first so a tight budget goes where it matters, and report
  failures via DebugLogEntry.capture instead of Rails.logger
- Realie: reject lookups whose returned city/ZIP contradict the entered
  address (street+state queries can match the wrong city); document that
  numeric use codes are unpublished and leave the subtype unset
- Add Faraday open/request timeouts to both provider clients
- Localize all user-facing provider error messages
  (config/locales/models/provider/en.yml)
- Add a check constraint restricting properties.avm_provider to known
  providers
- Use the min-h-80 scale token instead of min-h-[320px]
- Tests: exact-argument expectations on the lookup stub, provider stubs
  in the no-provider test, RentCast/Realie API key update tests,
  ProviderRequestCount unit tests, failed-balance regression test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Stub AVM provider registry lookups in settings system test

Settings::HostingsController#show now resolves the RentCast and Realie
providers for usage display; the system test's partial get_provider
stubbing treats the new lookups as unexpected invocations without these.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address second-round review: candidate scan, provider reuse, shared row partial

- Realie: when the address lookup returns multiple candidates, pick the
  first one consistent with the entered city/ZIP instead of judging only
  the first array element; the location-mismatch error now only fires
  when no candidate matches
- SyncPropertyValuationsJob: resolve one provider instance per key for
  the whole run, so the per-instance request throttle actually spaces
  requests across properties instead of resetting on each iteration
- Extract the AVM method selector row into a shared partial so the
  manual and provider options render one shape

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add retry middleware and sync index from review feedback

- Both AVM clients now retry transient connection failures (same
  Faraday retry config as Tiingo) so a network blip doesn't burn one of
  the tight monthly budget's requests
- Partial index on properties(avm_provider, avm_last_synced_on) backing
  the daily sync job's filter and stalest-first ordering

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Key the AVM sync index on avm_last_synced_on

The daily job orders by avm_last_synced_on ASC NULLS FIRST across all
AVM-linked properties, so leading the partial index with avm_provider
prevented it from serving the sort. Rekeyed on avm_last_synced_on with
matching null ordering; the partial predicate still covers the filter.
Amended in place since the migration is unmerged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Map Realie numeric use codes; skip incomplete addresses in sync job

- Realie documents its numeric use codes at
  docs.realie.ai/api-reference/feature-key (earlier skip reason was
  wrong — the table exists). Map the residential/land/agricultural codes
  to Property subtypes, leaving codes without a subtype equivalent
  (e.g. 1006 mobile/manufactured) unset like the RentCast mapping.
- The daily job now skips properties whose address is missing a street
  or state before resolving a provider, logging a warn-level
  DebugLogEntry, so a malformed address can't burn a monthly-budget
  request every day.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Guard currency mismatches in refresh; reset AVM keys in test teardown

- The daily job now skips (with a warn-level DebugLogEntry) properties
  whose account currency no longer matches the provider's valuation
  currency, checked via the concept's valuation_currency before spending
  a request — writing a USD valuation into a re-currencied account would
  corrupt the balance
- Hostings controller test teardown now clears rentcast_api_key and
  realie_api_key alongside the other cached global settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Validate AVM lookup inputs before spending a provider request

The form marks name and address fields required, but a forged or
JS-less submission bypasses that and would burn one of the tight
monthly-budget requests on a lookup that can't produce a property.
Property::AvmImport now validates name and the full address locally
(localized error) before calling the provider.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add preview-and-confirm step to the AVM property lookup

RentCast's AVM endpoint is location-based: a plausible but nonexistent
address still geocodes and returns an area-derived estimate with no
property record behind it. Instead of silently creating an account from
that, the lookup now shows what the provider returned — type, year
built, area (each "Unknown" when missing), and the estimated value —
with an explicit notice when no property record was found, and only the
user's confirmation creates the account.

The confirm step reuses the fetched data via the form (sanitized
server-side), so the flow still costs exactly one provider request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Require a complete US address before daily valuation refresh

The job's guard only required street and state, but a later-blanked
city or ZIP silently disables the Realie wrong-city check (blank
entered fields count as "no mismatch"), so a refresh could accept
another property's valuation. The guard now mirrors the import-time
completeness check and also skips addresses edited to a non-US country.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Sign the AVM preview payload; blur valuation in privacy mode

- The confirm step now rebuilds everything from a signed, 1-hour
  message-verifier token generated at lookup time, proving the lookup
  (and its counted provider request) actually ran — a direct confirm
  POST with fabricated data can no longer create provider-linked
  properties that the daily refresh job would then spend quota on.
  Forged/expired tokens re-render the lookup form with a clear error.
- The preview's market value now carries the privacy-sensitive class so
  privacy mode blurs it like other account amounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Treat zero Realie valuations as absent; bind preview token to family

Review follow-ups from PR #2727:
- Realie returns modelValue: 0 when it can't produce an AVM estimate;
  zero now falls back to the assessed market value instead of syncing
  a $0 balance, and an all-zero record errors out.
- Monetary values now parse via BigDecimal(value.to_s) rather than
  Float#to_d, avoiding binary float rounding artifacts in balances.
- The signed AVM preview token's purpose is scoped to the family that
  ran the lookup, so a leaked token can't be replayed cross-family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Refresh AVM property valuations monthly instead of daily

Per @jjmata's review on #2727: valuations change slowly and providers
enforce tight monthly request caps, so a daily cron adds no value.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 04:47:58 +02:00
pro3958
8b2d792dab fix(api): scope holdings API to accessible accounts (#2706)
Api::V1::HoldingsController built both the index and show queries from
current_resource_owner.family.holdings, filtering only on account status.
A read-scoped API key could therefore read holdings (account id/name,
quantity, price, market value) for accounts owned by other family members
that were never shared with the key owner, and the account_id / account_ids
filters made targeted enumeration of those accounts trivial.

Scope the query through accounts.accessible_by(current_resource_owner) so
results are limited to accounts the token owner owns or has been granted
access to. Route both index and set_holding through the new
accessible_holdings scope. This matches Api::V1::BalancesController and
the web HoldingsController, which already scope the same way.

Fixes #2467

Co-authored-by: agentloop <agentloop@localhost>
2026-07-25 04:44:27 +02:00
William Wei Ming
f210d1ca4c Perf/accounts controller index optimization (#1926)
* fix Prism issue - assigned but unused variable plaid_item

* measurement to improve accounts_controller__index performance based on skylight

* fix FEEDBACK - Stop eager-loading full sync histories on accounts index

* fix FEEDBACK - Remove sticky memoization from SimplefinItem#accounts

* fix lint error in PR review

* fix FEEDBACK - Avoid returning map misses as authoritative results

* fix test error

* fix FEEDBACK - Avoid redundant query by using already-loaded @manual_accounts

* Address maintainer review on accounts index sync preloading
Memoize SimplefinItem#accounts, align syncing? with Sync#visible?,
and add fallback regression tests.

* Add composite index for sync DISTINCT ON queries
Support latest_by_syncable ordering with syncable_type, syncable_id,
created_at DESC, and id DESC.

* fix error in dockerfile.preview file

* Suppress Pipelock false positives on CI database fixtures.
Pipelock 2.7.0 full-repo audit flags ephemeral postgres:// URLs in
workflow env blocks. Re-apply inline suppressions lost in the main merge.

* Address review: guard partial Current sync maps and drop unrelated diff
Add key? checks so partially populated Current sync maps fall back to DB
queries. Revert Dockerfile.preview and pipelock.yml changes unrelated to
the accounts index N+1 optimization.

* fix(sync): fully populate Current sync maps for all preloaded syncables

* fix(ci): skip scheduled preview cleanup on forks
Only run the hourly Cloudflare preview cleanup on we-promise/sure,
where the required secrets exist.

* Remove schema.rb Postgres-version churn, keep only new syncs index

Reset db/schema.rb to upstream/main and re-add only the
index_syncs_on_syncable_and_created_at_and_id index. The prior diff
included ~30 lines of noise (check-constraint reformatting, virtual
column reformat, and column reorderings) caused by dumping under a
different PostgreSQL version, which obscured the single intended
schema change and invited merge conflicts.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 03:35:35 +02:00
Sure Admin (bot)
ec76ffe503 Fix preview cleanup over-firing on PR close (#2784)
* Fix preview cleanup close trigger

* Address preview cleanup review feedback

* Handle Cloudflare delete response shapes
2026-07-25 00:38:07 +02:00
Tomasz Toczek
12f0303bbb Add missing Polish translations to reach 100% coverage (#2780)
* Add 35 missing Polish translation files to reach 100% coverage

* fix: add Polish few/many plural forms across all locale files

* fix: translate period labels, remove konto(a) patterns, add plural forms for complete_account_setup

* fix: add missing few/many plural forms for goals needs_this_month_sub, on_track_sub_parts, pending_pledge.title
2026-07-24 22:53:34 +02:00
Max Barbare
51c93649da feat(snaptrade): replace device-flow OAuth with authorization-code + PKCE flow (#2747)
* feat(snaptrade): replace device-flow OAuth with authorization-code + PKCE flow

Squashed from 16 commits on snaptrade-oauth-apps for a clean rebase onto
current upstream/main ahead of opening a PR.

* fix(snaptrade): address PR #2747 review feedback on OAuth PKCE flow

- Remove unreachable dead-code guard in import_latest_snaptrade_data
- Guard apply_oauth_tokens! against a malformed payload missing access_token
- Wrap token endpoint network errors in ApiError and retry like data calls
- Remove unused Provider::Snaptrade#revoke_token! instance method
- Preserve return_to/accountable_type through the SnapTrade portal callback
  so the account-linking flow no longer drops users back to accounts_path
- Show the real absolute OAuth callback URL in self-hosted setup instructions
- Refresh brakeman.ignore fingerprint for the connect redirect after the
  return_to/accountable_type params were added

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y8SCCmKX6RphB5E73WSUQQ

* fix(snaptrade): don't retry non-idempotent OAuth/API requests

CodeRabbit flagged that Provider::Snaptrade retried OAuth token
exchanges/refreshes and all API POST/DELETE calls (get_connection_url,
delete_connection) after timeouts/connection failures. If the response
is lost after SnapTrade already consumed a single-use auth code,
rotated the refresh token, or applied a POST/DELETE, replaying the
request either fails with invalid_grant on a token that actually
succeeded, or risks duplicate side effects. Retries are now limited to
GET requests; OAuth token requests and non-GET API calls translate a
network failure straight into an ApiError without replay.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrrGkgSBEqhjjBmmH1fcXL

* fix(snaptrade): stop querying non-deterministically encrypted token via empty-string compare

CodeRabbit flagged that the syncable scope's where.not(oauth_access_token:
[nil, ""]) re-encrypts "" with a random IV on every query, so the ""
comparison can never match a stored ciphertext and is a silent no-op.
No code path ever persists oauth_access_token as "" (only nil or a real
token via apply_oauth_tokens!), so the exclusion is unnecessary --
narrowed the scope to a plain NULL check, which encryption handles
transparently since nil is never encrypted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrrGkgSBEqhjjBmmH1fcXL

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 22:45:44 +02:00
Juan José Mata
c6eb7cdeed Revert "Refactor application workflows and update test coverage"
This reverts commit 565e049f89.
2026-07-23 13:39:46 -07:00