mirror of
https://github.com/we-promise/sure.git
synced 2026-07-19 08:15:21 +00:00
5935e56a8b238be4d1f80daa9c45259a04c2c430
247 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1f036fc526 |
feat: add preference to disable modal close on outside click (#2226)
* Add a user preference under Settings → Appearance that prevents modals
from closing when clicking outside them. Useful to avoid accidentally
losing unsaved form data.
- Add `disable_modal_click_outside?` helper to User model (JSONB prefs)
- DS::Dialog reads the user preference as default when not explicitly set
(existing callers passing disable_click_outside: true/false are unaffected)
- Wire up save in AppearancesController
- Add toggle in the Modals section of the Appearance settings page
- Add i18n strings
* fix(i18n): move modal keys to appearances.show namespace in 7 locales
The modal translation keys (modals_title, modals_subtitle,
disable_modal_click_outside_title, disable_modal_click_outside_description)
were under settings.preferences.show but the view calls t(".modals_title")
from settings/appearances/show.html.erb. Moved them to
settings.appearances.show in de, es, nb, nl, ro, tr, and zh-TW.
* refactor(ds): decouple Dialog from Current.user via defaults_provider
---------
Co-authored-by: neko <neko@nixos>
|
||
|
|
6910518e81 |
fix(settings): use design-system checkbox for securities providers (#2430)
The securities-provider checkboxes used raw Tailwind utilities (rounded border-primary text-primary focus:ring-primary) instead of the design-system .checkbox component. In dark mode text-primary resolves to white, so a checked box rendered a white check on a white fill and the checkmark was invisible. Switch to the theme-aware .checkbox checkbox--light classes used by every other checkbox in the app (settings/preferences, transaction filters, etc.), which render a dark check on a light fill in dark mode. |
||
|
|
b3f70c8951 |
feat:Add SnapTrade OAuth device flow (#2523)
* Add SnapTrade OAuth connection flow * Restore SnapTrade brokerage portal links * Guard SnapTrade OAuth setup completion * Move SnapTrade OAuth start to POST * Use one SnapTrade item in provider panel * Fix SnapTrade OAuth controller tests * Fix SnapTrade OAuth drawer completion redirect * Update SnapTrade limits message. * Restrict SnapTrade OAuth scopes |
||
|
|
9487e6cbfb |
Allow multiple active API keys per user (#2077)
* feat(api): allow multiple active API keys per user Previously a user could hold only one active API key; creating a new one silently revoked the existing key, breaking any app using it. Allow multiple named active keys instead. - Drop the one-active-key-per-source validation; add name uniqueness among the user's active visible keys (revoked names are reusable, same name allowed across users). - Rewrite Settings::ApiKeysController as a RESTful collection (index/show/new/create/destroy); create no longer revokes existing keys, and key lookup is scoped to the current user's active keys. - resource :api_key -> resources :api_keys. - Add an API keys list view with per-key revoke and an empty state; rewrite the show page for a single key. - Update i18n (remove single-key copy, pluralise nav label) and tests. * refactor(api): address review on multiple API keys - Remove unreachable destroy branches (cannot_revoke is guarded by the .visible 404; revoke! raises rather than returning false) and document that .visible is the demo-key revocation guard. - Delete the orphaned created.html.erb / created.turbo_stream.erb templates (no action renders them) and their unused locale keys. - Extract shared partials (_scope_badges, _status_indicator, _key_meta, _key_reveal, _usage) to de-duplicate the index and show views; unify the active-status indicator on the standard dot. - Carry forward the @container / @lg:flex-row / min-w-0 responsive fixes from #2079 into the shared key-reveal partial. * test(api): cover newly-created API key confirmation render * refactor(api): harden demo-key guard and address review nits - Document the demo-key revocation guard on ApiKey's `visible` scope (the authoritative spot) and add a model test locking the invariant that `.visible` excludes the demo monitoring key. - _scope_badges: use an i18n lookup with a humanize fallback instead of bypassing translation for unknown scopes. - _key_reveal: drop the hard-coded `id` from the shared partial; the system test now locates the key via its data-clipboard-target. * refactor(api-keys): migrate hand-rolled badges to DS::Pill Replace raw span elements in scope badges and status indicator with DS::Pill to align with the design system migration convention. * fix(loans): opening anchor now uses current balance, not original principal When creating a loan manually, the opening anchor valuation was being set to `initial_balance` (the original loan principal) instead of `account.balance` (the current outstanding balance). After the sync job ran, `account.balance` was overwritten to match the anchor, making every manually-created loan show its original principal as the current balance. Fix by always using `account.balance` for the opening anchor in `create_and_sync`, and reading `Loan#original_balance` from the `loans.initial_balance` column directly (with a fallback to `first_valuation_amount` for provider-synced loans that may not have the column populated). * fix(api-keys): strip accidental loan changes; rescue revoke! failures The fix(loans) commit was accidentally committed into this branch. Remove the loan-related changes from account.rb, loan.rb, and both test files, restoring them to their pre-loan-commit state. Also fix the destroy action: revoke! uses update! internally which raises ActiveRecord::RecordInvalid on failure rather than returning false. Add rescue for RecordInvalid and RecordNotDestroyed so failures produce a flash alert instead of a 500. Re-adds the revoke_failed locale key that was dropped from settings.api_keys.destroy. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
dc2a565b6a |
feat(up): add Up Bank (AU) provider integration (#2391)
* feat(up): add Up Bank (AU) provider integration Adds Up Bank as a per-family, token-based bank sync provider, modelled on the existing Akahu integration. Up uses a JSON:API REST API with a personal access token (Bearer), cursor pagination via links.next, and returns both HELD (pending) and SETTLED transactions from one endpoint. New: - Provider::Up client (JSON:API unwrap, links.next pagination, retries, typed errors, /util/ping) + Provider::UpAdapter (Factory-registered, Depository + Loan). - UpItem / UpAccount models with Provided, Unlinking, Syncer, SyncCompleteEvent, Importer, Processor, Transactions::Processor, and UpEntry::Processor (amount sign flip, HELD->pending, foreignAmount FX, merchant from description, stale-pending pruning). - Family::UpConnectable, UpItemsController, routes, settings panel + connect flow views, accounts index wiring, initializer, en locale, and model tests. Core wiring: - "up" added to Transaction::PENDING_PROVIDERS, the three pending-match SQL blocks in Account::ProviderImportAdapter, Provider::Metadata::REGISTRY, ProviderMerchant/DataEnrichment source enums, ProviderConnectionStatus, settings provider panels, and financial data reset. Migration create_up_items_and_accounts must be run before use. No external API endpoints added (no OpenAPI changes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(up): dump up tables to schema and make since filter TZ-safe The feature commit added the up_items/up_accounts migration but never re-dumped db/schema.rb, leaving the schema version and tables stale. Add the two table definitions and foreign keys and bump the schema version so a fresh DB load matches the migration. Also format a bare Date `since` as UTC midnight instead of the server's local zone, so `filter[since]` is deterministic regardless of where the app runs (previously shifted by the local UTC offset). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(up): address code review feedback Behavior/correctness: - Persist skipped accounts via a new up_accounts.ignored flag and a needs_setup scope, so skipped accounts stop resurfacing as "needs setup" on every sync. Linking clears the flag. - destroy now checks unlink_all! per-account results and aborts deletion (alert) if any unlink failed, instead of swallowing failures. - render_provider_panel_error redirect uses :see_other (was an invalid 4xx redirect status). - Up provider adapter falls back to item institution name/url when institution_metadata is absent (early return previously blocked it). Resilience/security: - fetch_all_resources guards against an API repeating the same links.next cursor (Set#add?), preventing infinite pagination. - HTTP client validates absolute URLs (from links.next) against Up's HTTPS host before sending the bearer token, preventing credential leakage to untrusted hosts. Diagnostics: - Route provider sync/import failures through DebugLogEntry.capture (controller, UpItem, syncer, unlinking) with family/account context. Low-level HTTP client and currency-normalization warnings keep Rails.logger to match existing provider conventions. Data integrity: - up_accounts.name and currency are NOT NULL (align with model presence validations); account_id stays nullable (allow_nil uniqueness). Forms: - select_existing_account radio is required; controller guards a blank/ unknown up_account_id with a friendly alert instead of RecordNotFound. Tests: - Add UpAccount needs_setup scope test, pagination loop guard test, untrusted-host rejection test; tighten filter[since] assertion to the exact UTC timestamp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(up): address second-round review feedback - Capture sync/import failures via DebugLogEntry so swallowed errors in account/transaction fetching and transaction processing surface in /settings/debug instead of only Rails.logger. - Gate UP_DEBUG_RAW raw payload dump to local envs to avoid leaking PII (merchant names, amounts, account IDs) in managed/production logs. - Collapse linked/unlinked/total account counts into one memoized query instead of 3 separate COUNTs per rendered item. - Rename "Set Up Up Accounts" locale title to "Link Up Accounts". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(up): add method docstrings and align failed_result keys Add docstrings to all Up provider source files (controller, models, providers, concerns) to satisfy the 80% docstring coverage threshold. Third-round review: failed_result now mirrors import's result shape (accounts_updated/created/failed, transactions_imported/failed) instead of the stale accounts_imported key, so failure results stay consistent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
9de9a23ce2 |
feat(prices): add T-Invest (T-Bank) securities + brand-logo provider (#2408)
Adds Provider::TinkoffInvest, a token-based securities provider built on the public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand logos via the T-Invest CDN — the authoritative logo source for MOEX instruments, which ISS (MoexPublic) does not provide. - Registry: register `tinkoff_invest` under the :securities concept; token via ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key. - Logos independent of the price provider: Security#import_brand_logo consults T-Invest for a logo whenever a token is configured (after the price-provider metadata fetch, so it never short-circuits website_url backfill). Gated on token presence, not the securities checklist. - display_logo_url: with no website domain, a stored provider logo (T-Invest) now beats the ticker-only Brandfetch lettermark; when a domain exists, Brandfetch still wins (unchanged). - MoexPublic no longer reports moex.com as the issuer website — it's the exchange, not the issuer, and would make Brandfetch render the exchange logo for every instrument and shadow the real brand logo. - Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9; bonds priced as percent-of-par x nominal (missing nominal raises, not 0). - Settings: encrypted token field (always shown) + provider checkbox + en locale. - Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b57bb938da |
feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider (#2394)
* feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider Add Provider::MoexPublic, a keyless provider built on the free MOEX ISS API (https://iss.moex.com/iss), modeled on Provider::BinancePublic. Securities: shares, funds/ETF/БПИФ (e.g. LQDT), and bonds (OFZ + corporate). Bonds are priced clean — LAST% × FACEVALUE / 100 in the instrument currency, with per-row FACEVALUE for amortizing issues; NKD/accrued coupon excluded. Exchange rates: also implements ExchangeRateConcept for RUB↔{USD,EUR,CNY} via selt TOM instruments (USD000UTSTOM/EUR_RUB__TOM/CNYRUB_TOM); the selt quote is X/RUB, inverted for RUB→X, nil for non-RUB-crossed pairs. Details: - Board/engine resolution via the ISS primary-board flag with a hardcoded priority fallback (TQBR, TQTF, TQOB, TQCB, …). - Instrument currency from CURRENCYID/FACEUNIT (handles USD/CNY eurobonds & FX funds), normalizing legacy SUR/RUR → RUB; default RUB. - Full history via from/till + start= pagination; current price fallback chain LAST → MARKETPRICE → LCURRENTPRICE → LCLOSEPRICE → PREVPRICE → latest history close. - Bare SECID identity, exchange_operating_mic=MISX, country_code=nil (wildcard like Binance); search accepts .ME/.MOEX/.MISX/.MCX aliases and ISIN. - RateLimitable throttling, SslConfigurable, Faraday retry/timeouts; all public methods wrapped in with_provider_response. Wired into Provider::Registry for both :securities and :exchange_rates, the hosting provider-selection UI, locales, and config/exchanges.yml (MISX). Docker-tested (devcontainer, Ruby 3.4.9): 29 new tests green, full provider suite + i18n green, rubocop clean; smoke-tested against live ISS (SBER price, OFZ clean price, USD/RUB FX). * fix(moex): address review — FX weekend lookback, dead branch, translated hints - fetch_exchange_rate now fetches a 10-day lookback window (not just the exact day) so a weekend/holiday request resolves to the prior trading day's close, matching Yahoo's behavior (Codex P2). - Remove dead identical if/else branches in history_row_price (CodeRabbit). - Translate moex_public_hint into ca/fr/hu/vi/zh-CN instead of English copy (CodeRabbit). - Add a test covering the FX prior-trading-day lookback. * fix(moex): guard ISS date parsing; doc TQTE in board priority Address maintainer review (jjmata): - parse_iss_date wraps Date.parse so a malformed ISS TRADEDATE skips just that row (with a contextual log warning) instead of failing the whole history/FX fetch. Used in history_row_price and fx_history. - Add TQTE to the BOARD_PRIORITY doc comment (it was in the constant but missing from the comment). - Add a test covering the unparseable-date skip. |
||
|
|
6945b5a296 |
feat(security): warn when ActiveRecord encryption is not configured (#2362)
* feat(security): warn when ActiveRecord encryption is not configured Self-hosted instances without explicit ACTIVE_RECORD_ENCRYPTION_* keys (or Rails credentials) store sensitive columns - API keys, provider/bank tokens, the MFA (TOTP) secret, and PII - unencrypted at rest. The app boots and works normally so this plaintext at rest state is easy to miss. Change: Make it visible: - log a clear startup warning (config/initializers/encryption_warning.rb) - show a warning banner on /settings/security when encryption is unconfigured * refactor(security): apply review feedback on encryption warning - list the three ACTIVE_RECORD_ENCRYPTION_* keys in the banner, rendered via the DS::Alert content block to match the log - drop the redundant respond_to?(:self_hosted?) guard in the initializer so it matches the controller check - add a managed-mode test asserting the banner is hidden |
||
|
|
2d3d974466 |
fix(ds): canonical separators and destructive tokens in usage/rules tables (#2332)
The LLM usage table (Settings → AI usage) and the rules recent-runs table used hardcoded color classes instead of design-system tokens: - `divide-gray-100` separators — a fixed light gray with no dark-theme variant, so the row dividers render wrong in dark mode. - Raw reds for failed rows (`bg-red-50`/`bg-red-950`, `text-red-500/600`). Swap to the canonical tokens used by every other table (settings/debugs, admin/users, …): - divide-gray-100 -> divide-alpha-black-200 theme-dark:divide-alpha-white-200 - bg-red-50 / bg-red-950/30 -> bg-red-tint-5 / bg-red-tint-10 - text-red-* -> text-destructive (via the icon helper's color: param) Token-only; no structural or behavior change. Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com> |
||
|
|
5608f2b3fa |
fix(settings): give the MCP copy button success feedback (#2314)
* fix(settings): give the MCP copy button success feedback The MCP server URL Copy button copied to the clipboard but showed no feedback. It is a DS::Button (single icon), but clipboard_controller's showSuccess() unconditionally toggled iconDefault/iconSuccess targets — which that markup does not have — so it threw right after the copy and the user saw nothing. Guard the icon-swap path (still used by invite codes, MFA and profiles) and add a fallback that briefly flips the button's own label to Copied! via a new copiedText value. Wire it up on the MCP page. * fix(settings): capture copy button before async clipboard resolve event.currentTarget is null by the time the writeText().then() callback runs (it's only valid during event dispatch), so showSuccess received null and the label never flipped. Capture the button synchronously in copy() and pass it through. Verified in-browser: Copy -> Copied! -> Copy. * refactor(clipboard): unify feedback reset delay, harden label lookup Extract a shared RESET_DELAY_MS so the icon-swap and label-flash paths last the same duration when both copy buttons render on one page. Scope the label lookup to span.truncate (the DS::Button text node) so it ignores any future icon span. Per review feedback. --------- Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com> |
||
|
|
635938ec7b |
fix(ds): normalize legacy tooltip spacing to one recipe (#2311)
The four inline (non-DS::Tooltip) tooltips had drifted: three used p-2 rounded w-64, one used p-3 rounded w-72 with shadow-lg, and all used rounded (4px) where DS::Tooltip uses rounded-md (6px). Unify them on p-2 rounded-md w-64 — radius now matches DS::Tooltip and the lone p-3/w-72/shadow-lg outlier is gone, so the dark tooltips read consistently. |
||
|
|
007f84db1a |
refactor(settings): debugs page onto settings_section (#2289)
Consolidate the bespoke header card + filter card into one settings_section (title + subtitle) -- the canonical surface + an h2 (was a second <h1 font-semibold> below the layout's page-title h1, a heading-level + weight break). The log table stays an edge-to-edge bg-container card on purpose (settings_section's p-4 would inset it and float the thead). Left as follow-ups: the filter inputs' 10x repeated class strings (shared partial) and the bespoke empty state -> DS::EmptyState (needs #2143 on main). Page is super-admin-gated (Admin::BaseController), not renderable in the demo; verified via erb_lint + headless ERB compile + no-stray-markup grep. |
||
|
|
a249f9bda0 |
fix(ds): route mercury/ibkr provider panels onto sibling tokens (#2290)
Two literal-color outliers in app/views/settings/providers/, both fixed by matching the sibling panels in the same directory: - _mercury_panel: the per-item initial avatar used bg-blue-600/10 + text-blue-600. Every other settings/providers panel renders this generic avatar neutral (akahu/brex use bg-surface|bg-container-inset + text-primary). The literal blue was also a dark-mode contrast risk. -> bg-surface + text-primary. - _ibkr_panel: the 'not configured' status dot used a literal bg-gray-400 while the sibling brex panel's equivalent dot uses the bg-surface-inset token (paired with bg-success for the configured state). -> bg-surface-inset. Token-only swaps, theme-safe, no layout change. |
||
|
|
2075c8c41c |
feat(mcp): OAuth 2.1 auth for MCP — connect Claude.ai with your Sure login (#2234)
* feat(mcp): add OAuth well-known discovery endpoints (RFC 8414 + RFC 9728) Serves /.well-known/oauth-protected-resource (RFC 9728) and /.well-known/oauth-authorization-server (RFC 8414) so MCP clients can auto-discover the authorization server. Both endpoints are unauthenticated and respect APP_URL for reverse-proxy deployments. * feat(mcp): add dynamic client registration endpoint (RFC 7591) POST /register creates a public Doorkeeper::Application on demand so MCP clients (e.g. Claude.ai) can self-register without manual setup. Validates redirect_uris (including blank entries), falls back to "MCP Client" name, returns no client_secret (public client, PKCE only). Rate-limited to 10 registrations/min/IP via Rack::Attack. * feat(mcp): authenticate via Doorkeeper OAuth2, keep MCP_API_TOKEN as fallback MCP endpoint now accepts OAuth2 Bearer tokens issued by Doorkeeper. Falls back to the existing MCP_API_TOKEN env-var flow so self-hosted deployments are not broken. Requires MCP_OAUTH_ENABLED or MCP_API_TOKEN to be set — the endpoint returns 503 otherwise. - OauthBase concern provides APP_URL-aware configured_base_url (trailing slash stripped to prevent double-slash URLs) - Bearer scheme parsed case-insensitively (RFC 7235) - Only read_write scope accepted — read scope would allow mutating tools (CreateGoal, ImportBankStatement), so read-only tokens are rejected - Deactivated users rejected even with a valid Doorkeeper token - WWW-Authenticate header on 401 points to RFC 9728 resource metadata - SHA-256 digest used for constant-time env-var comparison - Rack::Attack throttle added for POST /register - Routes wired: /.well-known/*, /register, use_doorkeeper * fix(mcp): disable Turbo on OAuth consent form for external redirect URIs Turbo was intercepting the authorization form POST and XHR-fetching the redirect_uri (e.g. https://claude.ai/api/mcp/auth_callback), which CORS blocks. Extend the existing turbo_disabled guard to cover any redirect_uri that doesn't originate from the app itself. * feat(mcp): add Settings::McpController with connected clients view - Settings > MCP page (under Advanced) shows the MCP server URL with copy button and step-by-step instructions for connecting Claude.ai - Lists active non-mobile OAuth tokens with app name and revoke action; mobile device tokens are excluded to prevent accidental disconnection - Removes the MCP_OAUTH_ENABLED env-var gate — OAuth auth is always available since Doorkeeper handles consent; MCP_API_TOKEN remains as a self-hosted fallback * fix(mcp): remove client_credentials from grant_types_supported metadata Only authorization_code is supported by the registration endpoint. Advertising client_credentials was misleading — a client that reads the metadata and attempts that flow would get an application with the wrong grant type. |
||
|
|
77dda53ffb |
feat(ds): one canonical focus ring across primitives (#2140)
* feat(ds): one canonical focus ring across primitives (#2136) Replaces the grab-bag of per-primitive focus indicators (neutral ring-alpha-black/white, outline-gray-900/white, faint form-field ring-4) with a single recipe — the #1737 accessibility follow-up. - New --color-focus-ring token: blue-600 (light) / blue-500 (dark), >=4:1 against both surfaces. - Canonical .focus-ring / .focus-ring-within in components.css: a 2px outline + 2px offset on :focus-visible only. Outline (not a box-shadow ring) so the offset gap is transparent on any surface with no layout shift; :focus-visible so it never shows for mouse/touch. - Applied to every focusable DS primitive: Button (had none), Link, Disclosure summary, Tabs nav, MenuItem (replaces the browser-default box), SearchInput, Tooltip trigger, Popover trigger, Select panel (focus-within), Toggle (peer-driven outline-focus-ring). .form-field adopts it via :focus-within, replacing the ~1:1 ring-4. - Dialog close button is a DS::Button icon variant, so it inherits the focus-visible-only ring and keeps no resting border (fixes "stuck ring"). Verified in-browser, light+dark: focus-visible ring on button, input, and full-width menu row — consistent blue 2px+offset, legible on both surfaces. Remaining follow-up: >=44px touch targets (disclosure trigger, composer send); bespoke notification / account-new close buttons that still carry a permanent border. * fix(ds): #2136 interactive-state follow-ups — touch target + close-button chrome - Disclosure default trigger: add min-h-11 (44px) so the standalone disclosure summary clears the touch-target minimum (was px-3 py-2 ~36px). Composer send + the coming-soon icons are already DS::Button icon/md (w-11 h-11). - Notification close buttons (sync_toast, notice): drop the resting border-alpha-black-50 box ("frame shouts, glyph muted"); keep a bg-container + shadow-xs chip so the corner control stays visible over the page, and brighten the muted glyph on hover (text-subdued -> hover:text-primary). * refactor(ds): focus ring -> neutral hugging box-shadow (was blue outline) Per design feedback: the blue 2px outline + 2px offset read as a loud, detached frame on the otherwise-neutral UI. Switch the canonical .focus-ring to a soft box-shadow ring that hugs the control (follows border-radius, no gap), in the theme-aware neutral focus-ring token (alpha-black/white-400). Transparent outline kept as a forced-colors fallback; toggle peer-driver switched from outline-* to ring-* to match. Still one token, :focus-visible only. Strength is tunable (currently subtle ~1.5:1). * fix(ds): focus ring vanished on shadowed controls — outline, not box-shadow The neutral box-shadow ring lived in the components layer, so any utility-layer shadow-* (or .form-field's focus-within:shadow-none) on the same element overrode it and the ring silently disappeared on shadowed buttons/inputs. Draw the same subtle neutral ring with a hugging `outline` (outline-offset: 0) instead — a separate property with no box-shadow conflict, and it doubles as the forced-colors indicator. Toggle peer-driver switched ring-* -> outline-* to match. Look is unchanged (neutral, hugging, subtle); it just no longer vanishes. * fix(a11y): enlarge sync-toast close-button touch target (p-0.5 -> p-1.5) The hover-revealed close button had ~2px padding around a 20px icon (~24px total), at the WCAG 2.5.8 AAA boundary. p-1.5 brings the interactive area to ~32px. Addresses CodeRabbit review on #2140. * fix(ds): keep form-field's resting halo; stop the outline color flash Two testing findings: - .form-field reverts to its original always-on soft ring (focus-within ring-4 at low alpha, theme-aware) instead of adopting the keyboard-only outline. It's a resting decoration, not a focus indicator, and the lower-opacity halo was the better look. The canonical block's comment documents the deliberate opt-out. - .focus-ring/.focus-ring-within now carry a base transparent 2px outline so consumers with transition-all (form-field had it) animate transparent -> token on focus instead of passing through currentColor, which flashed as a black border appearing and then fading out. * feat(ds): focus-ring token clears WCAG 3:1 non-text contrast alpha-black-400 (20%) measured ~1.6:1 against white — visible but below the AA bar for focus indicators. Bump to the 700 stop (50%): ~3.95:1 on light containers, ~4.6:1 on dark. Recipe unchanged; one token edit via tokens:build. * fix(ds): ring the hand-rolled privacy toggle too The header pair showed two different focus treatments: panel-right (a DS::Button) got the new token ring while the hand-rolled privacy toggle next to it fell back to the browser-default ring — the sweep covered DS primitives but not bespoke buttons. Both privacy toggles (mobile + desktop) now carry .focus-ring. Also documents the transition interplay on the focused-state rule: consumers with transition-colors fade the ring in over 150ms because Tailwind v4's color transition list includes outline-color. Verified settled value at the intended 50% alpha via Playwright. * fix(ds): ring the sidebar and settings nav links The reshoot caught both nav species falling back to the browser's blue default ring — main sidebar items and settings nav items are bespoke link_to markup the primitive sweep missed, and they're the primary keyboard path in the app. Both adopt .focus-ring (main nav adds rounded-lg so the outline follows a shape). * fix(ds): retire the legacy base-layer button ring for the canonical outline The @layer base button rule still painted a ring-2 ring-offset-2 box-shadow on :focus-visible. Box-shadow and outline are independent properties, so .focus-ring (an outline) could never clear it and every button-tag primitive double-painted both indicators on keyboard focus. Apply the canonical recipe to the base button rule itself: every <button> now gets the transparent resting outline + focus-ring token on :focus-visible by default. Two bespoke buttons suppressed the outline with focus:outline-none and relied on the base ring for their keyboard indicator (category dropdown rows, the sign-up password toggle). Drop the suppression so they pick up the canonical outline — :focus-visible keeps it keyboard-only, which is what the suppression was protecting against anyway. * fix(ds): segmented control adopts the canonical focus recipe The segment rule inlined its own focus-visible outline (offset 2, alpha-400 colors) with a comment noting it was temporary until the canonical token landed — this branch is that token. Drop the inlined utilities: button segments get the outline from the base button rule, and link segments now carry .focus-ring. |
||
|
|
74d0452a2b |
fix(ds): sync-settings env notice used undefined warning-* classes -> DS::Alert (#2278)
settings/hostings/_sync_settings.html.erb rendered the 'configured via env' notice with bg-warning-50 / border-warning-200 / text-warning-600 / text-warning-800 -- none of which exist as Tailwind utilities, so the box rendered fully unstyled (no tint, no border, default text color). Replace the hand-built box with the canonical DS::Alert(:warning), matching the warning-surface recipe and the ds-notice-neutral-text convention. |
||
|
|
8736c7c27e |
refactor(settings): consistency pass — header-less settings_section + guides (#2279)
* refactor(settings): header-less settings_section variant + migrate guides
Add a title:nil branch to settings_section so a page can route through the one
canonical surface recipe (bg-container shadow-border-xs rounded-xl p-4 space-y-4)
without a section header. No visual change to existing titled callers.
Migrate the guides page onto it: replace the hand-rolled card + hardcoded
'Guides' page title with settings_section + t('.page_title') (new locale key).
First step of the settings design-consistency pass.
* refactor(family-exports): un-nest exports list
The exports list rendered 'Exports' three times (page h1 + settings_section
title + inset count header) inside three stacked surfaces (section card >
bg-container-inset > inner bg-container table card). Flatten it:
- settings_section header-less (drop the duplicate title; the page h1 + the
inset count header already label it),
- drop the redundant inner space-y-4 wrapper,
- table sits directly in the inset (remove the inner bg-container card).
Now: one title, one card, one inset.
* refactor(settings): payments + appearances consistency
- payments: subscription summary row was bg-container inside the section's
bg-container (container-on-container) -> bg-container-inset + p-4.
- appearances: toggle-row labels used <h4> (heading-level break inside a
settings_section) -> <p font-medium>.
* refactor(settings): preferences consistency
- month_start_day warning: text-warning bg-warning/10 colored-body-text box
-> DS::Alert(:warning) (neutral-text recipe).
- preview-features block: hand-rolled <section bg-container shadow-border-xs
rounded-xl p-4> -> header-less settings_section (canonical surface).
- toggle-row <h4> -> <p font-medium>; text-[11px] base-currency badge -> text-xs.
* refactor(settings): profiles consistency
- unconfirmed-email notice was hardcoded English -> i18n
(unconfirmed_email_notice_html + resend_confirmation_link keys).
- role + pending chips: bespoke 'rounded-md bg-surface px-1.5 py-0.5 uppercase'
pills -> DS::Pill (tone: gray, badge mode).
- pending-invitation row border: border-alpha-black-25 -> shadow-border-xs
(match the member-row token).
|
||
|
|
d29591cff9 |
fix(ds): unify tab/chip controls on DS::SegmentedControl (#8) (#2284)
* fix(ds): goals status filter -> DS::SegmentedControl The goals status chips were a hand-rolled segmented control: active state toggled via ad-hoc bg-container/shadow-border-xs/text-* classes, NO hover on inactive chips, and a light-only focus ring (ring-alpha-black-100, invisible in dark mode). Migrate to DS::SegmentedControl button segments + toggle the canonical --active class in goals_filter_controller#syncChipState. Gains the dark-safe hover (hover:bg-gray-200 / theme-dark:hover:bg-gray-800) and the canonical focus ring. First control in the tab-consistency pass (#8). * fix(ds): provider filter chips -> DS::SegmentedControl Same hand-rolled segmented control as the goals chips: active toggled via ad-hoc bg-container/shadow/text classes, no inactive hover, light-only ring-alpha-black-100 focus ring. Migrate to DS::SegmentedControl + toggle the canonical --active class in providers_filter_controller#syncChipState. * fix(ds): transaction-type tabs -> DS::SegmentedControl Expense/Income/Transfer tabs were hand-rolled with a hover==active affordance bug: inactive hover raised bg to bg-container, identical to the active state, so hover and selected were indistinguishable. Migrate to DS::SegmentedControl (link segments + icons); the controller toggles the canonical --active class instead of swapping ad-hoc ACTIVE/INACTIVE class lists. The client-side nature switch (expense<->income updates the form's hidden nature field without navigating) is preserved -- verified live: switching to Income flips the active segment AND sets the nature field to inflow. * fix(ds): reports period tabs -> DS::SegmentedControl The Monthly/Quarterly/YTD/Last-6-Months/Custom period selector was five DS::Link ghost/secondary buttons -- a different tab idiom than the rest of the app. Migrate to DS::SegmentedControl link segments (server-rendered active via aria-current; pure navigation, no controller). Now matches the goals / provider / transaction-type controls + the AI provider picker. Also autocorrected a pre-existing single-quote in the next-period aria-label. |
||
|
|
c45b786e86 |
fix(ds): migrate SSO amber warning surfaces to DS::Alert / DS::Pill (#2246)
Apply the resolved warning-hue decision (use --color-warning; no separate amber token) to the admin SSO surfaces flagged in #2198: - admin/sso_providers: the "legacy providers" notice → DS::Alert(:warning); the "ENV configured" badge → DS::Pill(tone: :warning). - settings/securities: the single-OIDC password warning → DS::Alert(:warning). Replaces hand-rolled bg-amber-50 / border-amber-200 / text-amber-800 boxes and an amber badge with the functional warning token via DS components. The DS::Alert warning recipe (warning tint + neutral text + alert-triangle) matches the prior look while clearing the literal-token drift. Part of #2198. |
||
|
|
4dbfbf0bc8 |
fix(ds): replace invalid bg-surface-default with bg-surface (#2244)
`bg-surface-default` is not a defined design-system token — Tailwind emits no rule for it, so these surfaces render with no background at all. Five call sites were affected: - rules/index.html.erb — recent-runs table header - settings/llm_usages/show.html.erb — usage table header - settings/ai_prompts/show.html.erb — three prompt-preview boxes Replace with the canonical `bg-surface` token — the same fill the admin/users and settings/debugs table headers already use. Clears a Rule 2 (non-functional token) finding from the weekly DS drift scan. |
||
|
|
c375b8bf5c |
feat(ai): self-host settings UI for Anthropic provider (5/5) (#1987)
* feat(ai): add Anthropic provider with chat parity (1/5)
Introduces Provider::Anthropic alongside Provider::Openai, implementing
the LlmConcept chat_response contract over the official anthropic Ruby
SDK. Batch ops, PDF, and RAG land in follow-up PRs.
- Provider::Anthropic uses Messages API for sync and streaming responses
- ChatConfig builds requests with ephemeral prompt-cache markers on the
system prompt and the last tool definition
- MessageFormatter reconstructs multi-turn history (text + tool_use +
tool_result blocks) from raw Message records, including the paired
user-role tool_result turn Anthropic requires after every tool_use
- ChatParser maps Anthropic Message into the shared ChatResponse Data
- Registry, Setting, User, Chat default model wired for ANTHROPIC_*
envs and Setting.anthropic_*; LLM_PROVIDER selects between providers
- Responder forwards raw conversation_history (Array<Message>) so
providers without hosted conversation state can rebuild context
- OpenAI provider accepts and ignores the new kwarg (no behavior change)
Tests cover provider init, model gating, MessageFormatter for all turn
shapes, ChatConfig request building (max_tokens, system cache, tool
conversion), ChatParser for text / tool_use / mixed blocks, Registry
discovery, and mocked chat_response success / error / function_request
paths. Live VCR cassettes recorded in a follow-up with a real key.
Stacked PRs: 2/5 batch ops + cost ledger, 3/5 PDF, 4/5 pgvector RAG,
5/5 settings UI + disclosure.
* fix(ai): address PR review on Anthropic provider foundation
Surface fixes raised by Codex + CodeRabbit on PR 1/5:
- Provider::Anthropic#chat_response now accepts (and ignores) a
`messages:` kwarg. Assistant::Responder passes both `messages:`
(OpenAI-shape) and `conversation_history:` (raw Message records) for
cross-provider parity, so the previous signature raised
ArgumentError on the first chat turn through the Anthropic provider.
- Provider::Anthropic#supports_model? bypasses the `claude` prefix
gate when a custom base_url is configured, mirroring the OpenAI
provider. Bedrock-shaped IDs like
`anthropic.claude-sonnet-4-5-20250929-v1:0` and
`claude-opus-4@20250514` are otherwise rejected by
Assistant::Provided#get_model_provider and the chat dies.
- Setting.anthropic_access_token is now in
EncryptedSettingFields::ENCRYPTED_FIELDS so the Anthropic API key
is encrypted at rest like every other provider secret. Previously
plaintext while siblings (openai_access_token, twelve_data_api_key,
external_assistant_token) were ciphertext.
- Chat.default_model falls back to whichever provider is actually
configured. Previously, with LLM_PROVIDER=anthropic but no
Anthropic credentials, the default model resolved to a Claude ID
that no registered provider supported, so chats failed even when
OpenAI was fully configured. Adds Provider::{Anthropic,Openai}#configured?
class methods for the readable callsite.
- Provider::Anthropic.effective_model uses
`ENV["ANTHROPIC_MODEL"].presence || Setting.anthropic_model` so the
Setting lookup is only performed when the env var is absent — the
previous `ENV.fetch(KEY, default)` evaluated the default arg
eagerly on every call.
- Provider::Anthropic::ChatConfig#anthropic_input_schema strips both
`:strict` and `"strict"` keys so JSON-decoded schemas with string
keys cannot leak the OpenAI-only flag through to Anthropic.
Test coverage added: supports_model? bypass on custom endpoints,
chat_response messages: kwarg compatibility, default_model fallback
in the three credential combinations, configured? against ENV +
Setting, strict-flag stripping for both key types, and a
`Setting.expects(:anthropic_model).never` assertion proving the
ENV-precedence test now exercises the lazy path.
All 4365 tests pass (1 pre-existing libvips env error unrelated).
* test(chat): make default_model tests resilient to ENV model overrides
CodeRabbit flagged on PR review: the new default_model tests asserted
against Provider::*::DEFAULT_MODEL, but Chat.default_model actually
returns Provider::*.effective_model.presence (which reads
OPENAI_MODEL / ANTHROPIC_MODEL from the environment). With either env
var set, the tests would fail intermittently even though routing was
correct.
- New default_model tests now assert against the provider's
effective_model directly, so they verify the routing decision
(which provider's value wins) without coupling to the constant.
- Pre-existing "creates with default model" assertions had the same
brittleness; switch them to compare against Chat.default_model so
the chosen model is whatever the env / Setting cascade resolves to.
Verified by running `ANTHROPIC_MODEL=claude-haiku-4-5 OPENAI_MODEL=gpt-4o
bin/rails test test/models/chat_test.rb` — 16 runs, 0 failures
(previously 2 pre-existing failures + 0 from the new tests).
* fix(ai): address local review on Anthropic foundation
- Provider::Anthropic#supports_pdf_processing? bypasses prefix gate for
custom endpoints, mirroring supports_model?
- Provider::Anthropic#initialize raises Error when custom_endpoint? AND
model.blank?, parity with Provider::Openai
- stream_chat_response captures partial usage on mid-stream errors and
records it via the new on_partial callback so chat_response can skip
the duplicate error row in the outer rescue
- safe_accumulated_message swallows the secondary failure when the SDK
cannot reconstruct a snapshot
- langfuse_client memoizes properly (||= instead of =) so repeated calls
don't churn Langfuse instances
- MessageFormatter sorts tool_calls by created_at then id so the
message array is deterministic across replays; skips tool_calls
missing both provider_call_id and provider_id rather than sending
`id: nil` and getting rejected by Anthropic
- Setting.anthropic_access_token default falls back through
ENV["ANTHROPIC_API_KEY"].presence (was missing .presence, so an
empty-string env value bled through)
- User#openai_configured? / #anthropic_configured? delegate to the
Provider::* class methods — single source of truth
- Assistant::Responder renames the OpenAI-shape history builder
conversation_history → openai_messages_payload so the kwarg name
matches the local method name (messages: openai_messages_payload,
conversation_history: chat_message_records)
- Assistant::Builtin stale-history comment updated to reference both
builders
Adds a streaming chat_response test using ad-hoc subclasses of the
SDK event types so the case/when dispatch matches via is_a? without
stubbing class-level === behavior.
* test(ai): add Anthropic tool_use round-trip + multi-tool turn coverage
Addresses @jjmata's "worth confirming" note on PR #1983: tool-use turns
from prior assistant messages must round-trip correctly when retrieved
from the database.
- New `ChatParser → ToolCall::Function → MessageFormatter` test walks
the full path: Anthropic response with a tool_use block →
ChatFunctionRequest → ToolCall::Function.from_function_request →
persisted on the AssistantMessage → MessageFormatter rebuild on the
next turn. Asserts the original `tool_use.id` is preserved end-to-end
as both `tool_use.id` and the paired `tool_result.tool_use_id`, and
that the original `input` hash and serialized result content survive.
- New multi-tool assistant turn test confirms two tool_use blocks on a
single assistant message render as two tool_use blocks followed by
two paired tool_result blocks in a single user-role follow-up,
matching Anthropic's required alternation.
Both tests exercise the existing PR1 code without behavior changes.
* test(ai): require "ostruct" explicitly in Anthropic provider tests
OpenStruct is moving out of Ruby's default load path (warning in 3.4+,
removed in 3.5+). Tests work today because ActiveSupport transitively
loads it, but that's incidental. Match the existing convention in
test/controllers/settings/hostings_controller_test.rb which explicitly
requires ostruct for the same reason.
* fix(ai): sanitize Langfuse warn logs, normalize tool_use.input, dedup history fetch
Addresses three open CodeRabbit findings on PR #1983.
- Provider::Anthropic Langfuse rescue branches no longer include
`e.full_message` in `Rails.logger.warn`. `full_message` bundles the
backtrace + cause chain and on some SDK error types includes the
serialized request/response payload (prompt, model output). Logs
now report `#{e.class}: #{e.message}` only. Three sites:
create_langfuse_trace, log_langfuse_generation, upsert_langfuse_trace.
Note: Provider::Openai has the same pattern (copy-pasted source) —
harmonization deferred to a follow-up cleanup PR; this commit fixes
only the Anthropic provider to keep PR scope tight.
- MessageFormatter#parse_arguments now coerces any non-Hash parsed
result to `{}`. Anthropic's Messages API requires `tool_use.input`
to be a JSON object (map); a stored ToolCall::Function record whose
arguments parse to a scalar, bool, or array (corrupt row, legacy
data, cross-provider bleed) would otherwise produce a payload the
API rejects. Normal flow stores Hash arguments end-to-end so the
fix is defensive — adds 2 tests covering scalar/array JSON strings
and non-String non-Hash inputs.
- Assistant::Responder dedups the chat-history fetch. The previous
layout fired two near-identical `chat.messages.where(...).includes(
:tool_calls).ordered` queries per LLM turn (one for the OpenAI-shape
payload, one for the raw-records kwarg). A new memoized
`complete_chat_messages` fetches once; `chat_message_records` filters
out the current message via `Array#reject`, `openai_messages_payload`
iterates the cached array unchanged. One SQL query per turn instead
of two. Memoization scope = single Responder instance (per LLM call),
so cache invalidation is not a concern.
All 4370 tests pass (1 pre-existing libvips env error unrelated).
Rubocop + brakeman clean.
* fix(ci): replace sk-ant- prefixed test placeholders
Pipelock secret scanner pattern-matches `sk-ant-*` as a real Anthropic
API key and fails the PR security-scan check. Test stubs and
ClimateControl env values used `sk-ant-test`, `sk-ant-from-setting`,
`sk-ant-x`, `sk-ant-y` as obvious placeholders, but the scanner does
not care about value entropy.
Switched to `fake-anthropic-key-*` / `fake-token-*` strings so the
scanner stops flagging them. No production code touched, no behavior
change — Provider::Anthropic still accepts any non-blank token.
* feat(ai): add Anthropic batch ops + LLM cost ledger (2/5)
Implements auto_categorize, auto_detect_merchants, and
enhance_provider_merchants on Provider::Anthropic via forced tool calls,
plus the cost-ledger plumbing they need.
- Provider::Anthropic::AutoCategorizer, AutoMerchantDetector,
ProviderMerchantEnhancer each define a single output tool whose
input_schema mirrors the desired output, then force the model to call
it via tool_choice: { type: "tool", name: ..., disable_parallel_tool_use: true }.
Anthropic guarantees the tool_use.input matches the schema, so there
is no JSON parsing fragility, no <think> tag stripping, and no
json_object/json_schema fallback ladders.
- Concerns::UsageRecorder mirrors the OpenAI sibling but persists
cache_creation_input_tokens / cache_read_input_tokens to dedicated
columns instead of metadata.
- Migration adds cache_creation_tokens, cache_read_tokens (nullable
integers) to llm_usages. OpenAI rows leave them null.
- LlmUsage::PRICING gains Claude 4.x rows (opus-4-7 $15/$75, sonnet-4-6
$3/$15, haiku-4-5 $1/$5 per MTok). infer_provider returns "anthropic"
for claude-* via the existing exact/prefix lookup.
- Provider::Anthropic#chat_response now persists cache columns directly
rather than stashing them in metadata.
- 25-transaction batch cap mirrors the OpenAI provider so the cost
ledger sees the same shape regardless of which provider ran a batch.
Tests cover the forced-tool-call path, null/None normalization,
case-insensitive merchant matching, the missing-tool_use error path,
and Anthropic-specific pricing + provider inference on LlmUsage.
Stacked on #1983 (PR 1/5). 3/5 PDF + vision next.
* fix(ai): attribute Bedrock model IDs to anthropic + clean nil enum
- LlmUsage.infer_provider now returns "anthropic" for Bedrock /
Vertex shaped IDs (anthropic.* and anthropic/*), so cost-ledger
filtering by provider stays correct even when no per-MTok rate is
stored. Previously these IDs fell through to the "openai" default.
- AutoCategorizer drops the redundant nil sentinel from the
category_name enum — the union type [string, null] already permits
null, and some JSON Schema validators reject nil literals inside
enum arrays.
* test(ai): require "ostruct" in Anthropic batch op tests
Same rationale as the PR1 ostruct fix — explicit require so the tests
don't depend on ActiveSupport's transitive load when Ruby 3.5+ removes
OpenStruct from the default load path.
* feat(ai): Anthropic native PDF processing (3/5)
Implements process_pdf and extract_bank_statement on Provider::Anthropic
using the native `document` content block — no rasterization, no text
pre-extraction.
- Provider::Anthropic::PdfProcessor classifies the document, summarizes
it, and extracts statement metadata via a forced report_document_analysis
tool whose input_schema mirrors the existing Provider::Openai output
(document_type from Import::DOCUMENT_TYPES, summary, extracted_data).
- Provider::Anthropic::BankStatementExtractor returns the same
{ transactions, period, account_holder, account_number, bank_name,
opening_balance, closing_balance } shape via report_bank_statement so
downstream pdf_import code is provider-agnostic.
- Both attach the PDF as
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: <b64> } }
— Claude 3.5+ / 4.x accept this natively (up to 32MB / 100 pages).
No pdf-reader, no pdftoppm, no chunking for typical statements.
- supports_pdf_processing? (introduced in PR 1) already returns true for
claude-* models, gating process_pdf with a clear error otherwise.
- Cost ledger rows are persisted via the shared UsageRecorder concern,
including cache_creation/cache_read tokens.
Tests verify the document block shape, tool_choice forcing, normalized
document_type for unknown classifications, transaction normalization
(date / amount / reference → notes), and the missing-tool_use error
path. Blank pdf_content raises before any client call.
Stacked on #1984 (PR 2/5). 4/5 pgvector RAG next.
* fix(ai): guard PDF size + surface bank-statement truncation
- PdfProcessor and BankStatementExtractor raise upfront when
pdf_content.bytesize exceeds MAX_PDF_BYTES (32 MB, matching
Anthropic's hard limit). Previously a 100 MB PDF would be
base64-encoded (~133 MB) and packed into the JSON body before
the API rejected it — peak heap ~270 MB per Sidekiq worker.
- BankStatementExtractor inspects response.stop_reason; when the
model hit max_tokens it logs a warning and flags result[:truncated]
so downstream callers know the transaction list may be incomplete.
- ISO date pattern added to statement_period_start/end schema in
PdfProcessor so the model can't return "March 2026" — Anthropic
enforces the regex via the tool's input_schema.
Tests cover the size guard (raises before any client.messages call),
truncated-result flagging, and the warning log path.
* test(ai): require "ostruct" in Anthropic PDF tests
Match the explicit ostruct require added in PR1/PR2 — same Ruby 3.5+
load-path reason.
* feat(ai): default Anthropic installs to pgvector RAG (4/5)
The provider-agnostic vector store stack (VectorStore::Pgvector + the
Embeddable concern) already shipped to main. This PR closes the
Anthropic loop:
- VectorStore::Registry.adapter_name now returns :pgvector when
Setting.llm_provider == "anthropic" and no explicit
VECTOR_STORE_PROVIDER override is set. Anthropic has no hosted vector
store, so falling back to the local pgvector adapter is the only
correct default. Explicit VECTOR_STORE_PROVIDER still wins.
- SearchFamilyFiles surfaces a longer message when no adapter is wired
up — calling out pgvector + EMBEDDING_URI_BASE as the supported
Anthropic-only path so the user is not stuck with an "OpenAI required"
hint that is no longer accurate.
The Embeddable concern already pulls embeddings from
EMBEDDING_URI_BASE / EMBEDDING_ACCESS_TOKEN (with OpenAI as fallback),
so Anthropic installs point this at Voyage AI, a local Ollama instance,
or OpenAI embeddings — independent of the chat provider.
Tests cover the new default routing, the existing OpenAI default
staying intact, and explicit VECTOR_STORE_PROVIDER overriding the
Anthropic default.
Stacked on #1985 (PR 3/5). 5/5 settings UI + retention disclosure next.
* feat(ai): self-host settings UI for Anthropic provider (5/5)
Adds the Anthropic panel and the install-wide LLM provider selector to
the self-hosting settings page, plus a shared data-retention
disclosure that covers both OpenAI and Anthropic.
- New _llm_provider_selector partial: select for Setting.llm_provider
(openai | anthropic), respects the LLM_PROVIDER env var (disables the
control + shows the "configured through environment variables" hint
when set, mirroring the existing OpenAI panel behaviour), and renders
a compact data-handling block with one-line retention statements for
each provider.
- New _anthropic_settings partial mirrors _openai_settings exactly:
password-field for the API key with **** redaction, optional
base_url (for AWS Bedrock / GCP Vertex), optional default model. All
three fields disable when their ENV var is set.
- show.html.erb renders provider selector + OpenAI panel + Anthropic
panel under the same "General" section so users can configure either
(or both) without switching pages.
- Settings::HostingsController#update now permits and persists
anthropic_access_token (ignoring the **** placeholder, same pattern
as OpenAI), anthropic_base_url, anthropic_model, and llm_provider
(validated against %w[openai anthropic]). On Setting::ValidationError
the rescue branch preserves anthropic_base_url / anthropic_model
input so the form re-renders with the user's typed values intact —
parity with the issue #1824 fix for OpenAI.
- Locale keys added under settings.hostings.{llm_provider_selector,
anthropic_settings}.
Tests cover token update + placeholder redaction, base_url + model
update, llm_provider switch to anthropic, and rejection of unknown
provider values. The existing GET render test still passes, exercising
all three new partials.
Closes the 5/5 Anthropic series stacked on #1986.
* fix(ai): valid Tailwind token + base_url URL validation
- Data-handling block in _llm_provider_selector swaps the invalid
bg-surface-secondary token for bg-container-inset, matching the
inset-card pattern used elsewhere in sure-design-system/components.css.
bg-surface-secondary is not defined anywhere in the design system —
Tailwind treated it as a no-op, so the block rendered with no
background contrast.
- Settings::HostingsController validates anthropic_base_url as a
URI::HTTP (catches https too) and raises Setting::ValidationError
with a localized message when the input is not parseable.
Previously any string was persisted, surfacing as an opaque
connection error at request time instead of an immediate UX failure.
- Blank base_url now clears the setting (was already the case but
exercised explicitly in tests now).
* fix(ci): replace sk-ant- prefixed token in hostings controller test
Same pipelock secret-scan trigger as PR1 fix on registry/anthropic
tests. The sk-ant-* prefix is matched verbatim by the scanner
regardless of value entropy.
* fix(ai): provision pgvector table when it is the default store
#1986 makes pgvector the default vector store for Anthropic installs, but
CreateVectorStoreChunks only ran when VECTOR_STORE_PROVIDER=pgvector was set
explicitly — so a fresh Anthropic-only install migrated without the
vector_store_chunks table and failed on uploads/searches.
Add VectorStore::Registry.pgvector_effective? as the single source of truth
for "is pgvector active?" (explicit env OR the Anthropic default), and a new
idempotent migration that enables the extension + creates the table whenever
pgvector is effective and the table is missing — covering fresh and
already-migrated installs without drift. Addresses Codex P1.
* fix(ai): provision pgvector table for Anthropic-default installs
Migration gated on raw VECTOR_STORE_PROVIDER==pgvector, so an
Anthropic-default install (which selects pgvector implicitly via
Setting.llm_provider without setting VECTOR_STORE_PROVIDER) skipped
table creation and failed later on a missing vector_store_chunks
relation. Route through VectorStore::Registry.pgvector_effective? —
the single source of truth already shared by the adapter selection.
Addresses Codex P1 review finding.
* fix(ai): provision pgvector chunks table on schema-load installs
The ensure-migration only helps db:migrate upgraders. Fresh installs go
through bin/docker-entrypoint's db:prepare, which loads schema.rb (the
conditional table can't be dumped there — it needs the vector extension)
and marks every migration applied without running it. An Anthropic-only
fresh install therefore selected the pgvector adapter but had no table,
failing with raw PG errors on first upload or search.
Two layers close it:
- VectorStore::Pgvector#ensure_schema! provisions the table idempotently
on first use (mirrors CreateVectorStoreChunks; memoized; failures wrap
in VectorStore::Error, which with_response turns into a clean failed
response).
- VectorStore::Registry#build_pgvector now gates on
VectorStore::Pgvector.available? (table exists, or extension present),
so installs whose Postgres lacks pgvector entirely degrade to the
assistant's provider_not_configured message instead of raising
mid-chat.
Also resolves the schema.rb version conflict against main (keep the
branch's 2026_06_01_120000, on top of main's current tables).
* fix(ai): address review nitpicks on pgvector provisioning
- Registry: update the adapter doc comment to mention the
Anthropic-to-pgvector default alongside the openai fallback.
- ensure_schema!: guard the DDL with if_not_exists instead of a Mutex.
Adapter instances are built per call and never shared across threads,
so the realistic race is two processes (web + Sidekiq) provisioning
concurrently; IF NOT EXISTS makes the loser a no-op where a Mutex
would only serialize threads inside one process.
* fix(ai): address review on Anthropic settings UI
- Require an Anthropic model when a custom base URL is saved, mirroring the
OpenAI branch. Auto-submit-on-blur could persist a base URL with no model,
making Provider::Anthropic raise "Model is required..." on every LLM call.
- Narrow the LLM provider selector copy: only chat honors Setting.llm_provider;
categorization, merchant detection and PDF processing still always use OpenAI.
Stop advertising provider switching for those flows until they are wired.
- Reset global Setting.* in test teardown to prevent state leakage, and add a
test covering the new base-URL-requires-model validation.
* feat(ds): conditional LLM provider settings + merged copy
The self-hosting AI section showed both providers' credential blocks at once
and duplicated near-identical copy. Tidy it:
- Replace the provider <select> with a DS::SegmentedControl driving a new
provider-settings Stimulus controller: only the active provider's panel is
shown; switching reveals the other instantly and persists Setting.llm_provider.
- Merge the two byte-identical data-retention lines into one provider-neutral
Data handling note.
- Scope the token-budget copy to OpenAI-compatible calls (read only by
Provider::Openai) and add an inline 'add a key to activate' hint when the
active provider is unconfigured.
UI-only; no provider behavior change.
* feat(ds): responsive LLM provider picker (tabs >=sm, select on mobile)
The segmented tabs overflow a phone viewport once there are 3+ providers
(measured: 4 labels want ~409px in a 319px column at 390px wide). Below sm,
fall back to a native <select> -- which doubles as the submitted field -- while
keeping the segmented tabs at sm and up.
Both controls bind to the same provider-settings Stimulus controller (the
select reads its value, the tabs read data-provider), so adding a 3rd/4th
provider scales on mobile with no layout math.
* fix(hostings): sanitize llm provider selector
* test(hostings): avoid brittle provider hint assertion
---------
Co-authored-by: sure-admin <sure-admin@splashblot.com>
|
||
|
|
4e0da0c237 |
fix(ds): dark-parity for bespoke green status badges (#2142)
* fix(ds): dark-parity for bespoke green status badges (#2134) The status/type badges in recurring transactions, admin SSO providers, and the categorize group title used solid light-green tints (bg-green-50/100 + text-green-700/800) with no dark variant, so they rendered as pale "stickers" on dark. Add theme-dark variants mirroring DS::Pill's soft recipe (bg-green-tint-10 + text-green-200); dark text contrast 9.5-14.2:1. Their neutral siblings were already token-based. Audit-named _status_pill / _maturity_badge already render via DS::Pill; remaining solid-tint spots are alpha icon-containers (bg-*-500/5,/10) that composite fine on dark. Full DS::Pill consolidation tracked separately. * fix(ds): dark-parity for bespoke amber/blue notices + SSO badge (#2134) Continues the bespoke-badge tail: the raw-amber warning notices (securities SSO warning, admin SSO legacy-providers notice) + the env-configured badge, and the AI tool-call info box, were solid light tints (bg-amber-50/100 / bg-blue-50) with no dark variant -> pale "stickers" on dark. Add theme-dark variants mirroring the soft recipe: alpha-tint bg (amber/blue-500/10-15), lightened border, light text/icon (amber-200/400). Verified legible on dark. Pre-auth (sessions/new) + infra (redis error) amber notices render light-only, left as-is. Broader: these warning boxes want a shared DS notice/callout component (#2137). |
||
|
|
f499a3db01 |
fix(ds): .table-scroll — wide tables scroll instead of clipping (#2148)
* fix(ds): .table-scroll — wide tables scroll instead of clipping (#2137) The LLM-usage "Recent Usage" table wrapped in `overflow-hidden`, so on narrow viewports it pushed the Tokens/Cost columns off-screen with no way to reach them (the audit's "no scroll affordance"). Ship a reusable `.table-scroll`: `overflow-x: auto` plus a pure-CSS theme-aware scroll-shadow (cover gradients scroll with the content and reveal an edge shadow only when there is more to see), giving the affordance with no JS. Migrate the LLM-usage table as exemplar. Verified: at a mobile width the table now scrolls (content 591px in a 329px wrapper) with a scrollbar + edge shadow, instead of clipping. Deferred: roll `.table-scroll` out to the other overflow tables — reports use a nested container-inset / container wrapper, so the cover bg needs per-use tuning via `--table-scroll-bg`. * fix(a11y): make .table-scroll region keyboard-focusable Add tabindex=0 + role=region + aria-label so keyboard-only users can focus the horizontal scroll container and reach overflowed columns with arrow keys. Addresses CodeRabbit review on #2148. |
||
|
|
d22ffe5994 |
fix(ds-pill): default show_dot per mode (badges clean, markers keep dot) (#2107)
Closes #2001. DS::Pill defaulted show_dot: true for both modes, so every status/category badge got a leading dot by default — redundant with the pill shape + tone + label already carrying the signal, and noisy in dense lists. More than half the marker:false callsites were already passing show_dot: false to fight it. The default is now mode-aware: marker: true keeps the dot (stage markers), marker: false (badges) is dot-less. An explicit show_dot: still wins. Only one in-tree callsite relied on the old default without an icon and wants the dot: settings/providers/_status_pill (live connection state) — pinned with show_dot: true. The enable_banking "Beta" badge loses its dot, which is the desired outcome (ref #1997). Icon-bearing transaction badges are unaffected (an icon already suppresses the dot). Left the now-redundant show_dot: false overrides in place to avoid churn and conflicts with in-flight pill-migration branches; they're harmless (explicit false == new default). Adds tests pinning the per-mode default resolution; updates the Lookbook preview to show the opt-in dot vs the clean default. |
||
|
|
1b8b21760b |
feat(provider): Akahu integration (#1921)
* First pass of Akahu * fix up sync all * conflicts * fix db migration issue? - fix auto selection of akahu account type * Address Akahu PR feedback * Complete provider metadata * Fix PR 1921 CI tests * PR feedback * PR feedback * post merge --------- Co-authored-by: failing <failing@users.noreply.github.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: sure-admin <sure-admin@splashblot.com> |
||
|
|
174f7e6be6 |
feat(binance): add full account sync and transaction processing (#1822)
* feat(binance): add full account sync and transaction processing - Fixed a bug that hindered Account setup - Wire up Binance accounts, sync statistics, and unlinked account tracking in the accounts dashboard. - Support setting a sync_start_date during Binance account setup. - Set Binance accounts' opening balance to zero to ensure the ledger builds cleanly from the actual trade history. - Expand the Binance importer and processor to handle Spot, Margin, Earn, P2P, and Futures trades and assets. - Implement TransactionBuilder to parse raw Binance trades, accurately calculating fees, base/quote asset amounts, and market values for proper ledger integration. - Update Binance API timeout (`recvWindow`) to 60,000ms to prevent connection drops. These changes provide comprehensive support for tracking Binance portfolios, ensuring accurate historical ledgers and proper visibility of sync statuses in the frontend dashboard. * refactor(binance): enforce strong params, double-entry safety, and native fiat currency support - Implement strong parameters in BinanceItemsController#complete_account_setup to satisfy Rails security guidelines. - Add robust date parsing with a grace fallback to prevent controller crashes on malformed sync start dates. - Wrap P2P transaction creations inside a database transaction block to guarantee ledger integrity and prevent orphan records. - Optimize P2P deduplication queries by batching checks for both transaction and funding external IDs. - Shift P2P entry persistence from forced USD tracking to native fiat values extracted directly from the Binance API payload. - Update BinanceAccount::ProcessorTest assertions and fixtures to validate native fiat and fee calculation logic. * fix(binance): process sync trades before caching transaction payload - Reorder Binance processor execution to insert trade records into the database prior to updating the `raw_transactions_payload` cache. This guarantees that if a database insertion fails, the cache won't prematurely mark the sync as successful, ensuring the data is retried on the next run. - Move `set_opening_anchor_balance(balance: 0)` out of the generic crypto exchange account builder and apply it specifically during Binance account creation. - Refactor date parsing in BinanceItemsController to explicitly catch `ArgumentError` via a block instead of using a blanket inline `rescue`. - Clean up the `setup_accounts` view template by removing hardcoded default translation strings. * fix(binance): enhance trade sync logic and error propagation - Pass `startTime` (from `sync_start_date`) to spot and futures trade endpoints on initial sync to optimize data fetching. - Include previously synced futures pairs alongside spot pairs when resolving relevant symbols to properly recover sold-out assets. - Re-raise exceptions in processor rescue blocks to prevent silent failures and ensure errors are correctly propagated to background jobs. - Decrease Binance API `recvWindow` from 60000ms to 5000ms to align with recommended default timeout values. |
||
|
|
12aff3cef7 |
fix(settings): add nil-safe operator to Current.family.can_manage_subscription? (#2006)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> |
||
|
|
e0537a45e1 |
fix: Avoid overlay in provider section on mobile (#1990)
* fix: Avoid overlay in provider section on mobile * feat: Reduce gap between divs * fix: keep all the elements inside a dedicated container to avoid accessibility issues with the summary node |
||
|
|
8f5454ad29 |
fix(settings): preserve OpenAI form input on validation failure (#1862)
* fix(settings): preserve OpenAI form input on validation failure Fixes #1824. The OpenAI settings form auto-submits on blur, so typing the URI base before the model triggers cross-field validation. The rescue re-renders the page with values read from Setting.openai_*, which is still blank because the failed save was rejected — so the user's input disappears and they see 'OpenAI model is required' with no value to fix. Stash the submitted uri_base and model on rescue and prefer them over the saved Setting when rendering, so the user can finish typing the missing field and re-submit. * test(settings): cover openai_model preservation on validation fail (#1862) jjmata asked for symmetric coverage of the model field. Add a test where the user changes the URI base and clears the model in the same submit: the cross-field validation fails and the re-rendered model input must reflect the submitted (cleared) value rather than reverting to the saved model. Complements the existing uri_base preservation test. |
||
|
|
51271d9810 | Prevent Brand Fetch client ID autofill (#1914) | ||
|
|
ea51612ac7 |
refactor(views): migrate 6 residual inline alerts to DS::Alert (#1933)
* refactor(views): migrate 6 residual inline alerts to DS::Alert PR #1731 extended DS::Alert and migrated 9 inline alert blocks. Six hand-rolled alert blocks slipped through that sweep and stayed on raw palette tokens with no `theme-dark:` variants: - `app/views/settings/llm_usages/show.html.erb` — "About Cost Estimates" blue info block. Most visible offender: `bg-blue-50 border border-blue-200` + `text-blue-900 / text-blue-700 / text-blue-600` rendered as a bright white-blue island in dark mode (the bug spotted on the LLM usage page). - `app/views/accounts/confirm_unlink.html.erb` — yellow warning with bullet list. - `app/views/oidc_accounts/new_user.html.erb` — blue info heading. - `app/views/oidc_accounts/link.html.erb` — two blocks (yellow verify warning + blue create info). Also flips the file's pre-existing `text-gray-600` hint paragraph to `text-secondary` (caught by the `DeprecatedClasses` erb_lint rule on save). - `app/views/rules/confirm.html.erb` — AI cost notice. - `app/views/rules/confirm_all.html.erb` — AI cost notice. All six migrate to `DS::Alert.new(title:, variant:)` (with a block content slot for the rich/conditional bodies). DS::Alert resolves `bg-info/10`, `border-info/20`, etc. from the `@theme` semantic tokens, so dark mode now renders a subtle blue/yellow tint over the page surface instead of a hardcoded light-mode pill. Out of scope (left as-is, not alert-shaped): - `app/views/assistant_messages/_tool_calls.html.erb` — a tool-call display panel (not an alert; needs its own token sweep). - `app/views/import/rows/_form.html.erb` — inline cell-error tooltip (`bg-red-50 border border-red-200`) — also not alert-shaped; a future PR can swap it to `bg-destructive/10 border-destructive-subtle` once #1932 lands. Surfaced while scanning DS drift for the LLM usage page bug. Tracking issue: #1715 (closed but conceptually relevant) / #1911 (active drift patrol). * fix(oidc): keep alert description in <p>, retarget tests for DS::Alert title CI on #1933 caught three test failures introduced by migrating the two OIDC link alerts and the verify-redirect copy from hand-rolled `<h3>` / `<p>` markup to `DS::Alert`: 1. `OidcAccountsControllerTest#test_should_show_create_account_option_for_new_user` 2. `OidcAccountsControllerTest#test_does_not_show_create_account_button_when_JIT_link-only_mode` 3. `SessionsControllerTest#test_redirects_to_account_linking_when_no_OIDC_identity_exists` DS::Alert renders its `title:` slot as a `<p>` (semantically the alert heading lives on the container's `aria-labelledby`, not on a heading tag) and renders block / message content directly inside a `<div>`, not a `<p>`. The pre-migration markup used `<h3>` for the heading and `<p class="...text-blue-700">` for the description, so the tests above asserted those specific tags. Two fixes: - `app/views/oidc_accounts/link.html.erb` — wrap the html_safe description bodies in explicit `<p>` tags inside the DS::Alert block. Restores the `<p>` element the session-redirect test asserts on, and keeps the description as a semantic paragraph rather than a bare text node inside the alert container. - `test/controllers/oidc_accounts_controller_test.rb` — flip the two `assert_select "h3", text: "Create New Account"` calls to match the DS::Alert title `<p>`. The test was asserting an implementation detail of the pre-migration markup; switching to the new tag keeps the assertion meaningful (the heading text still has to render) without re-introducing an `<h3>` outside of DS::Alert. * fix(test): match Create New Account title with regex (sr-only "Info:" prefix) DS::Alert prepends `<span class="sr-only">Info:</span>` inside the title `<p>`, so the full text content is "Info: Create New Account", not "Create New Account". `assert_select "p", text: "Create New Account"` requires an exact text match and rejected the prefixed string. Switch to a regex match — keeps the heading-text assertion meaningful without coupling to the screen-reader prefix. |
||
|
|
f0e270f578 |
fix(design-system): restore dark-mode contrast on Toggle + destructive borders (#1932)
Two regressions from the recent token sweep, both producing low-contrast results in dark mode. ## DS::Toggle off-track PR #1843 (DS::Toggle a11y + token swaps) replaced the raw `bg-gray-100 theme-dark:bg-gray-700` off-track with `bg-surface-inset` for semantic alignment. `bg-surface-inset` resolves to gray-800 in dark mode, but the toggle typically sits inside `bg-container` (gray-900). The contrast ratio dropped from ~2.45:1 (gray-700 vs gray-900) to ~1.5:1 (gray-800 vs gray-900) — visibly worse than the pre-#1843 baseline and below WCAG 1.4.11 (3:1 for UI components). Most visible inside the transaction-edit modal SETTINGS section (`Exclude`, `One-time Expense`) where the off-state switches nearly vanished into the modal chrome. Introduce `--color-toggle-track` (light: gray-100, dark: gray-700) and swap `bg-surface-inset` → `bg-toggle-track` in DS::Toggle. Restores the pre-#1843 off-track contrast while keeping a semantic token (instead of the raw palette references the migration was trying to remove). ## border-destructive subtle borders PR #1849 (single-color tokens to @theme) flagged that `border-destructive/N` rendered the wrong shade (the `@utility border-destructive` block defined red-500 light, while `--color-destructive` in `@theme` is red-600 — `/N` resolves from @theme), and swapped a couple of callsites to solid `border-destructive`. Solid renders red-500/red-400 at full saturation in both modes, which reads as a loud error border on contexts that were meant to be subtle (left-rule on the provider-sync "view error details" pane, error-message box in SimpleFIN settings, alert-component border, provider connection error rows). Two callsites (`DS::Alert`, settings/providers/_connection_row) still carried the broken `border-destructive/20` / `/25` modifier — same off-shade footgun #1849 was meant to retire. Introduce `--color-destructive-subtle` (light: red-200, dark: red-800) and swap the four subtle-by-intent callsites to `border-destructive-subtle`: - app/components/DS/alert.rb (destructive variant) - app/views/settings/providers/_connection_row.html.erb (err status) - app/components/provider_sync_summary.html.erb (error-details left rule) - app/views/simplefin_items/edit.html.erb (error-message box) The handful of intentionally-loud `border-destructive` callsites (split-transaction over-allocation, blank-name account labels, etc.) keep the solid token. Regenerated `_generated.css` via `npm run tokens:build`. |
||
|
|
2ce8e858b6 |
refactor(providers): migrate provider badges to DS::Pill (#1751 PR C) (#1918)
* refactor(transactions): migrate 5 transaction badges to DS::Pill (#1751 PR B) Migrates the hand-rolled "Pending" / "Review recommended" / "Potential duplicate" / "Split" badges across the transaction views to the extended DS::Pill primitive from #1902. **Visual contract for badge mode** In #1902 the badge mode (`marker: false`) used `rounded-md` (chip shape) because the marker mode does. But every existing pill / status badge in the codebase uses `rounded-full` — see `settings/providers/_status_pill.html.erb`, `settings/providers/_maturity_badge.html.erb`, and the inline transaction badges this PR is migrating. To keep the visual contract consistent, this PR shifts `DS::Pill`'s badge mode to `rounded-full` (marker mode stays `rounded-md`, unchanged from #1829). The shape distinction now reads: markers are tags, badges are pills. **Callsites migrated** (5): - `app/views/transactions/_transaction.html.erb` — Pending, Review-recommended, Possible-duplicate, Split badges - `app/views/transactions/_header.html.erb` — Pending badge - `app/views/transactions/_split_parent_row.html.erb` — Split badge **Tone mapping** | Badge | Tone | Notes | |---|---|---| | Pending | `:neutral` | unchanged copy/icon, gains subtle DS-controlled bg | | Review recommended | `:neutral` | matches existing `bg-surface-inset` look | | Possible duplicate | `:warning` | DS semantic alias for the existing `text-warning` | | Split | `:neutral` | matches existing `bg-surface-inset` look | **Deferred to follow-up PRs** - `app/views/transactions/_transfer_match.html.erb` — uses two responsive-visibility variants (`hidden lg:inline-flex` for long copy, `inline-flex lg:hidden` for short). DS::Pill currently has no `class:` arg for caller-controlled wrapper classes; deferring until that lands. - `app/views/transactions/searches/filters/_badge.html.erb` — has a close button alongside the label (`button_to clear_filter_*`) and uses `rounded-3xl p-1.5` instead of a true pill. Closer to a removable filter chip — better fit for a separate `DS::FilterChip` primitive than for `DS::Pill`. Refs #1751. * refactor(providers): migrate provider badges to DS::Pill (#1751 PR C) Migrates the provider-bucket pill/badge callsites to the extended DS::Pill primitive (badge mode, rounded-full) from #1917. Callsites migrated (3): - app/views/settings/providers/_status_pill.html.erb — provider connection status pill. Status → tone mapping: :ok → :success, :warn → :warning, :err → :error, else → :neutral. - app/views/settings/providers/_maturity_badge.html.erb — alpha/beta label. Tone :neutral, no dot. - app/views/sophtron_items/_sophtron_item.html.erb (line 27) — "manual sync" warning. Tone :warning, no dot. The settings/providers/_status_pill partial wraps DS::Pill rather than being deleted, since _connection_row still calls it via `render "settings/providers/status_pill", status: status` — keeping the partial preserves the seam without a wider refactor. Dead code removed: SettingsHelper#status_pill_classes (no remaining callers after the migration). Skipped: - app/views/simplefin_items/_activity_badge.html.erb — not actually a pill/badge. It renders <p> text with `text-warning` plus an inline icon below the heading; no rounded-full shape and no chip semantics. Migrating it would change the layout, not consolidate a pill pattern. Refs #1751. Stacks on #1917. |
||
|
|
814505c5ea |
fix(settings/debugs): migrate in-cell <details> to DS::Disclosure :inline (#1916)
#1858's :inline variant landed (commit
|
||
|
|
548c4d1a3f |
fix(settings/debugs): replace 2 raw palette tokens flagged by DS drift scan (#1903)
`app/views/settings/debugs/show.html.erb` had two non-functional Tailwind classes flagged by sure-design's weekly merged-commit scan (#1895, #1898): - `bg-surface-default` → `bg-surface`. `bg-surface-default` doesn't map to any DS color variable (`--color-surface-default` isn't defined); `--color-surface` is the canonical token, auto-generates `bg-surface`. - `divide-gray-100` → `divide-alpha-black-200 theme-dark:divide-alpha-white-200`. Matches the existing pattern used by `admin/sso_providers/index.html.erb`, `admin/users/index.html.erb`, and `settings/preferences/show.html.erb` for tbody dividers. No `divide-primary` utility exists yet, so the bot's suggestion gets the same effect via the alpha tokens. The third drift finding on this file — the in-cell `<details>` metadata expander — is deferred until #1858's `DS::Disclosure :inline` variant lands on `main`. The `:default` variant renders a `bg-surface px-3 py-2 rounded-xl` card chrome that's wrong for an in-table-cell trigger; the `:inline` variant in #1858 is the right shape and will get a follow-up PR once that lands. Closes #1895 partially. Closes #1898 partially. Both bot issues stay open until the `<details>` migration also lands. |
||
|
|
8de14ed2a5 |
feat(design-system): DS::Disclosure :inline variant + migrate indexa_capital + snaptrade panels (#1715 §6) (#1858)
* feat(design-system): add :inline variant + migrate indexa_capital + snaptrade panels Adds an `:inline` variant to `DS::Disclosure` for plain text-link-style toggles that have no surface, no padding, no shadow — the disclosure reads as a clickable summary text + revealed content, nothing more. Use case: "Alternative auth" form section toggle in the Indexa Capital provider panel; "Manage connections" lazy-loaded toggle in the Snaptrade provider panel. Both were the last raw-`<details>` callsites in `app/views/settings/providers/`. Migrations: - `_indexa_capital_panel.html.erb` — single inline `<details>` revealing username / document / password form fields under an "Alternative auth" summary text. - `_snaptrade_panel.html.erb` — lazy-load `<details>` with `data-controller="lazy-load"` etc. The new `tag.details ... **opts` forwarding from #1857 lets the Stimulus controller attrs flow through cleanly via DS::Disclosure's `data:` keyword. Chevron rotation on snaptrade gets the standard `motion-safe:transition-transform motion-safe:duration-150` treatment (was `transition-transform` without the motion-safe gate). Variant summary now: | Variant | Details surface | Use case | |---|---|---| | `:default` | none / bg-surface summary | inline expander inside parent card | | `:card` | `bg-container shadow-border-xs rounded-xl p-4` | provider rows, settings sections | | `:card_inset` | `bg-surface-inset rounded-xl p-4` | inset sub-panels | | `:inline` | no surface | text-link-style toggles | * fix(review): guard variant.to_sym against nil in DS::Disclosure CodeRabbit on #1858 flagged that `variant: nil` crashed with `NoMethodError` at `variant.to_sym` before the explicit `VARIANTS` check could run. Use safe navigation (`variant&.to_sym`) so nil falls through to the validation, and inspect `@variant` in the error message so nil / non-symbol inputs render readably. Verified manually via runner: `DS::Disclosure.new(variant: nil)` now raises `ArgumentError: Invalid variant: nil. Must be one of [:default, :card, :card_inset, :inline]`. |
||
|
|
834ec19fdc |
feat(design-system): DS::Disclosure :card_inset variant + migrate ibkr_panel + settings/_section (#1715 §6) (#1857)
* feat(design-system): add :card_inset variant + migrate ibkr_panel and settings/_section Wraps up the disclosure migration cluster from #1715 §6: 1. **New `:card_inset` variant** on `DS::Disclosure`. Same contract as `:card` but uses `bg-surface-inset rounded-xl p-4` (no shadow) for inset sub-panels embedded inside a parent card surface. 2. **Migrate `_ibkr_panel.html.erb`** — the "flex query details" disclosure (`<details class="group bg-surface-inset rounded-xl p-4">`) was the one panel skipped from #1856 because it used the inset surface. Now uses `DS::Disclosure(variant: :card_inset)`. Chevron gets the `motion-safe:transition-transform motion-safe:duration-150` treatment along the way. 3. **Migrate `settings/_section.html.erb`** — the global "collapsible settings card" primitive backing 19 callsites via the `settings_section(...)` helper. The collapsible branch's `<details class="group bg-container shadow-border-xs rounded-xl p-4">` becomes `DS::Disclosure(variant: :card, open: open, data: ...)`. While here: - Update `disclosure.html.erb` to spread `**opts` onto the `<details>` element via `tag.details`. Previously opts were captured but never applied; the `settings/_section` migration needs `data-controller` + `data-auto-open-param-value` to flow through to the rendered `<details>`. - Non-collapsible branch in `settings/_section.html.erb` stays as raw `<section>` — different semantics (not expandable), DS::Disclosure can't replace because it always renders `<details>`. API: DS::Disclosure.new( variant: :card | :card_inset | :default, open: bool, data: { controller: "...", ... } # forwarded to <details> ) * fix(review): merge caller class in DS::Disclosure + i18n plaid deletion - DS::Disclosure: extract caller class: from opts and merge via class_names before forwarding to tag.details. Prevents the latent duplicate keyword arg error when callers pass class: alongside the variant-derived classes. - plaid_items/_plaid_item: localize "(deletion in progress...)" via t('.deletion_in_progress') + add en locale key, matching lunchflow / mercury / sophtron / coinstats convention. * fix(panels): replace text-white and bg-gray-tint-10 with semantic tokens `text-white` → `text-inverse` on the EnableBanking reauthorize button (`bg-warning` background); `bg-gray-tint-10` → `bg-container-inset` on the IndexaCapital item avatar wrapper. Both flagged by sure-design as non-functional palette tokens. Pre-existing on main; surfaced by the re-indentation that this PR applied during the disclosure migration. |
||
|
|
547dd21bf2 |
feat(design-system): migrate 3 provider panels to DS::Disclosure :card (#1715 §6) (#1856)
* feat(design-system): DS::Disclosure :card variant + migrate 14 provider items Resolves part of #1715 §6. The provider-item view templates (binance, brex, coinbase, coinstats, enable_banking, ibkr, indexa_capital, kraken, lunchflow, mercury, plaid, simplefin, snaptrade, sophtron — 14 in total) all hand-rolled the same `<details open class="group bg-container p-4 shadow-border-xs rounded-xl">` shell with a custom summary inside and content below. Extend `DS::Disclosure` with a `:card` variant that bakes the card chrome onto the `<details>` element itself; the summary becomes slot-driven via the existing `summary_content` slot. Provider items keep their custom summary content (logos, brand colors, status copy) unchanged — they just hand it to the slot instead of writing it between `<summary>` tags. API: DS::Disclosure.new(variant: :card, open: true) do |d| d.with_summary_content do <div class="flex items-center gap-2"> chevron + custom summary markup </div> end body content end While here: - Drop the no-op `group-open:transform` from the default chevron (Tailwind v4 applies `rotate-90` directly). - Add `motion-safe:transition-transform motion-safe:duration-150` to chevron rotation for reduced-motion respect (matches the pattern landing in #1841). - Extract `summary_classes` / `details_classes` helpers so the default and card surfaces stay readable side-by-side. Note: this PR touches `DS::Disclosure` and will textually conflict with #1841 (focus-ring + reduced-motion polish). Both changes are compatible — when #1841 merges first, the resolution is just preserving both edits (the focus-ring classes are already merged into `summary_classes` here). * feat(design-system): migrate 3 provider panels to DS::Disclosure :card variant Resolves the panel slice of #1715 §6. Continuation of the DS::Disclosure :card variant work — same migration pattern, applied to the 3 provider-PANEL templates that share the card shape with the provider-item templates landing on the parent branch. Migrated `<details class="group bg-container p-4 shadow-border-xs rounded-xl">` → `DS::Disclosure.new(variant: :card)` in: - `app/views/settings/providers/_kraken_panel.html.erb` — 1 details in the items-each loop. - `app/views/settings/providers/_mercury_panel.html.erb` — 1 details in the items-each loop. - `app/views/settings/providers/_brex_panel.html.erb` — 2 details: one in the items-each loop, one standalone "add connection" panel that opened by default when no active items existed. The conditional `<%= "open" unless active_items.any? %>` becomes `open: active_items.none?` on the `:card` disclosure. Panels do NOT show a chevron in their summary (different UX from the per-item rows in #1855), so the migration preserves that — no chevron inserted. NOT migrated (intentionally — different shapes): - `_ibkr_panel.html.erb` — `<details class="group bg-surface-inset rounded-xl p-4">`. Uses bg-surface-inset, not bg-container — needs a `:card-inset` variant we haven't built. Deferred. - `_indexa_capital_panel.html.erb` — `<details class="group">` with no card chrome. Inline expander; doesn't fit either disclosure variant. - `_snaptrade_panel.html.erb` — same inline pattern as indexa_capital. * fix(review): use ring-alpha-black-300 focus token in DS::Disclosure CodeRabbit P2: switch the focus-visible outline from raw gray-900/white palette values to the alpha-black-300 ring token, matching the established focus pattern on settings/provider_card.html.erb. This keeps theme behavior centralized in the design system tokens instead of branching on theme-dark: in the component. Applies to both :default and :card summary variants. * fix(review): stretch DS::Disclosure summary_content to full width Codex P2 follow-up on the disclosure-migration stack: \`<summary>\` is \`display: list-item\`, so a flex inner div inside the slot shrink-wraps to content width — any \`justify-between\` the caller adds has nothing to distribute, and the right-side admin actions collapse toward the title across every provider-item partial migrated to \`DS::Disclosure variant: :card\` in #1855 (and the panels in #1856 / #1857 / #1858 that inherit this component). Wrap the slot in \`<div class=\"w-full\">\` so caller-supplied flex rows stretch across the card. \`:default\` variant is unchanged (it never uses \`summary_content\`). * fix(review): stretch :card summary flex row to full width Codex P2 follow-up on #1856: the migrated kraken / mercury / brex panel summary rows wrap their content in \`<div class=\"flex items-center justify-between gap-X\">\`, but a flex container inside \`<summary>\` (\`display: list-item\`) shrink-wraps to content size, so \`justify-between\` had nothing to distribute and the right-side admin actions collapsed toward the title. Add \`w-full\` so the flex row stretches across the card. The deeper component-level fix lands in #1855 (wraps \`summary_content\` in a \`w-full\` block); this commit makes #1856 self-contained against the merge order. |
||
|
|
8e444ff98b |
feat(design-system): add DS::SearchInput primitive (closes #1715 §3) (#1853)
* feat(design-system): add DS::SearchInput + migrate 2 broken-focus callsites Resolves #1715 §3. Two standalone search-field callsites — `/settings/preferences` currency filter and `/settings/providers` filter row — had a hand- rolled markup that ended in `focus:ring-gray-500`. That utility has no backing token in the design system (`ring-gray-500` isn't in Tailwind's default + Sure doesn't register a gray ring color), so the input rendered with zero focus indicator on a bordered bg-container surface. Keyboard users couldn't tell when the field was focused. Introduce `DS::SearchInput` — icon-on-left, bordered, token-backed focus ring matching the DS::Button pattern landing in #1840 (`outline-2 outline-offset-2 outline-gray-900` with the dark-mode override). API: DS::SearchInput.new( name: "...", placeholder: "...", value: ..., aria_label: "...", # defaults to placeholder class: "...", # passed to the wrapper **opts # spread onto the <input>, e.g. data-* ) Migrate the two broken callsites. Three other "search" patterns stay as-is (out of scope for this PR): - `form.search_field :search` inside `styled_form_with` blocks (accounts/show/_activity.html.erb, UI::Account::ActivityFeed) — already routes through StyledFormBuilder's form-field CSS. - Embedded-dropdown search input inside DS::Select, DS::Menu, and the splits/category-select panels — uses a different shape (no border, no ring) because the parent panel provides the chrome. - Category dropdown's combobox search input (app/views/category/dropdowns/show.html.erb) — has a custom `role=combobox` flow and stays intentionally distinct. * feat(design-system): add embedded variant to DS::SearchInput, migrate 2 more callsites Adds `variant: :embedded` to `DS::SearchInput` for search inputs that live *inside* another DS panel (DS::Select dropdown, splits category filter, future DS::Popover-hosted filters). No own border / no own focus ring — the parent panel provides the chrome, so adding ring + outline competes with its `focus-within` state. API: DS::SearchInput.new(variant: :embedded, placeholder: "...", data: {...}) The `:standalone` default (from the previous commit) stays unchanged and remains the right choice for top-of-list filter inputs. Migrated: - `app/components/DS/select.html.erb` — the in-dropdown search input for `DS::Select.new(searchable: true)`. Was the only remaining internal raw <input type="search"> markup in the component. - `app/views/splits/_category_select.html.erb` — split-transaction category picker filter. Same shape as DS::Select's search but hand-rolled because the picker isn't a vanilla DS::Select. Three other search patterns stay out of scope (intentionally, per the previous commit): - `form.search_field :search` inside `styled_form_with` — uses form-field CSS, different visual contract. - `app/views/category/dropdowns/show.html.erb` — bespoke `role="combobox"` flow with `aria-expanded` / `aria-autocomplete` semantics that don't belong in this primitive. * fix(review): mobile font + embedded variant focus-within ring - DS::SearchInput: switch text-sm -> text-base sm:text-sm on both variants so the input keeps its 16px base size on mobile. iOS Safari zooms the viewport when a focused input is below 16px, which the unconditional text-sm was triggering on the Settings Preferences currency search and Settings Bank Sync provider search. - DS::Select (searchable variant) + splits/_category_select: add focus-within:ring-4 focus-within:ring-alpha-black-200 (with theme-dark variant) on the wrapper around the embedded search input. The embedded variant intentionally has no own focus ring so it inherits chrome from its parent panel — but the two current parent panels were not providing one, so keyboard focus on the dropdown search box rendered with no visible indicator. Ring matches the .form-field token used across the design system. * fix(merge): repair DS::Select search input merge resolution The previous merge of main left invalid Ruby inside the DS::SearchInput `data:` hash: aria-label="<%= t("helpers.select.search_placeholder") %>" This is an ERB string assignment masquerading as a hash entry — it does not parse and would have raised SyntaxError at render. Two follow-ups: - Drop the `aria-label` entry entirely. `DS::SearchInput` already defaults `aria_label` to `placeholder`, and `placeholder` is set on the call, so the resulting <input> already carries `aria-label="<%= t(...) %>"`. - Restore the `input->select#syncTabindex` action that main #1848 added on the embedded search input. It keeps the roving tabindex on the listbox in sync as filtered results change. Original PR branch had only `list-filter#filter`; reintegrate both with explicit `input->` event prefixes for parity with main. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
cdce00c71e |
refactor(design-system): migrate 38 hand-rolled provider buttons to DS::Button / DS::Link (#1715 §5 part B) (#1860)
* refactor(design-system): migrate 9 hand-rolled buttons with orphan btn-- classes to DS::Button / DS::Link Part of #1715 §5. The `btn`, `btn--primary`, `btn--outline`, `btn--ghost`, `btn--sm` CSS classes have no backing styles anywhere in the codebase (no .btn definition in app/assets/, no Bootstrap dependency). These callsites have been rendering unstyled buttons / links since the underlying CSS was last removed. Migrate the 9 broken callsites: - `app/views/transactions/show.html.erb` — duplicate-merge action buttons (×2): `button_to ... class: "btn btn--primary btn--sm"` / `class: "btn btn--outline btn--sm"` → DS::Button with href + variant + size + `data: { turbo_method: :post }`. - `app/views/snaptrade_items/select_existing_account.html.erb` — "Go to Provider Settings" link → DS::Link primary sm. - `app/views/indexa_capital_items/select_existing_account.html.erb` — same pattern → DS::Link primary sm. - `app/views/import/confirms/show.html.erb` — Publish button + Cancel link → DS::Button primary full-width + DS::Link ghost full-width. - `app/views/simplefin_items/new.html.erb` — Cancel link (`class: "btn"` only) + Connect submit → DS::Link secondary + bare `f.submit` (already routes to DS::Button via StyledFormBuilder). - `app/views/settings/providers/_ibkr_panel.html.erb`, `_snaptrade_panel.html.erb`, `_indexa_capital_panel.html.erb` — strip the orphan `class: "btn btn--primary"` from `f.submit` callers; the submit is already a styled DS::Button via the form builder. The next PR in this chain (Phase B) will tackle the larger inline- button cluster (~29 files, 38 instances) — provider panels and provider-item flows hand-rolling the same `inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors` string. * refactor(design-system): migrate 38 hand-rolled provider buttons to DS::Button / DS::Link (#1715 §5 part B) Bulk sweep of the second cluster from §5. 29 files, 38 button instances — each one hand-rolled the same long Tailwind string for the primary action button: inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-inverse bg-inverse hover:bg-inverse-hover focus:outline-none focus:ring-2 focus:ring-primary transition-colors (some variations used `button-bg-primary hover:button-bg-primary-hover` instead of `bg-inverse hover:bg-inverse-hover` — same intent). Every instance is now a DS::Button / DS::Link with `variant: :primary`, which: - Picks up the new focus-ring + touch-target work from #1840 once that merges. - Stops duplicating the long Tailwind string across 29 files — single source of truth in `DS::Buttonish::VARIANTS[:primary]`. - Picks up consistent `aria-label` derivation for icon-only forms. - Removes the misnamed `focus:ring-primary` (no token) — the new ring comes from `base.css` automatically. Migration patterns applied: - `f.submit text, class: "inline-flex …"` inside `styled_form_with` → bare `<%= f.submit text %>`. StyledFormBuilder routes through DS::Button. - `link_to text, path, class: "inline-flex …"` → DS::Link primary. - `button_to text, path, method: :X, class: "inline-flex …"` → DS::Button with `href: path` and `data: { turbo_method: :X }`. - `submit_tag text, class: "inline-flex …"` inside raw `form_with` → DS::Button with `type: :submit`. Notable adjustments: - `holdings/show.html.erb` — the form was `form_with` (not styled). Switched to `styled_form_with` so `f.submit` routes through DS::Button. `f.combobox` (hotwire_combobox) still works through the styled builder. - Two `link_to settings_providers_path` callsites in `coinstats_items/new.html.erb` + `enable_banking_items/new.html.erb` had `w-full inline-flex … hidden md:inline-flex` — the responsive pair conflicted (both `inline-flex` and `hidden md:inline-flex` on the same element). Migrated to `full_width: true` without the responsive split; the buttons now render at all breakpoints consistently. (Pre-existing copy-paste bug, fixed in passing.) - `enable_banking_panel` add-connection button gained `icon: "plus"` via the DS::Button API; the explicit `gap-2 … icon "plus"` markup is now redundant. Sibling buttons that don't match the primary spec (destructive trash, secondary outline-bordered, button-bg-secondary-strong on holdings/show.html.erb, etc.) are intentionally left alone — they need their own audit pass once #1840 lands and the focus-ring behavior on those variants is stable. * fix(review): restore SimpleFIN submit styling + i18n provider_form label - SimpleFIN new modal: switch form_with -> styled_form_with so f.submit picks up the DS::Button render via styled builder (Codex #1860). - _provider_form: replace hardcoded "Save and connect" with t(".save_and_connect") and add scoped key under settings.providers.provider_form (CodeRabbit). |
||
|
|
e07d641ead |
fix(design-system): DS::Button a11y audit — focus ring, touch target, type default, icon-only label (#1840)
* fix(design-system): DS::Button a11y audit Closes #1738. Four concrete fixes surfaced by the savings-goals audit + #1737 universal checklist: 1. Focus ring (WCAG 2.4.7). `base.css` had `focus-visible:outline-gray-900` which is **1.07:1** against the primary button's gray-900 background — invisible. Widen to `outline-2 outline-offset-2`, place outline outside the button via offset, and add a dark-mode `outline-white` so the ring is always visible against the page chrome regardless of the button surface. 2. Touch target (WCAG 2.5.5). Icon-only buttons at the default `:md` size were `w-9 h-9` = 36×36, below the 44×44 enhanced target. Bump `md.icon_container_classes` to `w-11 h-11` and `lg.icon_container_classes` to `w-12 h-12` to keep the size scale intact. `sm` stays at 32×32 (already passes WCAG 2.5.8 AA's 24×24 minimum; intentional compact-density variant). 3. Default button type. `content_tag(:button, ...)` inherits the HTML default `type="submit"`, so a DS::Button rendered inside a form steals Enter-key submission from the first text input (reproducible in the form stepper). Default to `type="button"` in the non-`href` branch; existing form submitters pass `type: "submit"` explicitly and continue to work. The `button_to` (href) branch keeps the submit default because button_to wraps its own form. 4. Icon-only accessible name. Icon-only buttons render no text node, so AT users hear "button" with no name. Derive a humanized aria-label from the icon key (e.g. `icon: "more-horizontal"` → `aria-label="More horizontal"`); explicit `aria: { label: }` on the caller still wins. Soft fallback — callers should still pass meaningful labels for richer copy. Plus: replace the stale `fg-white` icon class on the destructive variant with `text-inverse` (the `fg-*` namespace was deprecated in #1626 so `fg-white` resolved to nothing; the icon was using its helper-default color rather than the white the design intended). Out of scope: - Menu avatar trigger (custom 36×36 button bypassing DS::Button) — belongs to #1743 DS::Menu audit. - DS::FilledIcon `lg` size container (decorative, not interactive) — belongs to #1742. * fix(design-system): force type=submit on StyledFormBuilder#submit The DS::Button default-type-button change in the previous commit broke every `form.submit "Log in"` callsite because `StyledFormBuilder#submit` (app/helpers/styled_form_builder.rb) renders a DS::Button under the hood with no explicit `type:`. After the default flip, those submit buttons rendered as `type="button"`, so submitting forms (login, password reset, every form using `form.submit`) silently no-ops. CI surfaced this via ~30 system tests failing in the `sign_in` helper, which couldn't get past the login page. Pin `type: "submit"` on the DS::Button rendered by `StyledFormBuilder#submit`. The 22 view-level `f.submit` / `render DS::Button.new(type: :submit, ...)` callers already pass type explicitly and are unaffected. * fix(review): href-branch type-button bug + focus-ring tokens + profile Save submit CodeRabbit P1+P2 review on #1840: 1. button.rb: `merged_opts.delete(:href)` always returned nil because Buttonish#initialize strips :href from opts into @href, so the `if href.blank?` guard was ALWAYS true. Every DS::Button rendered via button_to (the href branch) got `type="button"` on the inner button, breaking submission of those button_to-generated forms (e.g. imports/_ready.html.erb publish button, imports/_failure.html.erb try-again button). Drop the local `href = merged_opts.delete(:href)` so the guard now reads the @href reader, leaving the href branch's HTML default intact. 2. settings/profiles/show.html.erb: the Save button is rendered with `render DS::Button.new(...)` inside `styled_form_with` (not via form.submit), so the StyledFormBuilder#submit type-pin from |
||
|
|
e8ce28648d |
refactor: rename beta features gate to preview features (#1837)
* refactor: rename beta features gate to preview features Renames the opt-in gate introduced in PR #1829 from "beta" to "preview". Same shape (per-user JSONB toggle, `before_action` concern, marker pill) just retitled so the surface speaks the language Sure uses elsewhere ("preview" reads as in-progress, "beta" had baggage with provider maturity copy and external testing programs). Renames: - BetaGateable -> PreviewGateable - require_beta_features! -> require_preview_features! - beta_features_enabled? -> preview_features_enabled? - preferences["beta_features_enabled"] -> preferences["preview_features_enabled"] - DS::Pill default label "Beta" -> "Preview" - Settings -> Preferences toggle copy "beta features" -> "preview features" - config/locales/views/beta/ -> config/locales/views/preview/ - docs/llm-guides/gating-a-beta-feature.md -> gating-a-preview-feature.md Includes a data migration that copies any existing `beta_features_enabled` JSONB key into `preview_features_enabled` so early opt-ins survive the rename, then removes the old key. The migration is fully reversible. Provider maturity copy ("maturity.beta = Beta" under Settings -> Bank sync) is intentionally untouched - that's a separate concept describing a provider's integration stability, not Sure's feature gate. * review: apply CodeRabbit findings on PR #1837 - Settings::PreferencesController#update now routes the `preview_features_enabled` input through strong params and casts via ActiveModel::Type::Boolean instead of reading raw params and string- comparing to "1". Matches Sure's controller convention for permitted params and avoids stringly-typed boolean handling. - Rename migration now wraps the destination JSONB key write in COALESCE so a row that somehow ends up with both keys keeps the destination value instead of having it overwritten by the source. Up and down paths get the same defensive shape. * 📝 CodeRabbit Chat: Implement requested code changes * 📝 CodeRabbit Chat: Implement requested code changes * fix: restore all missing translation keys; rename beta→preview label * fix: restore all missing sections (appearances, debugs, llm_usages, providers, etc.); rename beta→preview * fix: restore missing keys (member_removal_failed, confirm_delete, etc.); add preview section * fix(i18n/ca): use 'està en vista prèvia' instead of 'és una vista prèvia' * fix(i18n/ca): use 'en desenvolupament'; drop article in preview title * fix(i18n/es): use 'en desarrollo' instead of 'en progreso' * fix(i18n/ca): use 'funcions experimentals' instead of 'vista prèvia' * fix(i18n/es): use 'funciones experimentales' instead of 'vista previa' * fix(i18n/ca): use 'funcions experimentals' in preferences.show.preview * fix(i18n/es): use 'funciones experimentales' in preferences.show.preview * fix(i18n/ca): use 'Experimental' pill label instead of 'Vista prèvia' * fix(i18n/es): use 'Experimental' pill label instead of 'Vista previa' --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
1ddd8bd040 |
feat(i18n): complete Catalan translations + extract residual hardcoded strings (#1836)
* feat(i18n): complete Catalan translations + extract residual hardcoded strings
CA coverage
- All view/model/breadcrumb/doorkeeper/mailer locale files for ca: 0 missing
keys (was ~3,400). Translations follow informal "tu" register, sentence case,
domain glossary (Compte/Saldo/Transacció/Posició/Operació/Pressupost/...).
- Catalan pluralization test: ca uses one/other; mirrors
test/lib/polish_pluralization_test.rb.
- 8 LanguageTool-flagged grammar fixes applied (Connexió òrfena, Secret de
l'API, comma-pero, apostrophe elisions, etc).
Hardcoded string extraction (also fixes EN parity)
- UI::Account::Chart#title + chart.html.erb view tabs -> UI.account.chart.*
- UI::Account::BalanceReconciliation labels + tooltips ->
UI.account.balance_reconciliation.{labels,tooltips}.*
- transactions/_transfer_match.html.erb (Auto-matched, A/M, Confirm/Reject
match, Payment/Transfer is confirmed) -> transactions.transfer_match.*
- AccountOrder labels (Name/Balance asc/desc) -> account_order.* keys with
fallback to existing hardcoded labels.
- Depository::SUBTYPES surface in account list -> depositories.subtypes.*.*
- User role badge -> users.roles.* (admin / member / super_admin).
- 110+ country names -> countries.* (config/locales/countries.ca.yml).
Breadcrumb locale fix
- Breadcrumbable was a before_action that ran before Localize's around_action
switched I18n.locale, so default crumbs rendered in EN even when locale=ca.
- Convert to helper_method that defers translation to render-time (when
I18n.locale is already correct). Add all missing breadcrumb keys to ca + en.
- Layouts switched from @breadcrumbs to breadcrumbs helper.
Locale-aware helpers / formatters
- ApplicationHelper#localized_ordinal: ordinalize that respects ca
(1r/2n/3r/4t/Nè). Wired into preferences month_start_day select.
- Family#moniker_label / moniker_label_plural: translate the default "Family"/
"Group" monikers via shared.family_moniker.* with fallback to the family's
custom override.
- Budget#name: use I18n.l for month_year/short/long instead of strftime("%B %Y")
so the budget header date follows the active locale.
Tooling
- script/lt_check_ca.rb: batched LanguageTool checker (premium endpoint when
LT_USERNAME/LT_API_KEY are set, free fallback otherwise), picky mode,
motherTongue=en for false-friend detection.
- lib/tasks/i18n_screenshot.rake: dev-only rake to set user.locale=ca and
role=super_admin on the demo user so the i18n surfaces can be walked.
Out of scope (pre-existing, not introduced here)
- Native browser file input "Choose Files / No file chosen" (browser locale).
- D3.js client-side chart x-axis dates (JS-side Intl.DateTimeFormat needed).
- Sankey/donut labels = seed category names (data, not i18n).
- 2 rails-i18n datetime/errors interpolation warnings inherited from
config/locales/defaults/ca.yml.
* fix(i18n): apply idiomatic Catalan review (3-agent + native review)
Three parallel review agents flagged 203 findings (31 high / 73 medium / 99 low)
across all 111 ca.yml files. This commit applies the high-severity bugs plus a
curated subset of medium-impact fixes.
Grammar / agreement
- provider_sync_summary.health.stale_pending: `(exclòs)` -> `(exclosa/excloses)`
to agree with feminine `transacció(s)`.
- accounts.confirm_unlink.warning_no_sync: added reflexive `es` -
`el compte ja no es sincronitzarà`.
- sophtron_setup_required.heading: `no configurats` -> `sense configurar`
(avoids broken agreement across "ID" masc. + "clau" fem.).
- admin.sso_providers.form.errors_title: split into one/other pluralization
keys (en + ca); singular `ha impedit` was wrong for count > 1.
Brand consistency
- IndexaCapital -> Indexa Capital (37 occurrences across one file).
- Lunchflow -> Lunch Flow in two remaining places.
Anglicisms / domain mistranslations
- kraken_items setup_accounts.instructions: `ompliments d'operacions`
(lit. dental/food fillings) -> `execucions d'operacions`.
- settings kraken_panel.read_only_title: `Sincronització d'intercanvi`
(swap/trade) -> `Sincronització només de lectura amb l'exchange`.
- transactions convert_to_trade.security_custom + security_not_listed_hint:
`cotització` (price quote) -> `ticker` (the EN field IS a ticker symbol).
- loans.form.rate_type: `Tipus d'interès` collided with sibling
interest_rate -> `Modalitat del tipus`.
- brex_items.provider_panel.sandbox_note_html: `L'staging` (broken
contraction) -> `el staging`.
Idiom traps
- coinbase/binance/kraken wait_for_sync: `acabi de sincronitzar` is
ambiguous in CA (`acabar de + inf` reads as "has just done X") ->
`acabi la sincronització`.
- chats.ai_greeting.there: `a tothom` -> `''` (the EN fallback "Hey there"
is singular; literal CA `tothom` is plural and wrong for 1:1 chat).
- transactions.split_parent_row.split_label: `Divideix` (imperative) is
wrong as a status badge -> `Divisió` (noun).
- transactions.keep_both (2 occurrences): infinitive `mantenir ambdues` ->
imperative `mantén-les totes dues` to match the sibling Yes/No buttons.
- rules.clear_ai_cache: `Reinicia` (restart) -> `Buida` (empty/clear),
which matches the success notice (`s'està netejant`).
Moniker gender breakage (cross-file)
%{moniker} is interpolated downcased from family.moniker_label and may
resolve to feminine `família`/`llar` or masculine `grup`. Strings that
hard-code a gendered article ('al teu %{moniker}', 'aquesta %{moniker}',
'aquest/a %{moniker}') broke on at least one branch. Restructured the
affected sentences to drop the gendered determiner:
- account_sharings.show.no_members
- merchants.family_empty / family_title / provider_empty
- registrations.new.join_family_title
- settings.preferences.show.currencies_subtitle / sharing_subtitle
- simplefin_items.select_existing_account.no_accounts_found
- invitations.new.subtitle
- invitation_mailer.invite_email.subject (mailers/) + body (views/)
- snaptrade_items.providers.snaptrade.free_tier_warning
Terminology consistency
- models/account_statement/ca.yml attributes aligned with view-side
forms: `Saldo d'obertura`/`Saldo de tancament` ->
`Saldo inicial`/`Saldo final`; `Suggeriment de...` -> `Pista de...`.
- account_statements.coverage.status.not_expected:
`No s'esperava` -> `No previst` (status label, not past action).
- account_statements.index.empty_unmatched: aligned with the section's
own label `Safata sense aparellar`.
- imports.create.document_provider_not_configured + document_upload_failed:
`arxiu vectorial` -> `magatzem vectorial` (correct TermCat term).
- coinstats_items blockchain gender: `els blockchains` / `un blockchain` ->
`les blockchains` / `una blockchain` (feminine per TermCat).
- accounts.account.remove_default: `Treu el predeterminat` ->
`Treu com a predeterminat` (pairs with sibling `Estableix com a
predeterminat`).
- accounts.tax_treatments.tax_deferred: `Diferit fiscalment` (lit. calque)
-> `Tributació diferida` (standard CA tax-accounting term).
- settings.payments.show.currently_on_plan: `Actualment al` ->
`Actualment al pla:` (was a fragment).
Out of scope (review flagged, not applied here)
- LOW-severity stylistic preferences (Veure vs Mostra, etc).
- `models/category/ca.yml` default category names — seeded at family
creation, not via I18n at runtime, so changes wouldn't affect existing
families.
- `models/period/ca.yml` short labels mixing EN (MTD/YTD) and CA (STD/MA)
— needs a one-convention decision separately.
* fix(i18n,ca): drop gendered article in period_activity + tighten cash-flow terms
- pages.dashboard.investment_summary.period_activity: 'Activitat del
%{period}' contracted 'del' = 'de el' (masc.sg.). %{period} resolves
to mixed forms ('Setmana en curs' fem, 'Últims 30 dies' pl., 'Any en
curs' apostrophe), so hard-coded 'del' was wrong on most labels.
Replaced with 'Activitat — %{period}' (em-dash) to skip the
contraction entirely.
- pages.dashboard.outflows_donut.title / total_outflows: switched from
bare 'Sortides' / 'Total de sortides' to 'Sortides de caixa' /
'Total de sortides de caixa' to match TermCat's precise term
('sortida de caixa' = cash outflow).
* fix(i18n,ca): rephrase transfer source/destination amount labels
'Import d'origen' / 'Import de destinació' were literal calques of
'Source amount' / 'Destination amount'. In a multi-currency transfer
form (sender/receiver in different currencies) the natural CA pair is
'Import enviat' / 'Import rebut'.
* fix(i18n,ca): 'Dades en brut' -> 'Dades sense processar'
The literal calque of 'Raw data' read as too technical for personal-
finance UI. 'Dades sense processar' is the more natural Catalan
equivalent for raw/unprocessed data files.
* fix(i18n): localize Import col_sep label + separator options
The CSV upload form rendered 'Col sep' (the auto-humanized attribute
name) plus hardcoded English 'Comma (,)' / 'Semicolon (;)' options
from Import::SEPARATORS.
- activerecord.attributes.import.col_sep added (en + ca: 'Column
separator' / 'Separador de columnes').
- Import.separator_options class method returns translated tuples;
view switched from Import::SEPARATORS to Import.separator_options.
- activerecord.attributes.import.col_seps.{comma,semicolon} added so
the option labels follow the active locale.
* fix(i18n,ca): drop moniker apposition in sharing/currencies section titles
- sharing_title 'Compartició de %{moniker}' rendered as 'Compartició
de Família' (a noun-noun apposition that's odd in CA) -> 'Compartició
de comptes'.
- sharing_subtitle replaced '%{moniker}' with 'entre els membres' so
the sentence reads naturally and doesn't depend on moniker gender.
- currencies_title 'Divises de %{moniker}' had the same apposition
-> 'Divises'. Subtitle no longer references moniker either.
* fix(i18n,ca): keep 'Self Hosting' untranslated
Reverted 'Autoallotjament' / 'autoallotjada' / 'autoallotjats' usages
to the original English 'Self Hosting' (sidebar label, breadcrumbs,
hostings page title, chat assistant settings hint, redis configuration
subheading, LLM usages cost-estimates description).
The brand-style term reads more naturally in EN for technical users
configuring their own deployment.
* fix(i18n,ca): lowercase 'self hosting' (sentence case in labels)
* fix(i18n): extract budget_categories stepper + allocation_progress strings
Hardcoded English strings on the budget category editor:
- 'Setup' / 'Categories' stepper labels in budgets/_budget_nav.html.erb
- 'X% set' / '> 100% set' / 'left to allocate' / 'Budget exceeded by ...'
in budget_categories/_allocation_progress.erb
- '/m avg' caption + 'Shared' placeholder + 'Leave empty to share
parent's budget' tooltip in budget_categories/_budget_category_form
and _uncategorized_budget_category_form
Extracted to:
- budgets.budget_nav.{setup,categories}
- budget_categories.allocation_progress.{percent_set,over_set,left_to_allocate,budget_exceeded_html}
- budget_categories.budget_category_form.{monthly_average,shared_placeholder,shared_title}
CA translations added; EN keys mirror the prior literals.
* chore(i18n): drop translation tooling from PR
These were dev-only helpers used during the Catalan translation pass:
- script/lt_check_ca.rb: LanguageTool API checker (premium/free
endpoint, picky mode, batching). Useful for ongoing locale QA but
shouldn't ship in this feature PR.
- lib/tasks/i18n_screenshot.rake: rake task that flips user.locale and
role on the demo user for walking the i18n surfaces locally.
Both stay available locally; pulled out of the PR scope.
* fix(i18n): apply PR review feedback (CodeRabbit + Codex)
- balance_reconciliation crypto_items: use :end_balance_crypto tooltip
(was :end_balance_investment). Added new UI.account.balance_reconciliation.tooltips.end_balance_crypto key in en + ca.
- doorkeeper.ca.yml confidentiality.no: was YAML boolean false, now string 'No'.
- views/categories: 'Poor contrast, choose darker color or' continued with hardcoded 'auto-adjust.' button text; extracted to categories.form.auto_adjust key (en + ca).
- imports.create.document_upload_failed: 'a l'magatzem' was broken
contraction -> 'al magatzem'.
- invitation_mailer body + mailer subject: 'unir-se' -> 'unir-te' (was
3rd person, should be 2nd to match the rest of the copy).
- 7 strings across mercury_items / sophtron_items / simplefin_items /
lunchflow_items / brex_items / indexa_capital_items / other_assets:
'se sincronitzaran' -> 'es sincronitzaran', 'se segueixen' ->
'es segueixen' (correct reflexive pronoun before consonants).
- settings.providers.status: key was 'false' (YAML-coerced), now 'off'
to match settings/en.yml status.off used in view lookups.
- sophtron_items.sophtron_setup_required.message: stripped trailing
blank line from the quoted scalar.
- settings/profiles/show.html.erb: switched 'family_moniker ==
"Group"' branch checks to 'Current.family&.moniker == "Group"'.
After Family#moniker_label started returning translated values,
callers using the display label for branching would render the
household copy for group families in ca. Compare the stored sentinel
instead.
- Did not apply CodeRabbit's webauthn 'eliminada' -> 'desada' suggestion:
the key is wired to the destroy action (verified at
settings/webauthn_credentials_controller.rb:55), so 'eliminada' is
correct.
|
||
|
|
7411db5689 |
feat(i18n): add Hungarian translations for strings extracted in #1806 (#1817)
* add missing Hungarian translations for newly extracted strings Replace hard-coded UI strings with I18n lookups across controllers, models and views (breadcrumbs, dashboard, reports, settings, transactions, balance sheet, MFA status). Update models to use translations for category defaults, account/display names, classification group and period labels; remove a few hardcoded display_name methods. Add and update numerous locale files (English and extensive Hungarian translations, plus model/view/doorkeeper entries) to provide the required keys. These changes centralize copy for localization and prepare the app for Hungarian/English UI text. * Pluralize account type labels; tidy Crypto model Update English locale account type labels to use plural forms for consistency (Investment(s), Properties, Vehicles, Other Assets, Credit Cards, Loans, Other Liabilities). Also remove an extra blank line in app/models/crypto.rb to tidy up formatting. * Back to singular * fix(i18n): separate singular and group account labels * Update _accountable_group.html.erb * Use I18n plural names for account types Change Accountable#display_name to look up pluralized account type names via I18n (accounts.types_plural.<underscored_class>) with a fallback to the legacy display logic. Add legacy_display_name helper to preserve previous behavior (singular for Depository and Crypto, pluralized otherwise). Add corresponding types_plural entries in English and Hungarian locale files for various account types. --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: sure-admin <sure-admin@splashblot.com> |
||
|
|
5249842c76 |
feat: beta features toggle + Beta pill primitive (#1829)
* feat: beta features toggle + Beta pill primitive Adds the infrastructure for self-service beta opt-in. No call sites yet: this PR is meant to land first so feature PRs (Goals, etc.) can ship behind the gate incrementally. User opts in via a single toggle at the bottom of Settings → Preferences. The flag persists in the existing `users.preferences` JSONB column under `beta_features_enabled` — same shape as `dashboard_two_column` and `show_split_grouped`, so no migration is needed. Controllers gate a beta feature by adding `before_action :require_beta_features!` from the new `BetaGateable` concern (included in ApplicationController). Views use the `beta_features_enabled?` helper to hide / show nav items, banners, etc. Logged-out callers always return false. Ships `DS::BetaPill`, a small inline marker for tagging features as Beta / Canary in nav, headers, and lists. Five tones (violet by default, indigo, fuchsia, amber, gray) map to existing Sure color tokens — no raw hex. Three styles (soft / filled / outline) and two sizes (sm / md) cover the surfaces in the design handoff. The `dot_only:` mode renders just the colored dot for use on a collapsed sidebar. * review: rename to DS::Pill, fix CR/Codex nits, add tests CodeRabbit + Codex review feedback: - Rename DS::BetaPill → DS::Pill. The component was already generic in shape (tones, styles, sizes); the name was misleading scope. "Beta" becomes the default label (still i18n-driven). Goals' StatusPill can later refactor onto this primitive without a third pill. - Localize the default pill label via i18n (`ds.pill.default_label`) instead of hard-coding English. - Add role="img" to the dot-only span so the aria-label is consistently exposed to assistive tech. - Wrap the Preferences toggle row in <label for="…"> so the title and description become an honest click target for the toggle (matches the cursor-pointer affordance). - Drop arbitrary Tailwind values (py-[3px], gap-[5px], tracking-[…]) in favor of scale tokens. text-[10/11px] stays because the pill is intentionally sub-12px (Sure's smallest scale token is text-xs / 12px) to read as a marker, not a label. - Add User#beta_features_enabled? predicate tests covering default-off, explicit-true, and non-boolean truthy values. Won't fix: - Palette refs (`--color-violet-*` etc.). Sure has no semantic Beta/ Canary tokens; introducing them in this PR would be a design-system change beyond the scope. The component centralizes palette use in one `palette` method, matching the existing pattern in Goals::StatusPillComponent. * review: consistent title fallback in full-pill branch * docs: how to gate a feature behind the beta toggle * docs: unwrap doc lines to match existing style * chore(preview): run Cloudflare PR previews on basic instances (#1831) * fix(preview): use Rails health endpoint for container ping (#1823) * fix(preview): use Rails health endpoint for container ping * fix(preview): point container ping to localhost/up --------- Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com> |
||
|
|
70fc52769d |
Add super_admin debug event log (#1816)
* Add super-admin debug event log * Address debug log review feedback * Whitelist debug filter params * Make debug log retention configurable |
||
|
|
0c126b1674 |
feat(i18n): extract hardcoded English strings to locale files (#1806)
* Extract hardcoded strings to i18n
Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.
* Update en.yml
* Update preview-cleanup.yml
* Revert "Update preview-cleanup.yml"
This reverts commit
|
||
|
|
e59235fdc5 |
feat(statements): add account statement vault (#1753)
* feat(statements): add account statement vault Add web-only statement uploads, account linking, duplicate detection, and per-account coverage/reconciliation checks without mutating transactions. Extend ActiveStorage authorization and targeted tests for family/account scoping. * fix(statements): return deleted account statements to inbox Preserve linked statement records when an account is deleted by moving them back to the unmatched inbox, then expand coverage for upload validation, sanitized parser metadata, unavailable reconciliation, and missing-month coverage. * fix(statements): harden vault upload review flows Address review and security findings in the statement vault by preserving sanitized parser metadata, failing closed on orphaned statement blobs, avoiding account_id mass assignment permits, and adding regression coverage for link/delete edge cases. * fix(statements): harden vault upload and access controls * fix(statements): address vault hardening review * fix(statements): address vault review feedback Prioritize SHA-256 duplicate detection while preserving MD5 fallback for legacy rows. Remove free-form account notes from statement matching, document direct account-destroy unlinking, and add year-selectable historical coverage with muted out-of-range months. * fix(statements): harden vault review follow-ups Clarify legacy MD5 checksum use, whitelist statement balance helper dispatch, and preserve sanitized parser metadata. Hide statement management controls from read-only viewers while keeping server-side authorization unchanged. * fix(statements): repair settings system coverage Allow the changelog provider lookup in the self-hosting settings system test, include Statement Vault in settings navigation coverage, and align the feature title casing. Update the devcontainer so ActiveStorage and parallel system tests can run in the documented environment. * fix(statements): move vault beside accounts Place Statement Vault with account settings instead of between Imports and Exports. Keep settings footer ordering and system navigation coverage aligned, including the non-admin visibility guard. * fix(statements): address vault review cleanup Resolve CodeRabbit review feedback for statement upload validation, duplicate race handling, account statement matching semantics, metadata detection, ActiveStorage authorization tests, and small UI/style cleanups. * fix(statements): address vault cleanup review * fix(statements): deduplicate vault style helpers * fix(statements): close vault review follow-ups * fix(statements): refresh schema after upstream rebase * fix(statements): process vault uploads sequentially * fix(statements): close vault review follow-ups * fix(statements): scope vault index to accessible accounts * fix(statements): harden statement vault readiness Squash the statement vault migration hardening into the feature migration, tighten Active Storage authorization edge cases, bound CSV metadata detection, and add real PDF fixture coverage for stored statements. Validation: targeted statement/auth/controller/provider tests, full Rails suite, system tests, RuboCop, Biome, Brakeman, Zeitwerk, importmap audit, npm audit, ERB lint, CodeRabbit, and Codex Security all passed locally. * fix(statements): close vault review follow-ups Move statement unlinking to after account destroy commit, keep Kraken account creation on the shared crypto helper, and add statement metadata length limits with DB checks. Validation: fresh devcontainer with fresh DB via db:prepare, focused account/statement/Kraken/Binance tests, RuboCop, Brakeman, Zeitwerk, git diff --check, CodeRabbit, and Codex Security passed before commit. * fix(statements): address vault scan follow-ups Move statement tab data setup out of the ERB partial, harden reconciliation labels and coverage initialization, and tighten statement schema constraints. Validation: CodeRabbit and Codex Security reviewed the current PR diff; Rails focused tests, full Rails tests, system tests, RuboCop, Brakeman, Zeitwerk, ERB lint, npm lint, importmap audit, npm audit, and git diff --check passed. * fix(statements): defer vault tab loading --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
95f6451b39 |
feat(sync): add Brex provider connections (#1752)
* feat(sync): add Brex provider schema Adds Brex item and account tables with per-family credentials, scoped upstream account uniqueness, encrypted token storage, and sanitized provider payload columns. * feat(sync): add Brex provider core Adds Brex item/account models, provider client and adapter support, family connection helpers, and provider enum registration for read-only Brex cash and card data. * feat(sync): add Brex import pipeline Adds Brex account discovery, linked-account sync, cash/card balance processors, transaction import, sanitized metadata handling, and idempotent provider entry processing. * feat(sync): add Brex connection flows Adds Mercury-style Brex connection management, explicit item-scoped account selection and linking, settings provider UI, account index visibility, localized copy, and per-item cache handling. * test(sync): cover Brex provider workflows Adds targeted coverage for Brex provider requests, adapter config, item/account guards, importer behavior, entry processing, and Mercury-style controller flows. * fix(sync): align Brex API edge cases Tightens Brex account fetching against the official card-account response shape, sends transaction start filters as RFC3339 date-times, and keeps provider error bodies out of user-facing messages while expanding provider client guard coverage. * fix(sync): harden Brex provider integration Restrict Brex API base URLs to official hosts, tighten account-selection UI behavior, and add tests for invalid credentials, cache scoping, and provider setup edge cases. * test(sync): avoid Brex secret-shaped fixtures * refactor(sync): extract Brex account flows * fix(sync): address Brex provider review feedback * fix(sync): address Brex review follow-ups Move remaining Brex review cleanup into focused model behavior, tighten link/setup edge cases, localize summaries, and add regression coverage from CodeRabbit feedback. Also records the security-review pass as no-findings after diff-scoped inspection and Brakeman validation. * refactor(sync): split Brex account flow controllers Route Brex account selection and setup actions through small namespaced controllers while keeping existing URLs and helpers stable. Business flow remains in BrexItem::AccountFlow; the main Brex item controller now only handles connection CRUD, provider-panel rendering, destroy, and sync. * fix(sync): address Brex CodeRabbit review * fix(sync): address Brex follow-up review * fix(sync): address Brex review follow-ups * fix(sync): address Brex sync review findings * fix(sync): polish Brex review copy and errors * fix(sync): register Brex provider health * fix(sync): polish Brex bank sync presentation * fix(sync): address Brex review follow-ups * fix(sync): tighten Brex setup params * test(api): stabilize usage rate-limit window * fix(sync): polish Brex setup flow nits * fix(sync): harden Brex setup params * fix(sync): finalize Brex review cleanup --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
ce5d7dd736 |
Add Interactive Brokers Provider (#1722)
* Display multi-currency holdings correctly * Implement IBKR provider * Fix: Use historical exchange rate for historical prices * Add brokerage exchange rate for trades * Sync historical balances from IBKR * Add logos in activity history * Fix privacy mode blur in account view * Improve IBKR XML Flex report parser errors |