mirror of
https://github.com/we-promise/sure.git
synced 2026-08-04 08:02:15 +00:00
1e800e2f930ba6b2d7ddbc0c94a948db56042da8
3105 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1e800e2f93 | Include uncategorized spending in budget UI (#2877) | ||
|
|
2bdaa7a171 | Bump version after v0.7.3 release (#2879) | ||
|
|
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> |
||
|
|
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> |
||
|
|
078c49b35b | Stabilize net worth breakdown series test dates (#2863) | ||
|
|
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 |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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 |
||
|
|
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. |
||
|
|
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> |
||
|
|
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 |
||
|
|
c9a28e1aa4 | Bump versions | ||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
3d40d93bba | fix(schema): restore enable banking account columns | ||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
c6eb7cdeed |
Revert "Refactor application workflows and update test coverage"
This reverts commit
|