* feat(settings/providers): surface connection status in section headers
Lifts the per-panel status indicator up to each collapsed accordion
header so admins can see at a glance which providers are connected
without expanding every section. Connected providers sort first.
- Add optional status: and meta: locals to settings/_section partial;
pill hides via group-open:hidden when the section is expanded
- New settings/providers/_status_pill partial (ok/warn/err/off states)
- Add SettingsHelper#provider_summary to centralise the connected-vs-not
logic already scattered across panel partials
- Refactor show.html.erb to pass status to every section and sort
family_panels by connection state
- Add settings.providers.status.* i18n keys
- Add system tests asserting pill renders and sort order
https://claude.ai/code/session_01KW2HCN9rP1fiyQuw7Cju9D
* feat(settings/providers): group providers into Connected and Available
Partition the provider list in the controller into @connected_providers
and @available_providers based on provider_summary status, and render
each group under its own heading with a count. Auto-open the section
when only one provider is connected. Adds an empty-state line when
nothing is connected yet.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(settings/providers): health strip, action-needed group, and sync error surfacing
- Extend provider_summary to return :err/:warn with meta text by checking
latest sync per item (window function, same pattern as ProviderConnectionStatus)
and Enable Banking session expiry within 7 days
- Partition provider entries into three groups: Connected (:ok), Action needed
(:warn/:err, auto-opened), Available (:off)
- Add Settings::HealthSummary ViewComponent — four-tile grid showing Connected,
Action needed, Errors, and Accounts synced counts
- Render health strip directly under page description; omit Action needed heading
when group is empty
- Add i18n keys for tile labels, group heading, and all meta strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(settings/providers): card grid for available providers with connect drawer
- Add Provider::Metadata registry with static display data (region, kind,
tier, maturity, logo) for all 11 providers
- Add Settings::ProviderCard ViewComponent rendering logo square, name,
Beta/Alpha pill, meta line (region · type · tier), tagline, and Connect link
- Add connect_form action + route (GET /settings/providers/:key/connect_form)
that opens the existing panel partial or config form in a DS::Dialog drawer
- Replace the Available accordion loop with a 2-column responsive card grid;
empty state when all providers are connected
- Fix layout override: use turbo_rails/frame layout for frame requests so the
drawer response is not wrapped in the full settings layout (was causing
Turbo to pick the empty outer drawer frame instead of the filled one)
- Add SyncAllProvidersJob and last_sync_all_attempted_at migration (sync-all
throttle support)
- Unify Connected + Action needed into a single "Your connections" section;
items with warn/err status auto-open
- Fix Enable Banking grouping: items with expired sessions were returning
:off (Available) instead of :warn (Your connections); gate now checks
any? instead of any?(&:session_valid?)
- Add reconsent_required locale key for fully-expired EB sessions
- Surface Beta/Alpha maturity pills on connected provider accordion rows
via new badge: param on settings_section helper
- Add i18n taglines for all 11 providers; add connect and empty_available keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(settings): retire /settings/bank_sync; merge into providers page
- Delete Settings::BankSyncController and its views (the providers page is
now a strict superset of what bank_sync offered)
- Add permanent 301 redirect: GET /settings/bank_sync → /settings/providers
- Collapse nav to a single "Bank Sync" entry pointing at /settings/providers;
remove the duplicate admin-only "Providers" entry from the Advanced section
- Remove "Providers" from SETTINGS_ORDER; point "Bank Sync" at
settings_providers_path for next/prev navigation
- Rename page title to "Bank Sync"; replace admin-credential lede with
user-facing copy ("Connect external accounts…")
- Update breadcrumb: Home → Bank sync
- Add controller test asserting 301 status and Location header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Migrations are 7.2 here
* Minimize schema noise
* Schema duplication
* Small copy edits
* Fix tests
* Address provider settings review feedback
* refactor(settings/providers): finish design-review cleanup pass
Picks up the remaining items from Claude Design's review of #1710
that the previous review-feedback commit didn't cover.
DS / casing
- Sentence-case the page title ("Bank Sync" -> "Bank sync") and
align the nav label.
- Drop the card hover-lift (shadow-border-sm) in favour of
bg-container-hover; per the DS, card hover is colour-only.
- Whole-tile click target on each provider card — the inner
"Connect ->" link was a hit-target inversion.
- Set Sync all to whitespace-nowrap so the label stops wrapping at
narrow viewport widths.
UX simplifications
- Drop the four health-summary tiles (per-row warn/err pills already
surface the signal at the scale this app sees). Removes
Settings::HealthSummary, the @health_counts controller block, and
the now-unused health.* locale keys.
- Hide "Your connections" heading + empty-state line when no
providers are connected — the lede already invites a connect.
- Drop the redundant "Free" tier from per-card meta lines (printed
10x for one fact); "Paid" still surfaces on Plaid.
Tests updated to drop the obsolete tiles assertion and switch the
provider-card click selector to look up the (now whole-card) anchor
by provider name.
* feat(settings/providers): replace Add another provider CTA with a search + kind filter
Per the design review, the "Add another provider · Browse providers"
card was a redirect to content one scroll-tick away. A search input
plus kind chips lets users self-segment the catalog and is the right
tool once it grows beyond the four to twelve providers we ship today.
- New providers_filter Stimulus controller — case-insensitive free
text search across name/region/kind, plus a chip group with
All / Banks / Crypto / Investment that toggle visibility via
Tailwind's `hidden` class.
- _search_filters partial: search box (count-pluralized placeholder)
+ chip group, ARIA-labelled and aria-pressed for the chips.
- ProviderCard exposes filter_data (target + name/region/kind data
attrs) so the controller can match without re-rendering.
- Lunchflow's `kind` was "Lunch" — switched to "Bank" so it falls
under the Banks chip alongside its actual offering (it aggregates
banks).
- Drops the add_provider_cta partial and its locale entries; adds
search_filters.* and an empty_filter message.
* Private method fix
* refactor(settings/providers): drawer cleanup, header lock-up, trust statement
Per the design review's §07.
- Drop the trailing "Configured / Not configured" footer status from
every provider panel (binance, coinbase, coinstats, indexa_capital,
lunchflow, mercury, simplefin, snaptrade, sophtron, provider_form).
The parent details section's status pill already carries that
signal; the footer was redundant — and the copy/styling was
inconsistent across panels (free-text vs. dot pill, "configured"
vs. "not connected").
- Connect drawer gets a header lock-up: small logo chip + provider
name + maturity badge, mirroring the available-card layout.
Implemented as _drawer_header partial; connect_form passes
custom_header: true to DS::Dialog so we own the row.
- Drawer footer trust statement: "Read-only — Sure can never move
money. Stored encrypted." A single-line reassurance covering all
panels.
- Sentence-case the hardcoded primary buttons that were Title Case:
"Save Configuration" -> "Save and connect"
"Update Configuration" -> "Update connection"
"Connect Bank" -> "Connect bank"
Affects simplefin, lunchflow, enable_banking, provider_form. The
i18n'd panels (binance, coinbase, coinstats, indexa_capital,
mercury, snaptrade, sophtron) keep their existing keys.
* chore(locales): drop unused provider-panel status strings
Footer "Configured / Not configured" status was removed from each
provider panel partial in the prior drawer-cleanup pass; the matching
i18n keys are no longer referenced. Removing them across every
locale to keep the catalogue clean.
Dropped (15 keys × varying locale coverage, 36 line removals across
24 files):
- coinstats_items.new.{status_configured_html, status_not_configured}
- indexa_capital_items.panel.{status_configured_html, status_not_configured}
- mercury_items.provider_panel.{configured_html, not_configured, accounts_link}
- sophtron_items.sophtron_panel.status.{configured_html, not_configured}
(parent `status:` removed where it became empty)
- providers.snaptrade.{status_needs_registration, status_not_configured}
(status_connected stays — still used by the lazy-load summary)
- settings.providers.{binance_panel, coinbase_panel}.{status_connected, status_not_connected}
* feat(settings/providers): connected-state polish per design §05 + Linked institutions rename
Building the next phase of the design review. Pulls forward the
slim health strip, denser connection rows, and "Linked institutions"
heading rename — the small Phase A lift the designer flagged in
§08 of the doc.
- New _health_strip partial: single-line at-a-glance pulse —
connected count + needs-attention count + accounts syncing +
last-synced timestamp. Renders only when at least one provider
is linked or needs action.
- New _connection_row partial replaces the generic settings_section
call for providers. Tighter rows: text-sm title (was text-lg),
px-4 py-3.5 padding, single-line summary (chevron + name +
maturity badge + meta + status pill + sync action). Warn/error
rows get a coloured outline (border-warning/25 or
border-destructive/25) so the at-risk row stands out without
shouting.
- "Sync all" button restyled to match the design's secondary
button: text-primary, alpha-black-100 border, rounded-[10px],
padding 7px 12px (was the broader px-3 py-1.5 ghost).
- "Your connections" → "Linked institutions" heading, lifted from
the designer's Phase-C reconciliation note. Primes users for the
Option-C institution-search wizard six months early; existing
i18n key stays as `groups.your_connections` for now to keep the
rename to a single value flip.
- Controller computes the new @health hash (connected,
needs_attention, accounts_syncing, last_synced_at) feeding the
strip; brings back the single accounts query that was removed
with the four-tile component.
System test updated for the new heading copy.
* fix(settings/providers): align connected state with the final design mock
Tightening the §05 polish to match the user-confirmed final design.
- Revert "Linked institutions" → "Your connections". The §08
designer note about the Phase-A heading rename didn't carry
forward to the final mock; keep the original wording.
- Drop the warn/err auto-open on connection rows. The design shows
Enable Banking collapsed with a warn-outline and a status pill —
no auto-expanded form. Single-connection auto-open kept (handy
when the page is otherwise empty).
- Hide the "accounts syncing" segment in the health strip when the
count is 0 — the design mock assumes a populated number; an
always-visible "0 accounts syncing" reads as a placeholder.
- Strip the leading "about " from `time_ago_in_words` everywhere
the result is shown to the user (health strip "Last synced %{time}
ago" plus per-row "Synced %{time} ago" meta). Matches the design's
shorter copy.
* refactor(settings/providers): tighten paddings, dedupe maturity badge, semantic + a11y fixes
Pixel-level alignment to the design's §05 mock + cleanup from a DS
audit pass.
Paddings, margins, font sizes
- Health strip: my-4 → mt-4 mb-5 to match the design's 16px / 20px
vertical breathing room.
- Search filters bar: gap-2 → gap-2.5; mt-2 → mt-5 mb-3 (was missing
the 12px bottom margin entirely).
- Search box: rounded-lg → rounded-[10px]; px-3 py-2 → px-[14px]
py-[9px]. Search icon downsized w-4 → w-3.5 to match.
- Chip group: p-1 → p-[3px]; rounded-lg → rounded-[10px].
- Chip: py-1 → py-[5px]; rounded-md → rounded-lg.
- Group heading: mt-2 → mt-[18px]; mb-1 → mb-1.5.
- Status pill: text-xs → text-[11px].
- Provider card: gap-3 → gap-2.5 (outer + top); name gets explicit
text-sm; tagline + foot 14px → 13px; arrow icon w-4 → w-3.5.
- Sync icon button: p-1 → fixed w-7 h-7 (28×28) so the row hit
target matches the design's column width.
- Connect drawer header logo glyph: text-[10px] → text-xs (matches
the available card's logo-glyph treatment).
Component / partial cleanup (DS audit follow-ups)
- New _maturity_badge partial replaces the inline span that was
duplicated in 3 places (_connection_row, _drawer_header,
provider_card.html.erb).
- Settings::ProviderCard.maturity_label class method centralizes the
MATURITY_LABELS lookup; callers no longer reach into the constant.
- _connection_row title: <h2> → <h3> (the row sits inside the
"Your connections" h2 group heading; nested h2s flattened the
outline).
- show.html.erb encryption error: <h3> → <h2> for the same reason.
Locale
- Drop orphaned keys: settings.providers.groups.connected and
groups.needs_attention (no view code uses them) plus the leftover
show.coinbase_title block.
- Health strip "needs reconsent" → "needs attention" so the strip
copy lines up with the per-row status pill ("Action needed") and
the original group heading wording.
A11y
- focus-visible:ring-2 on chip buttons, provider-card link, and
focus-within:ring-2 on the search input wrapper. Keyboard users
now get a visible focus state.
- Search input: explicit autocomplete="off" (erb_lint hint).
* fix(settings/providers): icons + search input height
- Icons were rendering at 20px because the application_helper's `icon`
default size (`md` = w-5 h-5) was beating the inline class override
in compiled CSS source order. Pass `size: "sm"` and use the project's
`!w-3.5 !h-3.5` important-prefix pattern (precedent: dashboard.html.erb)
so chevron, refresh-cw, search, check, circle-alert, and arrow-right
all render at the design's 14px.
- Search input was 54px tall because @tailwindcss/forms applies
`padding: 8px 12px` to bare `<input type="search">`. Override with
`!p-0 focus:ring-0 focus:shadow-none` so the wrapping div's padding
alone defines the box (38px total — matches the design).
* refactor(settings/providers): align Sync all + search input with DS, address review feedback
- Sync all: replace the hand-rolled `button_to` with `DS::Link.new(variant: "outline", method: :post)` — same component as the
"Identify Patterns" button on the recurring-transactions page.
- Search input: switch to the icon-overlay pattern used by the
Manage-currencies and transaction filter rows
(relative wrapper + absolutely positioned search icon +
bordered input with `focus:ring-gray-500`). Brings the keyboard
focus state in line with the rest of the app's filterable lists.
- SnapTrade panel: restore the "needs registration" status row that
the drawer-cleanup pass dropped along with the redundant
Configured/Not configured footer. The unregistered case is
meaningful state, not redundant chrome.
- Move the slim health-strip computation out of the controller and
into `SettingsHelper#provider_health_strip` (Convention 2: skinny
controllers).
- Extract `concise_time_ago` helper so the "drop leading 'about '"
trick stops being duplicated 3x.
- `Settings::ProviderCard#maturity_label` (instance) now delegates
to `.maturity_label` (class) instead of duplicating the lookup.
- Drop unused `warn_or_err` local in `_connection_row`.
- Replace the `data-controller` string-injection + html_safe in
`_connection_row` with `tag.details(data: ...)`; safer and more
idiomatic.
- Add a system test for the empty-filter message wiring.
* fix(settings/providers): drawer trust statement uses border-tertiary
`border-secondary/10` was reaching for the text-foreground token at
10% opacity for a divider. The project ships a dedicated divider
token (`border-tertiary`, ~8% black) used by DS::Menu, the holdings
page, and admin/sso forms. Switching to it makes the trust-statement
HR match every other thin divider in Sure and stops misusing the
text token as a border.
* refactor(settings/providers): swap arbitrary Tailwind values for scale tokens
Per the user's directive — DS-compliance over pixel-perfect alignment
with the design mock. Walked the design audit and applied every swap
that lands within ±2px of the original.
Swaps:
- _health_strip: gap-[18px] → gap-5 (+2), px-[14px] → px-3.5 (=),
text-[13px] → text-sm (+1).
- _search_filters: chip group p-[3px] → p-1, rounded-[10px] →
rounded-xl (concentric with rounded-lg inner pills), chip py-[5px]
→ py-1.
- _status_pill: text-[11px] → text-xs.
- _group_heading: mt-[18px] → mt-5.
- _maturity_badge: text-[10px] → text-xs.
- provider_card: tagline + foot text-[13px] → text-sm.
Kept arbitrary: `min-w-[200px]` in _search_filters — nearest scale
tokens are min-w-48 (192px) and min-w-52 (208px); both are noticeable
layout shifts for a one-off responsive guard. Worth keeping the
arbitrary here.
Net: 9 of 10 arbitrary values gone. Visual delta: max +2px on a
single value. Design mock and DS scale now agree.
* revert(settings/providers): drop the slim health strip
Per-row status pills already carry the at-a-glance signal (connected
/ action needed) at the scale this app sees (1–4 connections per
family). The strip was redundant chrome for almost every user; only
worth bringing back if the catalog grows to a point where the row
list itself stops fitting on a single screen.
- Delete _health_strip.html.erb partial.
- Drop @health controller assignment + provider_health_strip helper.
- Drop unused settings.providers.health_strip.* locale keys.
- concise_time_ago helper stays — still used by per-row meta text.
* refactor(settings/providers): align with DS conventions
Two consistency wins from the screenshot/DS audit pass.
Sync icon button now renders DS::Button (variant: icon, size: sm)
instead of a hand-rolled `button_to`. Same component used by other
icon-only actions across the app (settings/profiles, layouts/imports).
Visual delta: 28×28 → 32×32 (DS sm size). Accept the +4px for
consistency. `event.stopPropagation()` still wired via the form opt
so the row's <details> doesn't toggle when the user clicks the
button.
Group heading now follows the established Sure section-label style
(`text-xs font-medium text-secondary uppercase`) used by
`_settings_nav` and the imports/categories surfaces. The previous
sentence-case `text-sm text-primary` was a one-off that didn't
match the rest of the app. Locale strings stay sentence-case;
uppercase comes from CSS `text-transform`. Tests updated to
case-insensitively match the rendered heading text.
* fix(provider/metadata): add plaid_eu entry
`plaid_eu` is registered as a separate Provider::ConfigurationRegistry
entry but had no Provider::Metadata row, so its card in the
Available grid fell through to the gray-500 default and rendered
empty (no region, kind, tier, or tagline). The title also came out
as "Plaid Eu" because `titleize` doesn't know "EU" is an initialism.
- Add a `plaid_eu` row to Provider::Metadata::REGISTRY with the same
shape as `plaid` (US → EU, otherwise identical).
- Introduce an optional `name:` field in metadata; controller falls
back to it before titleizing the provider key. Lets `plaid_eu`
render as "Plaid EU".
- Add the missing `settings.providers.taglines.plaid_eu` translation.
* fix(settings/providers): center-align Sync all next to the lede
`items-start` made the button hug the first line when the lede wrapped;
on a single line the button sat at the top of the text bounding box
which read slightly off. Center matches the dominant convention
across the rest of settings (api_keys, securities, hostings, _section,
_settings_nav_link_large).
* fix(settings/providers): drop colour palette + filter polish + drawer warnings
Round of design-feedback fixes.
Provider chips
- Drop the per-provider raw Tailwind palette (bg-blue-600 etc.) from
Provider::Metadata. All cards + drawer logo lock-up now use
bg-surface-inset + text-primary, matching the design's §04 "drop
colour entirely" recommendation. Solves the long-standing §01
BLOCKER without externalising brand assets. Re-introducing logos
later just means an optional logo_svg: field on metadata.
- ProviderCard component drops the `logo_bg:` parameter; the chip
is now styled in the template.
Filter / search
- "Available · N" count and the empty-filter state now update
client-side as the chip filter and free-text search narrow the
grid (new `count` Stimulus target + dedicated update path).
- Empty-filter state now offers a Clear filters button that resets
both the search input and the active chip in one click.
- Search placeholder drops the drifting "Search 9 providers" count
for plain "Search providers" — the section heading carries the
number.
- Chip labels normalised to plural where natural: "Banks · Crypto ·
Investments" (Crypto stays as the mass noun).
Drawer copy / treatment
- "IP Whitelisting Required" → "IP whitelisting required" (DS
sentence-case).
- Binance "do NOT enable withdrawal permissions" lifted out of
inline red-text into a proper bg-warning-50 border-warning-200
alert block with an alert-triangle icon. Matches the api_keys /
hosting alert pattern.
- SnapTrade free-tier inline alert-triangle now uses `size: "sm"`
so the icon stops rendering at 20px next to 14px body text.
Spacing
- Group-heading margin top bumped 5 → 6 (20→24px) so the eyebrow
has more breathing room above the search bar.
* refactor(settings/providers): drawer alerts use DS::Alert; drop card-in-card
Two consistency fixes from a design-review pass.
DS::Alert adoption
- Replaces 9 hand-rolled error blocks across the provider panels
(`bg-destructive/10 text-destructive ... line-clamp-3`) with
`DS::Alert(variant: :error)` — the project's existing primitive.
- Replaces the just-shipped Binance no-withdraw warning block with
`DS::Alert(variant: :warning)` instead of a hand-rolled
`bg-warning-50 border-warning-200` card.
- Replaces the SnapTrade free-tier inline icon-prefixed warning
paragraph with `DS::Alert(variant: :warning)` — proper alert
treatment for an actual warning, not body copy.
- Replaces the Enable Banking "Configuration locked" inline
`bg-warning/10` two-paragraph block with `DS::Alert(variant: :warning)`
using `safe_join` for the title + body.
- Replaces the encryption-error block at the top of show.html.erb
with `DS::Alert(variant: :error)`, again via `safe_join`.
Mercury card-within-card
- The "Add another Mercury connection" form was wrapped in a
`<details>` `bg-container shadow-border-xs rounded-xl` card. In
the Connect drawer (always 0 existing connections), that wrapping
card-inside-the-drawer-card has no value — the form is the only
thing on the surface. Drop the wrapper when no connections exist;
keep the heading + form inline. When 1+ connections exist (the
section page) the heading hints "+ Add another connection"
without the disclosure indirection.
Trade-off: the error-alert blocks lose their `line-clamp-3` /
`title=` truncation. Acceptable for now — DS::Alert can grow a
truncate option as a follow-up if needed.
Open follow-up: DS::Alert itself uses raw Tailwind palette
(`bg-yellow-50` etc.) instead of semantic tokens, and only accepts
a single string `message:`. A separate issue tracks this.
* fix(settings/providers): hoist warning alerts to top of drawer
DS::Alert convention across the rest of the app: alerts sit at the
top of the form / page / section, not floating between content
blocks. The Binance no-withdraw warning and SnapTrade free-tier
warning were rendering between the setup-instructions list and the
form fields — visually wonky.
Move both to the top of their respective panels so the warning is
the first thing the user sees when the connect drawer opens.
Existing precedents this aligns with:
- accounts/_form.html.erb (error alert above form)
- valuations/new.html.erb (error alert above form)
- other_assets/new.html.erb (info alert above form)
- holdings/show.html.erb (warn alerts above content)
* fix(DS::Alert): align icon to cap-height of first text line
`items-start` on the container made the icon's top edge flush with
the text's top edge, leaving the icon's optical center sitting below
the text's first-line center. The hand-rolled alerts elsewhere in
the codebase (api_keys/new, hostings/_sync_settings, holdings/show)
all add `mt-0.5` to the icon for the same reason — fold that into
the primitive so every caller gets the cap-height alignment.
* copy(settings/providers): tighten alert messaging per voice review
Copy expert pass on the new provider drawer alerts. House style:
sentence case for titles, lead with the action, drop "Warning:" /
"Please" filler (the alert variant icon already signals tone),
prefer one short sentence + optional title-paragraph for emphasis.
- Binance no-withdraw warning: was a single line "Warning: do NOT
enable withdrawal permissions" — alarmist without context. Now
splits into "Read-only key only" (title) + "Don't enable
withdrawal permissions when creating your Binance API key — Sure
only needs read access." (body).
- SnapTrade free-tier note: "Free tier includes 5 brokerage
connections. Additional connections require a paid SnapTrade
plan." → "SnapTrade's free tier covers 5 brokerage connections.
Upgrade on SnapTrade for more."
- SnapTrade connection-limit-info inside the brokerage list: cut
entirely. The drawer already shows the cap; restating it in the
list was noise.
- SnapTrade needs-registration: "Credentials saved — finish
registration to connect a brokerage." → "Credentials saved.
Finish setup to connect a brokerage." ("registration" was
ambiguous — register where, with whom?)
- Enable Banking "Configuration locked" body: "Credentials cannot
be changed while you have active bank connections. Remove all
connections first to update credentials." → "Disconnect all
linked banks before changing these credentials." Same meaning,
half the words.
- Encryption-error block: title-cased "Encryption Configuration
Required" → "Encryption keys missing"; body strips "Please
ensure" filler and the parenthetical credential dump, leaving
the three credential names inline as a clean list. Self-hosters
still get exactly the names they need to set.
* feat(settings/providers): SetupSteps partial for connect-drawer instructions
Per the design's drawer-cleanup follow-up. Replaces the per-panel
"Setup instructions:" + ordered list + "Field descriptions:" block
with a shared boxed-step component.
The new partial — `_setup_steps.html.erb` — takes a `steps:` array
of strings (or html_safe strings for inline links / code) plus an
optional `help:` hash for a docs link below the steps. The eyebrow
label is "Setup" (uppercase, tracking-wider) matching Sure's other
section labels.
Applied across all eleven provider panels:
- _provider_form (Plaid + Plaid EU): field descriptions move to
per-field helper text below the input.
- _binance, _coinbase, _coinstats, _indexa_capital,
_lunchflow, _mercury, _simplefin, _snaptrade, _sophtron,
_enable_banking: ordered list + duplicate "Field descriptions"
block both replaced by the partial.
- Some panels' inline copy tightened in the same pass (Lunch Flow,
SimpleFIN, Enable Banking) — the design copy is shorter than the
current legacy strings; a copy-pass through every panel can
follow as a separate cleanup.
Token notes: uses scale tokens (`rounded-xl`, `text-xs`/`text-sm`,
`tracking-wider`) instead of the design mock's exact arbitrary
values, per the consistency-over-design-specs directive on this
branch.
* fix(settings/providers): tighten panel spacing + relocate per-panel notes
Read-flow audit on each connect drawer. The uniform `space-y-4`
treated every block (alert, steps, info card, fields, button) the
same — visually they were five sibling boxes with no grouping. The
fix is per panel; some notes belong as helper text on a specific
field, others as a tightly-grouped pre-fill primer.
Per panel:
- Binance: IP-whitelisting card now matches the setup_steps box
(`bg-surface-inset rounded-xl`) and is wrapped with setup_steps
in an inner `space-y-2` so they read as a single pre-fill primer
cluster. Same eyebrow treatment ("IP whitelisting required") so
the two boxes look like sister panels, not unrelated chrome.
- SnapTrade: drop the description paragraph above setup_steps. The
available-providers card grid already markets SnapTrade
("Connect brokerage accounts via the SnapTrade aggregation
network."); repeating in the drawer was duplication.
- Mercury: move the sandbox-API note out of its standalone <p>
below setup_steps and into per-field helper text under the
base_url field — the user only cares about the sandbox URL when
they're filling that field. Applied to both the per-item edit
form and the add-new form.
- _setup_steps partial: drop the now-pointless `mb-2` (outer
`space-y-4` already controls the gap; bottom-margin was dead
CSS thanks to margin-collapse rules with the next sibling's
margin-top).
* fix(settings/providers): plaid + indexa drawers join the SetupSteps look
Two unifying fixes after the panel-by-panel screenshots showed
mixed treatments.
Plaid + Plaid EU
- The registry-driven panel (_provider_form) was still rendering
each adapter's markdown `description` block as plain prose
("Setup instructions: 1. Visit the Plaid Dashboard ..."). Other
panels switched to the SetupSteps box; Plaid was the odd one out.
- Drop the markdown `description` block from both plaid_adapter
and plaid_eu_adapter. Render setup_steps in _provider_form for
these two provider keys via inline ERB (link helper handles the
Plaid Dashboard link cleanly; the regional differences fold to
the same dashboard URL with a different account scope).
- Other registry-based providers fall through to the previous
markdown description path — no behavior change for them.
Indexa Capital
- The API token field was wrapped in a `bg-surface border` "card"
that duplicated the field label inside as a heading and put the
description above the input. Same pattern the user flagged as
the "card within input" anti-shape.
- Drop the wrapper. The styled-form input renders its own label;
description moves to per-field helper text below the input,
matching the pattern used by Plaid (provider_form) and Mercury.
* fix(settings/providers): surface configured plaid_eu + dedup show context
provider_summary had no plaid_eu branch — configured plaid_eu was
falling through to status :off and rendering in Available even with
credentials set. Collapse plaid + plaid_eu into a single registry
check.
Drawer title for non-panel configurations was provider_key.titleize,
which produced "Plaid Eu" while the available card grid used
metadata[:name] = "Plaid EU". Read from metadata first.
While here:
- compute_provider_sync_health no longer relies on
instance_variable_get; pass family_panel_items explicitly so the
hash-key/ivar-name coupling is gone.
- drop unused .includes(:syncs, :mercury_accounts) and
.includes(:snaptrade_accounts) from prepare_show_context. The show
view only consults summary[:status]; the eager-loads were carried
over from connect_form (which has its own load_provider_items).
* i18n(settings/providers): localize plaid setup steps + drop dead defaults
The plaid + plaid_eu setup steps in _provider_form.html.erb were
hardcoded English strings. Move them to settings.providers.plaid_panel
(shared) + plaid_eu_panel (EU-specific step 1) so they can be
translated like every other panel.
_setup_steps.html.erb was passing default: "Setup" / "Need help?" to
t(), masking missing translations in non-EN locales. Both keys exist
in en.yml — drop the defaults so missing translations actually
surface.
* test(settings/providers): cover plaid_eu, clear filters, warn outline
Three system test additions:
- Configured plaid_eu surfaces in Your connections (regression guard
for the helper fix; previously fell through to Available).
- Clear filters button resets input + chip state and brings cards
back into view.
- :warn-state connection row carries the border-warning/25 outline
that distinguishes it from an :ok row.
* copy(settings/providers): drop em dashes, naturalize phrasing
Sweep through every string this branch added and replace em-dash
splices with full sentences or simple connectives.
en.yml:
- drawer_trust_statement now reads "Read-only access. Sure can never
move money, and your credentials are stored encrypted." instead
of em-dash splicing.
- sync_all_recently / recently_synced split into two sentences.
- binance_panel.no_withdraw_body, plaid_panel.step_1_html / step_2,
plaid_eu_panel.step_1_html same treatment.
Hardcoded panel steps (enable_banking, lunchflow, simplefin) become
"Go to <link> and …" or "Go to <link> for …" instead of the
"<link> — get …" splice. Same setup_steps comment cleaned up.
* fix(settings/providers): address CodeRabbit pass on PR #1717
Fixed:
- Localize the setup steps in _enable_banking_panel,
_lunchflow_panel, and _simplefin_panel. The em-dash sweep had
rewritten these into hardcoded English; they now route through
settings.providers.{enable_banking,lunchflow,simplefin}_panel
step_1_html / step_2 / step_3 keys, mirroring the plaid_panel
treatment.
- connect_form: silent redirect when provider_key is unknown now
carries an alert (settings.providers.not_found) so misrouted
links don't drop users on the page with no feedback.
- sync action: redirect notice now reflects whether anything was
actually scheduled — adds settings.providers.sync_provider_no_items
for the "all items already syncing or none exist" path.
- Family::Syncer test: count plaid_items via the .syncable scope to
match what Family::Syncer actually schedules (already done for
binance_items in the same test).
Skipped, with reasons:
- focus:ring-gray-500/-gray-900 in coinstats / coinbase / simplefin /
search_filters: tracked under issue #1715 as part of the raw-palette
→ DS-token sweep across the whole codebase.
- Coinbase #0052FF brand-color wrapper: tracked under PR #1710's
follow-up tracking comment as the deferred Provider::Metadata
colour-palette decision (designer §01).
- Sophtron submit-button extraction into DS::Button: same
deferred sweep — every panel hand-rolls this class string;
one-off extraction would just churn.
- Redundant .html_safe on _html keys in coinstats: tracked in #1715.
- _provider_form.html.erb env hint, "Optional" placeholder, "Save and
connect" submit: pre-existing strings not added on this branch.
- Renaming sync_health_for's :stale to :data_stale: pre-existing
shape, refactor scope.
- Plaid_eu using plaid_panel.step_2/step_3 keys: deliberate. Same
English copy across both providers; duplicating keys would just
give translators twice the work for identical strings.
- _enable_banking_panel / _lunchflow_panel / _simplefin_panel
alert + submit + button labels: pre-existing hardcoded strings
from before this branch. Setup steps were the strings actually
touched in the em-dash sweep, so those got localized; the rest
belong in a broader panel-i18n pass.
Verified:
- bundle exec erb_lint on the three panels: clean.
- bin/rubocop on controller + test: clean.
- bin/rails test test/models/family/syncer_test.rb
test/controllers/settings/providers_controller_test.rb:
23 runs, 85 assertions, 0 failures.
- DISABLE_PARALLELIZATION=true bin/rails test
test/system/settings/providers_test.rb:
15 runs, 38 assertions, 0 failures.
* fix(db): rename migration to clear collision with main's 20260508120000
Main's PR #1705 (Sophtron manual sync) shipped a migration with
the same 20260508120000 timestamp as our
add_last_sync_all_attempted_at_to_families migration. The merge
that brought main into this branch left both files at the same
prefix, which trips Rails' "Duplicate migration" guard at
db:schema:load time and broke CI.
Renaming our migration to 20260510120000 keeps the column it adds
intact (already in db/schema.rb) and bumps the schema version to
match. No DB-level change.
* fix(settings/providers): card + strip a11y polish
- Bring back the slim health strip; gate behind 10+ accounts
(HEALTH_STRIP_MIN_ACCOUNTS) so it stays out of the way for
small libraries where per-row pills already carry the signal.
- Status pill: drop the bg-{c}/10 text-{c} pattern (failed AA
on warn / err); switch to bg-surface-inset text-primary with
the dot still carrying semantic colour. Passes AA in both
themes; the dot is the only colourful affordance.
- Maturity badge: bg-alpha-black-50 was invisible against the
hovered card bg in light mode and against bg-container in
dark mode. Move to bg-surface-inset + border-tertiary so it
stays delineated through hover and dark theme.
- Provider card: keep the bg shift on hover (now bg-surface-inset
for a perceptible delta), focus ring promoted alpha-black-100
-> alpha-black-300 (visible to keyboard users), meta line
text-subdued -> text-secondary (text-subdued failed AA at
2.86:1 against bg-container).
- Restore the per-provider logo palette dropped in 6abceb07.
Yellow-on-white was the BLOCKER then; bumped Binance to
yellow-600 and CoinStats to pink-600 (distinct from Binance
and AA-safe with white text).
- Health strip dividers: bg-alpha-black-100 was invisible in
dark mode. Switch to border-l border-secondary so the DS
variant flips correctly.
* fix(settings/providers): keep row height on open
The right-side meta + status pill + sync button group is hidden
via group-open:hidden, but the sync button (DS::Button size sm,
h-8) is what dictated the row's natural height. With it gone,
the row collapsed from 60px to 48px and the title appeared to
jump upward.
Pin a min-h-15 on the <summary> so the height stays constant
through open/close.
* Let's not regress IPv6
* Keep the only real change in schema.rb
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Signed-off-by: Guillem Arias Fauste <accounts@gariasf.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Guillem Arias <accounts@gariasf.com>
Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
* chore(design-system): swap raw gray classes for semantic tokens across remaining views
Finalizes the raw-color sweep started in #1652 (settings) and continued
in #1654 (holdings). Covers accounts, budgets, chats, pages, imports,
provider integrations (mercury, lunchflow, sophtron, enable_banking,
coinstats), auth flows (password reset, MFA, registrations), shared
layouts, and selected DS component hover states. 35 files, ~56 line
changes.
Mappings (matching the patterns established in the prior sweeps):
- text-white bg-gray-900 hover:bg-gray-800 (with optional focus:ring-gray-900)
-> text-inverse button-bg-primary hover:button-bg-primary-hover
-> focus:ring-button-bg-primary
- text-gray-500 / 600 / 700 -> text-secondary
- text-gray-800 -> text-primary
- text-gray-400 -> text-subdued
- hover:text-gray-700 / hover:text-gray-100 -> hover:text-primary
- bg-gray-50 / 100 / 200 (standalone) -> bg-surface-inset
- bg-gray-500/5 -> bg-gray-tint-5
- bg-gray-500/10 -> bg-gray-tint-10
- bg-gray-900 (decorative active states) -> bg-inverse
- hover:bg-gray-50 / 100 (standalone) -> hover:bg-surface-inset
- hover:bg-gray-300 -> hover:bg-surface-inset-hover
- bg-white hover:bg-gray-100 -> bg-container hover:bg-container-hover
- border-gray-300 -> border-secondary
- focus:border-gray-200 -> focus:border-secondary
- focus-within:border-gray-900 -> focus-within:border-primary
- DS::Buttonish outline / ghost / icon hover:
hover:bg-gray-100 theme-dark:hover:bg-gray-700
-> hover:bg-container-inset-hover
Left intentionally raw, with rationale:
- bg-gray-300 / bg-gray-400 decorative dots and avatar circles. The
raw value reads OK against both bg-container variants; no semantic
"neutral indicator" token exists. Same pattern as #1652 / #1654.
- bg-gray-400/20 theme-dark:bg-gray-500/20 (onboardings/trial). Custom
alpha tint with no equivalent token.
- bg-white theme-dark:bg-gray-700 (DS::Tabs active pill, budgets tabs).
Custom tab-pill pattern; gray-700 in dark mode (one shade lighter
than page bg-gray-900) is intentional for visibility.
- bg-gray-100 theme-dark:bg-gray-700 (DS::Toggle base bg). Closest
match (bg-container-inset-hover) is semantically a hover state.
- DS::Buttonish secondary variant gray-200/300/700/600 pattern. Same
pattern as #1654 holdings; needs button-bg-secondary-strong from
that PR. Will swap in a follow-up after #1654 merges.
- disabled:bg-gray-500 theme-dark:disabled:bg-gray-400 on inverse
buttons (DS::Buttonish primary, enable_banking, coinstats). Custom
disabled state for the inverse pair; no token.
- text-gray-300 SVG stroke (shared/_progress_circle).
- bg-white text-gray-900 (layouts/print). Print contexts intentionally
light regardless of theme.
- bg-gray-800 / border-gray-700 / text-white / hover:text-gray-100
(impersonation_sessions/_super_admin_bar). Admin overlay styled to
remain dark in both modes; not a theme-aware component.
Files covered by other in-flight PRs were skipped to avoid rebase
conflicts: chats/_ai_consent's fg-inverse swap (#1626), shared/_text_tooltip
and shared/_money_field tooltip pills (#1626), investments/_value_tooltip
(#1626), components/DS/tooltip (#1626).
* fix(design-system): keep changelog avatar text raw to preserve dark-mode contrast
The changelog avatar fallback (when @release_notes[:avatar] is missing)
sits inside the "decorative + raw" exception list — bg-gray-300 stays
fixed across themes since no semantic neutral-indicator token exists.
The earlier sweep partially themed the pair: bg-gray-300 stayed raw but
text-gray-600 became text-secondary. text-secondary resolves to gray-300
in dark mode, which matches the bg → text became invisible against its
own background.
Reverting only the text class to text-gray-600 restores the original
fixed-light placeholder behavior. Both classes raw, both themes
readable.
* fix(design-system): address review feedback on raw-color-sweep-finalize
Six issues caught by CodeRabbit + Codex review:
1. focus:ring-button-bg-primary silently emits no CSS (×6 files).
button-bg-primary is a custom @utility, not a theme color, so Tailwind's
ring-{name} resolution finds no --color-button-bg-primary. Replaces with
focus:ring-gray-900 theme-dark:focus:ring-white — same color flip as the
button bg, but resolved through theme colors so the ring actually renders.
Files: lunchflow/mercury/sophtron _api_error + _setup_required, coinstats_items/new.
2. accounts/show/_activity.html.erb: focus-within:ring-gray-100 was dead
(no ring-width on the parent). Removed.
3. import/confirms/show.html.erb: uniform hover:bg-surface-inset-hover
applied to both active and inactive step indicators created a jarring
dark-to-light flip on the active step (bg-inverse → bg-surface-inset-hover).
Now hover follows the resting state: active uses hover:bg-inverse-hover,
inactive uses hover:bg-surface-inset-hover.
4. password_resets/new.html.erb: bg-white left raw alongside the migrated
hover:bg-surface-inset. Swapped to bg-container so dark mode flips properly.
5. registrations/new.html.erb + password_validator_controller.js: view now
uses bg-surface-inset on password strength block lines, but the Stimulus
controller still toggled bg-gray-200 on validate. Updated controller to
add/remove bg-surface-inset matching the view, so unmet states reset to
the tokenized class instead of leaving raw gray-200 stuck on the element.
* refactor(design-system): migrate fg-* utilities to text-* and remove namespace
The design system carried two parallel namespaces for foreground colors:
text-* (canonical, ~2,000 uses) and fg-* (32 uses). Most fg-* tokens
were 1:1 duplicates of a text-* counterpart. fg-gray was nearly
identical to text-secondary, with a one-step shade difference in dark
mode.
This PR migrates all 32 usages to their text-* equivalents and removes
the fg-* block from the design tokens. Closes#1606.
Mapping:
- fg-inverse -> text-inverse (20 usages, identical light/dark values)
- fg-gray -> text-secondary (7 usages; light values match, dark is
one step lighter: gray-300 vs gray-400)
- fg-primary -> text-primary (3 usages, identical values)
- fg-subdued -> text-subdued (2 usages, identical values)
The four other fg-* tokens (fg-contrast, fg-primary-variant,
fg-secondary, fg-secondary-variant) had zero usages despite being
defined; they are removed without replacement.
JSON / build:
- design/tokens/sure.tokens.json: $version 1.0.0 -> 2.0.0 (breaking
schema change per the policy added in #1620). 8 fg-* token
definitions removed.
- button-bg-ghost-hover's dark value still references "fg-inverse"
internally; rewritten to "bg-gray-800 text-inverse" so the cleanup
doesn't break that utility.
- _generated.css regenerated. 42 utility blocks now (was 50).
Lookbook tokens preview:
- The Text & foregrounds section dropped its split between text-*
(canonical) and fg-* (legacy). Now a single section listing the
five text-* utilities. The "(legacy)" framing is gone since there's
no legacy left.
README:
- design/tokens/README.md's button-bg-ghost-hover edge-case example
updated to reflect the new "bg-gray-800 text-inverse" dark value.
Visual review needed in dark mode:
- Anywhere icons use the application_helper#icon helper with
color: "default" (most icons in the app). The default class moved
from fg-gray (gray-400 dark) to text-secondary (gray-300 dark), so
default-color icons render slightly lighter in dark mode.
- DS::Buttonish icons in secondary buttons (same shade shift).
- DS::Link icons (same).
- Time series chart axes (same).
- All tooltips, account add flow, settings hostings buttons,
invitations, AI consent, family export, danger-zone buttons --
these used fg-inverse, which is identical to text-inverse, so no
visual change expected.
* fix(design-system): use inverse pair on tooltips for readable dark mode
* fix(lookbook): use semantic tokens in menu preview header text
* fix(lookbook): set text-primary on layout body so previews inherit theme
* fix(design-system): keep shadows dark-toned in dark mode
Inverting shadows to white|8% on dark surfaces produces a halo
effect rather than an elevation cue, and stacks redundantly with
the alpha-white 1px ring already in shadow-border-*.
Switch dark-mode shadows to black at progressively higher alpha
(25%/30%/35%/40%/50% for xs..xl) so they read as actual cast
shadows on near-black surfaces. Surface-tint differences and the
existing alpha-white border ring continue to handle elevation
hierarchy and edge definition.
Approach matches Material 3, Apple HIG, IBM Carbon, Refactoring UI,
and the dark-mode shadows used in Linear/Vercel/Stripe.
* fix(design-system): set text-primary on DS::Dialog element
Browser UA stylesheets apply color: black directly to <dialog>,
which overrides ancestor inheritance even when a body or html
ancestor sets a theme-aware color. Unstyled child content then
renders black regardless of theme.
Setting text-primary on the dialog element itself defeats the UA
override and lets descendants inherit the semantic token.
* fix(lookbook): use shadow css vars in effects preview so dark theme renders
* Revert "fix(design-system): keep shadows dark-toned in dark mode"
This reverts commit 3e9d76ed0b.
* fix(design-system): use opacity-70 instead of text-inverse/70 in value tooltip
The custom @utility text-inverse expands to @apply text-white and
isn't modifier-aware, so text-inverse/70 produced no CSS at all and
the muted labels fell through to inherited color (invisible on the
white pill in dark mode).
Replace with text-inverse + opacity-70. Same visual effect, works
with the existing utility definition.
* Fix budget donut chart hiding center content on segment hover
* Preserve segment hover in center unless leaving the unused arc
The unused arc has no interactive link in its center template, so
moving from it into the center content should restore default content(including the Edit Budget link) immediately. All other segments have a DS::Link in their center template that must remain clickable after the cursor moves from the arc into the center.
Pass the D3 datum into the mouseleave handler to read d.data.id, then clear hover when either (a) the departing segment is the unused arc, or (b) the cursor is not heading into contentContainerTarget.
* Optimize UI in budget
* update locales
* Optimize UI
* optimize suggested_daily_spending
* try over_budget and on_track
* update locale
* optimize
* add budgets_helper.rb
* fix
* hide no buget and no expense sub-catogory
* Optimize
* Optimize button on phone
* Fix Pipelock CI noise
* using section to render both overbudget and onTrack
* hide last ruler
* fix
* update test
---------
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* feat: add currency management for families with enabled currencies
* feat: update currency selection logic and improve accessibility
* feat: update currency preferences to use group moniker in titles
---------
Signed-off-by: Ang Wei Feng (Ted) <hello@tedawf.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* feat(select): improve merchant dropdown behavior and placement controls
- add configurable menu_placement strategy to DS::Select (auto/down/up) with safe normalization
forward menu_placement through StyledFormBuilder#collection_select
- force Merchant dropdown to open downward in transaction create and editor forms
- fix select option/search text contrast by applying text-primary in DS select menu
- prevent form jump on open by scrolling only inside dropdown content instead of using scrollIntoView
- clamp internal dropdown scroll to valid bounds for stability
- refactor select controller placement logic for readability (placementMode, clamp) without changing behavior
* set menu_placement=auto for metchant selector
* fix: allow high precision for security prices in trade forms (to solve #1323)
* fix: prevent race conditions on currency selection in money field
* fix: silently ignore currency fetch errors in money field
* Add Quick Categorize Wizard (iteration 1)
Adds a step-by-step wizard for bulk-categorizing uncategorized transactions
and optionally creating auto-categorization rules, reducing friction after
connecting a new bank account.
New files:
- Transaction::Grouper abstraction + ByMerchantOrName strategy (groups by
merchant name when present, falls back to entry name; sorted by count desc)
- Transactions::CategorizesController (GET show / POST create)
- Wizard view at app/views/transactions/categorizes/show.html.erb
- Stimulus categorize_controller.js (Enter-key-to-select-first)
- Tests for grouper and controller
Modified files:
- routes.rb: resource :categorize inside namespace :transactions
- transactions_controller.rb: expose @uncategorized_count to index
- transactions/index.html.erb: Categorize (N) button in header
- family.rb: uncategorized_transaction_count query
- rules_controller.rb: return_to param support for wizard → rule editor flow
- rules/_form.html.erb, rules/new.html.erb: pass return_to through form
- i18n: categorizes show/create keys + rules.create.success
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Quick Categorize Wizard — iteration 2 polish
Six improvements from live testing:
- Breadcrumb: Home > Transactions > Categorize
- Layout: category picker + confirmation dialog above transaction list
- Inline confirmation dialog: clicking a category pill shows a <dialog>
summarising what will happen (N transactions → category, rule if checked)
with Confirm and Cancel buttons — no redirect to rule editor
- Direct rule creation: rule created with active: true in the controller
instead of redirecting to the rule editor; revert return_to plumbing from
RulesController, rules/_form, rules/new, rules/en.yml
- Individual row assignment: per-row category <select> submits via
PATCH /transactions/categorize/assign_entry and removes the row via
Turbo Stream (assign_entry action + route)
- Enter key guard: selectFirst only fires when exactly 1 pill is visible
after filtering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Quick Categorize Wizard — iteration 3 reliability fixes and UX polish
- Fix Stimulus controller not loading: remove invalid `@hotwired/turbo` named
import (not in importmap); use global `Turbo.renderStreamMessage` instead
- Fix Enter key submitting form with wrong category when search field is
unfocused: move keydown listener to document so it fires regardless of focus
- Prevent Enter from submitting when multiple categories are visible
- Clear search filter after bulk category assignment (pill click or Enter),
but not after individual row dropdown assignment
- Update group transaction count and total amount live as entries are assigned
via row dropdown or partial bulk assignment
- Add turbo frames for remaining count and group summary so they update
without a full page reload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Quick categorization polish
* refactoring
* Remove unused GROUPS_PER_BATCH constant, fix ERB self-closing tags
Wizard only ever uses one group at a time so limit: 1 is correct and
more honest than fetching 20 and discarding 19. ERB linter fixes are
whitespace/void-element corrections with no functional change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Move Categorize button into ... menu on transactions index
Reduces header clutter by putting it in the overflow menu at the bottom,
where it only appears when there are uncategorized transactions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Scope categorize wizard to accessible entries only
Fixes a security issue where users with restricted account access via
account sharing could view and categorize transactions from accounts
they cannot access through normal transaction flows.
- Pass Current.accessible_entries to Transaction::Grouper so the wizard
only displays groups from accounts the user can see
- Use Current.accessible_entries on all write paths in create and
assign_entry, matching the pattern in TransactionCategoriesController
- Refactor Grouper to accept an entries scope instead of a family object,
keeping authorization concerns in the controller
- Add tests verifying inaccessible entries are hidden from the wizard
and cannot be categorized via forged POST/PATCH params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Clamp position param to >= 0 to guard against negative offset
Prevents ArgumentError from Array#drop when a negative position is
passed via a tampered query string or form value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Surface rule creation failure and add accessible names to entry row
- Capture Rule.create_from_grouping! return value; set flash[:alert] when
nil so users who checked "Create Rule" know it wasn't created (e.g. a
duplicate already exists); stream the notification for partial updates
- Add aria-label to the per-row checkbox and category select in
_entry_row so screen readers can identify which transaction each
control belongs to
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Localize breadcrumb labels in categorizes controller
Follows the pattern used by FamilyExportsController and ImportsController.
Adds 'transactions' and 'categorize' keys to the breadcrumbs locale file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add error handling to categorize controller fetch calls
Check response.ok before parsing the body and add .catch handlers
so network failures and non-2xx responses are logged rather than
silently swallowed. On assignment failure the per-row select is
reset to empty so the user can retry.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Scope preview_rule to accessible entries only
Entry.uncategorized_matching now accepts an entries scope instead of a
family object, matching the same pattern used for Transaction::Grouper.
The preview_rule action passes Current.accessible_entries so rule
previews respect account sharing permissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Scope remaining count to accessible entries
Adds Entry.uncategorized_count(entries) following the same pattern as
uncategorized_matching. Replaces all three uses of
Current.family.uncategorized_transaction_count in the categorize
controller so the remaining-count badge reflects only the transactions
the current user can actually access and categorize.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Comments got separated from their function
* Remove quick-categorize-wizard dev notes
This was a planning document used during development, not intended
for the final branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Recompute remaining entries from server state after writes
Adds uncategorized_entries_for helper that reloads remaining entries
from the DB with a category_id IS NULL filter after each write, so
the partial-update Turbo Stream reflects server-side state rather than
trusting the client-provided remaining_ids. This handles the case where
a concurrent request has categorized one of the remaining entries
between page render and form submit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Rename create_from_grouping! to create_from_grouping
The method rescues RecordInvalid and returns nil, which contradicts
the bang convention. Dropping the ! correctly signals that callers
should check the return value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Clamp offset in grouper to guard against negative values
The controller already clamps position before passing it as offset,
but clamping in the grouper itself prevents ArgumentError from
Array#drop if the grouper is ever called directly with a negative offset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
* feat: improve QIF import date format selection
- Added a reusable date format auto-detection method.
- Show a live preview of the first parsed date that updates client-side
as the user changes the dropdown selection, via a new
qif-date-format Stimulus controller.
- Show an error alert and disable the submit button when no supported
date format can parse the file's dates.
* A few polishing fixes:
- Missing return on redirects
Stale REASONABLE_DATE_RANGE constant.
- Replaced the frozen constant with a class method
Bare inline rescue — Replaced Date.strptime(s, fmt) rescue nil with an explicit begin/rescue catching.
- save!(validate: false) in controller — Changed to update_column(:column_mappings, ...) in qif_category_selections_controller.rb:22, matching the pattern used in detect_and_set_qif_date_format!.
- Unescaped JSON in HTML attribute — Replaced the raw <div> with tag.div ... do block in show.html.erb:16, letting Rails properly escape the data attribute value.
* fix: address review feedback for QIF date format feature
- Add missing `return` after redirect for non-QIF imports
- Pass date_format to parse_opening_balance in will_adjust_opening_anchor?
- Return empty array when no usable date sample exists for format preview
- Add sr-only label to date format select for accessibility
- Consolidate duplicate try_parse_date/parse_qif_date into single method
- Remove misleading ambiguity scoring comment from detect_date_format
- Skip redundant sync_mappings when date format already triggered a sync
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use %{product_name} interpolation in locale strings
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Make category selection menus opaque for excluded transactions
* Allow keyboard navigation in category selection menu
* Fix category transparency on mobile
* Make checkbox opaque
* Remove text-secondary from amount container
* Submit form directly
* Handle aria labels
isTouchDevice() blocked mouse/trackpad drag on touch-capable laptops
(e.g. Windows with touchscreen). Now checks if a touch interaction is
actually in progress instead.
Also adds touchcancel binding to clean up hold timer state when the
OS interrupts a touch (notifications, palm rejection, etc).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Center-point distance made it harder to drag sections down than up
because tall sections (charts, sankey) have their center far from
the edge. Now uses distance to nearest edge — section height no
longer affects how far you need to drag to trigger a swap.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Edge-based insertion was too restrictive for tall uncollapsed sections
since the gap between them is only ~24px. The center-point approach
works better for the common case. The key improvements (800ms hold
delay, native dragstart cancellation, text selection prevention,
Euclidean movement cancellation) remain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1000ms was long enough to trigger browser text selection before drag
activated. Reduced to 800ms and added userSelect:none on the section
during the touch hold period, restored when touch ends.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sections have draggable="true" which triggers native HTML5 drag
on touch with zero delay, bypassing our 1000ms hold-to-drag logic
entirely. This was most noticeable with collapsed sections where a
brief touch-and-drag instantly reordered them. Now native dragstart
is cancelled on touch devices, forcing all touch reordering through
the hold delay path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Initial split transaction support
* Add support to unsplit and edit split
* Update show.html.erb
* FIX address reviews
* Improve UX
* Update show.html.erb
* Reviews
* Update edit.html.erb
* Add parent category to dialog
* Update en.yml
* Add UI indication to totals
* FIX ui update
* Add category select like rest of app
* Add split ui
* Add settings configuration for split transactions
- Adds a new settings section for appearance changes
- Also adds extra checks for delete and API calls
- Also adds checks for parent/child changes
* fixes
- split transactions dark mode fix
- add split transactions to context menu
* Update entry.rb
1. New validation split_child_date_matches_parent — prevents saving a split child with a date different from its parent. This is the root-cause fix that
protects all flows at once.
2. Bulk update guard — bulk_update! now strips :date from attributes when processing split children, preventing the validation from raising and silently
skipping the date change instead.
* N+1 fix for split_parent?
* Update entry.rb
Problem: In bulk_update!, when a split child has :date removed from attrs (line 432) and the remaining attrs is empty (e.g., the bulk update only
changed the date), entry.update! {} still ran as a no-op. But lock_saved_attributes! and mark_user_modified! at lines 443-444 executed unconditionally,
incorrectly marking untouched split children as user-modified and opting them out of future syncs.
Fix:
1. Added a changed flag to track whether any actual modification happened
2. Wrapped entry.update! in an if attrs.present? check so no-op updates are skipped
3. Gated lock_saved_attributes! and mark_user_modified! behind if changed, so they only run when the entry was actually modified (either via attribute
update or tag update)
* fixes
1. Indentation in show.html.erb Settings section — The split button block and delete block had extra indentation making them appear nested inside guard
blocks they weren't part of. Fixed to match actual nesting.
2. Skip @split_parents query when grouping is off — The controller now only loads split parent entries when show_split_grouped? is true, saving a query
with joins when the feature is disabled.
Resolve merge conflicts in investment summary/performance views where main's
grid layout refactoring conflicted with privacy-sensitive class additions.
Also add privacy-sensitive to transaction list amounts, transaction detail
header, and sankey cashflow chart containers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace center-point distance algorithm with edge-crossing logic.
The swap threshold is now at the midpoint of the gap between adjacent
sections, so moving a section up or down requires equal distance
regardless of section height. Also handles 2-column grid layout by
filtering to same-column siblings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Initial split transaction support
* Add support to unsplit and edit split
* Update show.html.erb
* FIX address reviews
* Improve UX
* Update show.html.erb
* Reviews
* Update edit.html.erb
* Add parent category to dialog
* Update en.yml
* Add UI indication to totals
* FIX ui update
* Add category select like rest of app
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Use total distance (diagonal-aware) instead of per-axis thresholds
to better detect scrolling gestures that travel diagonally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On mobile, swiping through dashboard sections (investments, net worth,
balance sheets, etc.) accidentally triggers drag-to-reorder because
the hold delay is only 150ms with no movement cancellation.
- Increase hold delay from 150ms to 500ms
- Cancel drag activation if finger moves >10px before hold triggers,
allowing normal scroll gestures to pass through unimpeded
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(transaction): add support for file attachments using Active Storage
* feat(attachments): implement transaction attachments with upload, show, and delete functionality
* feat(attachments): enhance attachment upload functionality to support multiple files and improved error handling
* feat(attachments): add attachment upload form and display functionality in transaction views
* feat(attachments): implement attachment validation for count, size, and content type; enhance upload form with validation hints
* fix(attachments): use correct UI components
* feat(attachments): Implement Turbo Stream responses for creating and deleting transaction attachments.
* fix(attachments): include auth in activestorage controller
* test(attachments): add test coverage for turbostream and auth
* feat(attachments): extract strings to i18n
* fix(attachments): ensure only newly added attachments are purged when transaction validation fails.
* fix(attachments): validate attachment params
* refactor(attachments): use stimulus declarative actions
* fix(attachments): add auth for other representations
* refactor(attachments): use Browse component for attachment uploads
* fix(attachments): reject empty values on attachment upload
* fix(attachments): hide the upload form if reached max uploads
* fix(attachments): correctly purge only newly added attachments on upload failure
* fix(attachments): ensure attachment count limit is respected within a transaction lock
* fix(attachments): update attachment parameter handling to avoid `ParameterMissing` errors.
* fix(components): adjust icon_only logic for buttonish
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
* Add invited users with delete button to admin users page
Shows pending invitations per family below active users in /admin/users/.
Each invitation row has a red Delete button aligned with the role column.
Alt/option-clicking any Delete button changes all invitation button labels
to "Delete All" and destroys all pending invitations for that family.
- Add admin routes: DELETE /admin/invitations/:id and DELETE /admin/families/:id/invitations
- Add Admin::InvitationsController with destroy and destroy_all actions
- Load pending invitations grouped by family in users controller index
- Render invitation rows in a dashed-border tbody below active user rows
- Add admin-invitation-delete Stimulus controller for alt-click behavior
- Add i18n strings for invitation UI and flash messages
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Fix destroy_all using params[:id] from member route
The member route /admin/families/:id/invitations sets params[:id],
not params[:family_id], so Family.find was always receiving nil.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Fix translation key in destroy_all to match locale
t(".success_all") looked up a nonexistent key; the locale defines
admin.invitations.destroy_all.success, so t(".success") is correct.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Scope bulk delete to pending invitations and allow re-inviting emails
- destroy_all now uses family.invitations.pending.destroy_all so accepted
and expired invitation history is preserved
- Replace blanket email uniqueness validation with a custom check scoped
to pending invitations only, so the same email can be invited again
after an invitation is deleted or expires
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Drop unconditional unique DB index on invitations(email, family_id)
The model-level uniqueness check was already scoped to pending
invitations, but the blanket unique index on (email, family_id)
still caused ActiveRecord::RecordNotUnique when re-inviting an
email that had any historical invitation record in the same family
(e.g. after an accepted invite or after an account deletion).
Replace it with no DB-level unique constraint — the
no_duplicate_pending_invitation_in_family model validation is the
sole enforcer and correctly scopes uniqueness to pending rows only.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Replace blanket unique index with partial unique index on pending invitations
Instead of dropping the DB-level uniqueness constraint entirely, replace
the unconditional unique index on (email, family_id) with a partial unique
index scoped to WHERE accepted_at IS NULL. This enforces the invariant at
the DB layer (no two non-accepted invitations for the same email in a
family) while allowing re-invites once a prior invitation has been accepted.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Fix migration version and make remove_index reversible
- Change Migration[8.0] to Migration[7.2] to match the rest of the codebase
- Pass column names to remove_index so Rails can reconstruct the old index on rollback
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix: Handle conditional loading of Plaid Link script
* fix: Plaid accounts not linking on first sync
* fix: Handle Plaid script loading edge cases
* fix: Use connection token for disconnect safety and retry failed script loads
* fix: Destroy Plaid Link handler on controller disconnect
* fix: Add timeout to Plaid CDN script loader to prevent deadlocks
parseLocaleFloat returns 0 for empty strings, which caused blank amount
fields to be overwritten with "0.00" when the user changed currency.
Guard against this by checking for a non-empty value before parsing.
https://claude.ai/code/session_01ThfszjiCmbDDPyb4TZqk2X
Number.parseFloat() only recognizes dots as decimal separators, causing
inputs like "256,54" (French locale) to be parsed as "256". Added a
parseLocaleFloat utility that detects whether comma or dot is the decimal
separator and normalizes accordingly before parsing.
Fixes#1138https://claude.ai/code/session_01ThfszjiCmbDDPyb4TZqk2X
* feat: add new UI component to display dropdown select with filter
* feat: use new dropdown componet for category selection in transactions
* feat: improve dropdown controller
* feat: Add checkbox indicator to highlight selected element in list
* feat: add possibility to define dropdown without search
* feat: initial implementation of variants
* feat: Add default color for dropdown menu
* feat: add "icon" variant for dropdown
* refactor: component + controller refactoring
* refactor: view + component
* fix: adjust min width in selection for mobile
* feat: refactor collection_select method to use new filter dropdown component
* fix: compute fixed position for dropdown
* feat: controller improvements
* lint issues
* feat: add dot color if no icon is available
* refactor: controller refactor + update naming for variant from icon to logo
* fix: set width to 100% for select dropdown
* feat: add variant to collection_select in new transaction form
* fix: typo in placeholder value
* fix: add back include_blank property
* refactor: rename component from FilterDropdown to Select
* fix: translate placeholder and keep value_method and text_method
* fix: remove duplicate variable assignment
* fix: translate placeholder
* fix: verify color format
* fix: use right autocomplete value
* fix: selection issue + controller adjustments
* fix: move calls to startAutoUpdate and stopAutoUpdate
* Update app/javascript/controllers/select_controller.js
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* fix: add aria-labels
* fix: pass html_options to DS::Select
* fix: unnecessary closing tag
* fix: use offsetvalue for position checks
* fix: use right classes for dropdown transitions
* include options[:prompt] in placeholder init
* fix: remove unused locale key
* fix: Emit a native change event after updating the input value.
* fix: Guard against negative maxHeight in constrained layouts.
* fix: Update test
* fix: lint issues
* Update test/system/transfers_test.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* Update test/system/transfers_test.rb
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
* refactor: move CSS class for button select form in maybe-design-system.css
---------
Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Move privacy toggle button from dashboard-only to global breadcrumbs bar
(desktop) and mobile top nav so it's accessible on every page
- Add _privacy_mode_check.html.erb inline script in <head> to prevent
flash of unblurred content on first page load (same pattern as dark mode)
- Improve Stimulus controller: add iconOn/iconOff targets to toggle between
eye/eye-off icons; update aria-pressed on all toggle targets simultaneously
- Remove hover-unblur (blur(4px) on hover) from CSS for security; add
pointer-events: none to prevent interaction with blurred values
- Move i18n key from pages.dashboard scope to layouts.application scope;
add privacy_mode translation to all 12 locales (EN, DE, FR, ES, CA,
NL, NB, PT-BR, RO, TR, ZH-CN, ZH-TW)
The polling controller was requesting turbo_stream format but only an
HTML template exists. Fix the Accept header to request text/html and
handle both formats in the controller.