mirror of
https://github.com/we-promise/sure.git
synced 2026-08-05 08:32:15 +00:00
* fix Prism issue - assigned but unused variable plaid_item * measurement to improve accounts_controller__index performance based on skylight * fix FEEDBACK - Stop eager-loading full sync histories on accounts index * fix FEEDBACK - Remove sticky memoization from SimplefinItem#accounts * fix lint error in PR review * fix FEEDBACK - Avoid returning map misses as authoritative results * fix test error * fix FEEDBACK - Avoid redundant query by using already-loaded @manual_accounts * Address maintainer review on accounts index sync preloading Memoize SimplefinItem#accounts, align syncing? with Sync#visible?, and add fallback regression tests. * Add composite index for sync DISTINCT ON queries Support latest_by_syncable ordering with syncable_type, syncable_id, created_at DESC, and id DESC. * fix error in dockerfile.preview file * Suppress Pipelock false positives on CI database fixtures. Pipelock 2.7.0 full-repo audit flags ephemeral postgres:// URLs in workflow env blocks. Re-apply inline suppressions lost in the main merge. * Address review: guard partial Current sync maps and drop unrelated diff Add key? checks so partially populated Current sync maps fall back to DB queries. Revert Dockerfile.preview and pipelock.yml changes unrelated to the accounts index N+1 optimization. * fix(sync): fully populate Current sync maps for all preloaded syncables * fix(ci): skip scheduled preview cleanup on forks Only run the hourly Cloudflare preview cleanup on we-promise/sure, where the required secrets exist. * Remove schema.rb Postgres-version churn, keep only new syncs index Reset db/schema.rb to upstream/main and re-add only the index_syncs_on_syncable_and_created_at_and_id index. The prior diff included ~30 lines of noise (check-constraint reformatting, virtual column reformat, and column reorderings) caused by dumping under a different PostgreSQL version, which obscured the single intended schema change and invited merge conflicts. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
246 lines
12 KiB
Plaintext
246 lines
12 KiB
Plaintext
<%# locals: (simplefin_item:) %>
|
|
|
|
<%= tag.div id: dom_id(simplefin_item) do %>
|
|
<% accounts = simplefin_item.accounts %>
|
|
<%# Compute unlinked count early so it's available for both menu and bottom section %>
|
|
<% unlinked_count = if defined?(@simplefin_unlinked_count_map) && @simplefin_unlinked_count_map
|
|
@simplefin_unlinked_count_map[simplefin_item.id] || 0
|
|
else
|
|
begin
|
|
simplefin_item.simplefin_accounts
|
|
.left_joins(:account, :account_provider)
|
|
.where(accounts: { id: nil }, account_providers: { id: nil })
|
|
.count
|
|
rescue => e
|
|
Rails.logger.warn("SimpleFin card: unlinked_count fallback failed: #{e.class} - #{e.message}")
|
|
0
|
|
end
|
|
end %>
|
|
<% setup_token_update_required = simplefin_item.setup_token_update_required? %>
|
|
|
|
<%= render DS::Disclosure.new(variant: :card, open: true) do |disclosure| %>
|
|
<% disclosure.with_summary_content do %>
|
|
<div class="flex items-center justify-between gap-2">
|
|
<div class="flex items-center gap-2">
|
|
<%= icon "chevron-right", class: "group-open:rotate-90 motion-safe:transition-transform motion-safe:duration-150" %>
|
|
|
|
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
|
<% if simplefin_item.logo.attached? %>
|
|
<%= image_tag simplefin_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
|
<% else %>
|
|
<div class="flex items-center justify-center">
|
|
<%= tag.p simplefin_item.name.first.upcase, class: "text-success text-xs font-medium" %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="pl-1 text-sm">
|
|
<div class="flex items-center gap-2">
|
|
<%= tag.p simplefin_item.name, class: "font-medium text-primary" %>
|
|
<% if simplefin_item.scheduled_for_deletion? %>
|
|
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
|
<% end %>
|
|
</div>
|
|
<% if accounts.any? %>
|
|
<p class="text-xs text-secondary">
|
|
<%= simplefin_item.institution_summary %>
|
|
</p>
|
|
<%# Extra inline badges from latest sync stats - only show warnings %>
|
|
<% stats = (@simplefin_sync_stats_map || {})[simplefin_item.id] || {} %>
|
|
<% has_warnings = stats["accounts_skipped"].to_i > 0 ||
|
|
stats["rate_limited"].present? ||
|
|
stats["rate_limited_at"].present? ||
|
|
stats["total_errors"].to_i > 0 ||
|
|
(stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
|
<% if has_warnings %>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<% if stats["accounts_skipped"].to_i > 0 %>
|
|
<%= render DS::Tooltip.new(text: t(".accounts_skipped_tooltip"), icon: "alert-triangle", size: "sm", color: "warning", as: :span) %>
|
|
<span class="text-xs text-warning"><%= t(".accounts_skipped_label", count: stats["accounts_skipped"].to_i) %></span>
|
|
<% end %>
|
|
|
|
<% if stats["rate_limited"].present? || stats["rate_limited_at"].present? %>
|
|
<% ts = stats["rate_limited_at"] %>
|
|
<% ago = (ts.present? ? (begin; time_ago_in_words(Time.parse(ts)); rescue StandardError; nil; end) : nil) %>
|
|
<%= render DS::Tooltip.new(
|
|
text: (ago ? t(".rate_limited_ago", time: ago) : t(".rate_limited_recently")),
|
|
icon: "clock",
|
|
size: "sm",
|
|
color: "warning",
|
|
as: :span
|
|
) %>
|
|
<% end %>
|
|
|
|
<% if stats["total_errors"].to_i > 0 || (stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
|
<% tooltip_text = simplefin_error_tooltip(stats) %>
|
|
<% if tooltip_text.present? %>
|
|
<%= render DS::Tooltip.new(text: tooltip_text, icon: "alert-octagon", size: "sm", color: "warning", as: :span) %>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<%# Determine if all reported errors are benign duplicate-skips (suppress scary banner). Computed in controller for testability. %>
|
|
<% duplicate_only_errors = (@simplefin_duplicate_only_map || {})[simplefin_item.id] || false %>
|
|
<% if simplefin_item.syncing? %>
|
|
<div class="text-secondary flex items-center gap-1">
|
|
<%= icon "loader", size: "sm", class: "animate-spin" %>
|
|
<%= tag.span t(".syncing") %>
|
|
</div>
|
|
<% elsif setup_token_update_required %>
|
|
<div class="text-warning flex items-center gap-1">
|
|
<%= icon "alert-triangle", size: "sm", color: "warning" %>
|
|
<%= tag.span t(".requires_update") %>
|
|
</div>
|
|
<% elsif (stale_status = simplefin_item.stale_sync_status)[:stale] %>
|
|
<div class="text-warning flex items-center gap-1">
|
|
<%= icon "alert-circle", size: "sm", color: "warning" %>
|
|
<%= tag.span stale_status[:message], class: "text-sm" %>
|
|
</div>
|
|
<% elsif (pending_status = simplefin_item.stale_pending_status)[:count] > 0 %>
|
|
<div class="text-secondary">
|
|
<div class="flex items-center gap-1">
|
|
<%= icon "clock", size: "sm", color: "secondary" %>
|
|
<%= tag.span pending_status[:message], class: "text-sm" %>
|
|
<span class="text-xs text-subdued"><%= t(".stale_pending_note") %></span>
|
|
</div>
|
|
<% if pending_status[:accounts]&.any? %>
|
|
<div class="text-xs text-subdued ml-5">
|
|
<%= t(".stale_pending_accounts", accounts: pending_status[:accounts].join(", ")) %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% elsif (reconciled_status = simplefin_item.last_sync_reconciled_status)[:count] > 0 %>
|
|
<div class="text-success flex items-center gap-1">
|
|
<%= icon "check-circle", size: "sm", color: "success" %>
|
|
<%= tag.span reconciled_status[:message], class: "text-sm" %>
|
|
<span class="text-xs text-subdued"><%= t(".reconciled_details_note") %></span>
|
|
</div>
|
|
<% elsif simplefin_item.rate_limited_message.present? %>
|
|
<div class="text-warning flex items-center gap-1">
|
|
<%= icon "clock", size: "sm", color: "warning" %>
|
|
<%= tag.span simplefin_item.rate_limited_message %>
|
|
</div>
|
|
<% elsif simplefin_item.sync_error.present? && !duplicate_only_errors %>
|
|
<div class="text-secondary flex items-center gap-1">
|
|
<%= render DS::Tooltip.new(text: simplefin_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
|
|
<%= tag.span t(".error"), class: "text-destructive" %>
|
|
</div>
|
|
<% elsif duplicate_only_errors %>
|
|
<div class="text-secondary flex items-center gap-1">
|
|
<%= icon "info", size: "sm" %>
|
|
<%= tag.span t(".duplicate_accounts_skipped"), class: "text-secondary" %>
|
|
</div>
|
|
<% else %>
|
|
<p class="text-secondary">
|
|
<% if simplefin_item.last_synced_at %>
|
|
<% if simplefin_item.sync_status_summary %>
|
|
<%= t(".status_with_summary", timestamp: time_ago_in_words(simplefin_item.last_synced_at), summary: simplefin_item.sync_status_summary) %>
|
|
<% else %>
|
|
<%= t(".status", timestamp: time_ago_in_words(simplefin_item.last_synced_at)) %>
|
|
<% end %>
|
|
<% else %>
|
|
<%= t(".status_never") %>
|
|
<% end %>
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<% if Current.user&.admin? %>
|
|
<div class="flex items-center gap-2">
|
|
<% if setup_token_update_required %>
|
|
<%= render DS::Link.new(
|
|
text: t(".update"),
|
|
icon: "refresh-cw",
|
|
variant: "secondary",
|
|
href: edit_simplefin_item_path(simplefin_item),
|
|
frame: "modal"
|
|
) %>
|
|
<% else %>
|
|
<%= icon(
|
|
"refresh-cw",
|
|
as_button: true,
|
|
href: sync_simplefin_item_path(simplefin_item),
|
|
disabled: simplefin_item.syncing?
|
|
) %>
|
|
<% end %>
|
|
|
|
<%= render DS::Menu.new do |menu| %>
|
|
<% if unlinked_count.to_i > 0 %>
|
|
<% menu.with_item(
|
|
variant: "link",
|
|
text: t(".setup_accounts_menu"),
|
|
icon: "settings",
|
|
href: setup_accounts_simplefin_item_path(simplefin_item),
|
|
frame: :modal
|
|
) %>
|
|
<% end %>
|
|
<% menu.with_item(
|
|
variant: "button",
|
|
text: t(".delete"),
|
|
icon: "trash-2",
|
|
href: simplefin_item_path(simplefin_item),
|
|
method: :delete,
|
|
confirm: CustomConfirm.for_resource_deletion(simplefin_item.name, high_severity: true)
|
|
) %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% unless simplefin_item.scheduled_for_deletion? %>
|
|
<div class="space-y-4 mt-4">
|
|
<%# Replacement suggestions from the ReplacementDetector (card-fraud swap, etc.).
|
|
Only rendered when the latest sync flagged candidate pairs. %>
|
|
<% suggestions = ((@simplefin_sync_stats_map || {})[simplefin_item.id] || {})["replacement_suggestions"] %>
|
|
<% if suggestions.is_a?(Array) && suggestions.any? %>
|
|
<%= render "simplefin_items/replacement_prompt",
|
|
simplefin_item: simplefin_item,
|
|
suggestions: suggestions %>
|
|
<% end %>
|
|
|
|
<% if accounts.any? %>
|
|
<%= render "accounts/index/account_groups", accounts: accounts %>
|
|
<% end %>
|
|
|
|
<%# Sync summary (collapsible) - using shared ProviderSyncSummary component
|
|
Prefer controller-provided map; fallback to latest sync stats so Turbo broadcasts
|
|
can render the summary without requiring a full page refresh. %>
|
|
<% stats = if defined?(@simplefin_sync_stats_map) && @simplefin_sync_stats_map
|
|
@simplefin_sync_stats_map[simplefin_item.id] || {}
|
|
else
|
|
# `latest_sync` is private on Syncable; access via association for broadcast renders.
|
|
simplefin_item.syncs.ordered.first&.sync_stats || {}
|
|
end %>
|
|
<%= render ProviderSyncSummary.new(
|
|
stats: stats,
|
|
provider_item: simplefin_item,
|
|
institutions_count: simplefin_item.connected_institutions.size
|
|
) %>
|
|
|
|
<% if unlinked_count.to_i > 0 && accounts.empty? %>
|
|
<%# No accounts imported yet - show prominent setup prompt %>
|
|
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
|
<p class="text-primary font-medium text-sm"><%= t(".setup_needed") %></p>
|
|
<p class="text-secondary text-sm"><%= t(".setup_description") %></p>
|
|
<%= render DS::Link.new(
|
|
text: t(".setup_action"),
|
|
icon: "settings",
|
|
variant: "primary",
|
|
href: setup_accounts_simplefin_item_path(simplefin_item),
|
|
frame: :modal
|
|
) %>
|
|
</div>
|
|
<% elsif accounts.empty? %>
|
|
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
|
<p class="text-primary font-medium text-sm"><%= t(".no_accounts_title") %></p>
|
|
<p class="text-secondary text-sm"><%= t(".no_accounts_description") %></p>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|