diff --git a/lib/pulse/dashboard_aggregator.ex b/lib/pulse/dashboard_aggregator.ex index 8f8e57f..3f9ce74 100644 --- a/lib/pulse/dashboard_aggregator.ex +++ b/lib/pulse/dashboard_aggregator.ex @@ -105,12 +105,27 @@ defmodule Pulse.DashboardAggregator do total_value: Float.round(total_value * 1.0, 2), popular_stocks: Enum.take(stock_counts, 10), portfolio_slugs: Enum.map(portfolios, & &1.slug), + recent_portfolios: recent_portfolios(portfolios), community_sectors: community_sectors(portfolios), community_yoc: community_average(portfolios, "yoc"), community_current_yield: community_average(portfolios, "currentYield") } end + # Top 10 portfolios sorted by most-recently-updated. Portfolios with no + # timestamp yet (e.g. workers restored from a pre-`updated_at` DETS row that + # haven't seen a Quantic broadcast since deploy) sort to the bottom but still + # appear — otherwise the whole list goes empty right after deploys. + defp recent_portfolios(portfolios) do + portfolios + |> Enum.sort_by(&sort_key/1, :desc) + |> Enum.take(10) + |> Enum.map(fn p -> %{slug: p.slug, updated_at: p.updated_at} end) + end + + defp sort_key(%{updated_at: %DateTime{} = dt}), do: DateTime.to_unix(dt) + defp sort_key(_), do: 0 + # Simple mean across portfolios that ship a stats block — answers "what's the # typical Quantic user's yield?" rather than a value-weighted aggregate. Nil # if no worker has stats yet (newer Rails ships them, older deploys don't). diff --git a/lib/pulse/portfolio_worker.ex b/lib/pulse/portfolio_worker.ex index cd8428d..82708b2 100644 --- a/lib/pulse/portfolio_worker.ex +++ b/lib/pulse/portfolio_worker.ex @@ -22,7 +22,7 @@ defmodule Pulse.PortfolioWorker do require Logger - defstruct [:slug, holdings: [], metrics: %{}, base_currency: "USD", stats: nil] + defstruct [:slug, :updated_at, holdings: [], metrics: %{}, base_currency: "USD", stats: nil] # Client API @@ -47,6 +47,9 @@ defmodule Pulse.PortfolioWorker do @impl true def init(slug) do Logger.info("Starting portfolio worker for #{slug}") + # updated_at stays nil until the first holdings payload arrives — the + # dashboard's "Recent portfolios" list uses presence of this field to + # decide whether the portfolio counts as active. {:ok, %__MODULE__{slug: slug}} end @@ -64,12 +67,18 @@ defmodule Pulse.PortfolioWorker do # LiveView templates already guard for it. stats = payload["stats"] + # On store restore the payload includes the original updated_at so a server + # restart doesn't make every portfolio look freshly active. Otherwise stamp + # with now. + updated_at = payload["__updated_at"] || DateTime.utc_now() + new_state = %{ state | holdings: holdings, base_currency: base_currency, metrics: compute_metrics(holdings), - stats: stats + stats: stats, + updated_at: updated_at } Pulse.Store.put(state.slug, new_state) diff --git a/lib/pulse/store.ex b/lib/pulse/store.ex index 610e65c..06b5a9a 100644 --- a/lib/pulse/store.ex +++ b/lib/pulse/store.ex @@ -30,7 +30,8 @@ defmodule Pulse.Store do persistable = %{ holdings: Map.get(state, :holdings, []), base_currency: Map.get(state, :base_currency, "USD"), - stats: Map.get(state, :stats) + stats: Map.get(state, :stats), + updated_at: Map.get(state, :updated_at) } GenServer.cast(__MODULE__, {:put, slug, persistable}) @@ -145,7 +146,8 @@ defmodule Pulse.Store do payload = %{ "holdings" => state[:holdings] || [], "base_currency" => state[:base_currency] || "USD", - "stats" => state[:stats] + "stats" => state[:stats], + "__updated_at" => state[:updated_at] } Pulse.PortfolioWorker.update_holdings(slug, payload) diff --git a/lib/pulse_web/live/dashboard_live.ex b/lib/pulse_web/live/dashboard_live.ex index 9d72cdb..1ea8f32 100644 --- a/lib/pulse_web/live/dashboard_live.ex +++ b/lib/pulse_web/live/dashboard_live.ex @@ -13,7 +13,7 @@ defmodule PulseWeb.DashboardLive do "Real-time community dividend portfolio dashboard. " <> "#{stats.portfolio_count} portfolios tracking #{stats.total_holdings} holdings." - top_visited = Pulse.Analytics.top_visited(5) + top_visited = Pulse.Analytics.top_visited(10) {:ok, assign(socket, @@ -26,6 +26,7 @@ defmodule PulseWeb.DashboardLive do show_value: stats.portfolio_count > 5 and stats.total_value > 100_000, popular_stocks: stats.popular_stocks, portfolio_slugs: stats.portfolio_slugs, + recent_portfolios: Map.get(stats, :recent_portfolios, []), community_sectors: Map.get(stats, :community_sectors, []), community_yoc: Map.get(stats, :community_yoc), community_current_yield: Map.get(stats, :community_current_yield), @@ -43,6 +44,7 @@ defmodule PulseWeb.DashboardLive do show_value: stats.portfolio_count > 5 and stats.total_value > 100_000, popular_stocks: stats.popular_stocks, portfolio_slugs: stats.portfolio_slugs, + recent_portfolios: Map.get(stats, :recent_portfolios, []), community_sectors: Map.get(stats, :community_sectors, []), community_yoc: Map.get(stats, :community_yoc), community_current_yield: Map.get(stats, :community_current_yield) @@ -181,9 +183,9 @@ defmodule PulseWeb.DashboardLive do
{gettext("No portfolios shared yet")}
@@ -192,21 +194,24 @@ defmodule PulseWeb.DashboardLive do {gettext("settings")}