fix: Enable Banking DNS issues and provide better UI sync feedback (#1021)

* fix(docker): add explicit DNS config to fix enable banking sync

* fix(enable-banking): surface sync errors in the UI

* fix: add spaces inside array brackets for RuboCop

* fix(enable-banking): surface sync errors and partial failures in UI
This commit is contained in:
Number Eight
2026-02-19 21:54:44 +01:00
committed by GitHub
parent f5e4fed5a4
commit 7725661a96
10 changed files with 102 additions and 18 deletions

View File

@@ -237,9 +237,11 @@ class AccountsController < ApplicationController
# Enable Banking sync stats # Enable Banking sync stats
@enable_banking_sync_stats_map = {} @enable_banking_sync_stats_map = {}
@enable_banking_latest_sync_error_map = {}
@enable_banking_items.each do |item| @enable_banking_items.each do |item|
latest_sync = item.syncs.ordered.first latest_sync = item.syncs.ordered.first
@enable_banking_sync_stats_map[item.id] = latest_sync&.sync_stats || {} @enable_banking_sync_stats_map[item.id] = latest_sync&.sync_stats || {}
@enable_banking_latest_sync_error_map[item.id] = latest_sync&.error
end end
# CoinStats sync stats # CoinStats sync stats

View File

@@ -15,6 +15,7 @@ module EnableBankingItems
@enable_banking_unlinked_count_map ||= {} @enable_banking_unlinked_count_map ||= {}
@enable_banking_duplicate_only_map ||= {} @enable_banking_duplicate_only_map ||= {}
@enable_banking_show_relink_map ||= {} @enable_banking_show_relink_map ||= {}
@enable_banking_latest_sync_error_map ||= {}
# Batch-check if ANY family has manual accounts (same result for all items from same family) # Batch-check if ANY family has manual accounts (same result for all items from same family)
family_ids = items.map { |i| i.family_id }.uniq family_ids = items.map { |i| i.family_id }.uniq
@@ -42,6 +43,7 @@ module EnableBankingItems
end end
stats = (latest_sync&.sync_stats || {}) stats = (latest_sync&.sync_stats || {})
@enable_banking_sync_stats_map[item.id] = stats @enable_banking_sync_stats_map[item.id] = stats
@enable_banking_latest_sync_error_map[item.id] = latest_sync&.error
# Whether the family has any manual accounts available to link (from batch query) # Whether the family has any manual accounts available to link (from batch query)
@enable_banking_has_unlinked_map[item.id] = families_with_manuals.include?(item.family_id) @enable_banking_has_unlinked_map[item.id] = families_with_manuals.include?(item.family_id)
@@ -68,13 +70,6 @@ module EnableBankingItems
@enable_banking_show_relink_map[item.id] = false @enable_banking_show_relink_map[item.id] = false
end end
end end
# Ensure maps are hashes even when items empty
@enable_banking_sync_stats_map ||= {}
@enable_banking_has_unlinked_map ||= {}
@enable_banking_unlinked_count_map ||= {}
@enable_banking_duplicate_only_map ||= {}
@enable_banking_show_relink_map ||= {}
end end
private private

View File

@@ -18,7 +18,8 @@ class EnableBankingItem::Importer
session_data = fetch_session_data session_data = fetch_session_data
unless session_data unless session_data
return { success: false, error: "Failed to fetch session data", accounts_updated: 0, transactions_imported: 0 } error_msg = @session_error || "Failed to fetch session data"
return { success: false, error: error_msg, accounts_updated: 0, transactions_imported: 0 }
end end
# Store raw payload # Store raw payload
@@ -92,17 +93,41 @@ class EnableBankingItem::Importer
end end
end end
{ result = {
success: accounts_failed == 0 && transactions_failed == 0, success: accounts_failed == 0 && transactions_failed == 0,
accounts_updated: accounts_updated, accounts_updated: accounts_updated,
accounts_failed: accounts_failed, accounts_failed: accounts_failed,
transactions_imported: transactions_imported, transactions_imported: transactions_imported,
transactions_failed: transactions_failed transactions_failed: transactions_failed
} }
if !result[:success] && (accounts_failed > 0 || transactions_failed > 0)
parts = []
parts << "#{accounts_failed} #{'account'.pluralize(accounts_failed)} failed" if accounts_failed > 0
parts << "#{transactions_failed} #{'transaction'.pluralize(transactions_failed)} failed" if transactions_failed > 0
result[:error] = parts.join(", ")
end
result
end end
private private
def extract_friendly_error_message(exception)
[ exception, exception.cause ].compact.each do |ex|
case ex
when SocketError then return "DNS resolution failed: check your network/DNS configuration"
when Net::OpenTimeout, Net::ReadTimeout then return "Connection timed out: the Enable Banking API may be unreachable"
when Errno::ECONNREFUSED then return "Connection refused: the Enable Banking API is unreachable"
end
end
msg = exception.message.to_s
return "DNS resolution failed: check your network/DNS configuration" if msg.include?("getaddrinfo") || msg.match?(/name or service not known/i)
return "Connection timed out: the Enable Banking API may be unreachable" if msg.include?("execution expired") || msg.include?("timeout") || msg.match?(/timed out/i)
return "Connection refused: the Enable Banking API is unreachable" if msg.include?("ECONNREFUSED") || msg.match?(/connection refused/i)
msg
end
def fetch_session_data def fetch_session_data
enable_banking_provider.get_session(session_id: enable_banking_item.session_id) enable_banking_provider.get_session(session_id: enable_banking_item.session_id)
rescue Provider::EnableBanking::EnableBankingError => e rescue Provider::EnableBanking::EnableBankingError => e
@@ -110,9 +135,11 @@ class EnableBankingItem::Importer
enable_banking_item.update!(status: :requires_update) enable_banking_item.update!(status: :requires_update)
end end
Rails.logger.error "EnableBankingItem::Importer - Enable Banking API error: #{e.message}" Rails.logger.error "EnableBankingItem::Importer - Enable Banking API error: #{e.message}"
@session_error = extract_friendly_error_message(e)
nil nil
rescue => e rescue => e
Rails.logger.error "EnableBankingItem::Importer - Unexpected error fetching session: #{e.class} - #{e.message}" Rails.logger.error "EnableBankingItem::Importer - Unexpected error fetching session: #{e.class} - #{e.message}"
@session_error = extract_friendly_error_message(e)
nil nil
end end

View File

@@ -6,20 +6,34 @@ class EnableBankingItem::SyncCompleteEvent
end end
def broadcast def broadcast
enable_banking_item.reload
# Update UI with latest account data # Update UI with latest account data
enable_banking_item.accounts.each do |account| enable_banking_item.accounts.each do |account|
account.broadcast_sync_complete account.broadcast_sync_complete
end end
# Update the Enable Banking item view family = enable_banking_item.family
return unless family
# Update the Enable Banking item view on the Accounts page
enable_banking_item.broadcast_replace_to( enable_banking_item.broadcast_replace_to(
enable_banking_item.family, family,
target: "enable_banking_item_#{enable_banking_item.id}", target: "enable_banking_item_#{enable_banking_item.id}",
partial: "enable_banking_items/enable_banking_item", partial: "enable_banking_items/enable_banking_item",
locals: { enable_banking_item: enable_banking_item } locals: { enable_banking_item: enable_banking_item }
) )
# Update the Settings > Providers panel
enable_banking_items = family.enable_banking_items.ordered.includes(:syncs)
enable_banking_item.broadcast_replace_to(
family,
target: "enable_banking-providers-panel",
partial: "settings/providers/enable_banking_panel",
locals: { enable_banking_items: enable_banking_items }
)
# Let family handle sync notifications # Let family handle sync notifications
enable_banking_item.family.broadcast_sync_complete family.broadcast_sync_complete
end end
end end

View File

@@ -17,6 +17,17 @@ class EnableBankingItem::Syncer
sync.update!(status_text: "Importing accounts from Enable Banking...") if sync.respond_to?(:status_text) sync.update!(status_text: "Importing accounts from Enable Banking...") if sync.respond_to?(:status_text)
import_result = enable_banking_item.import_latest_enable_banking_data import_result = enable_banking_item.import_latest_enable_banking_data
unless import_result[:success]
error_msg = import_result[:error]
if error_msg.blank? && (import_result[:accounts_failed].to_i > 0 || import_result[:transactions_failed].to_i > 0)
parts = []
parts << "#{import_result[:accounts_failed]} #{'account'.pluralize(import_result[:accounts_failed])} failed" if import_result[:accounts_failed].to_i > 0
parts << "#{import_result[:transactions_failed]} #{'transaction'.pluralize(import_result[:transactions_failed])} failed" if import_result[:transactions_failed].to_i > 0
error_msg = parts.join(", ")
end
raise StandardError.new(error_msg.presence || "Import failed")
end
# Phase 2: Check account setup status and collect sync statistics # Phase 2: Check account setup status and collect sync statistics
sync.update!(status_text: "Checking account configuration...") if sync.respond_to?(:status_text) sync.update!(status_text: "Checking account configuration...") if sync.respond_to?(:status_text)
total_accounts = enable_banking_item.enable_banking_accounts.count total_accounts = enable_banking_item.enable_banking_accounts.count

View File

@@ -86,11 +86,20 @@
<% end %> <% end %>
<%# Sync summary (collapsible) - using shared ProviderSyncSummary component %> <%# Sync summary (collapsible) - using shared ProviderSyncSummary component %>
<% stats = if defined?(@enable_banking_sync_stats_map) && @enable_banking_sync_stats_map <% if defined?(@enable_banking_sync_stats_map) && @enable_banking_sync_stats_map %>
@enable_banking_sync_stats_map[enable_banking_item.id] || {} <% stats = @enable_banking_sync_stats_map[enable_banking_item.id] || {} %>
else <% latest_sync_error = defined?(@enable_banking_latest_sync_error_map) && @enable_banking_latest_sync_error_map ? @enable_banking_latest_sync_error_map[enable_banking_item.id] : nil %>
enable_banking_item.syncs.ordered.first&.sync_stats || {} <% else %>
end %> <% latest_sync = enable_banking_item.syncs.ordered.first %>
<% stats = latest_sync&.sync_stats || {} %>
<% latest_sync_error = latest_sync&.error %>
<% end %>
<% if latest_sync_error.present? && stats.is_a?(Hash) %>
<% stats = stats.merge(
"total_errors" => 1,
"errors" => [{ "message" => latest_sync_error }]
) %>
<% end %>
<%= render ProviderSyncSummary.new( <%= render ProviderSyncSummary.new(
stats: stats, stats: stats,
provider_item: enable_banking_item provider_item: enable_banking_item

View File

@@ -112,7 +112,19 @@
<% items.each do |item| %> <% items.each do |item| %>
<div class="flex items-center justify-between p-3 rounded-lg bg-container border border-primary"> <div class="flex items-center justify-between p-3 rounded-lg bg-container border border-primary">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<% if item.session_valid? %> <% if item.syncing? %>
<div class="w-2 h-2 bg-primary rounded-full animate-pulse"></div>
<div>
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || t("settings.providers.enable_banking_panel.syncing", default: "Syncing") %></p>
<p class="text-xs text-secondary"><%= t("settings.providers.enable_banking_panel.syncing", default: "Syncing") %></p>
</div>
<% elsif item.sync_error.present? %>
<div class="w-2 h-2 bg-destructive rounded-full"></div>
<div>
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || t("settings.providers.enable_banking_panel.connection_error") %></p>
<p class="text-xs text-destructive" title="<%= item.sync_error %>"><%= item.sync_error.truncate(50) %></p>
</div>
<% elsif item.session_valid? %>
<div class="w-2 h-2 bg-success rounded-full"></div> <div class="w-2 h-2 bg-success rounded-full"></div>
<div> <div>
<p class="text-sm font-medium text-primary"><%= item.aspsp_name || "Connected Bank" %></p> <p class="text-sm font-medium text-primary"><%= item.aspsp_name || "Connected Bank" %></p>

View File

@@ -115,6 +115,9 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1
networks: networks:
- sure_net - sure_net
@@ -129,6 +132,9 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1
environment: environment:
<<: *rails_env <<: *rails_env
networks: networks:

View File

@@ -59,6 +59,9 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1
networks: networks:
- sure_net - sure_net
@@ -73,6 +76,9 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1
environment: environment:
<<: *rails_env <<: *rails_env
networks: networks:

View File

@@ -172,3 +172,5 @@ en:
disconnect_confirm: Are you sure you want to disconnect this Coinbase connection? Your synced accounts will become manual accounts. disconnect_confirm: Are you sure you want to disconnect this Coinbase connection? Your synced accounts will become manual accounts.
status_connected: Coinbase is connected and syncing your crypto holdings. status_connected: Coinbase is connected and syncing your crypto holdings.
status_not_connected: Not connected. Enter your API credentials above to get started. status_not_connected: Not connected. Enter your API credentials above to get started.
enable_banking_panel:
connection_error: Connection Error