1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-05 13:35:21 +02:00

Balance sheet cache layer, non-blocking sync UI (#2356)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions

* Balance sheet cache layer with cache-busting

* Update family cache timestamps during Sync

* Less blocking sync loaders

* Consolidate family data caching key logic

* Fix turbo stream broadcasts

* Remove dev delay

* Add back account group sorting
This commit is contained in:
Zach Gollwitzer 2025-06-10 18:20:06 -04:00 committed by GitHub
parent dab693d74f
commit 10ce2c8e23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 529 additions and 466 deletions

View file

@ -1,8 +0,0 @@
<%= turbo_frame_tag "#{params[:accountable_type]}_sparkline" do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-3 flex items-center justify-center">
<%= icon("alert-triangle", size: "sm", class: "text-warning") %>
</div>
<p class="font-mono text-right text-xs text-warning">Error</p>
</div>
<% end %>

View file

@ -1,13 +1,11 @@
<% cache Current.family.build_cache_key("#{@accountable.name}_sparkline_html") do %>
<%= turbo_frame_tag "#{@accountable.model_name.param_key}_sparkline" do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-3">
<%= render "shared/sparkline", id: dom_id(@accountable, :sparkline_chart), series: @series %>
</div>
<%= turbo_frame_tag "#{@accountable.model_name.param_key}_sparkline" do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-3">
<%= render "shared/sparkline", id: dom_id(@accountable, :sparkline_chart), series: @series %>
</div>
<%= tag.p @series.trend.percent_formatted,
<%= tag.p @series.trend.percent_formatted,
style: "color: #{@series.trend.color}",
class: "font-mono text-right text-xs font-medium text-primary" %>
</div>
<% end %>
</div>
<% end %>

View file

@ -41,7 +41,7 @@
) %>
<div>
<% family.balance_sheet.account_groups("asset").each do |group| %>
<% family.balance_sheet.assets.account_groups.each do |group| %>
<%= render "accounts/accountable_group", account_group: group, mobile: mobile %>
<% end %>
</div>
@ -61,7 +61,7 @@
) %>
<div>
<% family.balance_sheet.account_groups("liability").each do |group| %>
<% family.balance_sheet.liabilities.account_groups.each do |group| %>
<%= render "accounts/accountable_group", account_group: group, mobile: mobile %>
<% end %>
</div>
@ -82,7 +82,7 @@
<div>
<% family.balance_sheet.account_groups.each do |group| %>
<%= render "accounts/accountable_group", account_group: group, mobile: mobile %>
<%= render "accounts/accountable_group", account_group: group, mobile: mobile, all_tab: true %>
<% end %>
</div>
</div>

View file

@ -1,24 +1,21 @@
<%# locals: (account_group:, mobile: false, open: nil, **args) %>
<%# locals: (account_group:, mobile: false, all_tab: false, open: nil, **args) %>
<div id="<%= mobile ? "mobile_#{account_group.id}" : account_group.id %>">
<div id="<%= account_group.dom_id(tab: all_tab ? :all : nil, mobile: mobile) %>">
<% is_open = open.nil? ? account_group.accounts.any? { |account| page_active?(account_path(account)) } : open %>
<%= render DisclosureComponent.new(title: account_group.name, align: :left, open: is_open) do |disclosure| %>
<% disclosure.with_summary_content do %>
<% if account_group.syncing? %>
<div class="ml-2 group-open:hidden">
<%= render partial: "shared/sync_indicator", locals: { size: "xs" } %>
</div>
<% end %>
<div class="ml-auto text-right grow">
<% if account_group.syncing? %>
<div class="space-y-1">
<div class="h-5 w-24 rounded ml-auto bg-loader"></div>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-loader"></div>
</div>
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
<%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy", data: { controller: "turbo-frame-timeout", turbo_frame_timeout_timeout_value: 10000 } do %>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-loader"></div>
</div>
<% else %>
<%= tag.p format_money(account_group.total_money), class: "text-sm font-medium text-primary" %>
<%= turbo_frame_tag "#{account_group.key}_sparkline", src: accountable_sparkline_path(account_group.key), loading: "lazy", data: { controller: "turbo-frame-timeout", turbo_frame_timeout_timeout_value: 10000 } do %>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-loader"></div>
</div>
<% end %>
<% end %>
</div>
<% end %>
@ -34,29 +31,23 @@
<%= render "accounts/logo", account: account, size: "sm", color: account_group.color %>
<div class="min-w-0 grow">
<%= tag.p account.name, class: "text-sm text-primary font-medium mb-0.5 truncate" %>
<div class="flex items-center gap-2 mb-0.5">
<%= tag.p account.name, class: "text-sm text-primary font-medium truncate" %>
<% if account.syncing? %>
<%= render partial: "shared/sync_indicator", locals: { size: "xs" } %>
<% end %>
</div>
<%= tag.p account.short_subtype_label, class: "text-sm text-secondary truncate" %>
</div>
<% if account.syncing? %>
<div class="ml-auto text-right grow h-10">
<div class="space-y-1">
<div class="h-5 w-24 bg-loader rounded ml-auto"></div>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-loader"></div>
</div>
<div class="ml-auto text-right grow h-10">
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %>
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy", data: { controller: "turbo-frame-timeout", turbo_frame_timeout_timeout_value: 10000 } do %>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-loader"></div>
</div>
</div>
<% else %>
<div class="ml-auto text-right grow h-10">
<%= tag.p format_money(account.balance_money), class: "text-sm font-medium text-primary whitespace-nowrap" %>
<%= turbo_frame_tag dom_id(account, :sparkline), src: sparkline_account_path(account), loading: "lazy", data: { controller: "turbo-frame-timeout", turbo_frame_timeout_timeout_value: 10000 } do %>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="w-6 h-px bg-loader"></div>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
</div>

View file

@ -1,8 +0,0 @@
<%= turbo_frame_tag dom_id(@account, :sparkline) do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-5 flex items-center justify-center">
<%= icon("alert-triangle", size: "sm", class: "text-warning") %>
</div>
<p class="font-mono text-right text-xs text-warning">Error</p>
</div>
<% end %>

View file

@ -2,25 +2,21 @@
<% trend = series.trend %>
<%= turbo_frame_tag dom_id(@account, :chart_details) do %>
<% if @account.syncing? %>
<%= render "accounts/chart_loader" %>
<% else %>
<div class="px-4">
<%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: @period.comparison_label } %>
</div>
<div class="px-4">
<%= render partial: "shared/trend_change", locals: { trend: trend, comparison_label: @period.comparison_label } %>
</div>
<div class="h-64 pb-4">
<% if series.any? %>
<div
<div class="h-64 pb-4">
<% if series.any? %>
<div
id="lineChart"
class="w-full h-full"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= series.to_json %>"></div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
</div>
<% end %>
</div>
<% end %>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
</div>
<% end %>
</div>
<% end %>

View file

@ -9,18 +9,14 @@
<div class="flex items-center gap-1">
<%= tag.p account.investment? ? "Total value" : default_value_title, class: "text-sm font-medium text-secondary" %>
<% if !account.syncing? && account.investment? %>
<% if account.investment? %>
<%= render "investments/value_tooltip", balance: account.balance_money, holdings: account.balance_money - account.cash_balance_money, cash: account.cash_balance_money %>
<% end %>
</div>
<div class="flex flex-row gap-2 items-baseline">
<% if account.syncing? %>
<div class="bg-loader rounded-md h-7 w-20"></div>
<% else %>
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
<% if account.currency != Current.family.currency %>
<%= tag.p format_money(account.balance_money.exchange_to(Current.family.currency, fallback_rate: 1)), class: "text-sm font-medium text-secondary" %>
<% end %>
<%= tag.p format_money(account.balance_money), class: "text-primary text-3xl font-medium truncate" %>
<% if account.currency != Current.family.currency %>
<%= tag.p format_money(account.balance_money.exchange_to(Current.family.currency, fallback_rate: 1)), class: "text-sm font-medium text-secondary" %>
<% end %>
</div>
</div>

View file

@ -10,10 +10,16 @@
<div class="flex items-center gap-3 overflow-hidden">
<%= render "accounts/logo", account: account %>
<div class="truncate">
<h2 class="font-medium text-xl truncate"><%= title || account.name %></h2>
<% if subtitle.present? %>
<p class="text-sm text-secondary"><%= subtitle %></p>
<div class="flex items-center gap-2">
<div class="truncate">
<h2 class="font-medium text-xl truncate"><%= title || account.name %></h2>
<% if subtitle.present? %>
<p class="text-sm text-secondary"><%= subtitle %></p>
<% end %>
</div>
<% if account.syncing? %>
<%= render partial: "shared/sync_indicator", locals: { size: "sm" } %>
<% end %>
</div>
</div>

View file

@ -1,13 +1,11 @@
<% cache Current.family.build_cache_key("account_#{@account.id}_sparkline_html") do %>
<%= turbo_frame_tag dom_id(@account, :sparkline) do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-5">
<%= render "shared/sparkline", id: dom_id(@account, :sparkline_chart), series: @sparkline_series %>
</div>
<%= turbo_frame_tag dom_id(@account, :sparkline) do %>
<div class="flex items-center justify-end gap-1">
<div class="w-8 h-5">
<%= render "shared/sparkline", id: dom_id(@account, :sparkline_chart), series: @sparkline_series %>
</div>
<%= tag.p @sparkline_series.trend.percent_formatted,
<%= tag.p @sparkline_series.trend.percent_formatted,
style: "color: #{@sparkline_series.trend.color}",
class: "font-mono text-right text-xs font-medium text-primary" %>
</div>
<% end %>
</div>
<% end %>

View file

@ -20,12 +20,8 @@
<div class="col-span-2 flex justify-end items-center gap-2">
<% cash_weight = account.balance.zero? ? 0 : account.cash_balance / account.balance * 100 %>
<% if account.syncing? %>
<div class="w-16 h-6 bg-loader rounded-full"></div>
<% else %>
<%= render "shared/progress_circle", progress: cash_weight %>
<%= tag.p number_to_percentage(cash_weight, precision: 1) %>
<% end %>
<%= render "shared/progress_circle", progress: cash_weight %>
<%= tag.p number_to_percentage(cash_weight, precision: 1) %>
</div>
<div class="col-span-2 text-right">
@ -33,13 +29,7 @@
</div>
<div class="col-span-2 text-right">
<% if account.syncing? %>
<div class="flex justify-end">
<div class="w-16 h-6 bg-loader rounded-full"></div>
</div>
<% else %>
<%= tag.p format_money account.cash_balance_money %>
<% end %>
<%= tag.p format_money account.cash_balance_money %>
</div>
<div class="col-span-2 text-right">

View file

@ -17,9 +17,7 @@
</div>
<div class="col-span-2 flex justify-end items-center gap-2">
<% if holding.account.syncing? %>
<div class="w-16 h-6 bg-loader rounded-full"></div>
<% elsif holding.weight %>
<% if holding.weight %>
<%= render "shared/progress_circle", progress: holding.weight %>
<%= tag.p number_to_percentage(holding.weight, precision: 1) %>
<% else %>
@ -28,39 +26,21 @@
</div>
<div class="col-span-2 text-right">
<% if holding.account.syncing? %>
<div class="flex justify-end">
<div class="w-16 h-6 bg-loader rounded-full"></div>
</div>
<% else %>
<%= tag.p format_money holding.avg_cost %>
<%= tag.p t(".per_share"), class: "font-normal text-secondary" %>
<% end %>
<%= tag.p format_money holding.avg_cost %>
<%= tag.p t(".per_share"), class: "font-normal text-secondary" %>
</div>
<div class="col-span-2 text-right">
<% if holding.account.syncing? %>
<div class="flex flex-col gap-2 items-end">
<div class="w-16 h-4 bg-loader rounded-full"></div>
<div class="w-16 h-2 bg-loader rounded-full"></div>
</div>
<% if holding.amount_money %>
<%= tag.p format_money holding.amount_money %>
<% else %>
<% if holding.amount_money %>
<%= tag.p format_money holding.amount_money %>
<% else %>
<%= tag.p "--", class: "text-secondary" %>
<% end %>
<%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-secondary" %>
<%= tag.p "--", class: "text-secondary" %>
<% end %>
<%= tag.p t(".shares", qty: number_with_precision(holding.qty, precision: 1)), class: "font-normal text-secondary" %>
</div>
<div class="col-span-2 text-right">
<% if holding.account.syncing? %>
<div class="flex flex-col gap-2 items-end">
<div class="w-16 h-4 bg-loader rounded-full"></div>
<div class="w-16 h-2 bg-loader rounded-full"></div>
</div>
<% elsif holding.trend %>
<% if holding.trend %>
<%= tag.p format_money(holding.trend.value), style: "color: #{holding.trend.color};" %>
<%= tag.p "(#{number_to_percentage(holding.trend.percent, precision: 1)})", style: "color: #{holding.trend.color};" %>
<% else %>

View file

@ -3,26 +3,24 @@
<div class="space-y-4" id="balance-sheet">
<% balance_sheet.classification_groups.each do |classification_group| %>
<div class="bg-container shadow-border-xs rounded-xl space-y-4 p-4">
<h2 class="text-lg font-medium inline-flex items-center gap-1.5">
<span>
<%= classification_group.display_name %>
</span>
<div class="flex items-center gap-2">
<h2 class="text-lg font-medium inline-flex items-center gap-1.5">
<span>
<%= classification_group.name %>
</span>
<% if classification_group.account_groups.any? %>
<span class="text-secondary">&middot;</span>
<% if classification_group.syncing? %>
<div class="flex items-center w-8 h-4 ml-auto">
<div class="bg-loader w-full h-full rounded-md"></div>
</div>
<% else %>
<% if classification_group.account_groups.any? %>
<span class="text-secondary">&middot;</span>
<span class="text-secondary font-medium text-lg"><%= classification_group.total_money.format(precision: 0) %></span>
<% end %>
</h2>
<% if classification_group.syncing? %>
<%= render partial: "shared/sync_indicator", locals: { size: "sm" } %>
<% end %>
</h2>
</div>
<% if classification_group.account_groups.any? %>
<div class="space-y-4">
<div class="flex gap-1">
<% classification_group.account_groups.each do |account_group| %>
@ -30,19 +28,15 @@
<% end %>
</div>
<% if classification_group.syncing? %>
<p class="text-xs text-subdued animate-pulse">Calculating latest balance data...</p>
<% else %>
<div class="flex flex-wrap gap-4">
<% classification_group.account_groups.each do |account_group| %>
<div class="flex items-center gap-2 text-sm">
<div class="h-2.5 w-2.5 rounded-full" style="background-color: <%= account_group.color %>;"></div>
<p class="text-secondary"><%= account_group.name %></p>
<p class="text-primary font-mono"><%= number_to_percentage(account_group.weight, precision: 0) %></p>
</div>
<% end %>
</div>
<% end %>
<div class="flex flex-wrap gap-4">
<% classification_group.account_groups.each do |account_group| %>
<div class="flex items-center gap-2 text-sm">
<div class="h-2.5 w-2.5 rounded-full" style="background-color: <%= account_group.color %>;"></div>
<p class="text-secondary"><%= account_group.name %></p>
<p class="text-primary font-mono"><%= number_to_percentage(account_group.weight, precision: 0) %></p>
</div>
<% end %>
</div>
</div>
<div class="bg-surface rounded-xl p-1 space-y-1 overflow-x-auto">
@ -71,27 +65,15 @@
<p><%= account_group.name %></p>
</div>
<% if account_group.syncing? %>
<div class="flex items-center justify-between text-right gap-6">
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
<div class="bg-loader rounded-md h-4 w-12"></div>
</div>
<div class="w-40 shrink-0 flex justify-end">
<div class="bg-loader rounded-md h-4 w-12"></div>
</div>
<div class="flex items-center justify-between text-right gap-6">
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %>
</div>
<% else %>
<div class="flex items-center justify-between text-right gap-6">
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: account_group.weight, color: account_group.color %>
</div>
<div class="w-40 shrink-0">
<p><%= format_money(account_group.total_money) %></p>
</div>
<div class="w-40 shrink-0">
<p><%= format_money(account_group.total_money) %></p>
</div>
<% end %>
</div>
</summary>
<div>
@ -103,32 +85,20 @@
<%= link_to account.name, account_path(account) %>
</div>
<% if account.syncing? %>
<div class="ml-auto flex items-center text-right gap-6">
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
<div class="bg-loader rounded-md h-4 w-12"></div>
</div>
<div class="w-40 shrink-0 flex justify-end">
<div class="bg-loader rounded-md h-4 w-12"></div>
</div>
</div>
<% else %>
<div class="ml-auto flex items-center text-right gap-6">
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
<%
<div class="ml-auto flex items-center text-right gap-6">
<div class="w-28 shrink-0 flex items-center justify-end gap-2">
<%
# Calculate weight as percentage of classification total
classification_total = classification_group.total_money.amount
account_weight = classification_total.zero? ? 0 : account.converted_balance / classification_total * 100
%>
<%= render "pages/dashboard/group_weight", weight: account_weight, color: account_group.color %>
</div>
<div class="w-40 shrink-0">
<p><%= format_money(account.balance_money) %></p>
</div>
<%= render "pages/dashboard/group_weight", weight: account_weight, color: account_group.color %>
</div>
<% end %>
<div class="w-40 shrink-0">
<p><%= format_money(account.balance_money) %></p>
</div>
</div>
</div>
<% if idx < account_group.accounts.size - 1 %>

View file

@ -5,23 +5,22 @@
<div class="flex justify-between gap-4 px-4">
<div class="space-y-2">
<div class="space-y-2">
<p class="text-sm text-secondary font-medium"><%= t(".title") %></p>
<div class="flex items-center gap-2">
<p class="text-sm text-secondary font-medium"><%= t(".title") %></p>
<% if balance_sheet.syncing? %>
<div class="flex flex-col gap-2">
<div class="bg-loader rounded-md h-7 w-20"></div>
<div class="bg-loader rounded-md h-5 w-32"></div>
</div>
<% else %>
<p class="text-primary -space-x-0.5 text-3xl font-medium">
<%= series.trend.current.format %>
</p>
<% if series.trend.nil? %>
<p class="text-sm text-secondary"><%= t(".data_not_available") %></p>
<% else %>
<%= render partial: "shared/trend_change", locals: { trend: series.trend, comparison_label: period.comparison_label } %>
<% if balance_sheet.syncing? %>
<%= render partial: "shared/sync_indicator", locals: { size: "sm" } %>
<% end %>
</div>
<p class="text-primary -space-x-0.5 text-3xl font-medium">
<%= series.trend.current.format %>
</p>
<% if series.trend.nil? %>
<p class="text-sm text-secondary"><%= t(".data_not_available") %></p>
<% else %>
<%= render partial: "shared/trend_change", locals: { trend: series.trend, comparison_label: period.comparison_label } %>
<% end %>
</div>
</div>
@ -35,21 +34,16 @@
<% end %>
</div>
<% if balance_sheet.syncing? %>
<div class="w-full flex items-center justify-center p-4 h-52">
<div class="bg-loader rounded-md h-full w-full"></div>
</div>
<% else %>
<% if series.any? %>
<div
<% if series.any? %>
<div
id="netWorthChart"
class="w-full flex-1 min-h-52"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= series.to_json %>"></div>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
</div>
<% end %>
<% else %>
<div class="w-full h-full flex items-center justify-center">
<p class="text-secondary text-sm"><%= t(".data_not_available") %></p>
</div>
<% end %>
</div>

View file

@ -0,0 +1,5 @@
<%# locals: (size: "md") %>
<div class="animate-spin text-gray-500">
<%= icon "loader-circle", color: "current", size: size %>
</div>