* perf(accounts): kill sidebar/sparkline N+1s and cache the sidebar
The dashboard was issuing hundreds of per-account `SELECT 1` and
polymorphic `accountable` lookups on every page load. Sidebar render
alone hit the DB ~50–100× and ran twice per request (mobile + desktop).
Changes:
- AccountableSparklinesController: short-circuit
`requires_normalized_aggregation?` to Investment/Crypto only and
collapse the per-account `linked?` loop into a single `EXISTS`. Kills
the N+1 `AccountProvider Exists?` queries on every sparkline endpoint.
- BalanceSheet::AccountTotals#visible_accounts: preload `:accountable`,
`:plaid_account`, `:simplefin_account`, and
`account_providers: :provider` so the sidebar's
`account.subtype` / `account.linked?` / `account.provider` calls don't
trigger per-row polymorphic loads.
- AccountsController#index: same preloads on `@manual_accounts`.
- accounts/index/_account_groups.erb: extend the existing `Preloader`
call to batch-load accountable + provider associations so the
per-provider-item partials (Plaid, SimpleFIN, Coinbase, etc.) stop
re-issuing N+1s when rendering account rows on /accounts.
- accounts/_account_sidebar_tabs.html.erb: wrap the partial in a
`cache` block keyed on the family's data-version, the current user,
shares fingerprint, locale, mobile flag, active tab, and a
path-derived "current account" component (`sidebar_active_account_id`
helper). The sidebar is rendered on every page in the layout
(twice — mobile + desktop drawers), so most navigations now serve
the cached fragment instead of re-walking accounts/balances.
Local impact (DZG family, 23 accounts, 6.1k transactions):
- Dashboard `/`: ~6.5s → ~1.95s
- /accounts: ~2.7s → ~0.85s on warm cache
- /accountable_sparklines/*: per-request N+1s eliminated; remaining
cost is request boilerplate which can be addressed by bumping
`RAILS_MAX_THREADS` (the dashboard fans out 5 sparkline turbo frames
in parallel and Puma's default 3 threads serialize them).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(perf): address PR review on sidebar/sparkline perf changes
- AccountableSparklinesController#requires_normalized_aggregation?
also matches legacy plaid_account_id / simplefin_account_id links,
not just new-style account_providers, so investment/crypto accounts
in the legacy linking state still get LinkedInvestmentSeriesNormalizer
applied (Codex P1 / CodeRabbit major).
- Sidebar share fingerprint includes both `count` and `max(updated_at)`
so deleting a non-most-recent AccountShare invalidates the cached
fragment for users who lost access (Codex P1).
- Move the sidebar cache-key construction (incl. the AccountShare
query) from the ERB into a new `account_sidebar_tabs_cache_key`
helper, per the project's "no heavy logic in ERB" rule (CodeRabbit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(perf): address human review on perf PR
- Account.linked: new SQL-level scope mirroring `Account#linked?` so
the controller and per-instance method share one definition. Removes
the duplicated raw SQL string in
`AccountableSparklinesController#requires_normalized_aggregation?`,
which now reads `accounts.linked.exists?` (jjmata, sure-design).
- AccountsHelper: move `sidebar_active_account_id` and
`account_sidebar_tabs_cache_key` out of `ApplicationHelper`. The
cache-key helper also collapses the AccountShare `count` + `max(updated_at)`
fingerprint into a single `pick` query so we don't pay two round-trips
on every render (jjmata, sure-design).
- test/models/account/linkable_test.rb: pin the `Account.linked` scope
against all three link types (account_providers, legacy plaid_account,
legacy simplefin_account) so any future schema change that diverges
the SQL definition from `linked?` breaks a test instead of silently
serving wrong sparkline aggregations (sure-design).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(perf): correct shares cache fingerprint on raw-SQL pick
`pick(Arel.sql("count(*), max(updated_at)"))` passes a single comma-
separated fragment, which Rails returns as a String (per the documented
behavior of `pluck` with SQL fragments). The previous `max_at&.to_i`
silently truncated `"2025-05-06 12:34:56.789 UTC"` to `2025`, so the
sidebar cache key would not change for share `updated_at` movements
within the same calendar year — including share deletions — leaving
revoked users with a stale sidebar until the 12h expiry.
Pass the aggregates as two separate `Arel.sql` args and just concatenate
the raw String values into the cache key. The values only need to be
stable for a given DB state, not numerically meaningful.
Caught by CodeRabbit on PR #1683.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* third party provider scoping
* Simplify logic and allow only admins to mange providers
* Broadcast fixes
* FIX tests and build
* Fixes
* Reviews
* Scope merchants
* DRY fixes
Balance sheet totals and accountable type summaries used a SQL JOIN on
exchange_rates matching only today's date, which returned NULL (defaulting
to 1:1) when no rate existed for that exact date. This caused foreign
currency accounts to show incorrect totals.
Changes:
- Refactor BalanceSheet::AccountTotals to batch-fetch exchange rates via
ExchangeRate.rates_for, with provider fallback, instead of a SQL join
- Refactor Accountable.balance_money to use the same batch approach
- Add ExchangeRate.rates_for helper for deduplicated rate lookups
- Fix net worth chart query to fall back to the nearest future rate when
no historical rate exists for a given date
- Add composite index on accounts (family_id, status, accountable_type)
- Reuse nearest cached exchange rate within a 5-day lookback window
before calling the provider, preventing redundant API calls on
weekends and holidays when providers return prior-day rates
https://claude.ai/code/session_01GyssBJxQqdWnuYofQRjUu8
Co-authored-by: Claude <noreply@anthropic.com>
* Providers factory (#250)
* Implement providers factory
* Multiple providers sync support
- Proper Multi-Provider Syncing: When you click sync on an account with multiple providers (e.g., both Plaid and SimpleFin), all provider items are synced
- Better API: The existing account.providers method already returns all providers, and account.provider returns the first one for backward compatibility
- Correct Holdings Deletion Logic: Holdings can only be deleted if ALL providers allow it, preventing accidental deletions that would be recreated on next sync
TODO: validate this is the way we want to go? We would need to check holdings belong to which account, and then check provider allows deletion. More complex
- Database Constraints: The existing validations ensure an account can have at most one provider of each type (one PlaidAccount, one SimplefinAccount, etc.)
* Add generic provider_import_adapter
* Finish unified import strategy
* Update app/models/plaid_account.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: soky srm <sokysrm@gmail.com>
* Update app/models/provider/factory.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: soky srm <sokysrm@gmail.com>
* Fix account linked by plaid_id instead of external_id
* Parse numerics to BigDecimal
Parse numerics to BigDecimal before computing amount; guard nils.
Avoid String * String and float drift; also normalize date.
* Fix incorrect usage of assert_raises.
* Fix linter
* Fix processor test.
* Update current_balance_manager.rb
* Test fixes
* Fix plaid linked account test
* Add support for holding per account_provider
* Fix proper account access
Also fix account deletion for simpefin too
* FIX match tests for consistency
* Some more factory updates
* Fix account schema for multipe providers
Can do:
- Account #1 → PlaidAccount + SimplefinAccount (multiple different providers)
- Account #2 → PlaidAccount only
- Account #3 → SimplefinAccount only
Cannot do:
- Account #1 → PlaidAccount + PlaidAccount (duplicate provider type)
- PlaidAccount #123 → Account #1 + Account #2 (provider linked to multiple accounts)
* Fix account setup
- An account CAN have multiple providers (the schema shows account_providers with unique index on [account_id, provider_type])
- Each provider should maintain its own separate entries
- We should NOT update one provider's entry when another provider syncs
* Fix linter and guard migration
* FIX linter issues.
* Fixes
- Remove duplicated index
- Pass account_provider_id
- Guard holdings call to avoid NoMethodError
* Update schema and provider import fix
* Plaid doesn't allow holdings deletion
* Use ClimateControl for proper env setup
* No need for this in .git
---------
Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Update pages/dashboard locales (#255)
* Localization for an accountable group and sidebar
- Added localization for account types
- Updated account localization for en and ca
---------
Signed-off-by: soky srm <sokysrm@gmail.com>
Co-authored-by: soky srm <sokysrm@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Balance sheet cache layer with cache-busting
* Update family cache timestamps during Sync
* Less blocking sync loaders
* Consolidate family data caching key logic
* Fix turbo stream broadcasts
* Remove dev delay
* Add back account group sorting