#2356 build the accountable inside Account#subtype= so a top-level subtype
survives create. That fix assumes accountable_type is already set when
subtype= runs, but the real controller path violates it: strong-params
permit preserves filter order, and account_params lists :subtype before
:accountable_type. So on create subtype= runs while accountable_type (and
accountable_class) is still blank, the build is skipped, and the chosen
subtype is silently dropped — the account renders with the type's fallback
label (e.g. a Depository shows 'Cash' instead of 'Savings').
The existing regression test only covered the accountable_type-first order,
so it never caught this.
Make the writer order-independent: when subtype arrives before the type,
stash it and apply it from an accountable_type= override once the type is
known. Add regression tests for the permit order and for create_and_sync.
Follow-up to #2405. Replace remaining API controller reads of
Current.user, Current.family, and Current.session with
current_resource_owner. Add an architecture guard test to prevent
regression.
Scope is limited to the Current sweep only:
- Revert balance_sheet user-scoping (moves to account-auth PR B).
- Revert provider_connections DebugLogEntry logging (separate PR).
- Remove UsersController#destroy attempt to destroy unsaved API session.
* Fix QIF import for dd mmm yyyy dates (#2498)
American Express QIF exports use dd mmm yyyy D-fields (e.g. D26 Jan 2026).
Import previously failed with "Unable to detect date format" for two reasons:
1. QifParser.normalize_qif_date stripped all internal whitespace, turning
26 Jan 2026 into the unparseable 26Jan2026. For month-name dates the
space IS the separator, so collapse multiples to a single space instead
of removing them. Numeric dates keep the existing strip-all behavior.
2. Family::DATE_FORMATS had no candidate mapping to dd mmm yyyy, so
detect_date_format could not match it. Add "%d %b %Y" (DD MMM YYYY).
Extend the 2-digit-year expansion separator class to include space so
month-name dates with 2-digit years also normalize (26 Jan 26 -> 26 Jan 2026).
Covered by normalize/parse/detect and Amex-style row-generation regression
tests in qif_import_test.rb.
* test(qif): cover month-name 2-digit year normalization & parse
Addresses CodeRabbit nitpick on #2500: the production change extends
the 2-digit-year expansion regex separator class to include a space
so month-name dates like "26 Jan 26" normalize to "26 Jan 2026".
Add normalize_qif_date and parse_qif_date regressions for that branch.
* Add SnapTrade OAuth device flow
* Add SnapTrade OAuth device flow
### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.
### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.
### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.
* Add SnapTrade OAuth device flow
### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.
### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.
### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.
* Add SnapTrade OAuth device flow
### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.
### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.
### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.
* Add SnapTrade OAuth device flow
### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.
### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.
### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.
* Add SnapTrade OAuth device flow (provider, model, controller, routes, migration, tests)
### Motivation
- Add support for SnapTrade OAuth 2.0 device authorization flow so users can authorize SnapTrade via device codes in addition to the existing portal flow.
- Persist OAuth token metadata on `SnaptradeItem` for subsequent polling and usage by the provider and UI.
### Description
- Implemented OAuth device flow in the provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token`, plus HTTP helper methods `oauth_connection`, `oauth_client_id`, and `parse_oauth_response` in `Provider::Snaptrade`.
- Added controller endpoints `start_oauth_device_flow` and `complete_oauth_device_flow` in `SnaptradeItemsController` with robust error handling and helpers `oauth_error_payload` and `parse_oauth_error_body` to surface OAuth error fields.
- Extended `SnaptradeItem` with encrypted columns for `oauth_access_token`, `oauth_refresh_token`, token metadata, `oauth_token_active?`, and model methods `start_oauth_device_flow` and `complete_oauth_device_flow!` to store token metadata.
- Added migration `AddOauthDeviceFlowToSnaptradeItems` and updated `db/schema.rb` to include the new columns, registered a new initializer `config/initializers/snaptrade.rb` to read `SNAPTRADE_OAUTH_CLIENT_ID`, and updated `.env*.example` files to document `SNAPTRADE_OAUTH_CLIENT_ID`.
- Exposed new routes `start_oauth_device_flow` and `complete_oauth_device_flow` for `snaptrade_items`.
### Testing
- Added unit tests for provider OAuth behavior in `test/models/provider/snaptrade_oauth_test.rb`, model token persistence in `test/models/snaptrade_item_oauth_test.rb`, and controller error propagation in `test/controllers/snaptrade_items_controller_test.rb`.
- Ran the test suite with `bin/rails test` and the full test run (including the new SnapTrade OAuth tests) passed.
* Add SnapTrade OAuth device flow (start/poll), store tokens, and tests
### Motivation
- Add support for SnapTrade OAuth Device Authorization flow so administrators can start device authorization and poll for tokens without exposing provider internals.
- Persist OAuth token metadata on `SnaptradeItem` so the app can reuse and surface token state for SnapTrade integrations.
- Improve error handling and sanitization for OAuth API errors returned by SnapTrade to avoid leaking upstream internals.
### Description
- Introduces new environment examples and initializer: adds `SNAPTRADE_OAUTH_CLIENT_ID` to `.env.local.example`/`.env.test.example` and configures `Rails.configuration.x.snaptrade.oauth_client_id` in `config/initializers/snaptrade.rb`.
- Extends `Provider::Snaptrade` with OAuth device flow support, adding discovery URL, device grant constant, Faraday `oauth_connection`, and methods `oauth_authorization_server_metadata`, `start_device_authorization`, `poll_device_token`, `oauth_client_id`, and `parse_oauth_response`, plus improved retry and API error wrapping.
- Adds DB migration and schema changes to store OAuth fields on `snaptrade_items` (`oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, `oauth_token_expires_at`) and marks those attributes encrypted when ActiveRecord encryption is enabled.
- Adds `SnaptradeItem` helpers `start_oauth_device_flow`, `complete_oauth_device_flow!`, and `oauth_token_active?` to start/poll and persist token metadata.
- Adds controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` with sanitized error responses and helper methods `oauth_error_payload` and `parse_oauth_error_body`, and wires new member routes in `config/routes.rb`.
- Updates `SnaptradeItemsController` before_action lists to include the new actions and adds user-facing error message helper `start_oauth_device_flow_error_message`.
### Testing
- Added unit tests `test/models/provider/snaptrade_oauth_test.rb` to validate discovery, device authorization request, token polling, missing client ID handling, and OAuth error propagation, and all assertions passed.
- Added `test/models/snaptrade_item_oauth_test.rb` to assert `complete_oauth_device_flow!` stores tokens and expiry and that `oauth_token_active?` reports correctly, and the test passed.
- Extended `test/controllers/snaptrade_items_controller_test.rb` with controller-level tests confirming sanitized OAuth error payloads and behavior for start/complete endpoints, and these controller tests passed.
* Add SnapTrade OAuth device flow support and token storage
### Motivation
- Add support for the OAuth 2.0 device authorization flow for SnapTrade so users can link brokerages via a device-code flow without traditional browser-based client redirects.
- Persist OAuth token metadata on the SnaptradeItem so tokens can be reused and expiration tracked.
- Surface and sanitize provider error payloads to callers while avoiding leakage of internal configuration details.
### Description
- Added new DB columns and a migration (`oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, `oauth_token_expires_at`) and updated `db/schema.rb` to reflect the change.
- Encrypted the new token fields on `SnaptradeItem` and added `oauth_token_active?`, `start_oauth_device_flow`, and `complete_oauth_device_flow!` helpers to the model.
- Implemented OAuth logic in `Provider::Snaptrade` including discovery (`OAUTH_DISCOVERY_URL`), `start_device_authorization`, `poll_device_token`, request parsing, retry handling, and a small Faraday connection wrapper; added configuration accessors for `oauth_client_id` via `config.x.snaptrade`.
- Added controller endpoints `start_oauth_device_flow` and `complete_oauth_device_flow` with safe error handling and helpers to format OAuth error payloads, and registered new member routes for `snaptrade_items`.
- Added an initializer to load `SNAPTRADE_OAUTH_CLIENT_ID` into `Rails.configuration.x.snaptrade`, and updated `.env.local.example` and `.env.test.example` to include the new env var.
- Added unit tests for provider OAuth behavior (`test/models/provider/snaptrade_oauth_test.rb`), SnaptradeItem token persistence (`test/models/snaptrade_item_oauth_test.rb`), and controller error handling (`test/controllers/snaptrade_items_controller_test.rb`), and updated controller tests accordingly.
### Testing
- Ran provider-level tests in `Provider::SnaptradeOauthTest` to validate discovery, device authorization, token polling, and error handling, which passed.
- Ran model tests in `SnaptradeItemOauthTest` to verify token metadata is stored and `oauth_token_active?` works, which passed.
- Ran controller tests in `SnaptradeItemsControllerTest` that exercise the start/complete endpoints and error sanitization, which passed.
* Add SnapTrade OAuth device-flow support and token storage
### Motivation
- Add support for SnapTrade OAuth device authorization so users can perform device-code based OAuth without embedding secrets in the browser.
- Persist OAuth token metadata on `SnaptradeItem` and expose programmatic start/complete endpoints while avoiding leaking provider internals on errors.
- Make OAuth client ID configurable via environment or credentials for Sure deployments.
### Description
- Introduce `SNAPTRADE_OAUTH_CLIENT_ID` to `.env` examples and add `config.x.snaptrade.oauth_client_id` initializer for configuration.
- Add migration `AddOauthDeviceFlowToSnaptradeItems` and update `schema.rb` to add `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at` to `snaptrade_items`.
- Persist and encrypt new token fields in `SnaptradeItem` and add helper methods `oauth_token_active?`, `start_oauth_device_flow`, and `complete_oauth_device_flow!`.
- Extend `Provider::Snaptrade` with OAuth discovery, device authorization (`start_device_authorization`), token polling (`poll_device_token`), Faraday-based `oauth_connection`, parsing and error-wrapping logic, and retry handling.
- Add controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` to `SnaptradeItemsController`, plus helper methods to format OAuth error payloads and user-facing error messages.
- Wire up new routes (`post :start_oauth_device_flow`, `post :complete_oauth_device_flow`) and add `config/initializers/snaptrade.rb`.
- Add comprehensive tests for the OAuth flow and controller error handling and update a system test stub to avoid provider leakage during UI tests.
### Testing
- Added `Provider::SnaptradeOauthTest` which stubs discovery, device authorization and token endpoints and asserts request payloads and responses; tests passed.
- Added `SnaptradeItemOauthTest` which verifies token metadata persistence and `oauth_token_active?`; test passed.
- Extended `SnaptradeItemsControllerTest` with scenarios for successful and failing device-flow completion and start flow error handling; tests passed.
- Ran the affected system test changes in `TradesTest` (provider stubbing and modal behavior); the updated tests passed.
* fix(sync): store EnableBanking credit card debt as balance instead of available credit
Previously, the EnableBanking processor forcibly overrode the primary account balance for credit cards to be the available credit (credit_limit - debt) rather than the actual outstanding debt. Since credit cards are modeled as Liability accounts, this caused the balance sheet (net worth) to treat available credit mathematically as a debt.
This PR aligns the EnableBanking processor with the Plaid and SimpleFIN processors by storing the absolute debt as the account balance, while tracking the available credit via accountable metadata.
Fixes#2458
* docs(sync): update EnableBanking credit card processor documentation
Updates the inline processor comments to reflect the new behavior introduced by the previous commit, clarifying that outstanding debt is stored sequentially as the primary balance rather than the UX available credit overriding it.
* refactor(sync): clarify balance and available credit calculations in EnableBanking processor
Refactors the debt polarity assignments to clarify why liability balances are strictly parsed as absolute positive numbers. Replaces the implicit ordering dependency between the '.abs' conversion and the 'available_credit' math with an explicit 'outstanding_debt' variable to prevent regression by future maintainers.
* style: remove trailing whitespace in EnableBanking processor
Adds a toggle to mark accounts as excluded from all financial reports
while keeping them active and visible individually.
- Migration: add exclude_from_reports boolean column to accounts
- Model: included_in_reports scope
- BalanceSheet: filter excluded from ClassificationGroup and AccountGroup totals,
HistoricalAccountScope, AccountRow flag
- IncomeStatement: exclude via SQL fragments in Totals, FamilyStats, CategoryStats
- InvestmentStatement/Budget: chain included_in_reports scope
- ReportsController: filter in breakdown view, export queries, trades
- AccountsController: toggle_exclude_from_reports action + route
- UI: DS::Toggle in account form, eye-off indicator in sidebar, menu toggle items
- Locale: all labels in en.yml
- Tests: model scope, controller toggle, income statement/balance sheet filtering
- 5070 tests pass (0 failures, 0 regressions)
* feat(api): allow multiple active API keys per user
Previously a user could hold only one active API key; creating a new one
silently revoked the existing key, breaking any app using it. Allow
multiple named active keys instead.
- Drop the one-active-key-per-source validation; add name uniqueness
among the user's active visible keys (revoked names are reusable,
same name allowed across users).
- Rewrite Settings::ApiKeysController as a RESTful collection
(index/show/new/create/destroy); create no longer revokes existing
keys, and key lookup is scoped to the current user's active keys.
- resource :api_key -> resources :api_keys.
- Add an API keys list view with per-key revoke and an empty state;
rewrite the show page for a single key.
- Update i18n (remove single-key copy, pluralise nav label) and tests.
* refactor(api): address review on multiple API keys
- Remove unreachable destroy branches (cannot_revoke is guarded by the
.visible 404; revoke! raises rather than returning false) and document
that .visible is the demo-key revocation guard.
- Delete the orphaned created.html.erb / created.turbo_stream.erb
templates (no action renders them) and their unused locale keys.
- Extract shared partials (_scope_badges, _status_indicator, _key_meta,
_key_reveal, _usage) to de-duplicate the index and show views; unify
the active-status indicator on the standard dot.
- Carry forward the @container / @lg:flex-row / min-w-0 responsive
fixes from #2079 into the shared key-reveal partial.
* test(api): cover newly-created API key confirmation render
* refactor(api): harden demo-key guard and address review nits
- Document the demo-key revocation guard on ApiKey's `visible` scope (the
authoritative spot) and add a model test locking the invariant that
`.visible` excludes the demo monitoring key.
- _scope_badges: use an i18n lookup with a humanize fallback instead of
bypassing translation for unknown scopes.
- _key_reveal: drop the hard-coded `id` from the shared partial; the
system test now locates the key via its data-clipboard-target.
* refactor(api-keys): migrate hand-rolled badges to DS::Pill
Replace raw span elements in scope badges and status indicator
with DS::Pill to align with the design system migration convention.
* fix(loans): opening anchor now uses current balance, not original principal
When creating a loan manually, the opening anchor valuation was being
set to `initial_balance` (the original loan principal) instead of
`account.balance` (the current outstanding balance). After the sync
job ran, `account.balance` was overwritten to match the anchor,
making every manually-created loan show its original principal as
the current balance.
Fix by always using `account.balance` for the opening anchor in
`create_and_sync`, and reading `Loan#original_balance` from the
`loans.initial_balance` column directly (with a fallback to
`first_valuation_amount` for provider-synced loans that may not
have the column populated).
* fix(api-keys): strip accidental loan changes; rescue revoke! failures
The fix(loans) commit was accidentally committed into this branch.
Remove the loan-related changes from account.rb, loan.rb, and both
test files, restoring them to their pre-loan-commit state.
Also fix the destroy action: revoke! uses update! internally which
raises ActiveRecord::RecordInvalid on failure rather than returning
false. Add rescue for RecordInvalid and RecordNotDestroyed so failures
produce a flash alert instead of a 500. Re-adds the revoke_failed
locale key that was dropped from settings.api_keys.destroy.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* feat(layout): make the accounts and assistant sidebars resizable
Add draggable dividers on the left (accounts) and right (assistant)
sidebars. Width is driven by a `--{side}-sidebar-width` CSS variable on
the layout root and persisted per-device in localStorage; it composes
with the existing show/hide toggle (collapse still wins via `w-0`,
reopening restores the last width).
Clamping keeps each sidebar within its bounds (min 240px, max 480/560px)
and guarantees the center column never drops below 400px, so the
dashboard stays legible no matter how wide a sidebar is dragged.
- sidebar_resize_controller.js: pointer drag, keyboard nudge
(arrows / Home), double-click reset, localStorage persistence
- utils/sidebar_resize.js: pure, unit-tested clamp helper
- layouts/shared/_sidebar_resize_handle.html.erb: accessible
separator handle (role=separator, aria-label, focusable)
- test/javascript/utils/sidebar_resize_test.mjs: clamp logic tests
* fix(sidebar-resize): free a collapsed sidebar's space and guard localStorage read
- #clamp now uses the opposite sidebar's rendered width (0 when collapsed)
instead of its stored CSS variable, so closing one sidebar lets the other
use the freed space (addresses Codex review).
- #applyStoredWidth wraps the localStorage read in try/catch and falls back
to the default, so connect() can't bail in storage-blocked browsers
(addresses CodeRabbit review).
* fix: use ES256 instead of EdDSA for Coinbase CDP JWT signing
Coinbase Developer Platform issues EC P-256 keys, not Ed25519.
The previous EdDSA implementation would always return Unauthorized
for any key generated via portal.cdp.coinbase.com.
Switch to ES256 (ECDSA SHA-256) using OpenSSL, and convert the
DER-encoded signature to the raw r||s format required by the JWT spec.
API Secret field now accepts the full PEM private key directly
(including BEGIN/END headers) as provided by Coinbase.
* fix(coinbase): address PR review comments
- Normalize escaped \n in PEM key before parsing (fixes pasting directly
from the Coinbase CDP JSON download file where newlines are \n literals)
- Extract parse_ec_private_key helper with docstring explaining both
accepted PEM formats
- Fix double-space style nit on encoded_header assignment
- Remove ed25519 gem from Gemfile (no longer used after ES256 migration)
- Add Provider::CoinbaseTest covering: JWT 3-part structure, ES256 alg
header, kid claim, CDP payload claims, 64-byte raw r||s signature,
escaped-newline key acceptance, and cryptographic signature verification
* test(coinbase): fix base64url padding in JWT test assertions
* chore: update Gemfile.lock to remove ed25519 gem
* Sample credential in test
* Comments field, not real key use
* test(coinbase): generate EC key dynamically; drop scanner exclusions
Generate a fresh P-256 key per test run with OpenSSL::PKey::EC.generate
instead of hardcoding a PEM private key. This removes committed key
material and the need to exclude the provider and test files from the
pipelock secret scan, so those exclusions are removed too.
Addresses review findings on hardcoded test key + scan exclusions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(chat): surface and recover from undelivered assistant responses
When the background worker that runs AssistantResponseJob is down or not
polling the high_priority queue, the eager "pending" assistant message had no
safeguard: chat hung on "Thinking…" forever with no error, timeout, or retry,
and the LLM provider was never even called.
Add three layers of resilience:
- Client watchdog (chat_controller.js): a pending "Thinking…" bubble that waits
past a threshold (default 90s) with no response asks the server to fail it.
Keyed on the pending marker — which disappears the instant a real response
streams — so a slow-but-working response is never falsely timed out.
- Server failure capture (Chat#handle_undelivered_response! +
MessagesController#report_timeout): clears the dead bubble, records a friendly
"the assistant didn't respond" error with Retry, and writes a DebugLogEntry so
support can see it in /settings/debug.
- Worker liveness signal (BackgroundJobHealth + warning banner): reads Sidekiq's
process/queue state directly from Redis in the web process — so a down worker
is detectable even though the worker is the thing that's broken — and warns
self-hosted users when no worker polls high_priority or it's badly backed up.
Fails open so a Sidekiq/Redis blip never blocks chat.
Tests: Chat model (3), MessagesController (2), BackgroundJobHealth (5).
* fix(chat): address review — server-side timeout guard + watchdog retry safety
- Chat#handle_undelivered_response!: gate the state change behind a row lock and
a server-side minimum age (UNDELIVERED_RESPONSE_TIMEOUT = 60s). The browser
watchdog is untrusted, so re-read the row under lock and only fail a bubble
that is still pending AND has genuinely waited past the timeout — never racing
a worker that is finishing a slow response. (Codex P2 + CodeRabbit)
- chat_controller.js: only mark a report URL as reported on response.ok (fetch
resolves on HTTP 4xx/5xx, rejecting only on network errors), and guard
concurrent duplicate POSTs with an in-flight set, so a failed report retries
instead of stranding the bubble. (CodeRabbit)
- _worker_health_warning: drop the unused `chat:` local + its render arg. (CodeRabbit)
* feat(mcp): add get/create/update tools for tags and categories
* test(mcp): add tests for tag and category assistant functions
* fix(mcp): harden category/tag assistant functions against bad tool input
* refactor(mcp): use UuidFormat.valid? instead of local UUID_PATTERN
* chore(mcp): remove account_filter component files that don't belong in this branch
* feat(mobile): add SureFontWeights design tokens
Introduce named font-weight tokens so widgets reference a DS tier instead
of a raw FontWeight, mirroring the web design system's Tailwind weight
utilities (font-medium / font-semibold).
- design/tokens/sure.tokens.json: add font.weight.medium (500) and
.semibold (600) as the canonical source.
- generate_sure_tokens.mjs: emit SureTokens.weightMedium / weightSemibold
(with a font-weight resolver + validation), regenerate sure_tokens.dart.
- sure_tokens_test.dart: assert the generated weights match canonical.
- Migrate the dashboard-area money/hierarchy sites onto the tokens
(net_worth_card, account_card, dashboard, recent_transactions,
transactions_list): FontWeight.w500 -> SureTokens.weightMedium,
FontWeight.w600 -> SureTokens.weightSemibold.
Weight-only; other screens adopt the tokens in their own polish PRs.
166 tests pass; flutter analyze: no new issues; token generator --check
is clean.
* build(tokens): regenerate web _generated.css for new font weights
Adding font.weight.medium/semibold to sure.tokens.json also flows through
the web token generator (bin/tokens.mjs), which emits --font-weight-medium
and --font-weight-semibold. Commit the regenerated _generated.css so the
tokens:check gate stays green (node bin/tokens.mjs + git diff --quiet).
* test(system): de-flake account edit via account-menu
AccountsTest#assert_account_created opened the account menu and clicked
Edit immediately after visiting the account page. That page issues a
Turbo morph refresh shortly after load (turbo_refreshes_with :morph
reacting to a family-stream broadcast), which can detach the menu node
mid-click — "Selenium::WebDriver::Error: Node with given id does not
belong to the document" — or wipe the just-opened edit form. Capybara
does not auto-retry that inspector error, so the test flaked
intermittently in CI.
Extract open_account_edit_dialog, which retries the menu interaction
until the edit form is present, rescuing the transient detach/stale
errors. Mirrors the existing PropertyTest#open_account_edit_dialog
precedent for the same morph race, but also tolerates the click itself
raising while the refresh is in flight.
* fix(tinkoff): resolve the tradeable listing and price bonds via BondBy
Validated against the live T-Invest API and fixed three correctness issues in
Provider::TinkoffInvest:
- FindInstrument returns several listings per ticker (e.g. SBER on TQBR plus
dark/non-API boards 37M/SPEQ); only the apiTradeAvailableFlag listing has live
prices. resolve_short now ranks tradeable-first (then requested-MIC, then exact
ticker/ISIN) instead of taking the first match, so GetLastPrices/GetCandles
return data. Search now surfaces only tradeable instruments.
- find_instruments no longer hard-filters on apiTradeAvailableFlag, so a
qualified-investor instrument Tinkoff lists but can't API-trade still resolves
(for logos) and otherwise falls back to another price provider.
- Bond nominal comes from BondBy (the generic GetInstrumentBy omits it),
returning the current amortized nominal so percent-of-par converts correctly.
Verified end-to-end: SBER 313.65, T 282.76, LQDT 2.019, and the SFO Split bond
(amortized nominal 417.71) all price; logos resolve to real CDN PNGs.
* fix(tinkoff): honor requested MIC first, guard amortizing bonds, strip suffix
Address review feedback on the resolution/pricing path:
- resolve_short now ranks a requested-MIC match BEFORE tradeability, so a
security is never priced off another exchange's listing (e.g. a MISX security
no longer picks up a tradeable XSPX board's price); tradeability and exact
ticker/ISIN remain secondary tie-breaks.
- Amortizing bonds: BondBy returns only the current nominal, so applying it to
historical percent-of-par closes would underprice them. For amortizing bonds
we now return only the live price and skip the candle history; fixed-par bonds
and equities keep full history. bond_info exposes nominal + amortizationFlag.
- Strip exchange suffixes (.ME/.MOEX/.MISX/.MCX) before querying T-Invest, which
only knows the bare SECID — so a stored "T.MOEX" ticker resolves to "T".
* fix(tinkoff): don't cache nil resolution results (skip_nil) so a transient empty response isn't locked in for the 24h TTL
* feat(mobile): add SureCard primitive and migrate account cards
SureCard mirrors the web card chrome — `bg-container` + a hairline border +
rounded corners + the subtle DS shadow (`shadowXs`, from the scale shipped in
#2349) — resolved from the active SureColors palette so it stays in lockstep with
`sure.tokens.json` in light and dark. An optional `onTap` gives a flat ink
response clipped to the card radius.
Migrates AccountCard off the Material `Card` to `SureCard`.
Refs #2235.
* fix(coinstats): deterministic wallet batch order in bulk fetches
The bulk balance/transaction fetches built the "blockchain:address" param in
linked-account query order, which isn't stable across runs — so the batched
param (and its mocked expectations) varied seed to seed, intermittently failing
CoinstatsItem::ImporterTest and red-flagging unrelated PRs. Sort the wallets
before joining so the batch order is deterministic, and align the test
expectations to the sorted order.
* fix(mobile): tokenize swipe-reveal radius + cover SureCard dark theme
Address PR review:
- The account-card swipe-reveal background still used a hardcoded radius (12)
that mismatched SureCard's radiusLg (10) once the card was migrated — tokenize
it so the reveal corners line up during the swipe.
- Parameterize the SureCard chrome test over light + dark, since the card is
brightness-aware (catches palette regressions in either mode).
* test(mobile): assert SureCard onTap ink is clipped to the card radius
Address review nit: the onTap test claimed the ink is clipped to the card but
only checked the InkWell exists. Now it asserts the InkWell's borderRadius equals
SureTokens.radiusLg (tester.widget already enforces a single InkWell).
* feat(up): add Up Bank (AU) provider integration
Adds Up Bank as a per-family, token-based bank sync provider, modelled on
the existing Akahu integration. Up uses a JSON:API REST API with a personal
access token (Bearer), cursor pagination via links.next, and returns both
HELD (pending) and SETTLED transactions from one endpoint.
New:
- Provider::Up client (JSON:API unwrap, links.next pagination, retries,
typed errors, /util/ping) + Provider::UpAdapter (Factory-registered,
Depository + Loan).
- UpItem / UpAccount models with Provided, Unlinking, Syncer,
SyncCompleteEvent, Importer, Processor, Transactions::Processor, and
UpEntry::Processor (amount sign flip, HELD->pending, foreignAmount FX,
merchant from description, stale-pending pruning).
- Family::UpConnectable, UpItemsController, routes, settings panel + connect
flow views, accounts index wiring, initializer, en locale, and model tests.
Core wiring:
- "up" added to Transaction::PENDING_PROVIDERS, the three pending-match SQL
blocks in Account::ProviderImportAdapter, Provider::Metadata::REGISTRY,
ProviderMerchant/DataEnrichment source enums, ProviderConnectionStatus,
settings provider panels, and financial data reset.
Migration create_up_items_and_accounts must be run before use. No external
API endpoints added (no OpenAPI changes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(up): dump up tables to schema and make since filter TZ-safe
The feature commit added the up_items/up_accounts migration but never
re-dumped db/schema.rb, leaving the schema version and tables stale.
Add the two table definitions and foreign keys and bump the schema
version so a fresh DB load matches the migration.
Also format a bare Date `since` as UTC midnight instead of the server's
local zone, so `filter[since]` is deterministic regardless of where the
app runs (previously shifted by the local UTC offset).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(up): address code review feedback
Behavior/correctness:
- Persist skipped accounts via a new up_accounts.ignored flag and a
needs_setup scope, so skipped accounts stop resurfacing as "needs
setup" on every sync. Linking clears the flag.
- destroy now checks unlink_all! per-account results and aborts deletion
(alert) if any unlink failed, instead of swallowing failures.
- render_provider_panel_error redirect uses :see_other (was an invalid
4xx redirect status).
- Up provider adapter falls back to item institution name/url when
institution_metadata is absent (early return previously blocked it).
Resilience/security:
- fetch_all_resources guards against an API repeating the same
links.next cursor (Set#add?), preventing infinite pagination.
- HTTP client validates absolute URLs (from links.next) against Up's
HTTPS host before sending the bearer token, preventing credential
leakage to untrusted hosts.
Diagnostics:
- Route provider sync/import failures through DebugLogEntry.capture
(controller, UpItem, syncer, unlinking) with family/account context.
Low-level HTTP client and currency-normalization warnings keep
Rails.logger to match existing provider conventions.
Data integrity:
- up_accounts.name and currency are NOT NULL (align with model presence
validations); account_id stays nullable (allow_nil uniqueness).
Forms:
- select_existing_account radio is required; controller guards a blank/
unknown up_account_id with a friendly alert instead of RecordNotFound.
Tests:
- Add UpAccount needs_setup scope test, pagination loop guard test,
untrusted-host rejection test; tighten filter[since] assertion to the
exact UTC timestamp.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(up): address second-round review feedback
- Capture sync/import failures via DebugLogEntry so swallowed errors in
account/transaction fetching and transaction processing surface in
/settings/debug instead of only Rails.logger.
- Gate UP_DEBUG_RAW raw payload dump to local envs to avoid leaking PII
(merchant names, amounts, account IDs) in managed/production logs.
- Collapse linked/unlinked/total account counts into one memoized query
instead of 3 separate COUNTs per rendered item.
- Rename "Set Up Up Accounts" locale title to "Link Up Accounts".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(up): add method docstrings and align failed_result keys
Add docstrings to all Up provider source files (controller, models,
providers, concerns) to satisfy the 80% docstring coverage threshold.
Third-round review: failed_result now mirrors import's result shape
(accounts_updated/created/failed, transactions_imported/failed) instead
of the stale accounts_imported key, so failure results stay consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Build fresh API session contexts instead of reusing persisted web sessions that may carry impersonation state.
Reject deactivated report export API key owners and strengthen regression coverage for API key, OAuth, and report export authentication paths.
Adds Provider::TinkoffInvest, a token-based securities provider built on the
public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves
prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand
logos via the T-Invest CDN — the authoritative logo source for MOEX
instruments, which ISS (MoexPublic) does not provide.
- Registry: register `tinkoff_invest` under the :securities concept; token via
ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key.
- Logos independent of the price provider: Security#import_brand_logo consults
T-Invest for a logo whenever a token is configured (after the price-provider
metadata fetch, so it never short-circuits website_url backfill). Gated on
token presence, not the securities checklist.
- display_logo_url: with no website domain, a stored provider logo (T-Invest)
now beats the ticker-only Brandfetch lettermark; when a domain exists,
Brandfetch still wins (unchanged).
- MoexPublic no longer reports moex.com as the issuer website — it's the
exchange, not the issuer, and would make Brandfetch render the exchange logo
for every instrument and shadow the real brand logo.
- Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9;
bonds priced as percent-of-par x nominal (missing nominal raises, not 0).
- Settings: encrypted token field (always shown) + provider checkbox + en locale.
- Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic.
Co-authored-by: Claude <noreply@anthropic.com>
* perf(reports): avoid residual category lazy loads
* test(reports): reuse SQL query capture helper
Move the reports SQL capture regression helper into test/support and document the category loading and budget reload choices called out in review.
* perf(dashboard): streamline investment activity totals
* fix(dashboard): skip empty investment totals cache writes
Short-circuit empty investment account totals before cache fetch and move the new SQL query capture regression helper into test/support.
* refactor(dashboard): address review on investment totals query
- Drop defensive ArgumentError guards in InvestmentStatement::Totals#initialize.
The sole caller (totals_query) always passes correct types, and the
empty_result early-return already handles the zero-accounts case.
- Remove the redundant accounts JOIN and its family_id/status filters from the
aggregation SQL. account_ids is already scoped to the family's visible
(draft/active) investment accounts, so the join back to accounts is
unnecessary work in the query plan. Drop the now-unused family_id param.
- Add a regression assertion that the aggregate no longer joins accounts.
* feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider
Add Provider::MoexPublic, a keyless provider built on the free MOEX ISS API
(https://iss.moex.com/iss), modeled on Provider::BinancePublic.
Securities: shares, funds/ETF/БПИФ (e.g. LQDT), and bonds (OFZ + corporate).
Bonds are priced clean — LAST% × FACEVALUE / 100 in the instrument currency,
with per-row FACEVALUE for amortizing issues; NKD/accrued coupon excluded.
Exchange rates: also implements ExchangeRateConcept for RUB↔{USD,EUR,CNY} via
selt TOM instruments (USD000UTSTOM/EUR_RUB__TOM/CNYRUB_TOM); the selt quote is
X/RUB, inverted for RUB→X, nil for non-RUB-crossed pairs.
Details:
- Board/engine resolution via the ISS primary-board flag with a hardcoded
priority fallback (TQBR, TQTF, TQOB, TQCB, …).
- Instrument currency from CURRENCYID/FACEUNIT (handles USD/CNY eurobonds &
FX funds), normalizing legacy SUR/RUR → RUB; default RUB.
- Full history via from/till + start= pagination; current price fallback chain
LAST → MARKETPRICE → LCURRENTPRICE → LCLOSEPRICE → PREVPRICE → latest history
close.
- Bare SECID identity, exchange_operating_mic=MISX, country_code=nil (wildcard
like Binance); search accepts .ME/.MOEX/.MISX/.MCX aliases and ISIN.
- RateLimitable throttling, SslConfigurable, Faraday retry/timeouts; all public
methods wrapped in with_provider_response.
Wired into Provider::Registry for both :securities and :exchange_rates, the
hosting provider-selection UI, locales, and config/exchanges.yml (MISX).
Docker-tested (devcontainer, Ruby 3.4.9): 29 new tests green, full provider
suite + i18n green, rubocop clean; smoke-tested against live ISS (SBER price,
OFZ clean price, USD/RUB FX).
* fix(moex): address review — FX weekend lookback, dead branch, translated hints
- fetch_exchange_rate now fetches a 10-day lookback window (not just the exact
day) so a weekend/holiday request resolves to the prior trading day's close,
matching Yahoo's behavior (Codex P2).
- Remove dead identical if/else branches in history_row_price (CodeRabbit).
- Translate moex_public_hint into ca/fr/hu/vi/zh-CN instead of English copy
(CodeRabbit).
- Add a test covering the FX prior-trading-day lookback.
* fix(moex): guard ISS date parsing; doc TQTE in board priority
Address maintainer review (jjmata):
- parse_iss_date wraps Date.parse so a malformed ISS TRADEDATE skips just that
row (with a contextual log warning) instead of failing the whole history/FX
fetch. Used in history_row_price and fx_history.
- Add TQTE to the BOARD_PRIORITY doc comment (it was in the constant but missing
from the comment).
- Add a test covering the unparseable-date skip.
* fix(prices): resolve dashed crypto tickers (BTC-USD, TRX-USD) via BinancePublic
Provider::BinancePublic is the dedicated crypto price provider, but its
search_securities only matched bare base assets ("BTC") or unseparated
Binance pairs ("BTCUSDT"/"BTCUSD"). The canonical "<BASE>-USD" form that
Yahoo emits and users paste (e.g. "TRX-USD", "USDT-USD") never matched, so
those holdings fell through to an unpriced offline security whenever the
stock provider didn't also return the coin.
Collapse the base/quote separator in the search query (and strip it in
parse_ticker) so "BTC-USD" / "TRX/USDT" are treated like "BTCUSD" and
resolve to live Binance pricing. Stablecoins pasted as "USDT-USD" resolve
to the synthetic USD price via their stripped base.
* fix(prices): only synthesize stablecoin search for USD quote
Address review: gate the synthetic stablecoin result on a USD quote (bare
coin or "<coin>USD" form). A non-USD quote like "USDTEUR" / "USDT-EUR"
has a real Binance pair and now falls through to normal matching instead of
being replaced by the USD synthetic. Drops the now-unused base_asset_query
helper. Adds a USDT-EUR regression test.
Import::UploadsController#update persists CSV uploads with
save!(validate: false), which skips the before_validation
:ensure_utf8_encoding callback. Non-UTF-8 files (e.g. ISO-8859-1 /
Windows-1252 exports from Brazilian banks) were written to the text
column as-is and rejected by Postgres with PG::CharacterNotInRepertoire,
surfacing as a 500 during upload.
Also register ensure_utf8_encoding on before_save so the existing
normalization (rchardet detection + Latin-1/Windows fallback) still runs
when validations are skipped. The callback is idempotent and no-ops on
valid UTF-8, so the validated path is unchanged.
Fixes#2294
The Account#subtype= writer delegates to accountable&.subtype=, which is a
silent no-op while the accountable is nil. On create the accountable is built
from accountable_attributes via accepts_nested_attributes_for, but the form
submits subtype as a top-level account attribute. Mass-assignment applies
subtype before accountable_attributes, so the selected subtype was dropped on
create (update worked because the accountable already exists).
Build the accountable from the delegated type inside the writer when it is not
yet present, so the value is preserved; the later accountable_attributes
assignment (update_only) updates the same record.
Add regression tests covering the create flow and the assignment ordering.
Adds YnabImport (mirroring ActualImport) for YNAB "Export budget" register CSVs:
- Amount — combines the split Outflow/Inflow columns into a single signed amount
(inflow - |outflow|), stripping currency symbols and thousands separators. A
single signed Amount column takes precedence when present.
- Category — resolves across export shapes: the combined "Category Group/Category"
column, the split "Category Group" + "Category", or legacy YNAB 4
"Master Category" + "Sub Category".
- Names — falls back from a blank Payee to the Memo, then the default row name.
- Validation — requires at least one amount source (Outflow/Inflow or Amount); a
file exposing none leaves rows un-clean instead of importing zero-dollar entries.
Enables the previously-disabled YNAB option on the imports screen (using the YNAB
logo, like Mint) with its configuration partial, and removes the now-dead
imports.new.coming_soon locale key. Documents the type in the API import-type
enums (rswag request spec + swagger_helper + generated openapi.yaml).
Closes#1255.
* feat(security): warn when ActiveRecord encryption is not configured
Self-hosted instances without explicit ACTIVE_RECORD_ENCRYPTION_* keys (or Rails credentials) store sensitive columns - API keys, provider/bank tokens, the MFA (TOTP) secret, and PII - unencrypted at rest. The app boots and works normally so this plaintext at rest state is easy to miss.
Change: Make it visible:
- log a clear startup warning (config/initializers/encryption_warning.rb)
- show a warning banner on /settings/security when encryption is unconfigured
* refactor(security): apply review feedback on encryption warning
- list the three ACTIVE_RECORD_ENCRYPTION_* keys in the banner, rendered via the DS::Alert content block to match the log
- drop the redundant respond_to?(:self_hosted?) guard in the initializer so it matches the controller check
- add a managed-mode test asserting the banner is hidden
When a rule is re-applied from the UI, RulesController passes
ignore_attribute_locks: true, but Enrichable#enrich_attributes still
rejected locked attributes unconditionally, so locked (manually edited
or import-locked) transactions were silently skipped and reported as
blocked.
Thread the flag through enrich_attribute/enrich_attributes as a new
ignore_locks keyword (default false, so provider syncs and AI
enrichment keep respecting locks) and pass it from the six synchronous
rule action executors.
Fixes#2051
* fix(jobs): enqueue jobs after transaction commit to fix SyncJob deserialization race
Syncable#sync_later creates a Sync row and enqueues SyncJob inside the
same transaction. Rails 7.2 deferred such enqueues until commit by
default; Rails 8.0 changed the default to enqueue immediately and 8.1
left the global config toggle non-functional, so a worker could dequeue
the job before COMMIT, fail to resolve the Sync GlobalID
(ActiveJob::DeserializationError), and have it silently dropped by
discard_on -- surfacing as stuck syncs after the Rails 8.1 upgrade.
Set enqueue_after_transaction_commit = true on ApplicationJob (the
Rails 8.2 default, inherited by every job) and drop the dead :never
symbol override on DestroyJob. Add regression and invariant tests.
* test(jobs): use OpenStruct for DestroyJob failure case
Replace the bare mock and the respond_to? expectation (an implementation
detail of how DestroyJob probes the model) with an OpenStruct that genuinely
responds to scheduled_for_deletion. Keeps only the command-facing assertions:
destroy raises and update! is called with scheduled_for_deletion: false.
Matches the repo convention of preferring OpenStruct for mock instances.
* test(snaptrade): assert connection-cleanup enqueue defers until commit
SnaptradeAccount#after_destroy enqueues SnaptradeConnectionCleanupJob, which
references the item/account by id. Before the ApplicationJob fix, Rails 8.1
enqueued it immediately inside the destroy transaction, so a worker could run
before COMMIT, see the not-yet-deleted row in its shared-authorization guard,
skip the provider call, and leak the SnapTrade connection. This regression
test destroys an account inside a transaction and asserts the job is not
enqueued until the transaction commits.
* feat(dashboard): masonry packing + per-widget size controls
In two-column mode the dashboard used a row-based CSS grid, so cards
stretched to equal row height and left dead space (e.g. the Net Worth
chart padded out to match the tall Balance Sheet table). Replace the
row-based layout with masonry packing and add per-widget size guardrails.
- Masonry: CSS grid with computed row-spans + grid-auto-flow: dense, driven
by a dashboard-masonry Stimulus controller (ResizeObserver + turbo:frame-load).
The DOM stays a single flat list, so drag/keyboard reorder is unaffected.
Active only in multi-column mode; single column falls back to normal flow.
- Internal sizing: the net worth chart height is now driven by a
--dash-widget-h CSS var (fixes an inert flex-1) so the card no longer pads
out below the chart.
- Guardrails: per-widget layout metadata (col_span, grow, min_height,
width_toggle) in PagesController, with per-user overrides persisted under
preferences["dashboard_section_layout"], deep-merged so width and height coexist.
- Size menu: a hover control on size-capable cards — Width (Half/Full) for the
cashflow sankey and net worth chart, Height (Compact/Auto/Tall) for grow
widgets. The sankey defaults to full width.
Adds model + controller tests for preference persistence and i18n keys.
* refactor(dashboard): redesign size menu with segmented controls
The size menu used a plain radio list and a diagonal maximize-2 trigger that
collided with the cashflow sankey's modal-expand button. Replace it with a
layout-config popover: a sliders-horizontal trigger plus two labeled axis
groups (Width, Height), each rendered as a DS::SegmentedControl with the
active option filled. Clearer, more compact, and it reads as a card-layout
control. The widget-size controller now mirrors the segmented control's
active-class + aria-pressed contract.
* feat(dashboard): expose width toggle on balance sheet and investments
Tables benefit from horizontal room, so give Balance Sheet and Investments
the same Width (Half/Full) control as the sankey and net worth chart. Height
presets stay chart-only — a table sized to a fixed height would just add
whitespace or force scrolling. The Outflows donut is intentionally left out
(full width is mostly whitespace for a donut).
* fix(dashboard): address review feedback on size controls
- Only apply the full-width col-span and show the Width control when the
two-column layout is enabled. A full widget previously leaked
2xl:col-span-2 into the single-column grid, creating an implicit second
column at 2xl widths and breaking the single-column preference. This also
keeps the Width control coherent with the Appearance two-column setting
(it now appears only where it does something). [Codex]
- Stop size-menu keydowns from bubbling to the section reorder handler, so
keyboard users can open the menu and pick options without entering
grab/reorder mode. [Codex]
- Harden preferences params: ignore a malformed (non-hash)
dashboard_section_layout / collapsed_sections instead of raising a 500,
and require section_order to be an array. [CodeRabbit]
- Localize the dashboard sections aria-label. [CodeRabbit]
* chore(settings): mention per-widget size controls in two-column copy
Surface the new per-widget width/height controls in the Appearance
"Two-column layout" description so the capability is discoverable.
* test(dashboard): assert non-mutation for malformed layout input
Addresses review feedback: asserting assert_nil made the test depend on
the fixture happening to have no dashboard height for net_worth_chart.
Capture the pre-PATCH value and assert it is unchanged, so the test
stays valid (malformed input ignored) even if the fixture later gets a
default height.
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* test: cover CoinStats missing-wallet balance preservation
* fix: preserve CoinStats balances when wallet data is missing
* chore: route CoinStats sync warnings to debug log
* docs: make debug log guidance timeless
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Commit 5d0eb7f4 replaced the color picker's DS::Disclosure with a raw
<details> because DS::Disclosure wraps its body in an mt-2 div, and that
normal-flow margin shoved the form down ~8px whenever the absolutely-
positioned picker popover opened. DS Drift Patrol flagged the raw
<details> in #2272 and #2316.
Add a body_class: option to DS::Disclosure (default mt-2) so callers can
drop the body margin, and migrate the color picker back onto the
component with body_class: nil. The summary's accessible name moves from
aria-label to an sr-only span in the summary content (verified: the
summary still reads as "Choose color and icon").
* fix(chat): clear the assistant message bubble when a turn is destroyed
When an assistant turn fails before any text streams (e.g. a provider
auth/model/network error on the first call), Assistant::Builtin#respond_to
destroys the still-pending message. Message only broadcast on create and
update, never on destroy, so the rendered 'Thinking…' bubble was never
removed — the chat appeared stuck thinking forever even though the job
had already errored (and appended an error via chat#add_error below it).
Add after_destroy_commit broadcast_remove_to so a destroyed message is
removed from the page.
* refactor(chat): trim destroy-broadcast comment to one line
Project convention asks for comments only when the why is non-obvious; the
behaviour is already covered by the commit/PR description. Per review feedback.
---------
Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
* chore(deps): upgrade Rails 7.2 → 8.1
Rails 7.2 reaches end of life on 2026-08-09. Bump the framework to the
current 8.1.x line.
- Gemfile: rails "~> 8.0" (resolves 8.1.3); bundle update rails pulls the
Rails 8 framework gems plus the bumps it requires — ViewComponent
3.23 → 4.x (Rails 8 support), rails-i18n 7 → 8, rswag, and transitive deps.
- app/models/transfer.rb: make Transfer#date nil-safe
(inflow_transaction&.entry&.date). Rails 8's date_field evaluates the
field default on a new/unpersisted Transfer (the new-transfer form), where
the association is nil; without this, TransfersController#new raises
"undefined method 'entry' for nil". Matches the &. pattern already used in
Transfer#sync_account_later.
Framework behavioral defaults are unchanged (config.load_defaults stays as-is).
Validated on Rails 8.1.3: zeitwerk:check passes, full suite green
(4904 runs, 0 failures, 0 errors), rubocop and brakeman clean.
* fix(rails8): style textarea + deterministic property edit system test
The Rails 8 gem bump kept config.load_defaults at 7.2, but Rails 8 renamed
two ActionView::Helpers::FormBuilder field helpers regardless of defaults:
:text_area → :textarea and :check_box → :checkbox. StyledFormBuilder builds
its styled helpers from `field_helpers`, so `form.text_area` (e.g. the
account "Notes" field) silently fell through to the unstyled base helper and
rendered without a label — failing 8 system tests with
`Unable to find field "Notes"`.
- app/helpers/styled_form_builder.rb: exclude both spellings of the
non-text helpers (:check_box and :checkbox) and alias the legacy
`text_area` to the Rails 8 `textarea` so existing call sites stay styled.
Harmless on Rails 7.2 (old names present instead).
- test/system/property_test.rb: open the property edit dialog via the
account menu with a retry. The account page issues a Turbo morph refresh
shortly after load (turbo_refreshes_with :morph + a family-stream
broadcast); opening the modal while that refresh is in flight let the
morph re-render the page and wipe the just-loaded #modal turbo-frame.
Rails 8 timing made the race deterministic. Retrying once the refresh has
settled makes the test stable (confirmed via Turbo frame-load vs
full-page morph event traces; 3x green in isolation).
- config/brakeman.ignore: the added comment block shifted the pre-existing
(already-ignored, Weak) class_eval Dangerous Eval warning from line 5 -> 10,
changing its fingerprint. Re-point the existing suppression to the new
fingerprint/line so scan_ruby stays green.
Validated on Rails 8.1.3: full system suite green
(92 runs, 355 assertions, 0 failures, 0 errors), rubocop clean,
brakeman 0 warnings, CodeRabbit no findings.
* chore(deps): pin rails to the 8.1 minor line (~> 8.1.0)
Tighten the constraint from `~> 8.0` to `~> 8.1.0` (>= 8.1.0, < 8.2) so a
future `bundle update rails` tracks the 8.1.x line rather than silently
jumping to 8.2 when it ships. Matches the upgrade plan's stated intent
(target 8.1.x for the EOL runway) and a review note on #2301.
No resolved-version changes: bundle install keeps rails at 8.1.3 and every
other locked gem unchanged — only the Gemfile.lock DEPENDENCIES constraint
line moves. zeitwerk:check still passes; the already-green unit/system
suites ran on this exact resolved tree.
* chore(rails8): adopt Rails 8.1 framework defaults (config.load_defaults 8.1)
The gem bump above kept config.load_defaults at 7.2 so the change set could be
reasoned about in stages; this finalizes the upgrade by adopting the modern
framework defaults now that the suite is green on Rails 8.1.
Rails 8.0 added no new framework defaults (there is no new_framework_defaults_8_0
template), so 7.2 -> 8.1 is the single meaningful step. No incremental
new_framework_defaults_8_1.rb opt-in file is needed: the full suites pass with all
8.1 defaults enabled at once.
The 8.1 defaults this turns on include action_on_path_relative_redirect=:raise
(open-redirect hardening), raise_on_missing_required_finder_order_columns,
escape_json_responses=false / escape_js_separators_in_json=false (JSON perf), and
Ruby-parser template-dependency tracking.
Validated with no application code changes: bin/rails test 4904/0/0,
bin/rails test:system 92/0/0, rubocop + brakeman clean.
* chore(ci): restore brakeman CheckEOLRails now that the app is on Rails 8.1
config/brakeman.yml existed only to skip brakeman's CheckEOLRails. That check
fires on the calendar (it warns 60 days before a framework's EOL and escalates
as the date nears), so Rails 7.2's 2026-08-09 EOL turned `bin/brakeman` red
(exit 3) on every branch and on main regardless of the diff. The skip carried a
TODO to remove it once Sure upgraded off 7.2.
This PR puts the app on Rails 8.1 (EOL well in the future), so the skip is
obsolete; remove the file (its sole content was the skip) in the same change that
makes it unnecessary -- no stale-config window. brakeman auto-loads the file when
present and falls back to defaults when absent, and nothing references it
explicitly. CheckEOLRuby was already enabled and is unchanged; config/brakeman.ignore
is untouched.
Validated on Rails 8.1: bin/brakeman runs EOLRails + EOLRuby, 0 warnings,
0 errors, exit 0.
* feat(mobile): standardize money typography and semantic amount color
Add a brightness-aware SureColors theme extension and a MoneyText/SureMoney
primitive (semantic success/destructive/subdued tokens + tabular figures for
column-aligned digits), then migrate the transaction lists and balance cards
off raw Colors.green/red/grey.
Step 2 of the mobile design-system sequence (#2235), after #2237's theme
foundation. Primitive-first: screens consume shared tokens/typography.
* fix: review feedback — brightness-aware token fallback, de-flake Setting tests, Pipelock localhost FP
- SureColors.of falls back to the palette matching the active brightness (not
always light) when the extension is missing, so dark surfaces stay correct.
- Clear the rails-settings-cached cache before each test; its in-memory cache
survives the per-test transaction rollback, leaking Setting.* across tests and
flaking Settings::HostingsControllerTest (stale empty string vs nil).
Full unit suite: 4952 runs, 0 failures.
- Suppress the localhost test-DB DATABASE_URL false positive with line-level
`# pipelock:ignore` in ci.yml + llm-evals.yml instead of excluding whole files,
so those workflows stay scanned for real secrets.
* Update trades api with support for additional types
* rubocop fixes
* fix missing amount validation for interest type
* define missing schema reference
* fix test api_headers to use display_key per guidelines
* expand test coverage
* replaced duplicate JSON response blocks with helper method
* Add DB assertions to linked transfer test and fix invalid date test
* update brakeman.ignore fingerpint for refactored code
* Update the Brakeman ignore note to document validation for newly permitted keys
* fix API key auth in Minitest test to follow correct pattern
* update required in trades rswag spec to match the minimum fields that apply to all types
* extract dividend handling from build_investment_trade_params to dedicated method
* adjust response format to use the existing jbuilder views for Transfers and Transactions
* normalize type before passing to create form
* validate amount as a positive numeric value + tests
* rubocop fixes
* Add missing Trades API test coverage and docs
- Add Minitest tests for withdrawal (422, transfer linking), interest
(explicit ticker), and dividend update
- Add rswag 401/403/404/422 response docs for create, update, destroy
- Regenerate docs/api/openapi.yaml
* Update Security.find line reference in brakeman.ignore note
* Mark TransactionResponse account_type as nullable in rswag docs
* feat(cashflow): deep-link category labels to filtered transactions
Clicking a category's text label in the dashboard cashflow Sankey chart
now navigates to the transactions page filtered by that category and the
cashflow's active period date range. The colored node bar keeps its
existing zoom-into-subcategories behavior; structural nodes (Cash Flow,
Surplus) do not navigate.
URL-building lives in a pure, unit-tested utils/transactions_filter_url
module (mirroring utils/sankey_zoom) pinned in the importmap. Period dates
are threaded from the dashboard view into the Stimulus controller via data
values. This matches the existing donut chart's click-to-filter behavior.
* Update app/javascript/controllers/sankey_chart_controller.js
Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
Signed-off-by: Will Wilson <will@willwilson.uk>
---------
Signed-off-by: Will Wilson <will@willwilson.uk>
Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
PlaidAccount::TypeMappable maps the Plaid loan subtypes "home equity",
"line of credit", and "business" to home_equity, line_of_credit, and
business — but Loan::SUBTYPES never defined them. Linking any such
account (e.g. a HELOC reported by the institution as loan/"line of
credit") makes the item's sync fail with:
Validation failed: Accountable subtype is not included in the list
and, because Link itself succeeded, the failure is silent in the UI
(same UX gap as #1792).
Add the three subtypes to Loan::SUBTYPES, and add a regression test
asserting every subtype emitted by TYPE_MAPPING is valid for its
accountable so the mapper and models can't drift apart again.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(sharing): scope import account selects to accessible_by (#1803)
Three CSV / QIF account selects in import/uploads/show.html.erb and one
PDF-import account select in imports/_pdf_import.html.erb pulled their
options from `@import.family.accounts`. That listed every account in
the family — including the family admin's unshared personal accounts —
in the dropdown shown to any member running an import. Swap each call
site to `Current.user.accessible_accounts` (owned + explicitly shared
accounts only), matching the existing scoping used by the dashboard
sidebar, transactions controller, transfers controller, etc.
Adds a regression test that signs in as family_member and asserts the
unshared-account names from the dylan_family fixtures never appear in
the rendered upload page.
* test(import): scope leak assertions to account select (#2194 CodeRabbit)
CodeRabbit nitpick: assert_match on response.body could pass/fail on
text outside the account dropdown (sidebar, breadcrumb, error message,
etc.) and gave false confidence in the refute_match exclusions. Switch
to assert_select 'select[name="import[account_id]"] option', text: …
so the assertions only see the option nodes the leak test actually
cares about.
* test(import): cover PDF account-select scoping; pluck PDF partial (#2194 review)
jjmata: the _pdf_import.html.erb scoping change was not covered by the
existing test (which only hit /import/uploads). Add a regression test
hitting GET /imports/:id with a PdfImport fixture, asserting the
account dropdown options match accessible accounts only.
Also swap the PDF partial's accounts.map { |a| [a.name, a.id] } for
.pluck(:name, :id) to match the .pluck pattern the other three CSV/QIF
selects already use.
* test(import): stub pdf_uploaded? on PDF leak test (#2194 ci)
The new regression test hit ImportsController#show which redirects to
the upload page when @import.pdf_uploaded? is false. The pdf_with_rows
fixture has neither a pdf_file attached nor a statement, so the
redirect fired before the partial under test ever rendered, failing
with 302 in CI. Stub PdfImport#pdf_uploaded? to true so the test
exercises the account-select scoping path it was written to cover.
* fix(import): scope PDF form to :import so field names match (#2194 ci)
The PDF-import account-select form was `form_with model: import` with
no explicit scope. Because the model is a PdfImport, Rails derived the
param namespace from the class name, so the rendered field was named
`pdf_import[account_id]` — not `import[account_id]`. The
ImportsController#update action accepts either via
`params.dig(:pdf_import, :account_id) || params.dig(:import,
:account_id)` so live submissions still worked, but the regression
test added in 0685bbdf asserted on `select[name="import[account_id]"]`
and matched zero options.
Add `scope: :import` to align the rendered name with both the test
selector and the convention used by the CSV/QIF forms on the upload
page (which all use `scope: :import`).