Commit Graph

2566 Commits

Author SHA1 Message Date
Tristan the Katana
04b0122dbf feat(mobile): add suggested questions to empty chat screen (#1773)
* feat(mobile): add suggested questions to empty chat screen

- New constants file (lib/constants/suggested_questions.dart) for the 4
  suggested question chips, kept separate from screen logic with a clear
  l10n upgrade path noted in comments
- Empty chat screen now shows a personalised greeting and tappable
  OutlinedButton chips; tapping one pre-fills and sends the message
- Optimistic message insertion in ChatProvider.sendMessage so the user
  message and typing indicator appear instantly on tap, with rollback on
  failure
- Full AI response revealed only once polling detects stable content
  (2 consecutive polls with no growth), preventing partial responses
  from flashing on screen
- fetchChat stops any in-progress polling before fetching so a manual
  refresh always shows the authoritative server response
- Fixed updateChatTitle silently wiping messages when the title-update
  API response omits the messages array

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

* fix(mobile): address PR review comments

- Extract _rollbackOptimisticMessage helper to eliminate duplicated
  rollback logic in sendMessage failure and catch branches
- Replace raw 'Error: ${e.toString()}' user-facing strings with a
  generic message; retain technical details via debugPrint in each
  catch block
- Replace inline ternary in updateChatTitle with explicit if/else for
  readability while preserving message-preservation behaviour
- Fix non-reactive AuthProvider read inside Consumer<ChatProvider>
  builder (listen: false → listen: true) so greeting updates when
  user's firstName changes

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 19:57:13 +02:00
plind
12d799e0b8 fix(binance): support CRYPTO: prefix and USD stablecoins (#1771)
* fix(binance): support CRYPTO: prefix and USD stablecoins

Holdings processors (CoinStats, Coinbase, Kraken, SimpleFIN, Lunchflow,
Binance) store crypto securities with a "CRYPTO:" prefix, but
Provider::BinancePublic#parse_ticker only accepted Binance-search-style
tickers like "BTCUSD". As a result, every fetched price for tickers
like CRYPTO:USDT, CRYPTO:USDC, CRYPTO:SOL, CRYPTO:TRUMP, CRYPTO:KAITO
failed with "Unsupported Binance ticker".

- Strip the CRYPTO: prefix in parse_ticker.
- Short-circuit USD-pegged stablecoins (USDT, USDC, BUSD, DAI, FDUSD,
  TUSD, USDP, PYUSD) to a synthetic flat 1.0 USD price. Binance has no
  self-pair (USDTUSDT is invalid), and the few stablecoin/USDT pairs
  that do exist hover at ~1.0 with sub-cent noise.
- Default prefixed bare base assets (CRYPTO:SOL etc.) to the …USDT
  pair (USD). Only when prefixed, so unprefixed garbage like BTCBNB /
  BTCGBP still returns nil and the existing rejection tests still pass.
- fetch_security_info returns links: nil for stablecoins rather than a
  broken /trade/ URL.

Closes #1441.

* fix(binance): strip CRYPTO: prefix in search_securities

Security::Resolver calls search_provider with the raw holdings-processor
symbol (CRYPTO:SOL, CRYPTO:USDT) before any price fetch. Without prefix
handling here, first-time crypto imports never resolve to an online
Binance security and the new stablecoin/prefix paths in parse_ticker
were unreachable for that flow.

- Strip CRYPTO: from the search query.
- Short-circuit USD stablecoins to a synthetic search result (no
  exchangeInfo call, no Binance self-pair to find).
- Teach parse_ticker the "{stablecoin}USD" form produced by the
  synthetic result so price fetches route to stablecoin_prices.

---------

Co-authored-by: plind-junior <plind-junior@users.noreply.github.com>
2026-05-12 19:41:58 +02:00
Juan José Mata
fdffcd0dfd Add libvips to devcontainer Dockerfile
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
2026-05-12 16:08:31 +02:00
Juan José Mata
73b6077ac3 Constrain Lunchflow base URL to trusted endpoint (#1768)
* Constrain Lunchflow base URL to trusted endpoint

Prevent SSRF by ignoring user-provided Lunchflow base_url values unless they match the canonical Lunchflow HTTPS endpoint. Add model tests covering invalid host/scheme and valid canonicalization behavior.

* Linter
2026-05-12 12:18:17 +02:00
Juan José Mata
5ceb55be03 Scope SnapTrade orphan cleanup to current family (#1769)
* Scope SnapTrade orphan cleanup to current family

Restrict orphaned user listing and deletion to SnapTrade user IDs that belong to the current family namespace. Add model tests to prevent cross-family enumeration/deletion regressions.

* Update test/models/snaptrade_item_test.rb

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Juan José Mata <jjmata@jjmata.com>

* test: fix snaptrade orphaned users assertion

* style: fix snaptrade test array spacing

---------

Signed-off-by: Juan José Mata <jjmata@jjmata.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
2026-05-12 12:17:00 +02:00
Sure Admin (bot)
d943e32b15 fix: correct SnapTrade cash activity signs (#1634)
* fix: correct snaptrade cash activity signs

* test: update snaptrade withdrawal sign expectation

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
2026-05-12 00:55:46 +02:00
ghost
c1678181f0 fix(imports): import raw balance records (#1724)
* fix(imports): import raw balance records

* fix(imports): preserve partial balance components
2026-05-12 00:41:05 +02:00
Guillem Arias Fauste
7c06fe6296 feat(recurring): allow marking transfers as recurring (#895) (#1589)
Refs #895, discussion #1224.

Adds a "Mark as recurring" entry point on the transfer detail drawer
that creates a `RecurringTransaction` carrying both source and
destination accounts. The recurring index, settings toggle
(`recurring_transactions_disabled`), and projected upcoming feed all
light up automatically once the data shape is there.

Schema:

* `destination_account_id` nullable FK to accounts. `on_delete: :cascade`
  matches #20251030172500's precedent for accounts FKs. The existing
  `account_id` FK is widened to cascade in the same migration so
  Family destruction with a recurring transfer doesn't FK-violate.
* Two predicate-partitioned partial unique indexes per shape:
  non-transfer rows (`destination_account_id IS NULL`, original
  5-column shape preserved) and transfer rows (6-column shape
  including the destination). Postgres treats NULLs as distinct in
  unique indexes, so widening would have broken non-transfer dedupe.
* Two CHECK constraints enforcing transfer invariants in PostgreSQL:
  `chk_recurring_txns_transfer_requires_source` (destination implies
  source) and `chk_recurring_txns_transfer_distinct_accounts`
  (destination cannot equal source). Per CLAUDE.md "Enforce null
  checks, unique indexes, and simple validations in the database
  schema for PostgreSQL".
* `Account` gains an `inbound_recurring_transfers` inverse so the
  destroy chain reaches both ends.

Controller / behaviour:

* `transfers#mark_as_recurring` mirrors `transactions#mark_as_recurring`:
  i18n flashes (4 new keys: transfer_marked_as_recurring,
  transfer_already_exists, transfer_creation_failed,
  transfer_feature_disabled), `respond_to format.html`,
  `redirect_back_or_to transactions_path`, server-side gate on
  `recurring_transactions_disabled?`, and rescue both `RecordInvalid`
  and `RecordNotUnique` for the race window between the dedupe
  `find_by` and `create_from_transfer`. The `StandardError` rescue
  now logs the exception (class, message, transfer/family/user ids)
  before surfacing the generic flash so production failures aren't
  context-less.
* `RecurringTransaction.accessible_by(user)` now requires
  destination_account_id (when present) to be in the user's
  accessible set, so a recurring transfer never leaks to a user
  without access to BOTH endpoints.
* Model validation gains a `destination_account.blank?` branch in
  `transfer_endpoints_consistent` so a dangling
  `destination_account_id` (referenced row destroyed) surfaces as a
  normal validation error instead of an FK exception on save.
* `Identifier` filter for transfer-kind transactions moved into SQL.

UI:

* Recurring index table and projected feed render transfer rows with
  the existing letter-avatar and the row's `name` field
  ("Transfer to {destination}"). No special pill or icon -- every row
  in `/recurring_transactions` is recurring by definition. Amount
  column on transfers uses `text-secondary` (muted-but-live) instead
  of the income/expense colour, since transfers are zero-net for the
  family.

Out of scope (called out in the PR body):

* Auto-creation of future Transfer rows on a schedule
  (discussion #1224's primary ask). Behaviour change vs the
  current projection-only model.
* Auto-identification of recurring transfer pairs in `Identifier`.
* Frequency model richer than `expected_day_of_month`.
* `Cleaner` for recurring transfers (issue #1590 tracks this).

Tests:

* `RecurringTransaction#transfer?` predicate (with / without
  destination).
* `transfer_endpoints_consistent`: rejects same source and
  destination, rejects dangling destination_account_id, rejects
  cross-family destination.
* `RecurringTransaction.create_from_transfer` happy path;
  multi-currency variant stores source-side currency.
* `projected_entry` exposes source / destination on transfer rows.
* `Identifier` skips transfer-kind transactions; creates a pattern
  from expense halves while ignoring co-resident transfer halves.
* Destroying the destination account cascades to inbound recurring
  transfers (FK + AR association).
* Unique partial index still de-duplicates non-transfer rows after
  the destination_account_id widening.
* `transfers#mark_as_recurring` happy path, idempotent on second
  call, rejected when `recurring_transactions_disabled`.

Suite: 3261 / 0 / 0 / 24 on the latest upstream/main. Lint clean.
Brakeman clean.

Signed-off-by: Guillem Arias Fauste <gariasf@proton.me>
2026-05-12 00:37:47 +02:00
ghost
be598aecf0 feat(providers): add Kraken exchange sync (#1759)
* feat(providers): add Kraken exchange sync

Adds family-scoped Kraken API-key connections, read-only balance and trade import, account setup/linking flows, provider status wiring, and focused test coverage.

Closes #1758

* test(providers): avoid Kraken sample secret false positive

* fix(providers): address Kraken review findings

* fix(providers): address Kraken review cleanup

* test(imports): stabilize transaction import ordering
2026-05-12 00:22:37 +02:00
CrossDrain
33bc6b59c8 fix(enable-banking): import transactions missing transaction_id and entry_reference (#1767)
* fix(enable-banking): handle transactions missing transaction_id and entry_reference

Some ASPSPs omit both transaction_id and entry_reference from their transaction payloads, which is valid per the PSD2/Berlin Group spec. Previously, every such transaction raised an ArgumentError and was silently dropped during sync.

compute_external_id now falls back to a deterministic MD5 fingerprint (prefixed enable_banking_content_) derived from date, amount, currency, direction, counterparty, and remittance info. This fingerprint is stable across re-syncs, so duplicate imports are still correctly prevented. An ArgumentError is only raised for truly empty/unidentifiable payloads.

The importer is updated in three places to use compute_external_id
consistently: the pending pre-filter (before combining with booked),
the C4 stored-pending cleanup, and the new_transactions dedup. This means ID-less pending entries are now also removed when their settled booked counterpart arrives.

Tests cover compute_external_id directly (all 5 cases), end-to-end
fingerprint import, idempotency, and importer storage/dedup behaviour for ID-less transactions including the pending→booked settlement path.

* fix(enable-banking): implement dual-strategy matching for transaction settlement

When a stored pending row had only entry_reference (no transaction_id) and
the settled BOOK row arrived with a new transaction_id, compute_external_id
produced different fingerprints for each side (enable_banking_<ref> vs
enable_banking_<txn_id>). The fingerprint-only comparison introduced in the
previous commit never matched, leaving the stale pending entry in
raw_transactions_payload. Both rows were then imported as separate visible
transactions.

Restore a book_entry_refs set alongside book_fingerprints in both the
pending pre-filter and the C4 stored-pending cleanup. A pending entry is
now removed when either its fingerprint or its entry_reference matches a
booked counterpart — covering same-ID settlement, content-fingerprint
settlement, and the entry_reference cross-match settlement path.

Also updates the ArgumentError message in external_id to accurately
reflect that transaction_id, entry_reference, and content fingerprint
are all accepted identifiers, and aligns build_transaction_content_key
to use transaction_date as a fallback (matching compute_external_id).

Adds a regression test that stores a pending-only row and asserts it is removed when the booked counterpart arrives with a new transaction_id.
2026-05-12 00:17:49 +02:00
ghost
325084e342 fix(api): include disabled-account transaction history (#1723)
* fix(api): include disabled-account transaction history

* fix(api): hide pending deletion transaction history
2026-05-12 00:14:13 +02:00
ghost
9e6fbc1374 test(recurring): prove export roundtrip semantics (#1727)
* test(recurring): prove export roundtrip semantics

* test(recurring): assert export roundtrip payload
2026-05-12 00:02:15 +02:00
ghost
1fedc43f68 feat(api): add import preflight validation (#1755)
* feat(api): add import preflight validation

* fix(api): harden import preflight validation
2026-05-12 00:00:49 +02:00
ghost
6b6c3bd343 feat(exports): add attachment manifest (#1728)
* feat(exports): add attachment manifest

* fix(exports): include split parent receipts in manifest
2026-05-11 23:47:36 +02:00
Guillem Arias Fauste
f50c151e21 fix(design-system): DS::Alert alignment, accessibility, and hierarchy polish (#1734)
* fix(design-system): align DS::Alert icon with title

The icon was rendered at size 'sm' (w-4 h-4) and started at the very
top of the flex row (items-start without an offset), which optically
sat above the title's cap when the title was present and slightly
above the message baseline when it wasn't. The hand-rolled alerts
this PR replaced used 'w-5 h-5 mt-0.5' for exactly this reason —
restore the same combination in the component:

- size: sm -> md (w-4/h-4 -> w-5/h-5).
- class adds mt-0.5 so the icon's vertical center lines up with the
  bold title's cap-height (and with the body baseline in the title-less
  case).

No API change. Visual fix only.

Refs #1731

* fix(design-system): split DS::Alert into title-row + indented body

Replaces the items-start + margin-fudge approach with a two-row
layout that doesn't depend on icon-bounding-box vs text-cap-height
arithmetic:

- Title case: icon and bold title share a flex row with items-center,
  so the icon's vertical centre lines up with the title's line. Body
  (block content or message) renders below in a separate row, padded
  by pl-8 (= icon md width + gap-3) so it indents under the title
  text rather than under the icon.
- Block-only case (no title, no message — used by the alpha_vantage
  rate-limit alert): keeps the items-start fallback with a small mt-0.5
  on the icon so the cap of the first paragraph still sits near the
  icon centre.
- Single-line message case: items-center between icon and message, no
  fudge needed.

container_classes loses its 'flex items-start gap-3' base since the
outer div is no longer the flex container. Each branch declares its
own flex/items-* combination.

Refs #1731

* fix(design-system): a11y semantics + visual polish on DS::Alert

Builds on the title-row restructure with the items the design / a11y
review surfaced:

- live: keyword (default :none, accepts :status / :polite and
  :alert / :assertive) maps to role="status" or role="alert" on the
  outer div. Static, page-baked alerts (the migrated callsites in
  #1731) keep the default :none and stay role-less. Dynamic surfaces
  (flash, validation summaries appearing after a Turbo update) opt
  into the live role they need.
- aria-labelledby on the outer div pointing at the title <p> so AT
  picks the title as the alert's accessible name when one is set.
- Variant prefix in the title / message via an sr-only span. Screen
  reader hears 'Warning: …', 'Error: …', etc.; sighted users see no
  change. Variant labels live under ds.alert.variants.* in
  config/locales/views/components/en.yml.
- Body text inside titled alerts now defaults to text-secondary
  instead of text-primary, so hierarchy reads on weight + colour
  rather than weight alone (Refactoring UI: hierarchy needs both).
  Single-line message and block-only fallback keep text-primary
  since there is no second tier.
- Icon size goes back from md (20px) to sm (16px) — proportionally
  closer to text-sm body — and the items-center branches grow
  -mt-0.5 to compensate for the cap-centre vs line-centre offset
  that flex's items-center alone can't bridge.
- Title weight bumped from font-medium (500) to font-semibold (600)
  for clearer prominence against the now-softer body.

No API breakage: existing callers passing only message:/title:/variant:
keep working. The new live: arg defaults to the correct value for
the static migration sites.

Refs #1731

* fix(design-system): drop aria-labelledby when alert has no role; revert body to text-primary

Two corrections after numerical contrast analysis and CodeRabbit feedback:

1. aria-labelledby was being emitted on every titled alert, but the
   default live: :none leaves the outer <div> with no role. ARIA spec
   only honours the labelling relationship on elements with a host
   role, so on a generic <div> the attribute is invalid and
   accessibility validators flag it. Now only emitted when aria_role
   is set (live: :status or :alert). Static, page-baked callsites
   stay role-less and label-less; dynamic callers that opt into a
   live role get the proper accessible-name relationship.

2. text-secondary on bg-{variant}/10 in light mode lands at
   ~4.07-4.25:1 contrast — below WCAG AA's 4.5:1 for normal text.
   Reverting the body wrapper to text-primary brings it back to
   AAA (~15:1). Loses some of the Refactoring UI body-vs-title
   colour hierarchy; the title's font-semibold weight + larger
   optical mass against an otherwise plain body still reads as
   hierarchy. Single-line message and block-only fallback already
   used text-primary, so this just unifies the three branches.

The remaining contrast gap — text-success (green-600) icon on
bg-success/10 light surface at 2.77:1 — is documented in the PR
description; fixing it cleanly needs a token-level bump
(--color-success: green-600 -> green-700 in light mode) which is
out of scope for this PR.

Refs #1731

* fix(settings/providers): use DS::Alert title:+message: instead of inline content_tag

Three callsites added in #1710 passed block-level markup (`<p>`/`<h2>`)
through `message:` via `safe_join + content_tag`. The post-#1731 alert
template wraps `message:` in a `<p>`, which makes nesting a `<p>` or
`<h2>` invalid HTML — browsers auto-close the outer paragraph and the
indented body row collapses.

Each of the three is semantically a title + body pair, so swap them
to the proper `title:` + `message:` API. No new strings — the i18n
keys (`*.no_withdraw_title` / `_body`, `encryption_error.title` /
`.message`) already split that way; the inline assembly was the
artefact.

The encryption-error block loses an explicit `<h2>` wrapper around
the title; DS::Alert's title is a `<p>`. The visual hierarchy and
sr-only variant prefix are unchanged. Worth tracking heading semantics
as a follow-up against DS::Alert (a `heading_level:` arg) rather than
bringing back the manual markup.

* fix(design-system): make :destructive variant alias explicit in DS::Alert locale

Add `destructive: Error` to `ds.alert.variants` and drop the implicit
`:destructive -> :error` aliasing in `DS::Alert#variant_label`. Both the
locale file and the component now self-document the variant set; lookup
is direct, no conditional needed.

Per @jjmata review on #1734.
2026-05-11 23:29:05 +02:00
ghost
974f55e2d5 feat(api): add transaction idempotency keys (#1729)
* feat(api): add transaction idempotency keys

* fix(api): validate transaction idempotency source

* fix(api): tighten transaction idempotency params
2026-05-11 23:13:24 +02:00
Michal Tajchert
96c893ec18 Mobile: custom proxy headers + small login UX fixes (#1748)
* Add mobile custom proxy headers

* Clear login placeholders on focus

Email/password fields ship with example values pre-filled. Tapping the
field now clears the placeholder so users don't have to delete it
manually. Skips clearing if the user has already edited the value.

* Push Configuration as a route from Sign in

Opening Configuration from the Sign in screen now uses Navigator.push
instead of toggling a state flag, so Android back returns to Sign in
instead of quitting the app. Saving the URL auto-pops the route.

* Address PR review on custom proxy headers

- Test Connection no longer leaves global ApiConfig headers mutated;
  unsaved edits are restored in a finally block after the probe.
- _loadSavedUrl / _loadCustomHeaders wrap storage reads in try/catch and
  always finish initialization with sensible defaults.
- Sanitization is now a single CustomProxyHeader.sanitize() reused by
  ApiConfig.setCustomProxyHeaders and CustomProxyHeadersService.
- Brief comment on redactedValue explaining the length-obscuring design.

* Harden custom proxy header validation and load path

- validateValue now rejects ASCII control characters (CR/LF/tab/etc.)
  to prevent header-injection via crafted values.
- loadHeaders moves the secure-storage read inside the try block so
  platform exceptions are caught the same way JSON parse errors are.
2026-05-11 23:09:21 +02:00
github-actions[bot]
36960fe058 Bump version to next iteration after v0.7.1-alpha.5 release 2026-05-10 20:31:28 +00:00
Juan José Mata
f6f9feba8a Bank Sync cleanup (#1710)
* feat(settings/providers): surface connection status in section headers

Lifts the per-panel status indicator up to each collapsed accordion
header so admins can see at a glance which providers are connected
without expanding every section. Connected providers sort first.

- Add optional status: and meta: locals to settings/_section partial;
  pill hides via group-open:hidden when the section is expanded
- New settings/providers/_status_pill partial (ok/warn/err/off states)
- Add SettingsHelper#provider_summary to centralise the connected-vs-not
  logic already scattered across panel partials
- Refactor show.html.erb to pass status to every section and sort
  family_panels by connection state
- Add settings.providers.status.* i18n keys
- Add system tests asserting pill renders and sort order

https://claude.ai/code/session_01KW2HCN9rP1fiyQuw7Cju9D

* feat(settings/providers): group providers into Connected and Available

Partition the provider list in the controller into @connected_providers
and @available_providers based on provider_summary status, and render
each group under its own heading with a count. Auto-open the section
when only one provider is connected. Adds an empty-state line when
nothing is connected yet.

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

* feat(settings/providers): health strip, action-needed group, and sync error surfacing

- Extend provider_summary to return :err/:warn with meta text by checking
  latest sync per item (window function, same pattern as ProviderConnectionStatus)
  and Enable Banking session expiry within 7 days
- Partition provider entries into three groups: Connected (:ok), Action needed
  (:warn/:err, auto-opened), Available (:off)
- Add Settings::HealthSummary ViewComponent — four-tile grid showing Connected,
  Action needed, Errors, and Accounts synced counts
- Render health strip directly under page description; omit Action needed heading
  when group is empty
- Add i18n keys for tile labels, group heading, and all meta strings

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

* feat(settings/providers): card grid for available providers with connect drawer

- Add Provider::Metadata registry with static display data (region, kind,
  tier, maturity, logo) for all 11 providers
- Add Settings::ProviderCard ViewComponent rendering logo square, name,
  Beta/Alpha pill, meta line (region · type · tier), tagline, and Connect link
- Add connect_form action + route (GET /settings/providers/:key/connect_form)
  that opens the existing panel partial or config form in a DS::Dialog drawer
- Replace the Available accordion loop with a 2-column responsive card grid;
  empty state when all providers are connected
- Fix layout override: use turbo_rails/frame layout for frame requests so the
  drawer response is not wrapped in the full settings layout (was causing
  Turbo to pick the empty outer drawer frame instead of the filled one)
- Add SyncAllProvidersJob and last_sync_all_attempted_at migration (sync-all
  throttle support)
- Unify Connected + Action needed into a single "Your connections" section;
  items with warn/err status auto-open
- Fix Enable Banking grouping: items with expired sessions were returning
  :off (Available) instead of :warn (Your connections); gate now checks
  any? instead of any?(&:session_valid?)
- Add reconsent_required locale key for fully-expired EB sessions
- Surface Beta/Alpha maturity pills on connected provider accordion rows
  via new badge: param on settings_section helper
- Add i18n taglines for all 11 providers; add connect and empty_available keys

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

* feat(settings): retire /settings/bank_sync; merge into providers page

- Delete Settings::BankSyncController and its views (the providers page is
  now a strict superset of what bank_sync offered)
- Add permanent 301 redirect: GET /settings/bank_sync → /settings/providers
- Collapse nav to a single "Bank Sync" entry pointing at /settings/providers;
  remove the duplicate admin-only "Providers" entry from the Advanced section
- Remove "Providers" from SETTINGS_ORDER; point "Bank Sync" at
  settings_providers_path for next/prev navigation
- Rename page title to "Bank Sync"; replace admin-credential lede with
  user-facing copy ("Connect external accounts…")
- Update breadcrumb: Home → Bank sync
- Add controller test asserting 301 status and Location header

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

* Migrations are 7.2 here

* Minimize schema noise

* Schema duplication

* Small copy edits

* Fix tests

* Address provider settings review feedback

* refactor(settings/providers): finish design-review cleanup pass

Picks up the remaining items from Claude Design's review of #1710
that the previous review-feedback commit didn't cover.

DS / casing
- Sentence-case the page title ("Bank Sync" -> "Bank sync") and
  align the nav label.
- Drop the card hover-lift (shadow-border-sm) in favour of
  bg-container-hover; per the DS, card hover is colour-only.
- Whole-tile click target on each provider card — the inner
  "Connect ->" link was a hit-target inversion.
- Set Sync all to whitespace-nowrap so the label stops wrapping at
  narrow viewport widths.

UX simplifications
- Drop the four health-summary tiles (per-row warn/err pills already
  surface the signal at the scale this app sees). Removes
  Settings::HealthSummary, the @health_counts controller block, and
  the now-unused health.* locale keys.
- Hide "Your connections" heading + empty-state line when no
  providers are connected — the lede already invites a connect.
- Drop the redundant "Free" tier from per-card meta lines (printed
  10x for one fact); "Paid" still surfaces on Plaid.

Tests updated to drop the obsolete tiles assertion and switch the
provider-card click selector to look up the (now whole-card) anchor
by provider name.

* feat(settings/providers): replace Add another provider CTA with a search + kind filter

Per the design review, the "Add another provider · Browse providers"
card was a redirect to content one scroll-tick away. A search input
plus kind chips lets users self-segment the catalog and is the right
tool once it grows beyond the four to twelve providers we ship today.

- New providers_filter Stimulus controller — case-insensitive free
  text search across name/region/kind, plus a chip group with
  All / Banks / Crypto / Investment that toggle visibility via
  Tailwind's `hidden` class.
- _search_filters partial: search box (count-pluralized placeholder)
  + chip group, ARIA-labelled and aria-pressed for the chips.
- ProviderCard exposes filter_data (target + name/region/kind data
  attrs) so the controller can match without re-rendering.
- Lunchflow's `kind` was "Lunch" — switched to "Bank" so it falls
  under the Banks chip alongside its actual offering (it aggregates
  banks).
- Drops the add_provider_cta partial and its locale entries; adds
  search_filters.* and an empty_filter message.

* Private method fix

* refactor(settings/providers): drawer cleanup, header lock-up, trust statement

Per the design review's §07.

- Drop the trailing "Configured / Not configured" footer status from
  every provider panel (binance, coinbase, coinstats, indexa_capital,
  lunchflow, mercury, simplefin, snaptrade, sophtron, provider_form).
  The parent details section's status pill already carries that
  signal; the footer was redundant — and the copy/styling was
  inconsistent across panels (free-text vs. dot pill, "configured"
  vs. "not connected").
- Connect drawer gets a header lock-up: small logo chip + provider
  name + maturity badge, mirroring the available-card layout.
  Implemented as _drawer_header partial; connect_form passes
  custom_header: true to DS::Dialog so we own the row.
- Drawer footer trust statement: "Read-only — Sure can never move
  money. Stored encrypted." A single-line reassurance covering all
  panels.
- Sentence-case the hardcoded primary buttons that were Title Case:
  "Save Configuration" -> "Save and connect"
  "Update Configuration" -> "Update connection"
  "Connect Bank" -> "Connect bank"
  Affects simplefin, lunchflow, enable_banking, provider_form. The
  i18n'd panels (binance, coinbase, coinstats, indexa_capital,
  mercury, snaptrade, sophtron) keep their existing keys.

* chore(locales): drop unused provider-panel status strings

Footer "Configured / Not configured" status was removed from each
provider panel partial in the prior drawer-cleanup pass; the matching
i18n keys are no longer referenced. Removing them across every
locale to keep the catalogue clean.

Dropped (15 keys × varying locale coverage, 36 line removals across
24 files):

- coinstats_items.new.{status_configured_html, status_not_configured}
- indexa_capital_items.panel.{status_configured_html, status_not_configured}
- mercury_items.provider_panel.{configured_html, not_configured, accounts_link}
- sophtron_items.sophtron_panel.status.{configured_html, not_configured}
  (parent `status:` removed where it became empty)
- providers.snaptrade.{status_needs_registration, status_not_configured}
  (status_connected stays — still used by the lazy-load summary)
- settings.providers.{binance_panel, coinbase_panel}.{status_connected, status_not_connected}

* feat(settings/providers): connected-state polish per design §05 + Linked institutions rename

Building the next phase of the design review. Pulls forward the
slim health strip, denser connection rows, and "Linked institutions"
heading rename — the small Phase A lift the designer flagged in
§08 of the doc.

- New _health_strip partial: single-line at-a-glance pulse —
  connected count + needs-attention count + accounts syncing +
  last-synced timestamp. Renders only when at least one provider
  is linked or needs action.
- New _connection_row partial replaces the generic settings_section
  call for providers. Tighter rows: text-sm title (was text-lg),
  px-4 py-3.5 padding, single-line summary (chevron + name +
  maturity badge + meta + status pill + sync action). Warn/error
  rows get a coloured outline (border-warning/25 or
  border-destructive/25) so the at-risk row stands out without
  shouting.
- "Sync all" button restyled to match the design's secondary
  button: text-primary, alpha-black-100 border, rounded-[10px],
  padding 7px 12px (was the broader px-3 py-1.5 ghost).
- "Your connections" → "Linked institutions" heading, lifted from
  the designer's Phase-C reconciliation note. Primes users for the
  Option-C institution-search wizard six months early; existing
  i18n key stays as `groups.your_connections` for now to keep the
  rename to a single value flip.
- Controller computes the new @health hash (connected,
  needs_attention, accounts_syncing, last_synced_at) feeding the
  strip; brings back the single accounts query that was removed
  with the four-tile component.

System test updated for the new heading copy.

* fix(settings/providers): align connected state with the final design mock

Tightening the §05 polish to match the user-confirmed final design.

- Revert "Linked institutions" → "Your connections". The §08
  designer note about the Phase-A heading rename didn't carry
  forward to the final mock; keep the original wording.
- Drop the warn/err auto-open on connection rows. The design shows
  Enable Banking collapsed with a warn-outline and a status pill —
  no auto-expanded form. Single-connection auto-open kept (handy
  when the page is otherwise empty).
- Hide the "accounts syncing" segment in the health strip when the
  count is 0 — the design mock assumes a populated number; an
  always-visible "0 accounts syncing" reads as a placeholder.
- Strip the leading "about " from `time_ago_in_words` everywhere
  the result is shown to the user (health strip "Last synced %{time}
  ago" plus per-row "Synced %{time} ago" meta). Matches the design's
  shorter copy.

* refactor(settings/providers): tighten paddings, dedupe maturity badge, semantic + a11y fixes

Pixel-level alignment to the design's §05 mock + cleanup from a DS
audit pass.

Paddings, margins, font sizes
- Health strip: my-4 → mt-4 mb-5 to match the design's 16px / 20px
  vertical breathing room.
- Search filters bar: gap-2 → gap-2.5; mt-2 → mt-5 mb-3 (was missing
  the 12px bottom margin entirely).
- Search box: rounded-lg → rounded-[10px]; px-3 py-2 → px-[14px]
  py-[9px]. Search icon downsized w-4 → w-3.5 to match.
- Chip group: p-1 → p-[3px]; rounded-lg → rounded-[10px].
- Chip: py-1 → py-[5px]; rounded-md → rounded-lg.
- Group heading: mt-2 → mt-[18px]; mb-1 → mb-1.5.
- Status pill: text-xs → text-[11px].
- Provider card: gap-3 → gap-2.5 (outer + top); name gets explicit
  text-sm; tagline + foot 14px → 13px; arrow icon w-4 → w-3.5.
- Sync icon button: p-1 → fixed w-7 h-7 (28×28) so the row hit
  target matches the design's column width.
- Connect drawer header logo glyph: text-[10px] → text-xs (matches
  the available card's logo-glyph treatment).

Component / partial cleanup (DS audit follow-ups)
- New _maturity_badge partial replaces the inline span that was
  duplicated in 3 places (_connection_row, _drawer_header,
  provider_card.html.erb).
- Settings::ProviderCard.maturity_label class method centralizes the
  MATURITY_LABELS lookup; callers no longer reach into the constant.
- _connection_row title: <h2> → <h3> (the row sits inside the
  "Your connections" h2 group heading; nested h2s flattened the
  outline).
- show.html.erb encryption error: <h3> → <h2> for the same reason.

Locale
- Drop orphaned keys: settings.providers.groups.connected and
  groups.needs_attention (no view code uses them) plus the leftover
  show.coinbase_title block.
- Health strip "needs reconsent" → "needs attention" so the strip
  copy lines up with the per-row status pill ("Action needed") and
  the original group heading wording.

A11y
- focus-visible:ring-2 on chip buttons, provider-card link, and
  focus-within:ring-2 on the search input wrapper. Keyboard users
  now get a visible focus state.
- Search input: explicit autocomplete="off" (erb_lint hint).

* fix(settings/providers): icons + search input height

- Icons were rendering at 20px because the application_helper's `icon`
  default size (`md` = w-5 h-5) was beating the inline class override
  in compiled CSS source order. Pass `size: "sm"` and use the project's
  `!w-3.5 !h-3.5` important-prefix pattern (precedent: dashboard.html.erb)
  so chevron, refresh-cw, search, check, circle-alert, and arrow-right
  all render at the design's 14px.
- Search input was 54px tall because @tailwindcss/forms applies
  `padding: 8px 12px` to bare `<input type="search">`. Override with
  `!p-0 focus:ring-0 focus:shadow-none` so the wrapping div's padding
  alone defines the box (38px total — matches the design).

* refactor(settings/providers): align Sync all + search input with DS, address review feedback

- Sync all: replace the hand-rolled `button_to` with `DS::Link.new(variant: "outline", method: :post)` — same component as the
  "Identify Patterns" button on the recurring-transactions page.
- Search input: switch to the icon-overlay pattern used by the
  Manage-currencies and transaction filter rows
  (relative wrapper + absolutely positioned search icon +
  bordered input with `focus:ring-gray-500`). Brings the keyboard
  focus state in line with the rest of the app's filterable lists.
- SnapTrade panel: restore the "needs registration" status row that
  the drawer-cleanup pass dropped along with the redundant
  Configured/Not configured footer. The unregistered case is
  meaningful state, not redundant chrome.
- Move the slim health-strip computation out of the controller and
  into `SettingsHelper#provider_health_strip` (Convention 2: skinny
  controllers).
- Extract `concise_time_ago` helper so the "drop leading 'about '"
  trick stops being duplicated 3x.
- `Settings::ProviderCard#maturity_label` (instance) now delegates
  to `.maturity_label` (class) instead of duplicating the lookup.
- Drop unused `warn_or_err` local in `_connection_row`.
- Replace the `data-controller` string-injection + html_safe in
  `_connection_row` with `tag.details(data: ...)`; safer and more
  idiomatic.
- Add a system test for the empty-filter message wiring.

* fix(settings/providers): drawer trust statement uses border-tertiary

`border-secondary/10` was reaching for the text-foreground token at
10% opacity for a divider. The project ships a dedicated divider
token (`border-tertiary`, ~8% black) used by DS::Menu, the holdings
page, and admin/sso forms. Switching to it makes the trust-statement
HR match every other thin divider in Sure and stops misusing the
text token as a border.

* refactor(settings/providers): swap arbitrary Tailwind values for scale tokens

Per the user's directive — DS-compliance over pixel-perfect alignment
with the design mock. Walked the design audit and applied every swap
that lands within ±2px of the original.

Swaps:
- _health_strip: gap-[18px] → gap-5 (+2), px-[14px] → px-3.5 (=),
  text-[13px] → text-sm (+1).
- _search_filters: chip group p-[3px] → p-1, rounded-[10px] →
  rounded-xl (concentric with rounded-lg inner pills), chip py-[5px]
  → py-1.
- _status_pill: text-[11px] → text-xs.
- _group_heading: mt-[18px] → mt-5.
- _maturity_badge: text-[10px] → text-xs.
- provider_card: tagline + foot text-[13px] → text-sm.

Kept arbitrary: `min-w-[200px]` in _search_filters — nearest scale
tokens are min-w-48 (192px) and min-w-52 (208px); both are noticeable
layout shifts for a one-off responsive guard. Worth keeping the
arbitrary here.

Net: 9 of 10 arbitrary values gone. Visual delta: max +2px on a
single value. Design mock and DS scale now agree.

* revert(settings/providers): drop the slim health strip

Per-row status pills already carry the at-a-glance signal (connected
/ action needed) at the scale this app sees (1–4 connections per
family). The strip was redundant chrome for almost every user; only
worth bringing back if the catalog grows to a point where the row
list itself stops fitting on a single screen.

- Delete _health_strip.html.erb partial.
- Drop @health controller assignment + provider_health_strip helper.
- Drop unused settings.providers.health_strip.* locale keys.
- concise_time_ago helper stays — still used by per-row meta text.

* refactor(settings/providers): align with DS conventions

Two consistency wins from the screenshot/DS audit pass.

Sync icon button now renders DS::Button (variant: icon, size: sm)
instead of a hand-rolled `button_to`. Same component used by other
icon-only actions across the app (settings/profiles, layouts/imports).
Visual delta: 28×28 → 32×32 (DS sm size). Accept the +4px for
consistency. `event.stopPropagation()` still wired via the form opt
so the row's <details> doesn't toggle when the user clicks the
button.

Group heading now follows the established Sure section-label style
(`text-xs font-medium text-secondary uppercase`) used by
`_settings_nav` and the imports/categories surfaces. The previous
sentence-case `text-sm text-primary` was a one-off that didn't
match the rest of the app. Locale strings stay sentence-case;
uppercase comes from CSS `text-transform`. Tests updated to
case-insensitively match the rendered heading text.

* fix(provider/metadata): add plaid_eu entry

`plaid_eu` is registered as a separate Provider::ConfigurationRegistry
entry but had no Provider::Metadata row, so its card in the
Available grid fell through to the gray-500 default and rendered
empty (no region, kind, tier, or tagline). The title also came out
as "Plaid Eu" because `titleize` doesn't know "EU" is an initialism.

- Add a `plaid_eu` row to Provider::Metadata::REGISTRY with the same
  shape as `plaid` (US → EU, otherwise identical).
- Introduce an optional `name:` field in metadata; controller falls
  back to it before titleizing the provider key. Lets `plaid_eu`
  render as "Plaid EU".
- Add the missing `settings.providers.taglines.plaid_eu` translation.

* fix(settings/providers): center-align Sync all next to the lede

`items-start` made the button hug the first line when the lede wrapped;
on a single line the button sat at the top of the text bounding box
which read slightly off. Center matches the dominant convention
across the rest of settings (api_keys, securities, hostings, _section,
_settings_nav_link_large).

* fix(settings/providers): drop colour palette + filter polish + drawer warnings

Round of design-feedback fixes.

Provider chips
- Drop the per-provider raw Tailwind palette (bg-blue-600 etc.) from
  Provider::Metadata. All cards + drawer logo lock-up now use
  bg-surface-inset + text-primary, matching the design's §04 "drop
  colour entirely" recommendation. Solves the long-standing §01
  BLOCKER without externalising brand assets. Re-introducing logos
  later just means an optional logo_svg: field on metadata.
- ProviderCard component drops the `logo_bg:` parameter; the chip
  is now styled in the template.

Filter / search
- "Available · N" count and the empty-filter state now update
  client-side as the chip filter and free-text search narrow the
  grid (new `count` Stimulus target + dedicated update path).
- Empty-filter state now offers a Clear filters button that resets
  both the search input and the active chip in one click.
- Search placeholder drops the drifting "Search 9 providers" count
  for plain "Search providers" — the section heading carries the
  number.
- Chip labels normalised to plural where natural: "Banks · Crypto ·
  Investments" (Crypto stays as the mass noun).

Drawer copy / treatment
- "IP Whitelisting Required" → "IP whitelisting required" (DS
  sentence-case).
- Binance "do NOT enable withdrawal permissions" lifted out of
  inline red-text into a proper bg-warning-50 border-warning-200
  alert block with an alert-triangle icon. Matches the api_keys /
  hosting alert pattern.
- SnapTrade free-tier inline alert-triangle now uses `size: "sm"`
  so the icon stops rendering at 20px next to 14px body text.

Spacing
- Group-heading margin top bumped 5 → 6 (20→24px) so the eyebrow
  has more breathing room above the search bar.

* refactor(settings/providers): drawer alerts use DS::Alert; drop card-in-card

Two consistency fixes from a design-review pass.

DS::Alert adoption
- Replaces 9 hand-rolled error blocks across the provider panels
  (`bg-destructive/10 text-destructive ... line-clamp-3`) with
  `DS::Alert(variant: :error)` — the project's existing primitive.
- Replaces the just-shipped Binance no-withdraw warning block with
  `DS::Alert(variant: :warning)` instead of a hand-rolled
  `bg-warning-50 border-warning-200` card.
- Replaces the SnapTrade free-tier inline icon-prefixed warning
  paragraph with `DS::Alert(variant: :warning)` — proper alert
  treatment for an actual warning, not body copy.
- Replaces the Enable Banking "Configuration locked" inline
  `bg-warning/10` two-paragraph block with `DS::Alert(variant: :warning)`
  using `safe_join` for the title + body.
- Replaces the encryption-error block at the top of show.html.erb
  with `DS::Alert(variant: :error)`, again via `safe_join`.

Mercury card-within-card
- The "Add another Mercury connection" form was wrapped in a
  `<details>` `bg-container shadow-border-xs rounded-xl` card. In
  the Connect drawer (always 0 existing connections), that wrapping
  card-inside-the-drawer-card has no value — the form is the only
  thing on the surface. Drop the wrapper when no connections exist;
  keep the heading + form inline. When 1+ connections exist (the
  section page) the heading hints "+ Add another connection"
  without the disclosure indirection.

Trade-off: the error-alert blocks lose their `line-clamp-3` /
`title=` truncation. Acceptable for now — DS::Alert can grow a
truncate option as a follow-up if needed.

Open follow-up: DS::Alert itself uses raw Tailwind palette
(`bg-yellow-50` etc.) instead of semantic tokens, and only accepts
a single string `message:`. A separate issue tracks this.

* fix(settings/providers): hoist warning alerts to top of drawer

DS::Alert convention across the rest of the app: alerts sit at the
top of the form / page / section, not floating between content
blocks. The Binance no-withdraw warning and SnapTrade free-tier
warning were rendering between the setup-instructions list and the
form fields — visually wonky.

Move both to the top of their respective panels so the warning is
the first thing the user sees when the connect drawer opens.

Existing precedents this aligns with:
- accounts/_form.html.erb (error alert above form)
- valuations/new.html.erb (error alert above form)
- other_assets/new.html.erb (info alert above form)
- holdings/show.html.erb (warn alerts above content)

* fix(DS::Alert): align icon to cap-height of first text line

`items-start` on the container made the icon's top edge flush with
the text's top edge, leaving the icon's optical center sitting below
the text's first-line center. The hand-rolled alerts elsewhere in
the codebase (api_keys/new, hostings/_sync_settings, holdings/show)
all add `mt-0.5` to the icon for the same reason — fold that into
the primitive so every caller gets the cap-height alignment.

* copy(settings/providers): tighten alert messaging per voice review

Copy expert pass on the new provider drawer alerts. House style:
sentence case for titles, lead with the action, drop "Warning:" /
"Please" filler (the alert variant icon already signals tone),
prefer one short sentence + optional title-paragraph for emphasis.

- Binance no-withdraw warning: was a single line "Warning: do NOT
  enable withdrawal permissions" — alarmist without context. Now
  splits into "Read-only key only" (title) + "Don't enable
  withdrawal permissions when creating your Binance API key — Sure
  only needs read access." (body).
- SnapTrade free-tier note: "Free tier includes 5 brokerage
  connections. Additional connections require a paid SnapTrade
  plan." → "SnapTrade's free tier covers 5 brokerage connections.
  Upgrade on SnapTrade for more."
- SnapTrade connection-limit-info inside the brokerage list: cut
  entirely. The drawer already shows the cap; restating it in the
  list was noise.
- SnapTrade needs-registration: "Credentials saved — finish
  registration to connect a brokerage." → "Credentials saved.
  Finish setup to connect a brokerage." ("registration" was
  ambiguous — register where, with whom?)
- Enable Banking "Configuration locked" body: "Credentials cannot
  be changed while you have active bank connections. Remove all
  connections first to update credentials." → "Disconnect all
  linked banks before changing these credentials." Same meaning,
  half the words.
- Encryption-error block: title-cased "Encryption Configuration
  Required" → "Encryption keys missing"; body strips "Please
  ensure" filler and the parenthetical credential dump, leaving
  the three credential names inline as a clean list. Self-hosters
  still get exactly the names they need to set.

* feat(settings/providers): SetupSteps partial for connect-drawer instructions

Per the design's drawer-cleanup follow-up. Replaces the per-panel
"Setup instructions:" + ordered list + "Field descriptions:" block
with a shared boxed-step component.

The new partial — `_setup_steps.html.erb` — takes a `steps:` array
of strings (or html_safe strings for inline links / code) plus an
optional `help:` hash for a docs link below the steps. The eyebrow
label is "Setup" (uppercase, tracking-wider) matching Sure's other
section labels.

Applied across all eleven provider panels:
- _provider_form (Plaid + Plaid EU): field descriptions move to
  per-field helper text below the input.
- _binance, _coinbase, _coinstats, _indexa_capital,
  _lunchflow, _mercury, _simplefin, _snaptrade, _sophtron,
  _enable_banking: ordered list + duplicate "Field descriptions"
  block both replaced by the partial.
- Some panels' inline copy tightened in the same pass (Lunch Flow,
  SimpleFIN, Enable Banking) — the design copy is shorter than the
  current legacy strings; a copy-pass through every panel can
  follow as a separate cleanup.

Token notes: uses scale tokens (`rounded-xl`, `text-xs`/`text-sm`,
`tracking-wider`) instead of the design mock's exact arbitrary
values, per the consistency-over-design-specs directive on this
branch.

* fix(settings/providers): tighten panel spacing + relocate per-panel notes

Read-flow audit on each connect drawer. The uniform `space-y-4`
treated every block (alert, steps, info card, fields, button) the
same — visually they were five sibling boxes with no grouping. The
fix is per panel; some notes belong as helper text on a specific
field, others as a tightly-grouped pre-fill primer.

Per panel:

- Binance: IP-whitelisting card now matches the setup_steps box
  (`bg-surface-inset rounded-xl`) and is wrapped with setup_steps
  in an inner `space-y-2` so they read as a single pre-fill primer
  cluster. Same eyebrow treatment ("IP whitelisting required") so
  the two boxes look like sister panels, not unrelated chrome.

- SnapTrade: drop the description paragraph above setup_steps. The
  available-providers card grid already markets SnapTrade
  ("Connect brokerage accounts via the SnapTrade aggregation
  network."); repeating in the drawer was duplication.

- Mercury: move the sandbox-API note out of its standalone <p>
  below setup_steps and into per-field helper text under the
  base_url field — the user only cares about the sandbox URL when
  they're filling that field. Applied to both the per-item edit
  form and the add-new form.

- _setup_steps partial: drop the now-pointless `mb-2` (outer
  `space-y-4` already controls the gap; bottom-margin was dead
  CSS thanks to margin-collapse rules with the next sibling's
  margin-top).

* fix(settings/providers): plaid + indexa drawers join the SetupSteps look

Two unifying fixes after the panel-by-panel screenshots showed
mixed treatments.

Plaid + Plaid EU
- The registry-driven panel (_provider_form) was still rendering
  each adapter's markdown `description` block as plain prose
  ("Setup instructions: 1. Visit the Plaid Dashboard ..."). Other
  panels switched to the SetupSteps box; Plaid was the odd one out.
- Drop the markdown `description` block from both plaid_adapter
  and plaid_eu_adapter. Render setup_steps in _provider_form for
  these two provider keys via inline ERB (link helper handles the
  Plaid Dashboard link cleanly; the regional differences fold to
  the same dashboard URL with a different account scope).
- Other registry-based providers fall through to the previous
  markdown description path — no behavior change for them.

Indexa Capital
- The API token field was wrapped in a `bg-surface border` "card"
  that duplicated the field label inside as a heading and put the
  description above the input. Same pattern the user flagged as
  the "card within input" anti-shape.
- Drop the wrapper. The styled-form input renders its own label;
  description moves to per-field helper text below the input,
  matching the pattern used by Plaid (provider_form) and Mercury.

* fix(settings/providers): surface configured plaid_eu + dedup show context

provider_summary had no plaid_eu branch — configured plaid_eu was
falling through to status :off and rendering in Available even with
credentials set. Collapse plaid + plaid_eu into a single registry
check.

Drawer title for non-panel configurations was provider_key.titleize,
which produced "Plaid Eu" while the available card grid used
metadata[:name] = "Plaid EU". Read from metadata first.

While here:
- compute_provider_sync_health no longer relies on
  instance_variable_get; pass family_panel_items explicitly so the
  hash-key/ivar-name coupling is gone.
- drop unused .includes(:syncs, :mercury_accounts) and
  .includes(:snaptrade_accounts) from prepare_show_context. The show
  view only consults summary[:status]; the eager-loads were carried
  over from connect_form (which has its own load_provider_items).

* i18n(settings/providers): localize plaid setup steps + drop dead defaults

The plaid + plaid_eu setup steps in _provider_form.html.erb were
hardcoded English strings. Move them to settings.providers.plaid_panel
(shared) + plaid_eu_panel (EU-specific step 1) so they can be
translated like every other panel.

_setup_steps.html.erb was passing default: "Setup" / "Need help?" to
t(), masking missing translations in non-EN locales. Both keys exist
in en.yml — drop the defaults so missing translations actually
surface.

* test(settings/providers): cover plaid_eu, clear filters, warn outline

Three system test additions:

- Configured plaid_eu surfaces in Your connections (regression guard
  for the helper fix; previously fell through to Available).
- Clear filters button resets input + chip state and brings cards
  back into view.
- :warn-state connection row carries the border-warning/25 outline
  that distinguishes it from an :ok row.

* copy(settings/providers): drop em dashes, naturalize phrasing

Sweep through every string this branch added and replace em-dash
splices with full sentences or simple connectives.

en.yml:
- drawer_trust_statement now reads "Read-only access. Sure can never
  move money, and your credentials are stored encrypted." instead
  of em-dash splicing.
- sync_all_recently / recently_synced split into two sentences.
- binance_panel.no_withdraw_body, plaid_panel.step_1_html / step_2,
  plaid_eu_panel.step_1_html same treatment.

Hardcoded panel steps (enable_banking, lunchflow, simplefin) become
"Go to <link> and …" or "Go to <link> for …" instead of the
"<link> — get …" splice. Same setup_steps comment cleaned up.

* fix(settings/providers): address CodeRabbit pass on PR #1717

Fixed:
- Localize the setup steps in _enable_banking_panel,
  _lunchflow_panel, and _simplefin_panel. The em-dash sweep had
  rewritten these into hardcoded English; they now route through
  settings.providers.{enable_banking,lunchflow,simplefin}_panel
  step_1_html / step_2 / step_3 keys, mirroring the plaid_panel
  treatment.
- connect_form: silent redirect when provider_key is unknown now
  carries an alert (settings.providers.not_found) so misrouted
  links don't drop users on the page with no feedback.
- sync action: redirect notice now reflects whether anything was
  actually scheduled — adds settings.providers.sync_provider_no_items
  for the "all items already syncing or none exist" path.
- Family::Syncer test: count plaid_items via the .syncable scope to
  match what Family::Syncer actually schedules (already done for
  binance_items in the same test).

Skipped, with reasons:
- focus:ring-gray-500/-gray-900 in coinstats / coinbase / simplefin /
  search_filters: tracked under issue #1715 as part of the raw-palette
  → DS-token sweep across the whole codebase.
- Coinbase #0052FF brand-color wrapper: tracked under PR #1710's
  follow-up tracking comment as the deferred Provider::Metadata
  colour-palette decision (designer §01).
- Sophtron submit-button extraction into DS::Button: same
  deferred sweep — every panel hand-rolls this class string;
  one-off extraction would just churn.
- Redundant .html_safe on _html keys in coinstats: tracked in #1715.
- _provider_form.html.erb env hint, "Optional" placeholder, "Save and
  connect" submit: pre-existing strings not added on this branch.
- Renaming sync_health_for's :stale to :data_stale: pre-existing
  shape, refactor scope.
- Plaid_eu using plaid_panel.step_2/step_3 keys: deliberate. Same
  English copy across both providers; duplicating keys would just
  give translators twice the work for identical strings.
- _enable_banking_panel / _lunchflow_panel / _simplefin_panel
  alert + submit + button labels: pre-existing hardcoded strings
  from before this branch. Setup steps were the strings actually
  touched in the em-dash sweep, so those got localized; the rest
  belong in a broader panel-i18n pass.

Verified:
- bundle exec erb_lint on the three panels: clean.
- bin/rubocop on controller + test: clean.
- bin/rails test test/models/family/syncer_test.rb
  test/controllers/settings/providers_controller_test.rb:
  23 runs, 85 assertions, 0 failures.
- DISABLE_PARALLELIZATION=true bin/rails test
  test/system/settings/providers_test.rb:
  15 runs, 38 assertions, 0 failures.

* fix(db): rename migration to clear collision with main's 20260508120000

Main's PR #1705 (Sophtron manual sync) shipped a migration with
the same 20260508120000 timestamp as our
add_last_sync_all_attempted_at_to_families migration. The merge
that brought main into this branch left both files at the same
prefix, which trips Rails' "Duplicate migration" guard at
db:schema:load time and broke CI.

Renaming our migration to 20260510120000 keeps the column it adds
intact (already in db/schema.rb) and bumps the schema version to
match. No DB-level change.

* fix(settings/providers): card + strip a11y polish

- Bring back the slim health strip; gate behind 10+ accounts
  (HEALTH_STRIP_MIN_ACCOUNTS) so it stays out of the way for
  small libraries where per-row pills already carry the signal.
- Status pill: drop the bg-{c}/10 text-{c} pattern (failed AA
  on warn / err); switch to bg-surface-inset text-primary with
  the dot still carrying semantic colour. Passes AA in both
  themes; the dot is the only colourful affordance.
- Maturity badge: bg-alpha-black-50 was invisible against the
  hovered card bg in light mode and against bg-container in
  dark mode. Move to bg-surface-inset + border-tertiary so it
  stays delineated through hover and dark theme.
- Provider card: keep the bg shift on hover (now bg-surface-inset
  for a perceptible delta), focus ring promoted alpha-black-100
  -> alpha-black-300 (visible to keyboard users), meta line
  text-subdued -> text-secondary (text-subdued failed AA at
  2.86:1 against bg-container).
- Restore the per-provider logo palette dropped in 6abceb07.
  Yellow-on-white was the BLOCKER then; bumped Binance to
  yellow-600 and CoinStats to pink-600 (distinct from Binance
  and AA-safe with white text).
- Health strip dividers: bg-alpha-black-100 was invisible in
  dark mode. Switch to border-l border-secondary so the DS
  variant flips correctly.

* fix(settings/providers): keep row height on open

The right-side meta + status pill + sync button group is hidden
via group-open:hidden, but the sync button (DS::Button size sm,
h-8) is what dictated the row's natural height. With it gone,
the row collapsed from 60px to 48px and the title appeared to
jump upward.

Pin a min-h-15 on the <summary> so the height stays constant
through open/close.

* Let's not regress IPv6

* Keep the only real change in schema.rb

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Signed-off-by: Guillem Arias Fauste <accounts@gariasf.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillem Arias <accounts@gariasf.com>
Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
2026-05-10 22:13:57 +02:00
Sure Admin (bot)
f62aed66e4 i18n(es): fill high-value locale gaps (#1733)
* i18n(es): fill small high-value locale gaps

* Small tweak

---------

Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-05-10 17:17:45 +02:00
Juan José Mata
7f0569357a Defensive coding on @variant 2026-05-10 15:15:40 +00:00
Guillem Arias Fauste
57d71cd55e refactor(design-system): extend DS::Alert and migrate 9 inline alert blocks (#1731)
* feat(design-system): add info semantic color token

Mirrors success/warning/destructive: --color-info maps to blue-600 in
light mode, blue-500 in dark mode. Unblocks the DS::Alert info variant
from carrying a raw 'blue-600' literal in icon_color and lets surface
tokens use bg-info/N alpha modifiers like the rest of the system.

Refs #1715

* refactor(design-system): adopt semantic tokens and add body slot in DS::Alert

Replaces the bg-{blue,green,yellow,red}-50 / text-{...}-700 / border-{...}-200
palette block in DS::Alert with semantic alpha-modifier surfaces
(bg-{info,success,warning,destructive}/10 + matching /20 borders).
Drops the 'blue-600' literal that icon_color was returning for the
info variant; helpers#icon now accepts color: :info backed by the
new --color-info token.

Adds an optional title: kwarg and an opt-in block-content slot so
rich alerts (title + paragraph, lists, embedded actions) can render
without callers reaching for a hand-rolled flex layout. The existing
message: API stays backward-compatible — nothing in the codebase that
already calls DS::Alert.new(message: ..., variant: ...) needs to change.

Lookbook gains with_title and with_body_slot examples covering the
new shapes.

Refs #1715

* refactor(views): migrate api_keys, hostings, lunchflow alerts to DS::Alert

Cleans up nine bespoke alert blocks that hand-rolled the same
flex + icon + bordered-surface shape DS::Alert already provides:

- settings/api_keys/{new,created,created.turbo_stream}.html.erb — three
  near-identical 'Security Warning' / 'Important Security Note' boxes
  using the broken bg-warning-50 / text-warning-700 raw-palette pair.
- settings/hostings/{_alpha_vantage,_eodhd,_yahoo_finance,_twelve_data,_provider_selection}_settings.html.erb —
  five amber-50 / amber-200 warning boxes covering rate-limit notes,
  health-check failure messaging, and the env-configured override
  banner. The twelve_data plan-restriction block keeps its bullet
  list and pricing link inside the new DS::Alert body slot.
- lunchflow_items/{_api_error,_setup_required}.html.erb — two modal
  alert headers whose flex+icon scaffolding now collapses onto
  DS::Alert. The surrounding bg-surface 'Common issues' / 'Setup
  steps' info cards stay as-is; this PR only touches the alert
  shape itself.

No functional or behavioural changes. Locale keys preserved.
amber-* palette uses on the alerts disappear; remaining bg-amber-*
hits in the codebase live outside the alert pattern and stay for
follow-up sub-PRs of #1715.

Refs #1715
2026-05-10 17:14:06 +02:00
Guillem Arias Fauste
712d6baca9 docs(agents): add Design System Hygiene checklist for UI PRs (#1732)
AGENTS.md already nails API-endpoint consistency for backend work but
says nothing about UI conventions. Result: contributor PRs and review
agents repeatedly flag the same shape — raw Tailwind palette where a
semantic token exists, hand-rolled alert / button / disclosure blocks
when DS::* already covers them — and the rule has to be re-discovered
in review every time.

Adds a short Design-System-Hygiene block (token use, 'reach for DS::*
first', the 'two copies => lift to DS' threshold, and the icon helper /
t() conventions) so the rule lives next to the API one and reviewers
have something to cite. Mirrors the weight of the API section, links
the DS-consolidation umbrella issue (#1715).

Refs #1715
2026-05-10 16:41:46 +02:00
Juan José Mata
c92b984cef [codex] Add Sophtron manual sync fixes (#1714)
* Add manual Sophtron sync flow (#1705)

Branch-to-branch merge.

* Copy edits

* Make Sophtron manual sync institution scoped

* Populate Sophtron manual sync stats

* Restore Sophtron bank credential copy

* Address Sophtron manual sync review feedback

* Scope manual sync processing failure handling

* Hide raw Sophtron processor errors from flash

* Clear Sophtron manual sync pointers on provider errors

* Keep manual Sophtron MFA on manual sync records

* Preserve manual sync processing error details
2026-05-09 21:55:20 +02:00
Juan José Mata
acb82790d5 Update README to reflect Maybe Finance repo status
Clarified the status of the Maybe Finance team and the repository.

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
2026-05-09 21:26:36 +02:00
CrossDrain
0b7fa732ae feat(splits): add exclusion support for splits and improve rendering (#1661)
* feat(splits): add excluded attribute support for split children and improve rendering of split transactions

* address coderabbitai suggestions to improve code quality

* Fix split excluded coercion, DRY helpers, and clean up view partials

Fix boolean coercion bug where string "false" from form params was
truthy in Ruby, causing all split children to be marked excluded.
Use ActiveModel::Type::Boolean for explicit casting in Entry#split!.

Additional changes addressing code review feedback:

- Extract duplicated in_split_group logic from TransactionsController
  and TransactionCategoriesController into TransactionsHelper
- Remove redundant local_assigns.fetch calls in partials that already
  declare defaults via the Rails 7.1 locals: magic comment
- Simplify ternary in _transaction.html.erb to pass grouped directly
- Guard hidden_field_tag :grouped to only emit when value is "true"
- Add model tests for excluded on split children (boolean and string)
- Add controller test for excluded param through full HTTP stack
- Add test confirming excluded children are dropped from balance queries

* fix(splits): simplify excluded attribute boolean check

* refactor(splits): extract truthy values constant for excluded check

Extract the array of truthy values used for excluded attribute check
into a private constant to improve code maintainability and avoid
duplication of the magic array.

* refactor: simplify split grouping link generation and add test coverage for excluded split parameters
2026-05-09 12:36:41 +02:00
CrossDrain
96b1d28d5d feat(enable-banking): safe pending transaction merge with sync re-import prevention (#1709)
* feat(enable-banking): safe pending transaction merge with sync re-import prevention

* preserve all merged pending IDs across syncs

* fix(enable-banking): harden merge locking, safe logging, and non-blocking index

* fix(enable-banking): use safe external ID in invalid currency log

* refactor(models): centralize pending transaction SQL logic

Move the SQL fragment used to identify pending transactions from the `Entry` model to a constant in the `Transaction` model. This improves maintainability and ensures that the logic for determining if a transaction is pending is defined in a single location.

* fix(enable-banking): drop dead manual_merge index, use lateral join for excluded IDs

* No net schema changes

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-05-09 11:56:16 +02:00
thomasbaker9010251
43e7e35e7e fix(transactions): update dialog content class for new transaction view (#1693)
* fix(transactions): update dialog content class for new transaction view

* feat(credit_card): add validation for expiration date and update form to prevent past dates

- Implemented a validation method to ensure the expiration date of credit cards is not in the past.
- Updated the credit card form to set a minimum date for the expiration date field, preventing users from selecting past dates.

* fix(credit_card): update expiration date validation error message format

- Changed the error message assertion for the expiration date validation to check for the symbol :greater_than_or_equal_to instead of a specific date string. This improves the flexibility and clarity of the validation error handling.

* fix(transactions): enhance dialog content class for improved overflow handling

* rebase

---------

Co-authored-by: ms1112 <milosdelic.tech@gmail.com>
Co-authored-by: petermilord <petermilord6@gmail.com>
2026-05-09 11:01:23 +02:00
Juan José Mata
b74014ab42 Reject revoked OAuth tokens in API auth (#1711) 2026-05-09 01:39:10 +02:00
ghost
8abecf8a8d feat(exports): preserve transfer decisions (#1639)
* feat(exports): preserve transfer decisions

* fix(api): apply transfer date filters to both sides

* fix(api): refine transfer decision handling

* fix(api): align transfer decision schemas

* fix(api): use current context for transfer filters

* fix(api): include either side in transfer date filters

* fix(api): deduplicate transfer decision filters

* fix(api): guard transfer decision exports
2026-05-08 23:03:57 +02:00
Juan José Mata
81cdccb768 [codex] Complete Sophtron account mapping (#1698)
* Complete Sophtron account mapping

* Clarify Sophtron login challenge flow

* Add Sophtron connection UI timeout

* Treat Sophtron timeout jobs as failed

* Reset failed Sophtron connection state

* Handle stale Sophtron connection jobs

* Advance Sophtron polling timeout

* Shorten Sophtron connection timeout

* Fix Sophtron modal polling updates

* Stabilize Sophtron MFA polling

* Give Sophtron OTP challenges more time

* Clarify Sophtron institution login failures

* Extend Sophtron polling during login progress

* Probe Sophtron accounts after completed MFA step

* Align Sophtron dialogs with design system

* Start Sophtron initial load after linking accounts

* Fix Sophtron initial transaction load

* Fail Sophtron sync without institution connection

* Fix tests

* Wrap Sophtron account linking in transaction

* Wrap Sophtron provider responses

* Fix Sophtron MFA security tests

* Guard Sophtron MFA challenge arrays

* Respect Sophtron initial load window

* Use unique Sophtron MFA answer field ids

* Address Sophtron review follow-ups

* Fix Sophtron transaction sync refresh

* Avoid blocking Sophtron refresh polling

* Move Sophtron account helpers to model

* Keep Sophtron grouping provider-level

* Start new Sophtron institution links

* Isolate Sophtron institution connections

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
2026-05-08 15:15:23 +02:00
github-actions[bot]
5fa1c034b4 Bump version to next iteration after v0.7.1-alpha.4 release 2026-05-07 23:12:24 +00:00
Elvis De Abreu
cf31c6e398 Fix avg_cost to return per-share cost basis (#1692)
* Fix avg_cost to return per-share cost basis

* Revert "Fix avg_cost to return per-share cost basis"

This reverts commit 38c438a614.

* Normalize SimpleFIN holding cost basis

* Track SimpleFIN cost basis source field

* Add SimpleFIN cost basis normalization tests

* Update SimpleFIN value cost basis expectation

* Handle missing SimpleFIN quantity for cost basis

* Ignore missing SimpleFIN cost basis fields

* Fix SimpleFIN holdings processor test setup
2026-05-07 23:31:26 +02:00
GermanDZ
7e1de420ca perf(accounts): kill sidebar/sparkline N+1s and cache the sidebar (#1683)
* 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>
2026-05-07 17:31:16 +02:00
ghost
45c5284148 feat(api): expose provider connection health (#1636)
* feat(api): expose provider connection health

* fix(api): harden provider health review paths

* fix(api): refine provider health responses

* test(api): align provider health docs key scope

* fix(api): clarify provider connection status

* fix(api): batch provider connection sync status

* fix(api): polish provider connection status review feedback

* fix(api): correct provider connection summaries
2026-05-07 00:42:32 +02:00
GermanDZ
d1081547ec feat(api): allow creating categories via API (#1676)
* feat(api): allow creating categories via API

Adds POST /api/v1/categories so external integrations (e.g. bulk
classification scripts that import already-categorized data from
another system) can create categories without going through the web UI.
Mirrors the existing tags create endpoint: requires the read_write
scope, accepts name/color/icon/parent_id, auto-suggests an icon when
omitted, and rejects parent_ids from other families.

Also adds Minitest behavioural coverage, an rswag docs spec, a
CategoryCreateRequest schema, and regenerates docs/api/openapi.yaml.

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

* fix(api): address review feedback on POST /api/v1/categories

- Re-raise ActionController::ParameterMissing in #create so the
  BaseController rescue_from handles it as a 400 instead of the
  generic 500 from the broad rescue inside the action.
- Add a 403 'insufficient scope' response block to the rswag POST
  example so the generated OpenAPI documents read-only key rejection.
- Switch the new create-action Minitest cases to API key auth via
  X-Api-Key + api_headers (using the existing api_keys fixtures),
  matching the project's API endpoint consistency rule.
- Add Minitest coverage for two more 4xx paths: rejecting third-level
  nesting (parent_id pointing at a depth-2 subcategory) and rejecting
  requests without the category payload (400).

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

* refactor(test): migrate categories API index/show tests to X-Api-Key

The pre-existing index and show tests in this file authenticated via
Doorkeeper bearer tokens. Per the project's API endpoint consistency
rule (CLAUDE.md, .cursor/rules/api-endpoint-consistency.mdc) Minitest
controller tests under test/controllers/api/v1/ must use ApiKey +
X-Api-Key auth. Drops the Doorkeeper application/access-token setup
and routes every request through the existing api_keys fixtures and
the api_headers helper, matching the create-action tests already in
this file (and the pattern used in sync/users/family_settings tests).

No behavioural change.

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

* fix(api): address second-round review on POST /api/v1/categories

- Add a 400 response block to the POST rswag example so the generated
  OpenAPI documents the missing-category-payload contract that
  BaseController#handle_bad_request already returns. Regenerate
  docs/api/openapi.yaml.
- Replace fixture-backed read_write_api_key / read_only_api_key
  helpers with explicit ApiKey.create! calls (matching the pattern in
  sync_controller_test, users_controller_test, and
  family_settings_controller_test). Setup now destroys active keys for
  the test user so the one-active-key-per-source validation does not
  collide with fixtures.

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

* test(api): tighten 422 create-category cases

- Pass color and icon explicitly in the duplicate-name and
  third-level-nesting tests so each case is self-documenting about
  which validation it isolates (the model's color presence check is
  satisfied by the column default today, but reviewers — human and
  bot — flagged the implicit reliance).
- Assert the JSON error envelope (error key + present message) on every
  422 path so the response shape stays consistent and a regression in
  the rendered error body is caught uniformly.

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

* fix(api): tighten POST /api/v1/categories per review

- Drop the no-op `rescue ActionController::ParameterMissing; raise` and
  the broad `rescue => e` from the create action. The BaseController
  already has rescue_from ActionController::ParameterMissing → 400, and
  unexpected exceptions are best left to Rails' default 500 handling
  (which logs identically). Keeps the action focused on its happy path
  and the two real error branches.
- Stop accepting `lucide_icon` as a request key. The OpenAPI schema
  documents only `icon`; the dual permit was undocumented and pointless.
  `icon` is now the single canonical request key, mapped to
  `lucide_icon` on the model in category_params.
- Migrate the Minitest helpers to the project's documented API key
  pattern: ApiKey.generate_secure_key + api_key.plain_key in the
  X-Api-Key header (matching the rswag spec in this PR and the rule in
  .cursor/rules/api-endpoint-consistency.mdc), instead of hand-built
  display_key strings.

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

* Botched conflict merge

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-05-06 22:59:55 +02:00
Brendon Scheiber
dce2213a98 feat: add Hungarian (hu) localization (#1677)
* Add Hungarian (hu) localization

Add complete Hungarian translation files and register hu locale

* Update hu.yml

* Hungarian locale: currency formatting & fixes
2026-05-06 22:38:51 +02:00
ghost
9e369831ce feat(api): expose sync status (#1635)
* feat(api): expose sync status

* fix(api): harden sync status review paths

* fix(api): address sync status review

* fix(api): tighten sync status review fixes

* fix(api): address sync status review

* test(api): avoid secret-like sync fixture key

* test(api): reuse sync status fixture key

* fix(api): align sync route helpers

* fix(api): tighten sync status scoping

* fix(api): make sync status schema nullable-compliant
2026-05-06 22:02:21 +02:00
Julien Roy
1e0666eca2 Update link text for new account to use translation (#1690)
Signed-off-by: Julien Roy <royto81+github@gmail.com>
2026-05-06 21:36:08 +02:00
ghost
2d38cfb011 feat(api): expose budget state (#1640)
* feat(api): expose budget state

* fix(api): guard malformed budget ids

* fix(api): address budget state review

* fix(api): address budget state review

* fix(api): document budget id formats

* fix(api): align budget category docs auth

* fix(api): lighten budget category index payload

* fix(api): use shared pagination clamp

* fix(api): centralize budget filter handling
2026-05-06 20:50:46 +02:00
sentry[bot]
4b93bdb447 fix(transactions): Ignore blank date in entry params (#1688)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-05-06 20:27:18 +02:00
sentry[bot]
ec4559ba26 feat(entries): Add amount validation and robustify monetizable concern (#1680)
* feat(entries): Add amount validation and robustify monetizable concern

* fix(valuations): localize blank amount errors

---------

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: SureBot <sure-bot@we-promise.com>
2026-05-05 20:07:40 +02:00
ghost
41339b0494 feat(api): expose balance history (#1641)
* feat(api): expose balance history

* fix(api): address balance history review

* fix(api): address balance history review

* fix(api): tighten balance history docs

* fix(exports): preserve balance chronology

* fix(api): guard nullable balance account type

* test(api): align balances api key helper

* fix(api): use shared pagination clamp

* test(export): set explicit balance flows factor
2026-05-05 19:09:36 +02:00
Juan José Mata
a9661253f4 Revert "feat(accounts): Highlight matching activity search text in entry names" (#1682)
Explanation in #1679
2026-05-05 18:48:06 +02:00
Sure Admin (bot)
e535a7ab0b Merge pull request #1671 from bugbug11111/feat/search-highlight
feat(accounts): Highlight matching activity search text in entry names
2026-05-05 12:21:34 +02:00
github-actions[bot]
96ba3daa92 Bump version to next iteration after v0.7.1-alpha.3 release 2026-05-05 10:07:28 +00:00
bugbug11111
86d92508cb fix(accounts): sanitize activity entry names for highlighting
* Updated the `highlight_activity_entry_name` method to escape HTML in activity entry names before highlighting. This change prevents potential XSS vulnerabilities and ensures safe rendering of user-generated content.
2026-05-05 12:07:04 +02:00
bugbug11111
5519716274 fix(transactions): correct HTML syntax in split parent row view
* Fixed a minor syntax issue in the _split_parent_row.html.erb file by ensuring the closing tag for the link_to helper is properly formatted. This change enhances code readability and maintains consistency in the view structure.
2026-05-05 11:59:53 +02:00
Sure Admin (bot)
1646abb938 Fix SSO icon rendering for mixed-case provider icons (#1674)
* fix(sso): normalize icon names and fail gracefully

* fix(sso): fall back to key icon

---------

Co-authored-by: SureBot <sure-bot@we-promise.com>
2026-05-05 11:52:40 +02:00
bugbug11111
bba32a3e61 feat(accounts): add activity entry highlighting in summary cards
* Introduced a new helper method `highlight_activity_entry_name` to highlight search terms in activity entry names.
* Updated various views to utilize the new highlighting method for improved user experience in displaying relevant entries.
2026-05-05 08:08:48 +02:00