Skip to content

Bank Sync cleanup#1710

Open
jjmata wants to merge 11 commits intomainfrom
feat/surface-provider-status
Open

Bank Sync cleanup#1710
jjmata wants to merge 11 commits intomainfrom
feat/surface-provider-status

Conversation

@jjmata
Copy link
Copy Markdown
Collaborator

@jjmata jjmata commented May 8, 2026

Summary

Completes the "Option A" Claude Design redesign of the Bank Sync settings page.

  • Health strip & sync actions: connected-provider count tiles, per-row sync buttons, sync-all with throttle, action-needed group
  • Card grid & connect drawer: replace the Available accordion with a responsive 2-column card grid; each card has a logo square, Beta/Alpha pill, region · type · tier meta, tagline, and a Connect link that opens the existing panel in a DS::Dialog drawer; Provider::Metadata registry for all 11 providers; maturity pills also surface on connected accordion rows
  • Fix Enable Banking grouping: items with expired sessions were returning :off (Available) instead of :warn (Your connections)
  • Unified "Your connections" section: Connected + Action needed merged into one group; warn/err items auto-open
  • Retire /settings/bank_sync: 301 permanent redirect; nav collapsed from two entries to a single "Bank Sync"; page title and lede refreshed

Test plan

  • /settings/bank_sync → 301 redirect to /settings/providers
  • Nav shows exactly one "Bank Sync" entry for both admin and non-admin users
  • Available providers render as a 2-column card grid; Connect link opens drawer
  • Connected providers show in "Your connections" with maturity badges
  • Enable Banking with expired session lands in "Your connections" (warn), not the card grid
  • bin/rails test green
  • DISABLE_PARALLELIZATION=true bin/rails test:system green

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Health summary strip with four tiles and richer provider status indicators
    • "Sync all" (rate-limited) and per-provider "Sync now" controls
    • Reorganized Providers UI into "Your connections", "Available", and "Action needed"
    • Provider cards with maturity badges, status pills, connect drawer form, and "Add provider" CTA
    • Bank Sync now redirects to Providers
  • Bug Fixes
    • More accurate sync-health tracking and status reporting across providers
  • Tests
    • System and integration tests covering provider flows, sync actions, and redirects

claude and others added 5 commits May 8, 2026 18:30
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
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 <[email protected]>
… 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 <[email protected]>
…ect 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 <[email protected]>
- 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 <[email protected]>
@brin-security-scanner brin-security-scanner Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 8, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Consolidates bank-sync into a unified Providers flow: adds Provider::Metadata and migration, computes provider health across family panels, adds sync/connect controller actions and SyncAllProvidersJob, introduces view components/partials and routes, updates i18n, and adds tests.

Changes

Provider Management UI Consolidation

Layer / File(s) Summary
Provider Metadata Registry & Database Schema
app/models/provider/metadata.rb, db/migrate/20260508120000_add_last_sync_all_attempted_at_to_families.rb, db/schema.rb
Introduces Provider::Metadata REGISTRY and lookup; adds families.last_sync_all_attempted_at datetime column; schema updates including sophtron_items columns and constraint typing changes.
Reusable View Components
app/components/settings/health_summary.rb, app/components/settings/health_summary.html.erb, app/components/settings/provider_card.rb, app/components/settings/provider_card.html.erb
Adds Settings::HealthSummary (builds tiles from counts) and Settings::ProviderCard (renders provider card with logo, maturity, meta, tagline, connect link).
Supporting UI Partials
app/views/settings/_section.html.erb, app/views/settings/providers/_status_pill.html.erb, app/views/settings/providers/_sync_button.html.erb, app/views/settings/providers/_group_heading.html.erb, app/views/settings/providers/_add_provider_cta.html.erb
Expands _section locals to accept status, meta, actions, badge; adds status_pill, sync_button, group_heading, and add_provider_cta partials used by providers UI.
Controller Actions & Sync Orchestration
app/controllers/settings/providers_controller.rb, app/jobs/sync_all_providers_job.rb
Removes Settings::BankSyncController. Refactors ProvidersController with turbo-aware layout, adds sync_all, sync, connect_form actions, family panel loading, and provider health computation using windowed Sync queries. Adds SyncAllProvidersJob with per-family Sidekiq locking.
Settings Helper Status & Summary
app/helpers/settings_helper.rb
Updates SETTINGS_ORDER (Bank Sync → settings_providers_path), expands settings_section signature, and adds provider_summary plus private helpers (sync_based_summary, enable_banking_summary) to compute provider status/meta.
Routes & Navigation
config/routes.rb, app/views/settings/_settings_nav.html.erb
Replaces bank_sync route with 301 redirect to /settings/providers; expands providers resource with sync_all, :provider_key/sync, and :provider_key/connect_form routes; updates navigation to link Bank Sync to providers and removes standalone Providers item.
Provider View Templates
app/views/settings/providers/show.html.erb, app/views/settings/providers/connect_form.html.erb
Reworks providers show to render intro, conditional Sync All button, Settings::HealthSummary, grouped connected/needs-attention entries with per-entry sync/forms, Add Provider CTA, and Settings::ProviderCard grid; adds connect_form drawer template.
Localization & Tests
config/locales/views/settings/en.yml, test/controllers/settings/providers_controller_test.rb, test/system/settings/providers_test.rb, test/system/settings_test.rb
Adds settings.providers i18n strings; adds controller test for /settings/bank_sync redirect; adds extensive system tests validating UI, sync workflows, ordering, and drawer interactions; updates sidebar navigation test.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • we-promise/sure#278: Also updates SettingsHelper::SETTINGS_ORDER and navigation entries.
  • we-promise/sure#111: Introduced the original BankSync controller/views that this PR removes and consolidates.
  • we-promise/sure#116: Modifies Settings navigation and related helpers (SETTINGS_ORDER).

Suggested labels

enhancement

Suggested reviewers

  • sokie
  • sure-design

🐰 A registry of providers bright,
Where health tiles glow in sync-light,
No more split views, one path to take,
Connected, available—all awake! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Bank Sync cleanup' accurately summarizes the main focus of the changeset, which involves refactoring the Bank Sync settings page and removing the old BankSyncController while introducing new components and routing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/surface-provider-status

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b59dd64c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

items: [
{ label: t(".accounts_label"), path: accounts_path, icon: "layers" },
{ label: t(".bank_sync_label"), path: settings_bank_sync_path, icon: "banknote" },
{ label: t(".bank_sync_label"), path: settings_providers_path, icon: "banknote" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Hide Bank Sync link from non-admin users

This link is rendered in the general settings nav for every user, but Settings::ProvidersController#show is still protected by ensure_admin, which redirects unauthorized users back to settings_providers_path. For family members, clicking the new Bank Sync nav item therefore lands on an admin-only route that redirects to itself rather than showing the old informational page or a usable destination.

Useful? React with 👍 / 👎.


def perform(family_id)
family = Family.find(family_id)
family.sync_later
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Schedule Binance in provider-wide syncs

For families with Binance connections, the new Sync all flow enqueues this job but then delegates to family.sync_later; Family::Syncer::SYNCABLE_ITEM_ASSOCIATIONS does not include :binance_items, even though this page now treats Binance as a connected/syncable provider via PANEL_SYNCABLE_TYPES. In that context the UI says all connected providers are syncing, but Binance is never scheduled unless the user clicks its per-row sync button.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
db/schema.rb (2)

1390-1413: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Duplicate account_number_mask column in sophtron_accounts is the direct cause of the CI failure.

account_number_mask is defined at line 1406 (pre-existing) and again at line 1409 (marked as changed). Rails db:schema:load raises ArgumentError: you can't define an already defined column 'account_number_mask' on 'sophtron_accounts' — confirmed by the pipeline failure.

🐛 Proposed fix — remove the duplicate line
     t.string "customer_id"
     t.string "member_id"
     t.string "account_number_mask"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
-    t.string "account_number_mask"
     t.index ["account_id"], name: "index_sophtron_accounts_on_account_id"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@db/schema.rb` around lines 1390 - 1413, The schema defines the column
account_number_mask twice on the sophtron_accounts table which causes Rails to
raise an ArgumentError during db:schema:load; remove the duplicate definition so
account_number_mask is only declared once in the create_table block for
sophtron_accounts (update the create_table "sophtron_accounts" block to delete
the redundant t.string "account_number_mask" entry), then run schema lint/tests
to confirm the CI error is resolved.

1432-1452: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

sophtron_items has 8 columns duplicated — will cause a second db:schema:load failure after sophtron_accounts is fixed.

customer_id, customer_name, raw_customer_payload, user_institution_id, current_job_id, job_status, raw_job_payload, and last_connection_error all appear at lines 1432–1439 (existing) and again at lines 1442–1449 (marked as new). Additionally, last_connection_error silently changes type from text to string between the two occurrences.

The only genuinely new additions to this table should be manual_sync (line 1450) and current_job_sophtron_account_id (line 1451) plus the index (line 1452). The entire block at lines 1442–1449 should be removed.

🐛 Proposed fix — remove the duplicated lines 1442–1449
     t.text "last_connection_error"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
-    t.string "customer_id"
-    t.string "customer_name"
-    t.jsonb "raw_customer_payload"
-    t.string "user_institution_id"
-    t.string "current_job_id"
-    t.string "job_status"
-    t.jsonb "raw_job_payload"
-    t.string "last_connection_error"
     t.boolean "manual_sync", default: false, null: false
     t.uuid "current_job_sophtron_account_id"
     t.index ["current_job_sophtron_account_id"], ...

If the intent was also to change last_connection_error from text to string, that requires a separate migration with change_column.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@db/schema.rb` around lines 1432 - 1452, The schema contains duplicated column
definitions for the sophtron_items table; remove the repeated block that
re-declares customer_id, customer_name, raw_customer_payload,
user_institution_id, current_job_id, job_status, raw_job_payload, and
last_connection_error so only the original column definitions remain and only
add the new columns manual_sync and current_job_sophtron_account_id plus its
index; ensure last_connection_error stays as text in the schema (any type change
must be implemented via a separate migration using change_column rather than
editing schema.rb).
app/controllers/settings/providers_controller.rb (1)

143-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid redirecting unauthorized users back into the same protected action.

On GET /settings/providers, a non-admin will hit ensure_admin, get redirected to settings_providers_path, and immediately hit ensure_admin again. That creates a redirect loop instead of denying access.

Suggested fix
 def ensure_admin
-  redirect_to settings_providers_path, alert: "Not authorized" unless Current.user.admin?
+  return if Current.user.admin?
+
+  redirect_to root_path, alert: t("settings.providers.not_authorized")
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/settings/providers_controller.rb` around lines 143 - 145, The
ensure_admin before_action is redirecting non-admins back to
settings_providers_path, causing a redirect loop; update ensure_admin (method
ensure_admin in providers_controller) to either render a 403/forbidden response
(render plain: "Not authorized", status: :forbidden) or redirect to a safe
non-protected route (e.g., root_path or dashboard_path) with the alert, instead
of redirecting to settings_providers_path, so unauthorized users are not sent
back into the same protected action.
🧹 Nitpick comments (5)
app/views/settings/providers/_status_pill.html.erb (1)

2-8: ⚡ Quick win

Case statement computing CSS classes belongs in a helper or ViewComponent, not the template.

Per guidelines: "Keep domain logic out of views: compute values like button classes, conditional logic, and data transformations in the component file, not the template file." This partial is also reused across multiple views and has variants by status—criteria that specifically recommend a ViewComponent.

♻️ Suggested refactor — extract to a helper
# app/helpers/settings_helper.rb (or providers_helper.rb)
def status_pill_classes(status)
  case status.to_sym
  when :ok   then { dot: "bg-success",     pill: "bg-success/10 text-success" }
  when :warn then { dot: "bg-warning",     pill: "bg-warning/10 text-warning" }
  when :err  then { dot: "bg-destructive", pill: "bg-destructive/10 text-destructive" }
  else            { dot: "bg-gray-400",    pill: "bg-gray-100 text-secondary" }
  end
end
 <%# locals: (status:) %>
-<%
-  dot_class, pill_class = case status.to_sym
-  when :ok   then [ "bg-success",     "bg-success/10 text-success" ]
-  when :warn then [ "bg-warning",     "bg-warning/10 text-warning" ]
-  when :err  then [ "bg-destructive", "bg-destructive/10 text-destructive" ]
-  else            [ "bg-gray-400",    "bg-gray-100 text-secondary" ]
-  end
-%>
+<% classes = status_pill_classes(status) %>
+<% dot_class, pill_class = classes[:dot], classes[:pill] %>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/settings/providers/_status_pill.html.erb` around lines 2 - 8, The
template currently contains presentation logic (the case statement building CSS
classes) which should be moved to a helper or ViewComponent; extract that logic
into a new helper method (e.g., status_pill_classes(status)) or a
StatusPillComponent that returns the dot and pill class values (as keys like
:dot and :pill), then update the partial to call status_pill_classes(status) and
use the returned classes instead of the inline case; ensure the helper/component
handles symbolizing the status and preserves the same mappings for :ok, :warn,
:err and the default case.
app/jobs/sync_all_providers_job.rb (1)

6-6: ⚡ Quick win

Use find_by with a nil guard to avoid unrecoverable job failures when a family is deleted mid-queue.

Family.find raises ActiveRecord::RecordNotFound if the record is gone by the time the job runs. With Sidekiq's default retry behavior this leads to repeated failures until retries are exhausted.

♻️ Proposed fix
-    family = Family.find(family_id)
-    family.sync_later
+    family = Family.find_by(id: family_id)
+    family&.sync_later
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/jobs/sync_all_providers_job.rb` at line 6, Replace the unsafe Family.find
lookup with a nil-safe lookup and early return: change the
Family.find(family_id) call to Family.find_by(id: family_id) and add a guard
like return unless family (or log and return) in the job's perform method (the
place where family = Family.find(...) is used) so the job exits gracefully if
the family was deleted instead of raising ActiveRecord::RecordNotFound and
retrying.
app/views/settings/_section.html.erb (1)

19-27: ⚡ Quick win

Generic section partial couples to a provider-specific partial.

_section.html.erb is a shared layout component, but line 24 hard-codes a dependency on "settings/providers/status_pill". If any other settings section ever needs a status: value (e.g., a future "Accounts" health section), it would either not render correctly or render an unrelated provider pill. The actions local already demonstrates the right pattern — pass pre-rendered content in from the caller.

♻️ Suggested approach

Pass the rendered pill from the call site (e.g. settings_section(status: render("settings/providers/status_pill", status: ...))), and drop the inline render from the generic partial:

-          <%= render "settings/providers/status_pill", status: status %>
+          <%= status if status.present? %>

The caller can then pass status: capture { render "settings/providers/status_pill", status: :ok } or any other rendered content, keeping _section.html.erb subsystem-agnostic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/settings/_section.html.erb` around lines 19 - 27, The shared
partial _section.html.erb currently hard-codes render
"settings/providers/status_pill" for the status display, coupling this generic
layout to a provider-specific partial; change it to accept rendered status
content from the caller (like actions already does) by removing the inline
render and using the passed-in status local as-is, and update callers to pass
status: render("settings/providers/status_pill", status: ...) or status: capture
{ render "settings/providers/status_pill", status: :ok } so _section.html.erb
remains subsystem-agnostic and callers control the actual markup.
app/views/settings/providers/show.html.erb (1)

1-18: ⚡ Quick win

Move the new page title and intro copy into i18n.

"Bank Sync" and the lede are hard-coded English while the surrounding UI already uses translations. Please switch these to t(...) so this page stays consistent with the rest of settings copy.

As per coding guidelines, "Always use t() helper for user-facing strings. Update config/locales/en.yml for new strings."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/settings/providers/show.html.erb` around lines 1 - 18, Replace the
hard-coded page title and intro copy in the view: change content_for
:page_title, "Bank Sync" to use t("settings.providers.bank_sync.page_title") and
replace the paragraph text ("Connect external accounts so transactions, balances
and holdings flow into Sure automatically.") with
t("settings.providers.bank_sync.lede"); then add corresponding entries under
settings.providers.bank_sync in config/locales/en.yml (page_title and lede) with
the existing English strings as values so the view uses the t(...) helper and
stays consistent with other translations.
app/components/settings/provider_card.rb (1)

2-18: ⚡ Quick win

Localize the maturity badge labels instead of hard-coding English here.

These strings are rendered in the UI, so storing "Beta" / "Alpha" in the component bypasses locale switching. Keep translation keys (or symbols) in the constant and resolve them with t(...) at render time.

As per coding guidelines, "Always use t() helper for user-facing strings. Update config/locales/en.yml for new strings."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/settings/provider_card.rb` around lines 2 - 18, Replace
hard-coded English labels in MATURITY_LABELS with translation keys (e.g., :beta
and :alpha or symbol strings) and resolve them via the Rails i18n helper in the
maturity_label method; specifically, keep MATURITY_LABELS as a mapping of
maturity symbols to translation keys, then update the maturity_label method to
call t(...) on the mapped key (using the t helper), and add the corresponding
entries under config/locales/en.yml so the UI strings are localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/settings/health_summary.html.erb`:
- Line 1: The grid currently forces four columns on all viewports (the <div
class="grid grid-cols-4 gap-2">) causing very narrow tiles on mobile; change the
grid classes to be responsive so small screens use two columns (or one) and
larger screens use four — e.g. replace or augment grid-cols-4 with responsive
Tailwind classes like grid-cols-2 sm:grid-cols-4 (or grid-cols-1 md:grid-cols-4)
on that same div to ensure tiles have a mobile-friendly layout.

In `@app/components/settings/provider_card.html.erb`:
- Line 4: Replace the raw Tailwind utility "text-white" with the design-system
functional token "text-inverse" in the span that renders the logo text (the span
showing <%= logo_text %>), i.e., update the class attribute on that span to
remove "text-white" and add "text-inverse" so it follows the functional token
convention used by the design system.

In `@app/controllers/settings/providers_controller.rb`:
- Around line 80-89: The sync_all action is race-prone because the
present?/>update pair can be run concurrently; make it atomic by performing a
single conditional DB update on the Family row and only enqueueing
SyncAllProvidersJob when that update succeeds. Replace the current
check+family.update!(last_sync_all_attempted_at: ...) with a conditional update
scoped to Current.family (e.g. a where on Family.id and a predicate that
last_sync_all_attempted_at is nil or <= 30.seconds.ago) and proceed to call
SyncAllProvidersJob.perform_later(family.id) only if the conditional update
affected one row; otherwise redirect with the "sync_all_recently" notice. Ensure
you reference sync_all, Current.family, last_sync_all_attempted_at, and
SyncAllProvidersJob in the change.

In `@app/views/settings/providers/_group_heading.html.erb`:
- Line 2: The div currently always emits id="" when the local variable anchor is
nil; change the ERB so the id attribute is only rendered when anchor is present
(e.g., use a conditional like `if anchor.present?` or an inline ERB conditional)
so the <div> element does not produce an empty id and is only anchorable when a
non-empty anchor value exists; update the line that generates the div (the one
referencing anchor) to conditionally include the id attribute rather than always
outputting id.

In `@app/views/settings/providers/_sync_button.html.erb`:
- Around line 3-9: The button created by button_to
(sync_provider_settings_providers_path) is icon-only and needs an explicit
accessible name; add an aria-label (or include hidden sr-only text) that matches
the sync action instead of relying on title. Update the button_to call to
include aria-label: recently_synced ? t("settings.providers.recently_synced") :
t("settings.providers.sync_provider") (or render an adjacent sr-only <span> with
the same translated text) so screen readers receive a clear label while keeping
the existing title, disabled, provider_key, and form attributes intact.

In `@db/migrate/20260508120000_add_last_sync_all_attempted_at_to_families.rb`:
- Line 1: The migration class AddLastSyncAllAttemptedAtToFamilies currently
inherits from ActiveRecord::Migration[8.0]; change its superclass to
ActiveRecord::Migration[7.2] so it matches the project's schema version and
coding guidelines (replace [8.0] with [7.2] in the class declaration). Ensure no
other parts of this file reference migration version 8.0.

In `@test/system/settings/providers_test.rb`:
- Around line 162-173: The test "clicking a provider card connect link opens the
connect drawer" is clicking the first Connect link in the grid
(find("a[data-turbo-frame='drawer']", match: :first)) which can target the wrong
provider; instead scope the click to the SimpleFIN card by locating the provider
card element that contains the "SimpleFIN" text (e.g., within the card element
matching its title/label) and then call find("a[data-turbo-frame='drawer']",
text: /Connect/).click inside that scoped block so the test always opens
SimpleFIN's drawer before asserting dialog[open] and "Setup Token".
- Around line 20-38: The tests "shows not configured pill for an unconfigured
provider" and "connected providers render before unconfigured ones" still query
accordion markup; update them to target the new available-provider card layout:
replace within("details", text: "SimpleFIN") with a within(...) that scopes to
the available-provider card container (e.g. the container/class that holds
`@available` provider cards) and assert the "Not configured" pill inside that card
for SimpleFIN; similarly change the second test to collect the visible provider
card headers (replace all("details summary") with all(...) selecting the
provider card header/title elements) and compare the index of the SimpleFIN card
vs the Binance card to assert ordering. Also apply the same selector changes to
the related assertions around lines 52-70 (other tests expecting
accordion/summary markup).

---

Outside diff comments:
In `@app/controllers/settings/providers_controller.rb`:
- Around line 143-145: The ensure_admin before_action is redirecting non-admins
back to settings_providers_path, causing a redirect loop; update ensure_admin
(method ensure_admin in providers_controller) to either render a 403/forbidden
response (render plain: "Not authorized", status: :forbidden) or redirect to a
safe non-protected route (e.g., root_path or dashboard_path) with the alert,
instead of redirecting to settings_providers_path, so unauthorized users are not
sent back into the same protected action.

In `@db/schema.rb`:
- Around line 1390-1413: The schema defines the column account_number_mask twice
on the sophtron_accounts table which causes Rails to raise an ArgumentError
during db:schema:load; remove the duplicate definition so account_number_mask is
only declared once in the create_table block for sophtron_accounts (update the
create_table "sophtron_accounts" block to delete the redundant t.string
"account_number_mask" entry), then run schema lint/tests to confirm the CI error
is resolved.
- Around line 1432-1452: The schema contains duplicated column definitions for
the sophtron_items table; remove the repeated block that re-declares
customer_id, customer_name, raw_customer_payload, user_institution_id,
current_job_id, job_status, raw_job_payload, and last_connection_error so only
the original column definitions remain and only add the new columns manual_sync
and current_job_sophtron_account_id plus its index; ensure last_connection_error
stays as text in the schema (any type change must be implemented via a separate
migration using change_column rather than editing schema.rb).

---

Nitpick comments:
In `@app/components/settings/provider_card.rb`:
- Around line 2-18: Replace hard-coded English labels in MATURITY_LABELS with
translation keys (e.g., :beta and :alpha or symbol strings) and resolve them via
the Rails i18n helper in the maturity_label method; specifically, keep
MATURITY_LABELS as a mapping of maturity symbols to translation keys, then
update the maturity_label method to call t(...) on the mapped key (using the t
helper), and add the corresponding entries under config/locales/en.yml so the UI
strings are localized.

In `@app/jobs/sync_all_providers_job.rb`:
- Line 6: Replace the unsafe Family.find lookup with a nil-safe lookup and early
return: change the Family.find(family_id) call to Family.find_by(id: family_id)
and add a guard like return unless family (or log and return) in the job's
perform method (the place where family = Family.find(...) is used) so the job
exits gracefully if the family was deleted instead of raising
ActiveRecord::RecordNotFound and retrying.

In `@app/views/settings/_section.html.erb`:
- Around line 19-27: The shared partial _section.html.erb currently hard-codes
render "settings/providers/status_pill" for the status display, coupling this
generic layout to a provider-specific partial; change it to accept rendered
status content from the caller (like actions already does) by removing the
inline render and using the passed-in status local as-is, and update callers to
pass status: render("settings/providers/status_pill", status: ...) or status:
capture { render "settings/providers/status_pill", status: :ok } so
_section.html.erb remains subsystem-agnostic and callers control the actual
markup.

In `@app/views/settings/providers/_status_pill.html.erb`:
- Around line 2-8: The template currently contains presentation logic (the case
statement building CSS classes) which should be moved to a helper or
ViewComponent; extract that logic into a new helper method (e.g.,
status_pill_classes(status)) or a StatusPillComponent that returns the dot and
pill class values (as keys like :dot and :pill), then update the partial to call
status_pill_classes(status) and use the returned classes instead of the inline
case; ensure the helper/component handles symbolizing the status and preserves
the same mappings for :ok, :warn, :err and the default case.

In `@app/views/settings/providers/show.html.erb`:
- Around line 1-18: Replace the hard-coded page title and intro copy in the
view: change content_for :page_title, "Bank Sync" to use
t("settings.providers.bank_sync.page_title") and replace the paragraph text
("Connect external accounts so transactions, balances and holdings flow into
Sure automatically.") with t("settings.providers.bank_sync.lede"); then add
corresponding entries under settings.providers.bank_sync in
config/locales/en.yml (page_title and lede) with the existing English strings as
values so the view uses the t(...) helper and stays consistent with other
translations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3452438-7448-40e0-b004-bbd46d7796fd

📥 Commits

Reviewing files that changed from the base of the PR and between 8abecf8 and a235b5a.

📒 Files selected for processing (26)
  • app/components/settings/health_summary.html.erb
  • app/components/settings/health_summary.rb
  • app/components/settings/provider_card.html.erb
  • app/components/settings/provider_card.rb
  • app/controllers/settings/bank_sync_controller.rb
  • app/controllers/settings/providers_controller.rb
  • app/helpers/settings_helper.rb
  • app/jobs/sync_all_providers_job.rb
  • app/models/provider/metadata.rb
  • app/views/settings/_section.html.erb
  • app/views/settings/_settings_nav.html.erb
  • app/views/settings/bank_sync/_provider_link.html.erb
  • app/views/settings/bank_sync/show.html.erb
  • app/views/settings/providers/_add_provider_cta.html.erb
  • app/views/settings/providers/_group_heading.html.erb
  • app/views/settings/providers/_status_pill.html.erb
  • app/views/settings/providers/_sync_button.html.erb
  • app/views/settings/providers/connect_form.html.erb
  • app/views/settings/providers/show.html.erb
  • config/locales/views/settings/en.yml
  • config/routes.rb
  • db/migrate/20260508120000_add_last_sync_all_attempted_at_to_families.rb
  • db/schema.rb
  • test/controllers/settings/providers_controller_test.rb
  • test/system/settings/providers_test.rb
  • test/system/settings_test.rb
💤 Files with no reviewable changes (3)
  • app/controllers/settings/bank_sync_controller.rb
  • app/views/settings/bank_sync/_provider_link.html.erb
  • app/views/settings/bank_sync/show.html.erb

@@ -0,0 +1,8 @@
<div class="grid grid-cols-4 gap-2">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

grid-cols-4 has no mobile fallback — tiles will be very narrow on small screens.

On narrow viewports the four tiles will each be ~70px wide with padding, which can truncate count labels or cause overflow. Consider a two-column layout on small screens.

🛠️ Proposed fix
-<div class="grid grid-cols-4 gap-2">
+<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/settings/health_summary.html.erb` at line 1, The grid
currently forces four columns on all viewports (the <div class="grid grid-cols-4
gap-2">) causing very narrow tiles on mobile; change the grid classes to be
responsive so small screens use two columns (or one) and larger screens use four
— e.g. replace or augment grid-cols-4 with responsive Tailwind classes like
grid-cols-2 sm:grid-cols-4 (or grid-cols-1 md:grid-cols-4) on that same div to
ensure tiles have a mobile-friendly layout.

<div class="bg-container shadow-border-xs rounded-xl p-4 flex flex-col gap-3 hover:shadow-border-sm transition-shadow">
<div class="flex items-start gap-3">
<div class="w-9 h-9 rounded-lg flex items-center justify-center shrink-0 <%= logo_bg %>">
<span class="text-xs font-bold text-white"><%= logo_text %></span>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

text-white violates the functional token convention — use text-inverse.

As per coding guidelines, raw Tailwind utilities like text-white should be replaced with design system functional tokens.

🛠️ Proposed fix
-      <span class="text-xs font-bold text-white"><%= logo_text %></span>
+      <span class="text-xs font-bold text-inverse"><%= logo_text %></span>

As per coding guidelines: "Use functional tokens defined in design system such as text-primary instead of text-white".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span class="text-xs font-bold text-white"><%= logo_text %></span>
<span class="text-xs font-bold text-inverse"><%= logo_text %></span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/settings/provider_card.html.erb` at line 4, Replace the raw
Tailwind utility "text-white" with the design-system functional token
"text-inverse" in the span that renders the logo text (the span showing <%=
logo_text %>), i.e., update the class attribute on that span to remove
"text-white" and add "text-inverse" so it follows the functional token
convention used by the design system.

Comment on lines +80 to +89
def sync_all
family = Current.family

if family.last_sync_all_attempted_at.present? && family.last_sync_all_attempted_at > 30.seconds.ago
return redirect_to settings_providers_path, notice: t("settings.providers.sync_all_recently")
end

family.update!(last_sync_all_attempted_at: Time.current)
SyncAllProvidersJob.perform_later(family.id)
redirect_to settings_providers_path, notice: t("settings.providers.sync_all_in_progress")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the sync-all throttle atomic.

This check/update pair is race-prone: two concurrent POSTs can both pass Line 83, both write last_sync_all_attempted_at, and both enqueue SyncAllProvidersJob. That defeats the throttle and can hammer downstream providers.

Suggested fix
 def sync_all
   family = Current.family
+  throttled = false

-  if family.last_sync_all_attempted_at.present? && family.last_sync_all_attempted_at > 30.seconds.ago
-    return redirect_to settings_providers_path, notice: t("settings.providers.sync_all_recently")
-  end
-
-  family.update!(last_sync_all_attempted_at: Time.current)
+  family.with_lock do
+    throttled = family.last_sync_all_attempted_at.present? &&
+      family.last_sync_all_attempted_at > 30.seconds.ago
+    family.update!(last_sync_all_attempted_at: Time.current) unless throttled
+  end
+
+  return redirect_to(settings_providers_path, notice: t("settings.providers.sync_all_recently")) if throttled
+
   SyncAllProvidersJob.perform_later(family.id)
   redirect_to settings_providers_path, notice: t("settings.providers.sync_all_in_progress")
 end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/controllers/settings/providers_controller.rb` around lines 80 - 89, The
sync_all action is race-prone because the present?/>update pair can be run
concurrently; make it atomic by performing a single conditional DB update on the
Family row and only enqueueing SyncAllProvidersJob when that update succeeds.
Replace the current check+family.update!(last_sync_all_attempted_at: ...) with a
conditional update scoped to Current.family (e.g. a where on Family.id and a
predicate that last_sync_all_attempted_at is nil or <= 30.seconds.ago) and
proceed to call SyncAllProvidersJob.perform_later(family.id) only if the
conditional update affected one row; otherwise redirect with the
"sync_all_recently" notice. Ensure you reference sync_all, Current.family,
last_sync_all_attempted_at, and SyncAllProvidersJob in the change.

@@ -0,0 +1,12 @@
<%# locals: (title:, count: nil, description: nil, anchor: nil) %>
<div id="<%= anchor %>" class="flex items-baseline justify-between gap-3 mt-2 mb-1 px-1">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

id="" renders invalid HTML when anchor is nil.

<%= nil %> outputs an empty string, producing id="". The HTML5 spec requires id values to be non-empty. The element also won't be targetable as an anchor.

🐛 Proposed fix
-<div id="<%= anchor %>" class="flex items-baseline justify-between gap-3 mt-2 mb-1 px-1">
+<div <%= "id=\"#{anchor}\"".html_safe if anchor.present? %> class="flex items-baseline justify-between gap-3 mt-2 mb-1 px-1">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div id="<%= anchor %>" class="flex items-baseline justify-between gap-3 mt-2 mb-1 px-1">
<div <%= "id=\"#{anchor}\"".html_safe if anchor.present? %> class="flex items-baseline justify-between gap-3 mt-2 mb-1 px-1">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/settings/providers/_group_heading.html.erb` at line 2, The div
currently always emits id="" when the local variable anchor is nil; change the
ERB so the id attribute is only rendered when anchor is present (e.g., use a
conditional like `if anchor.present?` or an inline ERB conditional) so the <div>
element does not produce an empty id and is only anchorable when a non-empty
anchor value exists; update the line that generates the div (the one referencing
anchor) to conditionally include the id attribute rather than always outputting
id.

Comment on lines +3 to +9
<%= button_to sync_provider_settings_providers_path(provider_key: provider_key),
method: :post,
disabled: recently_synced,
title: recently_synced ? t("settings.providers.recently_synced") : t("settings.providers.sync_provider"),
class: "inline-flex items-center p-1 rounded text-secondary hover:text-primary hover:bg-alpha-black-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
form: { onclick: "event.stopPropagation()", class: "inline-flex" } do %>
<%= icon "refresh-cw", class: "w-3.5 h-3.5" %>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an explicit accessible name to this icon-only button.

title is not a reliable accessible name, so screen-reader users may only get an unlabeled “button” here. Please add an aria-label (or sr-only text) that matches the sync action.

Suggested fix
 <% recently_synced = last_synced_at.present? && last_synced_at > 60.seconds.ago %>
+<% button_label = recently_synced ? t("settings.providers.recently_synced") : t("settings.providers.sync_provider") %>
 <%= button_to sync_provider_settings_providers_path(provider_key: provider_key),
               method: :post,
               disabled: recently_synced,
-              title: recently_synced ? t("settings.providers.recently_synced") : t("settings.providers.sync_provider"),
+              title: button_label,
+              aria: { label: button_label },
               class: "inline-flex items-center p-1 rounded text-secondary hover:text-primary hover:bg-alpha-black-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
               form: { onclick: "event.stopPropagation()", class: "inline-flex" } do %>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/settings/providers/_sync_button.html.erb` around lines 3 - 9, The
button created by button_to (sync_provider_settings_providers_path) is icon-only
and needs an explicit accessible name; add an aria-label (or include hidden
sr-only text) that matches the sync action instead of relying on title. Update
the button_to call to include aria-label: recently_synced ?
t("settings.providers.recently_synced") : t("settings.providers.sync_provider")
(or render an adjacent sr-only <span> with the same translated text) so screen
readers receive a clear label while keeping the existing title, disabled,
provider_key, and form attributes intact.

Comment thread db/migrate/20260508120000_add_last_sync_all_attempted_at_to_families.rb Outdated
Comment thread test/system/settings/providers_test.rb Outdated
Comment thread test/system/settings/providers_test.rb
Copy link
Copy Markdown

CI root cause: db/schema.rb contains duplicate column definitions for sophtron_accounts.account_number_mask and 8 columns in sophtron_items. This causes db:schema:load to fail on a fresh database, which is why the ci/test job can't complete. The duplicate entries need to be deduplicated before CI can pass — everything else in this PR looks solid.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

jjmata commented May 9, 2026

Great consolidation — the unified provider page and health strip are a meaningful UX improvement. One design-system violation to fix before merge:

text-white in provider_card.html.erb

<span class="text-xs font-bold text-white"><%= logo_text %></span>

text-white is a raw Tailwind utility, not a functional design-system token. Per the project conventions (and sure-design-system.css), colour classes must use functional tokens so they respond correctly to theme changes (including dark mode). text-white will be invisible or incorrect on light-coloured logo backgrounds in certain themes.

Replace with the appropriate functional token — likely text-primary or a dedicated inverse token if one exists in the design system for content that sits on a coloured background.

(The bg-*-600 / bg-*-500 logo background classes have a similar concern — if those colours are hardcoded Tailwind utilities rather than design-system tokens they'll also bypass theming, though I recognise they may be intentional brand colours with no functional equivalent.)


Generated by Claude Code


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Development

Successfully merging this pull request may close these issues.

3 participants