diff --git a/lib/pulse_web/live/portfolio_live.ex b/lib/pulse_web/live/portfolio_live.ex
index 9dc897b..537c9fe 100644
--- a/lib/pulse_web/live/portfolio_live.ex
+++ b/lib/pulse_web/live/portfolio_live.ex
@@ -22,6 +22,7 @@ defmodule PulseWeb.PortfolioLive do
meta_url: url(~p"/p/#{slug}"),
slug: slug,
portfolio: portfolio,
+ has_radar: radar_exists?(slug),
not_found: is_nil(portfolio)
)}
end
@@ -38,6 +39,15 @@ defmodule PulseWeb.PortfolioLive do
end
end
+ # Cross-link gating: only show the "View radar" button if a RadarWorker
+ # actually exists for this slug.
+ defp radar_exists?(slug) do
+ case Registry.lookup(Pulse.RadarRegistry, slug) do
+ [{_pid, _}] -> true
+ [] -> false
+ end
+ end
+
@impl true
def render(assigns) do
~H"""
@@ -58,9 +68,17 @@ defmodule PulseWeb.PortfolioLive do
<%!-- Navigation --%>
+ <.link
+ :if={@has_radar}
+ navigate={~p"/r/#{@slug}"}
+ class="btn btn-ghost btn-sm"
+ >
+ <.icon name="hero-eye" class="size-4" /> {gettext("View radar")}
+
{@slug |> String.first() |> String.upcase()}
-
+
{@slug}
{ngettext("1 holding", "%{count} holdings", length(@portfolio.holdings))}
+
+ {format_date(Date.utc_today())}
+
<%!-- Portfolio stats (YoC, current yield, sectors) — only when Rails ships a stats block --%>
@@ -139,7 +161,7 @@ defmodule PulseWeb.PortfolioLive do
:for={{sector, idx} <- Enum.with_index(@portfolio.stats["sectors"])}
class={"h-full " <> allocation_color(idx)}
style={"width: #{sector["percent"]}%"}
- title={"#{sector["sector"]}: #{Float.round(sector["percent"] * 1.0, 1)}%"}
+ title={"#{translate_sector(sector["sector"])}: #{Float.round(sector["percent"] * 1.0, 1)}%"}
>
@@ -149,7 +171,7 @@ defmodule PulseWeb.PortfolioLive do
class="flex items-center gap-1.5"
>
allocation_color(idx)}>
-
{sector["sector"]}
+
{translate_sector(sector["sector"])}
{Float.round(sector["percent"] * 1.0, 1)}%
diff --git a/lib/pulse_web/live/radar_live.ex b/lib/pulse_web/live/radar_live.ex
new file mode 100644
index 0000000..e59485a
--- /dev/null
+++ b/lib/pulse_web/live/radar_live.ex
@@ -0,0 +1,333 @@
+defmodule PulseWeb.RadarLive do
+ @moduledoc """
+ Public radar page at `/r/:slug`. Visually mirrors `PortfolioLive` (same
+ capturable card, share/save-image hooks, branding footer) with radar-
+ specific content: a stocks table with current price, the user's target
+ price, and a color-coded delta. Cross-links to `/p/:slug` when the
+ same slug also has a PortfolioWorker.
+ """
+ use PulseWeb, :live_view
+
+ alias Phoenix.LiveView.JS
+
+ @impl true
+ def mount(%{"slug" => slug}, _session, socket) do
+ if connected?(socket) do
+ Phoenix.PubSub.subscribe(Pulse.PubSub, "radar:#{slug}")
+ Pulse.RadarAnalytics.track_visit(slug)
+ end
+
+ radar = fetch_radar(slug)
+
+ stock_count = if radar, do: length(radar.stocks), else: 0
+ description = "#{slug}'s stock radar on Pulse — #{stock_count} stocks tracked"
+
+ {:ok,
+ assign(socket,
+ page_title: "#{slug}'s Radar",
+ meta_description: description,
+ meta_url: url(~p"/r/#{slug}"),
+ slug: slug,
+ radar: radar,
+ has_portfolio: portfolio_exists?(slug),
+ not_found: is_nil(radar)
+ )}
+ end
+
+ @impl true
+ def handle_info({:radar_updated, radar}, socket) do
+ {:noreply, assign(socket, radar: radar, not_found: false)}
+ end
+
+ defp fetch_radar(slug) do
+ case Registry.lookup(Pulse.RadarRegistry, slug) do
+ [{_pid, _}] -> Pulse.RadarWorker.get_radar(slug)
+ [] -> nil
+ end
+ end
+
+ defp portfolio_exists?(slug) do
+ case Registry.lookup(Pulse.PortfolioRegistry, slug) do
+ [{_pid, _}] -> true
+ [] -> false
+ end
+ end
+
+ # Per-stock status: below_target / above_target / at_target / unknown.
+ defp status_for(price, target) when is_number(price) and is_number(target) do
+ cond do
+ price < target -> :below_target
+ price > target -> :above_target
+ true -> :at_target
+ end
+ end
+
+ defp status_for(_, _), do: :unknown
+
+ # Mini-card text color for the delta number.
+ defp status_text_color(:below_target), do: "text-success"
+ defp status_text_color(:above_target), do: "text-error"
+ defp status_text_color(:at_target), do: "text-base-content/70"
+ defp status_text_color(:unknown), do: "text-base-content/40"
+
+ # Mini-card status dot background.
+ defp status_dot_color(:below_target), do: "bg-success"
+ defp status_dot_color(:above_target), do: "bg-error"
+ defp status_dot_color(:at_target), do: "bg-base-content/40"
+ defp status_dot_color(:unknown), do: "bg-base-300"
+
+ # Subtle card-border tint that reinforces the status colour without
+ # overpowering the grid.
+ defp status_border(:below_target), do: "border-success/30"
+ defp status_border(:above_target), do: "border-error/30"
+ defp status_border(_), do: "border-base-300"
+
+ # Delta as % between current price and target. Negative = below target
+ # (opportunity), positive = above target.
+ defp delta_pct(price, target) when is_number(price) and is_number(target) and target != 0 do
+ (price - target) / target * 100
+ end
+
+ defp delta_pct(_, _), do: nil
+
+ defp delta_label(nil), do: "—"
+
+ defp delta_label(pct) when is_number(pct) do
+ sign = if pct >= 0, do: "+", else: ""
+ "#{sign}#{:erlang.float_to_binary(pct, decimals: 1)}%"
+ end
+
+ defp format_price(nil, _), do: "—"
+
+ defp format_price(price, currency) when is_number(price) do
+ "#{currency || ""} #{:erlang.float_to_binary(price / 1, decimals: 2)}"
+ end
+
+ defp below_target_count(stocks) do
+ Enum.count(stocks, fn s ->
+ status_for(s["price"], s["target_price"]) == :below_target
+ end)
+ end
+
+ # Sort order for the radar grid: most-below-target first (biggest
+ # opportunity), then at-target, then above-target, then stocks without
+ # a target last. Within each band, sort by absolute delta (largest
+ # gap first) so the most striking cards lead.
+ defp sorted_for_grid(stocks) do
+ Enum.sort_by(stocks, fn s ->
+ pct = delta_pct(s["price"], s["target_price"])
+
+ cond do
+ pct == nil -> {3, 0}
+ pct < 0 -> {0, pct}
+ pct == 0 -> {1, 0}
+ pct > 0 -> {2, pct}
+ end
+ end)
+ end
+
+ # CSS-safe DOM id from a ticker. Symbols may contain dots (e.g. REP.MC);
+ # we replace them with hyphens so the resulting `to: "#..."` selector
+ # used by phx-click works without escaping.
+ defp detail_id(symbol), do: "rd-#{String.replace(symbol, ".", "-")}"
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+ <.icon name="hero-eye" class="size-16 mx-auto text-base-content/20 mb-4" />
+
{gettext("Radar not found")}
+
+ {gettext("The radar \"%{slug}\" doesn't exist or hasn't been shared yet.",
+ slug: @slug
+ )}
+
+ <.link navigate="/" class="btn btn-primary btn-sm mt-4">
+ {gettext("Back to dashboard")}
+
+
+
+
+ <%!-- Navigation: matches PortfolioLive ordering exactly. --%>
+
+ <.link
+ :if={@has_portfolio}
+ navigate={~p"/p/#{@slug}"}
+ class="btn btn-ghost btn-sm"
+ >
+ <.icon name="hero-briefcase" class="size-4" />
+ {gettext("View portfolio")}
+
+
+ <.icon name="hero-camera-micro" class="size-4" />
+ {gettext("Save Image")}
+
+
+ <.icon name="hero-share-micro" class="size-4" />
+ {gettext("Share Link")}
+
+ <.link navigate="/" class="btn btn-ghost btn-sm">
+ <.icon name="hero-arrow-left-micro" class="size-4" /> {gettext("Dashboard")}
+
+
+
+ <%!-- Capturable card — id="portfolio-capture" so the SaveImage hook
+ (which targets that id) works without duplication. --%>
+
+ <%!-- Header: avatar + slug + summary. The avatar uses a different
+ accent (eye icon + indigo) so screenshots are distinguishable
+ from portfolios at a glance. --%>
+
+
+ <.icon name="hero-eye" class="size-6" />
+
+
+
+ {@slug}
+ · {gettext("Radar")}
+
+
+ {ngettext("1 stock tracked", "%{count} stocks tracked", length(@radar.stocks))}
+
+
+
+ {format_date(Date.utc_today())}
+
+
+
+ <%!-- Radar summary stats: total + targeted + below-target count. --%>
+
0} class="mb-6 grid grid-cols-3 gap-3">
+
+
+
{gettext("Stocks tracked")}
+
{length(@radar.stocks)}
+
+
+
+
+
{gettext("With target")}
+
+ {Enum.count(@radar.stocks, fn s -> s["target_price"] end)}
+
+
+
+
+
+
{gettext("Below target")}
+
+ {below_target_count(@radar.stocks)}
+
+
+
+
+
+ <%!-- Stock mini-cards: sorted by delta-from-target so the best
+ opportunities (most-below-target → green) are first, then
+ at-target, then above-target, then stocks without a target.
+
+ Denser than the portfolio grid because radars can be 50+
+ stocks. Each card has a small info icon (top-right) that
+ toggles an overlay panel with the price + target. Works on
+ tap (mobile) and click (desktop) — no `title` tooltip so
+ touch devices have a real affordance. --%>
+
+
status_border(status_for(s["price"], s["target_price"]))}
+ >
+
+ <.icon name="hero-information-circle-micro" class="size-3.5" />
+
+
+
+
+ <.stock_logo symbol={s["symbol"]} size={28} />
+
+
{s["symbol"]}
+
+
status_dot_color(status_for(s["price"], s["target_price"]))}>
+
+
status_text_color(status_for(s["price"], s["target_price"]))}>
+ {delta_label(delta_pct(s["price"], s["target_price"]))}
+
+
+
+
+ <%!-- Detail overlay — tap the icon to toggle, tap close to dismiss. --%>
+
+
+ <.icon name="hero-x-mark-micro" class="size-3.5" />
+
+
+ {gettext("Price")}
+
+
+ {format_price(s["price"], s["currency"])}
+
+
+ {gettext("Target")}
+
+
+ {format_price(s["target_price"], s["currency"])}
+
+
+
+
+
+ <%!-- Empty state --%>
+
+ <.icon name="hero-eye-slash" class="size-12 mx-auto text-base-content/20 mb-3" />
+
{gettext("This radar is empty.")}
+
+
+ <%!-- Branding footer for screenshot — same as PortfolioLive. --%>
+
+
+ pulse.quantic.es
+
+
+
+
+ """
+ end
+end
diff --git a/lib/pulse_web/router.ex b/lib/pulse_web/router.ex
index 67d6ff0..2dd9456 100644
--- a/lib/pulse_web/router.ex
+++ b/lib/pulse_web/router.ex
@@ -23,6 +23,7 @@ defmodule PulseWeb.Router do
live_session :default, on_mount: [PulseWeb.Live.Hooks.SetLocale] do
live "/", DashboardLive, :index
live "/p/:slug", PortfolioLive, :show
+ live "/r/:slug", RadarLive, :show
end
end
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 2e53a54..b816b85 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -11,7 +11,7 @@
msgid ""
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:102
+#: lib/pulse_web/live/portfolio_live.ex:121
#, elixir-autogen, elixir-format
msgid "1 holding"
msgid_plural "%{count} holdings"
@@ -23,201 +23,210 @@ msgstr[1] ""
msgid "Actions"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:127
-#: lib/pulse_web/components/layouts.ex:139
+#: lib/pulse_web/components/layouts.ex:126
+#: lib/pulse_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:121
+#: lib/pulse_web/live/dashboard_live.ex:158
#, elixir-autogen, elixir-format
msgid "Available soon"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:54
+#: lib/pulse_web/live/portfolio_live.ex:64
+#: lib/pulse_web/live/radar_live.ex:147
#, elixir-autogen, elixir-format
msgid "Back to dashboard"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:261
+#: lib/pulse_web/live/dashboard_live.ex:593
#, elixir-autogen, elixir-format
msgid "Build Your Portfolio"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:81
+#: lib/pulse_web/components/layouts.ex:80
#, elixir-autogen, elixir-format
msgid "Built with Elixir and Phoenix LiveView"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:65
-#: lib/pulse_web/live/portfolio_live.ex:80
+#: lib/pulse_web/live/portfolio_live.ex:83
+#: lib/pulse_web/live/portfolio_live.ex:99
+#: lib/pulse_web/live/radar_live.ex:167
+#: lib/pulse_web/live/radar_live.ex:183
#, elixir-autogen, elixir-format
msgid "Capturing..."
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:20
+#: lib/pulse_web/live/dashboard_live.ex:22
#, elixir-autogen, elixir-format
msgid "Community Dashboard"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:115
+#: lib/pulse_web/live/dashboard_live.ex:152
#, elixir-autogen, elixir-format
msgid "Community Value"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:73
+#: lib/pulse_web/components/layouts.ex:72
#, elixir-autogen, elixir-format
msgid "Community portfolios"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:81
+#: lib/pulse_web/live/portfolio_live.ex:100
+#: lib/pulse_web/live/radar_live.ex:184
#, elixir-autogen, elixir-format
msgid "Copied!"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:88
+#: lib/pulse_web/live/portfolio_live.ex:107
+#: lib/pulse_web/live/radar_live.ex:191
#, elixir-autogen, elixir-format
msgid "Dashboard"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:280
+#: lib/pulse_web/live/dashboard_live.ex:612
#, elixir-autogen, elixir-format
msgid "Enable sharing in settings to go public"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:184
+#: lib/pulse_web/live/dashboard_live.ex:221
#, elixir-autogen, elixir-format
msgid "Enable sharing in your"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:67
+#: lib/pulse_web/live/portfolio_live.ex:85
+#: lib/pulse_web/live/radar_live.ex:169
#, elixir-autogen, elixir-format
msgid "Failed"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:96
+#: lib/pulse_web/live/dashboard_live.ex:133
#, elixir-autogen, elixir-format
msgid "Holdings"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:255
+#: lib/pulse_web/live/dashboard_live.ex:587
#, elixir-autogen, elixir-format
msgid "How It Works"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:178
-#, elixir-autogen, elixir-format
-msgid "Latest Portfolios"
-msgstr ""
-
-#: lib/pulse_web/live/dashboard_live.ex:287
+#: lib/pulse_web/live/dashboard_live.ex:619
#, elixir-autogen, elixir-format
msgid "Live Updates"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:182
+#: lib/pulse_web/live/dashboard_live.ex:219
#, elixir-autogen, elixir-format
msgid "No portfolios shared yet"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:139
+#: lib/pulse_web/live/dashboard_live.ex:176
#, elixir-autogen, elixir-format
msgid "No stocks yet"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:219
+#: lib/pulse_web/live/dashboard_live.ex:259
+#: lib/pulse_web/live/dashboard_live.ex:507
#, elixir-autogen, elixir-format
msgid "No visits yet"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:77
+#: lib/pulse_web/components/layouts.ex:76
#, elixir-autogen, elixir-format
msgid "Part of"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:135
+#: lib/pulse_web/live/dashboard_live.ex:172
#, elixir-autogen, elixir-format
msgid "Popular Stocks"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:289
+#: lib/pulse_web/live/dashboard_live.ex:621
#, elixir-autogen, elixir-format
msgid "Portfolio changes stream in real-time"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:47
+#: lib/pulse_web/live/portfolio_live.ex:57
#, elixir-autogen, elixir-format
msgid "Portfolio not found"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:80
+#: lib/pulse_web/live/dashboard_live.ex:104
+#: lib/pulse_web/live/dashboard_live.ex:117
#, elixir-autogen, elixir-format
msgid "Portfolios"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:66
+#: lib/pulse_web/live/dashboard_live.ex:96
#, elixir-autogen, elixir-format
msgid "Real-time community dividend portfolio dashboard"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:64
-#: lib/pulse_web/live/portfolio_live.ex:71
+#: lib/pulse_web/live/portfolio_live.ex:82
+#: lib/pulse_web/live/portfolio_live.ex:89
+#: lib/pulse_web/live/radar_live.ex:166
+#: lib/pulse_web/live/radar_live.ex:173
#, elixir-autogen, elixir-format
msgid "Save Image"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:66
+#: lib/pulse_web/live/portfolio_live.ex:84
+#: lib/pulse_web/live/radar_live.ex:168
#, elixir-autogen, elixir-format
msgid "Saved!"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:278
+#: lib/pulse_web/live/dashboard_live.ex:610
#, elixir-autogen, elixir-format
msgid "Share It"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:79
-#: lib/pulse_web/live/portfolio_live.ex:85
+#: lib/pulse_web/live/portfolio_live.ex:98
+#: lib/pulse_web/live/portfolio_live.ex:104
+#: lib/pulse_web/live/radar_live.ex:182
+#: lib/pulse_web/live/radar_live.ex:188
#, elixir-autogen, elixir-format
msgid "Share Link"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:141
+#: lib/pulse_web/live/dashboard_live.ex:178
#, elixir-autogen, elixir-format
msgid "Share your portfolio from"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:134
+#: lib/pulse_web/components/layouts.ex:133
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:49
+#: lib/pulse_web/live/portfolio_live.ex:59
#, elixir-autogen, elixir-format
msgid "The portfolio \"%{slug}\" doesn't exist or hasn't been shared yet."
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:158
+#: lib/pulse_web/live/portfolio_live.ex:235
#, elixir-autogen, elixir-format
msgid "This portfolio has no holdings yet."
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:263
+#: lib/pulse_web/live/dashboard_live.ex:595
#, elixir-autogen, elixir-format
msgid "Track your holdings on"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:215
+#: lib/pulse_web/live/dashboard_live.ex:255
+#: lib/pulse_web/live/dashboard_live.ex:503
#, elixir-autogen, elixir-format
msgid "Trending This Week"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:221
+#: lib/pulse_web/live/dashboard_live.ex:261
#, elixir-autogen, elixir-format
msgid "Visit a portfolio to see it here"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:122
+#: lib/pulse_web/components/layouts.ex:121
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
@@ -227,7 +236,351 @@ msgstr ""
msgid "close"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:186
+#: lib/pulse_web/live/dashboard_live.ex:223
#, elixir-autogen, elixir-format
msgid "settings"
msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:694
+#, elixir-autogen, elixir-format
+msgid "%{count}d ago"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:693
+#, elixir-autogen, elixir-format
+msgid "%{count}h ago"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:692
+#, elixir-autogen, elixir-format
+msgid "%{count}m ago"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:695
+#, elixir-autogen, elixir-format
+msgid "%{count}w ago"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:505
+#, elixir-autogen, elixir-format
+msgid "%{month} %{day}, %{year}"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:211
+#, elixir-autogen, elixir-format
+msgid "1 stock tracked"
+msgid_plural "%{count} stocks tracked"
+msgstr[0] ""
+msgstr[1] ""
+
+#: lib/pulse_web/components/core_components.ex:517
+#, elixir-autogen, elixir-format
+msgid "Apr"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:521
+#, elixir-autogen, elixir-format
+msgid "Aug"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:330
+#, elixir-autogen, elixir-format
+msgid "Average Current Yield"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:311
+#, elixir-autogen, elixir-format
+msgid "Average Yield on Cost"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:418
+#: lib/pulse_web/live/dashboard_live.ex:546
+#, elixir-autogen, elixir-format
+msgid "Below Community Target"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:237
+#, elixir-autogen, elixir-format
+msgid "Below target"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:294
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:344
+#, elixir-autogen, elixir-format
+msgid "Community Sectors"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:435
+#, elixir-autogen, elixir-format
+msgid "Community Watchlist"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:144
+#, elixir-autogen, elixir-format
+msgid "Current Yield"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:525
+#, elixir-autogen, elixir-format
+msgid "Dec"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:515
+#, elixir-autogen, elixir-format
+msgid "Feb"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:514
+#, elixir-autogen, elixir-format
+msgid "Jan"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:520
+#, elixir-autogen, elixir-format
+msgid "Jul"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:519
+#, elixir-autogen, elixir-format
+msgid "Jun"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:516
+#, elixir-autogen, elixir-format
+msgid "Mar"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:518
+#, elixir-autogen, elixir-format
+msgid "May"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:575
+#, elixir-autogen, elixir-format
+msgid "Needs 3+ users targeting the same stock with current price below the average target"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:572
+#, elixir-autogen, elixir-format
+msgid "No consensus buy signals yet"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:472
+#, elixir-autogen, elixir-format
+msgid "No radars shared yet"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:439
+#, elixir-autogen, elixir-format
+msgid "No stocks tracked yet"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:524
+#, elixir-autogen, elixir-format
+msgid "Nov"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:523
+#, elixir-autogen, elixir-format
+msgid "Oct"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:299
+#, elixir-autogen, elixir-format
+msgid "Price"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:208
+#, elixir-autogen, elixir-format
+msgid "Radar"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:140
+#, elixir-autogen, elixir-format
+msgid "Radar not found"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:373
+#, elixir-autogen, elixir-format
+msgid "Radars"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:215
+#: lib/pulse_web/live/dashboard_live.ex:468
+#, elixir-autogen, elixir-format
+msgid "Recently Updated"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:158
+#, elixir-autogen, elixir-format
+msgid "Sectors"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:522
+#, elixir-autogen, elixir-format
+msgid "Sep"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:386
+#, elixir-autogen, elixir-format
+msgid "Shared Radars"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:266
+#, elixir-autogen, elixir-format
+msgid "Show %{symbol} details"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:402
+#, elixir-autogen, elixir-format
+msgid "Stocks Tracked"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:223
+#, elixir-autogen, elixir-format
+msgid "Stocks tracked"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:308
+#, elixir-autogen, elixir-format
+msgid "Target"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:142
+#, elixir-autogen, elixir-format
+msgid "The radar \"%{slug}\" doesn't exist or hasn't been shared yet."
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:320
+#, elixir-autogen, elixir-format
+msgid "This radar is empty."
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:160
+#, elixir-autogen, elixir-format
+msgid "View portfolio"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:76
+#, elixir-autogen, elixir-format
+msgid "View radar"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:509
+#, elixir-autogen, elixir-format
+msgid "Visit a radar to see it here"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:229
+#, elixir-autogen, elixir-format
+msgid "With target"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:138
+#, elixir-autogen, elixir-format
+msgid "Yield on Cost"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:548
+#, elixir-autogen, elixir-format
+msgid "consensus buy signal"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:691
+#, elixir-autogen, elixir-format
+msgid "just now"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:536
+#, elixir-autogen, elixir-format
+msgid "Basic Materials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:537
+#, elixir-autogen, elixir-format
+msgid "Communication Services"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:538
+#, elixir-autogen, elixir-format
+msgid "Consumer Cyclical"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:539
+#, elixir-autogen, elixir-format
+msgid "Consumer Defensive"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:540
+#, elixir-autogen, elixir-format
+msgid "Consumer Discretionary"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:541
+#, elixir-autogen, elixir-format
+msgid "Consumer Staples"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:542
+#, elixir-autogen, elixir-format
+msgid "Energy"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:543
+#, elixir-autogen, elixir-format
+msgid "Financial Services"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:544
+#, elixir-autogen, elixir-format
+msgid "Financials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:545
+#, elixir-autogen, elixir-format
+msgid "Health Care"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:546
+#, elixir-autogen, elixir-format
+msgid "Healthcare"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:547
+#, elixir-autogen, elixir-format
+msgid "Industrials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:548
+#, elixir-autogen, elixir-format
+msgid "Information Technology"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:549
+#, elixir-autogen, elixir-format
+msgid "Materials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:550
+#, elixir-autogen, elixir-format
+msgid "Real Estate"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:551
+#, elixir-autogen, elixir-format
+msgid "Technology"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:553
+#, elixir-autogen, elixir-format
+msgid "Unknown"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:552
+#, elixir-autogen, elixir-format
+msgid "Utilities"
+msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index a11eaf2..9a3e031 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -11,7 +11,7 @@ msgstr ""
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: lib/pulse_web/live/portfolio_live.ex:102
+#: lib/pulse_web/live/portfolio_live.ex:121
#, elixir-autogen, elixir-format
msgid "1 holding"
msgid_plural "%{count} holdings"
@@ -23,201 +23,210 @@ msgstr[1] ""
msgid "Actions"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:127
-#: lib/pulse_web/components/layouts.ex:139
+#: lib/pulse_web/components/layouts.ex:126
+#: lib/pulse_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:121
+#: lib/pulse_web/live/dashboard_live.ex:158
#, elixir-autogen, elixir-format
msgid "Available soon"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:54
+#: lib/pulse_web/live/portfolio_live.ex:64
+#: lib/pulse_web/live/radar_live.ex:147
#, elixir-autogen, elixir-format
msgid "Back to dashboard"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:261
+#: lib/pulse_web/live/dashboard_live.ex:593
#, elixir-autogen, elixir-format
msgid "Build Your Portfolio"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:81
+#: lib/pulse_web/components/layouts.ex:80
#, elixir-autogen, elixir-format
msgid "Built with Elixir and Phoenix LiveView"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:65
-#: lib/pulse_web/live/portfolio_live.ex:80
+#: lib/pulse_web/live/portfolio_live.ex:83
+#: lib/pulse_web/live/portfolio_live.ex:99
+#: lib/pulse_web/live/radar_live.ex:167
+#: lib/pulse_web/live/radar_live.ex:183
#, elixir-autogen, elixir-format
msgid "Capturing..."
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:20
+#: lib/pulse_web/live/dashboard_live.ex:22
#, elixir-autogen, elixir-format
msgid "Community Dashboard"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:115
+#: lib/pulse_web/live/dashboard_live.ex:152
#, elixir-autogen, elixir-format
msgid "Community Value"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:73
+#: lib/pulse_web/components/layouts.ex:72
#, elixir-autogen, elixir-format
msgid "Community portfolios"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:81
+#: lib/pulse_web/live/portfolio_live.ex:100
+#: lib/pulse_web/live/radar_live.ex:184
#, elixir-autogen, elixir-format
msgid "Copied!"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:88
+#: lib/pulse_web/live/portfolio_live.ex:107
+#: lib/pulse_web/live/radar_live.ex:191
#, elixir-autogen, elixir-format
msgid "Dashboard"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:280
+#: lib/pulse_web/live/dashboard_live.ex:612
#, elixir-autogen, elixir-format
msgid "Enable sharing in settings to go public"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:184
+#: lib/pulse_web/live/dashboard_live.ex:221
#, elixir-autogen, elixir-format
msgid "Enable sharing in your"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:67
+#: lib/pulse_web/live/portfolio_live.ex:85
+#: lib/pulse_web/live/radar_live.ex:169
#, elixir-autogen, elixir-format
msgid "Failed"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:96
+#: lib/pulse_web/live/dashboard_live.ex:133
#, elixir-autogen, elixir-format
msgid "Holdings"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:255
+#: lib/pulse_web/live/dashboard_live.ex:587
#, elixir-autogen, elixir-format
msgid "How It Works"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:178
-#, elixir-autogen, elixir-format
-msgid "Latest Portfolios"
-msgstr ""
-
-#: lib/pulse_web/live/dashboard_live.ex:287
+#: lib/pulse_web/live/dashboard_live.ex:619
#, elixir-autogen, elixir-format
msgid "Live Updates"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:182
+#: lib/pulse_web/live/dashboard_live.ex:219
#, elixir-autogen, elixir-format
msgid "No portfolios shared yet"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:139
+#: lib/pulse_web/live/dashboard_live.ex:176
#, elixir-autogen, elixir-format
msgid "No stocks yet"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:219
+#: lib/pulse_web/live/dashboard_live.ex:259
+#: lib/pulse_web/live/dashboard_live.ex:507
#, elixir-autogen, elixir-format
msgid "No visits yet"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:77
+#: lib/pulse_web/components/layouts.ex:76
#, elixir-autogen, elixir-format
msgid "Part of"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:135
+#: lib/pulse_web/live/dashboard_live.ex:172
#, elixir-autogen, elixir-format
msgid "Popular Stocks"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:289
+#: lib/pulse_web/live/dashboard_live.ex:621
#, elixir-autogen, elixir-format
msgid "Portfolio changes stream in real-time"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:47
+#: lib/pulse_web/live/portfolio_live.ex:57
#, elixir-autogen, elixir-format
msgid "Portfolio not found"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:80
+#: lib/pulse_web/live/dashboard_live.ex:104
+#: lib/pulse_web/live/dashboard_live.ex:117
#, elixir-autogen, elixir-format
msgid "Portfolios"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:66
+#: lib/pulse_web/live/dashboard_live.ex:96
#, elixir-autogen, elixir-format
msgid "Real-time community dividend portfolio dashboard"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:64
-#: lib/pulse_web/live/portfolio_live.ex:71
+#: lib/pulse_web/live/portfolio_live.ex:82
+#: lib/pulse_web/live/portfolio_live.ex:89
+#: lib/pulse_web/live/radar_live.ex:166
+#: lib/pulse_web/live/radar_live.ex:173
#, elixir-autogen, elixir-format
msgid "Save Image"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:66
+#: lib/pulse_web/live/portfolio_live.ex:84
+#: lib/pulse_web/live/radar_live.ex:168
#, elixir-autogen, elixir-format
msgid "Saved!"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:278
+#: lib/pulse_web/live/dashboard_live.ex:610
#, elixir-autogen, elixir-format
msgid "Share It"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:79
-#: lib/pulse_web/live/portfolio_live.ex:85
+#: lib/pulse_web/live/portfolio_live.ex:98
+#: lib/pulse_web/live/portfolio_live.ex:104
+#: lib/pulse_web/live/radar_live.ex:182
+#: lib/pulse_web/live/radar_live.ex:188
#, elixir-autogen, elixir-format
msgid "Share Link"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:141
+#: lib/pulse_web/live/dashboard_live.ex:178
#, elixir-autogen, elixir-format
msgid "Share your portfolio from"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:134
+#: lib/pulse_web/components/layouts.ex:133
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:49
+#: lib/pulse_web/live/portfolio_live.ex:59
#, elixir-autogen, elixir-format
msgid "The portfolio \"%{slug}\" doesn't exist or hasn't been shared yet."
msgstr ""
-#: lib/pulse_web/live/portfolio_live.ex:158
+#: lib/pulse_web/live/portfolio_live.ex:235
#, elixir-autogen, elixir-format
msgid "This portfolio has no holdings yet."
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:263
+#: lib/pulse_web/live/dashboard_live.ex:595
#, elixir-autogen, elixir-format
msgid "Track your holdings on"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:215
+#: lib/pulse_web/live/dashboard_live.ex:255
+#: lib/pulse_web/live/dashboard_live.ex:503
#, elixir-autogen, elixir-format
msgid "Trending This Week"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:221
+#: lib/pulse_web/live/dashboard_live.ex:261
#, elixir-autogen, elixir-format
msgid "Visit a portfolio to see it here"
msgstr ""
-#: lib/pulse_web/components/layouts.ex:122
+#: lib/pulse_web/components/layouts.ex:121
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr ""
@@ -227,7 +236,351 @@ msgstr ""
msgid "close"
msgstr ""
-#: lib/pulse_web/live/dashboard_live.ex:186
+#: lib/pulse_web/live/dashboard_live.ex:223
#, elixir-autogen, elixir-format
msgid "settings"
msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:694
+#, elixir-autogen, elixir-format
+msgid "%{count}d ago"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:693
+#, elixir-autogen, elixir-format
+msgid "%{count}h ago"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:692
+#, elixir-autogen, elixir-format
+msgid "%{count}m ago"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:695
+#, elixir-autogen, elixir-format
+msgid "%{count}w ago"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:505
+#, elixir-autogen, elixir-format
+msgid "%{month} %{day}, %{year}"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:211
+#, elixir-autogen, elixir-format
+msgid "1 stock tracked"
+msgid_plural "%{count} stocks tracked"
+msgstr[0] ""
+msgstr[1] ""
+
+#: lib/pulse_web/components/core_components.ex:517
+#, elixir-autogen, elixir-format
+msgid "Apr"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:521
+#, elixir-autogen, elixir-format
+msgid "Aug"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:330
+#, elixir-autogen, elixir-format
+msgid "Average Current Yield"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:311
+#, elixir-autogen, elixir-format
+msgid "Average Yield on Cost"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:418
+#: lib/pulse_web/live/dashboard_live.ex:546
+#, elixir-autogen, elixir-format
+msgid "Below Community Target"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:237
+#, elixir-autogen, elixir-format
+msgid "Below target"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:294
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Close"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:344
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Community Sectors"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:435
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Community Watchlist"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:144
+#, elixir-autogen, elixir-format
+msgid "Current Yield"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:525
+#, elixir-autogen, elixir-format
+msgid "Dec"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:515
+#, elixir-autogen, elixir-format
+msgid "Feb"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:514
+#, elixir-autogen, elixir-format
+msgid "Jan"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:520
+#, elixir-autogen, elixir-format
+msgid "Jul"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:519
+#, elixir-autogen, elixir-format
+msgid "Jun"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:516
+#, elixir-autogen, elixir-format
+msgid "Mar"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:518
+#, elixir-autogen, elixir-format
+msgid "May"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:575
+#, elixir-autogen, elixir-format
+msgid "Needs 3+ users targeting the same stock with current price below the average target"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:572
+#, elixir-autogen, elixir-format
+msgid "No consensus buy signals yet"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:472
+#, elixir-autogen, elixir-format
+msgid "No radars shared yet"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:439
+#, elixir-autogen, elixir-format, fuzzy
+msgid "No stocks tracked yet"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:524
+#, elixir-autogen, elixir-format
+msgid "Nov"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:523
+#, elixir-autogen, elixir-format
+msgid "Oct"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:299
+#, elixir-autogen, elixir-format
+msgid "Price"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:208
+#, elixir-autogen, elixir-format
+msgid "Radar"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:140
+#, elixir-autogen, elixir-format
+msgid "Radar not found"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:373
+#, elixir-autogen, elixir-format
+msgid "Radars"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:215
+#: lib/pulse_web/live/dashboard_live.ex:468
+#, elixir-autogen, elixir-format
+msgid "Recently Updated"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:158
+#, elixir-autogen, elixir-format
+msgid "Sectors"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:522
+#, elixir-autogen, elixir-format
+msgid "Sep"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:386
+#, elixir-autogen, elixir-format
+msgid "Shared Radars"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:266
+#, elixir-autogen, elixir-format
+msgid "Show %{symbol} details"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:402
+#, elixir-autogen, elixir-format
+msgid "Stocks Tracked"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:223
+#, elixir-autogen, elixir-format
+msgid "Stocks tracked"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:308
+#, elixir-autogen, elixir-format
+msgid "Target"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:142
+#, elixir-autogen, elixir-format, fuzzy
+msgid "The radar \"%{slug}\" doesn't exist or hasn't been shared yet."
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:320
+#, elixir-autogen, elixir-format
+msgid "This radar is empty."
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:160
+#, elixir-autogen, elixir-format
+msgid "View portfolio"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:76
+#, elixir-autogen, elixir-format
+msgid "View radar"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:509
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Visit a radar to see it here"
+msgstr ""
+
+#: lib/pulse_web/live/radar_live.ex:229
+#, elixir-autogen, elixir-format
+msgid "With target"
+msgstr ""
+
+#: lib/pulse_web/live/portfolio_live.ex:138
+#, elixir-autogen, elixir-format
+msgid "Yield on Cost"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:548
+#, elixir-autogen, elixir-format
+msgid "consensus buy signal"
+msgstr ""
+
+#: lib/pulse_web/live/dashboard_live.ex:691
+#, elixir-autogen, elixir-format
+msgid "just now"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:536
+#, elixir-autogen, elixir-format
+msgid "Basic Materials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:537
+#, elixir-autogen, elixir-format
+msgid "Communication Services"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:538
+#, elixir-autogen, elixir-format
+msgid "Consumer Cyclical"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:539
+#, elixir-autogen, elixir-format
+msgid "Consumer Defensive"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:540
+#, elixir-autogen, elixir-format
+msgid "Consumer Discretionary"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:541
+#, elixir-autogen, elixir-format
+msgid "Consumer Staples"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:542
+#, elixir-autogen, elixir-format
+msgid "Energy"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:543
+#, elixir-autogen, elixir-format
+msgid "Financial Services"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:544
+#, elixir-autogen, elixir-format
+msgid "Financials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:545
+#, elixir-autogen, elixir-format
+msgid "Health Care"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:546
+#, elixir-autogen, elixir-format
+msgid "Healthcare"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:547
+#, elixir-autogen, elixir-format
+msgid "Industrials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:548
+#, elixir-autogen, elixir-format
+msgid "Information Technology"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:549
+#, elixir-autogen, elixir-format
+msgid "Materials"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:550
+#, elixir-autogen, elixir-format
+msgid "Real Estate"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:551
+#, elixir-autogen, elixir-format
+msgid "Technology"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:553
+#, elixir-autogen, elixir-format
+msgid "Unknown"
+msgstr ""
+
+#: lib/pulse_web/components/core_components.ex:552
+#, elixir-autogen, elixir-format
+msgid "Utilities"
+msgstr ""
diff --git a/priv/gettext/es/LC_MESSAGES/default.po b/priv/gettext/es/LC_MESSAGES/default.po
index 0379d6a..e21056e 100644
--- a/priv/gettext/es/LC_MESSAGES/default.po
+++ b/priv/gettext/es/LC_MESSAGES/default.po
@@ -11,7 +11,7 @@ msgstr ""
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: lib/pulse_web/live/portfolio_live.ex:102
+#: lib/pulse_web/live/portfolio_live.ex:121
#, elixir-autogen, elixir-format
msgid "1 holding"
msgid_plural "%{count} holdings"
@@ -23,201 +23,210 @@ msgstr[1] "%{count} posiciones"
msgid "Actions"
msgstr "Acciones"
-#: lib/pulse_web/components/layouts.ex:127
-#: lib/pulse_web/components/layouts.ex:139
+#: lib/pulse_web/components/layouts.ex:126
+#: lib/pulse_web/components/layouts.ex:138
#, elixir-autogen, elixir-format
msgid "Attempting to reconnect"
msgstr "Intentando reconectar"
-#: lib/pulse_web/live/dashboard_live.ex:121
+#: lib/pulse_web/live/dashboard_live.ex:158
#, elixir-autogen, elixir-format
msgid "Available soon"
msgstr "Disponible pronto"
-#: lib/pulse_web/live/portfolio_live.ex:54
+#: lib/pulse_web/live/portfolio_live.ex:64
+#: lib/pulse_web/live/radar_live.ex:147
#, elixir-autogen, elixir-format
msgid "Back to dashboard"
msgstr "Volver al panel"
-#: lib/pulse_web/live/dashboard_live.ex:261
+#: lib/pulse_web/live/dashboard_live.ex:593
#, elixir-autogen, elixir-format
msgid "Build Your Portfolio"
msgstr "Construye tu Cartera"
-#: lib/pulse_web/components/layouts.ex:81
+#: lib/pulse_web/components/layouts.ex:80
#, elixir-autogen, elixir-format
msgid "Built with Elixir and Phoenix LiveView"
msgstr "Hecho con Elixir y Phoenix LiveView"
-#: lib/pulse_web/live/portfolio_live.ex:65
-#: lib/pulse_web/live/portfolio_live.ex:80
+#: lib/pulse_web/live/portfolio_live.ex:83
+#: lib/pulse_web/live/portfolio_live.ex:99
+#: lib/pulse_web/live/radar_live.ex:167
+#: lib/pulse_web/live/radar_live.ex:183
#, elixir-autogen, elixir-format
msgid "Capturing..."
msgstr "Capturando..."
-#: lib/pulse_web/live/dashboard_live.ex:20
+#: lib/pulse_web/live/dashboard_live.ex:22
#, elixir-autogen, elixir-format
msgid "Community Dashboard"
msgstr "Panel de la Comunidad"
-#: lib/pulse_web/live/dashboard_live.ex:115
+#: lib/pulse_web/live/dashboard_live.ex:152
#, elixir-autogen, elixir-format
msgid "Community Value"
msgstr "Valor de la Comunidad"
-#: lib/pulse_web/components/layouts.ex:73
+#: lib/pulse_web/components/layouts.ex:72
#, elixir-autogen, elixir-format
msgid "Community portfolios"
msgstr "Carteras de la comunidad"
-#: lib/pulse_web/live/portfolio_live.ex:81
+#: lib/pulse_web/live/portfolio_live.ex:100
+#: lib/pulse_web/live/radar_live.ex:184
#, elixir-autogen, elixir-format
msgid "Copied!"
msgstr "¡Copiado!"
-#: lib/pulse_web/live/portfolio_live.ex:88
+#: lib/pulse_web/live/portfolio_live.ex:107
+#: lib/pulse_web/live/radar_live.ex:191
#, elixir-autogen, elixir-format
msgid "Dashboard"
msgstr "Panel"
-#: lib/pulse_web/live/dashboard_live.ex:280
+#: lib/pulse_web/live/dashboard_live.ex:612
#, elixir-autogen, elixir-format
msgid "Enable sharing in settings to go public"
msgstr "Activa la opción de compartir en ajustes para hacerla pública"
-#: lib/pulse_web/live/dashboard_live.ex:184
+#: lib/pulse_web/live/dashboard_live.ex:221
#, elixir-autogen, elixir-format
msgid "Enable sharing in your"
msgstr "Activa compartir en tu"
-#: lib/pulse_web/live/portfolio_live.ex:67
+#: lib/pulse_web/live/portfolio_live.ex:85
+#: lib/pulse_web/live/radar_live.ex:169
#, elixir-autogen, elixir-format
msgid "Failed"
msgstr "Error"
-#: lib/pulse_web/live/dashboard_live.ex:96
+#: lib/pulse_web/live/dashboard_live.ex:133
#, elixir-autogen, elixir-format
msgid "Holdings"
msgstr "Posiciones"
-#: lib/pulse_web/live/dashboard_live.ex:255
+#: lib/pulse_web/live/dashboard_live.ex:587
#, elixir-autogen, elixir-format
msgid "How It Works"
msgstr "Cómo Funciona"
-#: lib/pulse_web/live/dashboard_live.ex:178
-#, elixir-autogen, elixir-format
-msgid "Latest Portfolios"
-msgstr "Últimas Carteras"
-
-#: lib/pulse_web/live/dashboard_live.ex:287
+#: lib/pulse_web/live/dashboard_live.ex:619
#, elixir-autogen, elixir-format
msgid "Live Updates"
msgstr "Actualizaciones en Vivo"
-#: lib/pulse_web/live/dashboard_live.ex:182
+#: lib/pulse_web/live/dashboard_live.ex:219
#, elixir-autogen, elixir-format
msgid "No portfolios shared yet"
msgstr "Aún no hay carteras compartidas"
-#: lib/pulse_web/live/dashboard_live.ex:139
+#: lib/pulse_web/live/dashboard_live.ex:176
#, elixir-autogen, elixir-format
msgid "No stocks yet"
msgstr "Aún no hay acciones"
-#: lib/pulse_web/live/dashboard_live.ex:219
+#: lib/pulse_web/live/dashboard_live.ex:259
+#: lib/pulse_web/live/dashboard_live.ex:507
#, elixir-autogen, elixir-format
msgid "No visits yet"
msgstr "Aún no hay visitas"
-#: lib/pulse_web/components/layouts.ex:77
+#: lib/pulse_web/components/layouts.ex:76
#, elixir-autogen, elixir-format
msgid "Part of"
msgstr "Parte de"
-#: lib/pulse_web/live/dashboard_live.ex:135
+#: lib/pulse_web/live/dashboard_live.ex:172
#, elixir-autogen, elixir-format
msgid "Popular Stocks"
msgstr "Acciones Populares"
-#: lib/pulse_web/live/dashboard_live.ex:289
+#: lib/pulse_web/live/dashboard_live.ex:621
#, elixir-autogen, elixir-format
msgid "Portfolio changes stream in real-time"
msgstr "Los cambios en la cartera se transmiten en tiempo real"
-#: lib/pulse_web/live/portfolio_live.ex:47
+#: lib/pulse_web/live/portfolio_live.ex:57
#, elixir-autogen, elixir-format
msgid "Portfolio not found"
msgstr "Cartera no encontrada"
-#: lib/pulse_web/live/dashboard_live.ex:80
+#: lib/pulse_web/live/dashboard_live.ex:104
+#: lib/pulse_web/live/dashboard_live.ex:117
#, elixir-autogen, elixir-format
msgid "Portfolios"
msgstr "Carteras"
-#: lib/pulse_web/live/dashboard_live.ex:66
+#: lib/pulse_web/live/dashboard_live.ex:96
#, elixir-autogen, elixir-format
msgid "Real-time community dividend portfolio dashboard"
msgstr "Panel de carteras de dividendos de la comunidad en tiempo real"
-#: lib/pulse_web/live/portfolio_live.ex:64
-#: lib/pulse_web/live/portfolio_live.ex:71
+#: lib/pulse_web/live/portfolio_live.ex:82
+#: lib/pulse_web/live/portfolio_live.ex:89
+#: lib/pulse_web/live/radar_live.ex:166
+#: lib/pulse_web/live/radar_live.ex:173
#, elixir-autogen, elixir-format
msgid "Save Image"
msgstr "Guardar Imagen"
-#: lib/pulse_web/live/portfolio_live.ex:66
+#: lib/pulse_web/live/portfolio_live.ex:84
+#: lib/pulse_web/live/radar_live.ex:168
#, elixir-autogen, elixir-format
msgid "Saved!"
msgstr "¡Guardado!"
-#: lib/pulse_web/live/dashboard_live.ex:278
+#: lib/pulse_web/live/dashboard_live.ex:610
#, elixir-autogen, elixir-format
msgid "Share It"
msgstr "Compártela"
-#: lib/pulse_web/live/portfolio_live.ex:79
-#: lib/pulse_web/live/portfolio_live.ex:85
+#: lib/pulse_web/live/portfolio_live.ex:98
+#: lib/pulse_web/live/portfolio_live.ex:104
+#: lib/pulse_web/live/radar_live.ex:182
+#: lib/pulse_web/live/radar_live.ex:188
#, elixir-autogen, elixir-format
msgid "Share Link"
msgstr "Compartir Enlace"
-#: lib/pulse_web/live/dashboard_live.ex:141
+#: lib/pulse_web/live/dashboard_live.ex:178
#, elixir-autogen, elixir-format
msgid "Share your portfolio from"
msgstr "Comparte tu cartera desde"
-#: lib/pulse_web/components/layouts.ex:134
+#: lib/pulse_web/components/layouts.ex:133
#, elixir-autogen, elixir-format
msgid "Something went wrong!"
msgstr "¡Algo salió mal!"
-#: lib/pulse_web/live/portfolio_live.ex:49
+#: lib/pulse_web/live/portfolio_live.ex:59
#, elixir-autogen, elixir-format
msgid "The portfolio \"%{slug}\" doesn't exist or hasn't been shared yet."
msgstr "La cartera \"%{slug}\" no existe o aún no ha sido compartida."
-#: lib/pulse_web/live/portfolio_live.ex:158
+#: lib/pulse_web/live/portfolio_live.ex:235
#, elixir-autogen, elixir-format
msgid "This portfolio has no holdings yet."
msgstr "Esta cartera aún no tiene posiciones."
-#: lib/pulse_web/live/dashboard_live.ex:263
+#: lib/pulse_web/live/dashboard_live.ex:595
#, elixir-autogen, elixir-format
msgid "Track your holdings on"
msgstr "Gestiona tus posiciones en"
-#: lib/pulse_web/live/dashboard_live.ex:215
+#: lib/pulse_web/live/dashboard_live.ex:255
+#: lib/pulse_web/live/dashboard_live.ex:503
#, elixir-autogen, elixir-format
msgid "Trending This Week"
msgstr "Tendencias de la Semana"
-#: lib/pulse_web/live/dashboard_live.ex:221
+#: lib/pulse_web/live/dashboard_live.ex:261
#, elixir-autogen, elixir-format
msgid "Visit a portfolio to see it here"
msgstr "Visita una cartera para verla aquí"
-#: lib/pulse_web/components/layouts.ex:122
+#: lib/pulse_web/components/layouts.ex:121
#, elixir-autogen, elixir-format
msgid "We can't find the internet"
msgstr "No podemos encontrar la conexión"
@@ -227,7 +236,351 @@ msgstr "No podemos encontrar la conexión"
msgid "close"
msgstr "cerrar"
-#: lib/pulse_web/live/dashboard_live.ex:186
+#: lib/pulse_web/live/dashboard_live.ex:223
#, elixir-autogen, elixir-format
msgid "settings"
msgstr "ajustes"
+
+#: lib/pulse_web/live/dashboard_live.ex:694
+#, elixir-autogen, elixir-format
+msgid "%{count}d ago"
+msgstr "hace %{count}d"
+
+#: lib/pulse_web/live/dashboard_live.ex:693
+#, elixir-autogen, elixir-format
+msgid "%{count}h ago"
+msgstr "hace %{count}h"
+
+#: lib/pulse_web/live/dashboard_live.ex:692
+#, elixir-autogen, elixir-format
+msgid "%{count}m ago"
+msgstr "hace %{count}m"
+
+#: lib/pulse_web/live/dashboard_live.ex:695
+#, elixir-autogen, elixir-format
+msgid "%{count}w ago"
+msgstr "hace %{count}sem"
+
+#: lib/pulse_web/components/core_components.ex:505
+#, elixir-autogen, elixir-format
+msgid "%{month} %{day}, %{year}"
+msgstr "%{day} %{month} %{year}"
+
+#: lib/pulse_web/live/radar_live.ex:211
+#, elixir-autogen, elixir-format
+msgid "1 stock tracked"
+msgid_plural "%{count} stocks tracked"
+msgstr[0] "1 acción seguida"
+msgstr[1] "%{count} acciones seguidas"
+
+#: lib/pulse_web/components/core_components.ex:517
+#, elixir-autogen, elixir-format
+msgid "Apr"
+msgstr "abril"
+
+#: lib/pulse_web/components/core_components.ex:521
+#, elixir-autogen, elixir-format
+msgid "Aug"
+msgstr "agosto"
+
+#: lib/pulse_web/live/dashboard_live.ex:330
+#, elixir-autogen, elixir-format
+msgid "Average Current Yield"
+msgstr "Yield Actual Promedio"
+
+#: lib/pulse_web/live/dashboard_live.ex:311
+#, elixir-autogen, elixir-format
+msgid "Average Yield on Cost"
+msgstr "Yield on Cost Promedio"
+
+#: lib/pulse_web/live/dashboard_live.ex:418
+#: lib/pulse_web/live/dashboard_live.ex:546
+#, elixir-autogen, elixir-format
+msgid "Below Community Target"
+msgstr "Por Debajo del Objetivo de la Comunidad"
+
+#: lib/pulse_web/live/radar_live.ex:237
+#, elixir-autogen, elixir-format
+msgid "Below target"
+msgstr "Por debajo del objetivo"
+
+#: lib/pulse_web/live/radar_live.ex:294
+#, elixir-autogen, elixir-format
+msgid "Close"
+msgstr "Cerrar"
+
+#: lib/pulse_web/live/dashboard_live.ex:344
+#, elixir-autogen, elixir-format
+msgid "Community Sectors"
+msgstr "Sectores de la Comunidad"
+
+#: lib/pulse_web/live/dashboard_live.ex:435
+#, elixir-autogen, elixir-format
+msgid "Community Watchlist"
+msgstr "Radar de la Comunidad"
+
+#: lib/pulse_web/live/portfolio_live.ex:144
+#, elixir-autogen, elixir-format
+msgid "Current Yield"
+msgstr "Yield Actual"
+
+#: lib/pulse_web/components/core_components.ex:525
+#, elixir-autogen, elixir-format
+msgid "Dec"
+msgstr "diciembre"
+
+#: lib/pulse_web/components/core_components.ex:515
+#, elixir-autogen, elixir-format
+msgid "Feb"
+msgstr "febrero"
+
+#: lib/pulse_web/components/core_components.ex:514
+#, elixir-autogen, elixir-format
+msgid "Jan"
+msgstr "enero"
+
+#: lib/pulse_web/components/core_components.ex:520
+#, elixir-autogen, elixir-format
+msgid "Jul"
+msgstr "julio"
+
+#: lib/pulse_web/components/core_components.ex:519
+#, elixir-autogen, elixir-format
+msgid "Jun"
+msgstr "junio"
+
+#: lib/pulse_web/components/core_components.ex:516
+#, elixir-autogen, elixir-format
+msgid "Mar"
+msgstr "marzo"
+
+#: lib/pulse_web/components/core_components.ex:518
+#, elixir-autogen, elixir-format
+msgid "May"
+msgstr "mayo"
+
+#: lib/pulse_web/live/dashboard_live.ex:575
+#, elixir-autogen, elixir-format
+msgid "Needs 3+ users targeting the same stock with current price below the average target"
+msgstr "Requiere 3 o más usuarios siguiendo la misma acción con el precio actual por debajo del objetivo promedio"
+
+#: lib/pulse_web/live/dashboard_live.ex:572
+#, elixir-autogen, elixir-format
+msgid "No consensus buy signals yet"
+msgstr "Aún no hay señales de compra de consenso"
+
+#: lib/pulse_web/live/dashboard_live.ex:472
+#, elixir-autogen, elixir-format
+msgid "No radars shared yet"
+msgstr "Aún no hay radares compartidos"
+
+#: lib/pulse_web/live/dashboard_live.ex:439
+#, elixir-autogen, elixir-format
+msgid "No stocks tracked yet"
+msgstr "Aún no hay acciones seguidas"
+
+#: lib/pulse_web/components/core_components.ex:524
+#, elixir-autogen, elixir-format
+msgid "Nov"
+msgstr "noviembre"
+
+#: lib/pulse_web/components/core_components.ex:523
+#, elixir-autogen, elixir-format
+msgid "Oct"
+msgstr "octubre"
+
+#: lib/pulse_web/live/radar_live.ex:299
+#, elixir-autogen, elixir-format
+msgid "Price"
+msgstr "Precio"
+
+#: lib/pulse_web/live/radar_live.ex:208
+#, elixir-autogen, elixir-format
+msgid "Radar"
+msgstr "Radar"
+
+#: lib/pulse_web/live/radar_live.ex:140
+#, elixir-autogen, elixir-format
+msgid "Radar not found"
+msgstr "Radar no encontrado"
+
+#: lib/pulse_web/live/dashboard_live.ex:373
+#, elixir-autogen, elixir-format
+msgid "Radars"
+msgstr "Radares"
+
+#: lib/pulse_web/live/dashboard_live.ex:215
+#: lib/pulse_web/live/dashboard_live.ex:468
+#, elixir-autogen, elixir-format
+msgid "Recently Updated"
+msgstr "Actualizados Recientemente"
+
+#: lib/pulse_web/live/portfolio_live.ex:158
+#, elixir-autogen, elixir-format
+msgid "Sectors"
+msgstr "Sectores"
+
+#: lib/pulse_web/components/core_components.ex:522
+#, elixir-autogen, elixir-format
+msgid "Sep"
+msgstr "septiembre"
+
+#: lib/pulse_web/live/dashboard_live.ex:386
+#, elixir-autogen, elixir-format
+msgid "Shared Radars"
+msgstr "Radares Compartidos"
+
+#: lib/pulse_web/live/radar_live.ex:266
+#, elixir-autogen, elixir-format
+msgid "Show %{symbol} details"
+msgstr "Mostrar detalles de %{symbol}"
+
+#: lib/pulse_web/live/dashboard_live.ex:402
+#, elixir-autogen, elixir-format
+msgid "Stocks Tracked"
+msgstr "Acciones Seguidas"
+
+#: lib/pulse_web/live/radar_live.ex:223
+#, elixir-autogen, elixir-format
+msgid "Stocks tracked"
+msgstr "Acciones seguidas"
+
+#: lib/pulse_web/live/radar_live.ex:308
+#, elixir-autogen, elixir-format
+msgid "Target"
+msgstr "Objetivo"
+
+#: lib/pulse_web/live/radar_live.ex:142
+#, elixir-autogen, elixir-format
+msgid "The radar \"%{slug}\" doesn't exist or hasn't been shared yet."
+msgstr "El radar \"%{slug}\" no existe o aún no ha sido compartido."
+
+#: lib/pulse_web/live/radar_live.ex:320
+#, elixir-autogen, elixir-format
+msgid "This radar is empty."
+msgstr "Este radar está vacío."
+
+#: lib/pulse_web/live/radar_live.ex:160
+#, elixir-autogen, elixir-format
+msgid "View portfolio"
+msgstr "Ver cartera"
+
+#: lib/pulse_web/live/portfolio_live.ex:76
+#, elixir-autogen, elixir-format
+msgid "View radar"
+msgstr "Ver radar"
+
+#: lib/pulse_web/live/dashboard_live.ex:509
+#, elixir-autogen, elixir-format
+msgid "Visit a radar to see it here"
+msgstr "Visita un radar para verlo aquí"
+
+#: lib/pulse_web/live/radar_live.ex:229
+#, elixir-autogen, elixir-format
+msgid "With target"
+msgstr "Con objetivo"
+
+#: lib/pulse_web/live/portfolio_live.ex:138
+#, elixir-autogen, elixir-format
+msgid "Yield on Cost"
+msgstr "Yield on Cost"
+
+#: lib/pulse_web/live/dashboard_live.ex:548
+#, elixir-autogen, elixir-format
+msgid "consensus buy signal"
+msgstr "señal de compra de consenso"
+
+#: lib/pulse_web/live/dashboard_live.ex:691
+#, elixir-autogen, elixir-format
+msgid "just now"
+msgstr "ahora mismo"
+
+#: lib/pulse_web/components/core_components.ex:536
+#, elixir-autogen, elixir-format
+msgid "Basic Materials"
+msgstr "Materiales Básicos"
+
+#: lib/pulse_web/components/core_components.ex:537
+#, elixir-autogen, elixir-format
+msgid "Communication Services"
+msgstr "Servicios de Comunicación"
+
+#: lib/pulse_web/components/core_components.ex:538
+#, elixir-autogen, elixir-format
+msgid "Consumer Cyclical"
+msgstr "Consumo Cíclico"
+
+#: lib/pulse_web/components/core_components.ex:539
+#, elixir-autogen, elixir-format
+msgid "Consumer Defensive"
+msgstr "Consumo Defensivo"
+
+#: lib/pulse_web/components/core_components.ex:540
+#, elixir-autogen, elixir-format
+msgid "Consumer Discretionary"
+msgstr "Consumo Discrecional"
+
+#: lib/pulse_web/components/core_components.ex:541
+#, elixir-autogen, elixir-format
+msgid "Consumer Staples"
+msgstr "Consumo Básico"
+
+#: lib/pulse_web/components/core_components.ex:542
+#, elixir-autogen, elixir-format
+msgid "Energy"
+msgstr "Energía"
+
+#: lib/pulse_web/components/core_components.ex:543
+#, elixir-autogen, elixir-format
+msgid "Financial Services"
+msgstr "Servicios Financieros"
+
+#: lib/pulse_web/components/core_components.ex:544
+#, elixir-autogen, elixir-format
+msgid "Financials"
+msgstr "Finanzas"
+
+#: lib/pulse_web/components/core_components.ex:545
+#, elixir-autogen, elixir-format
+msgid "Health Care"
+msgstr "Salud"
+
+#: lib/pulse_web/components/core_components.ex:546
+#, elixir-autogen, elixir-format
+msgid "Healthcare"
+msgstr "Salud"
+
+#: lib/pulse_web/components/core_components.ex:547
+#, elixir-autogen, elixir-format
+msgid "Industrials"
+msgstr "Industria"
+
+#: lib/pulse_web/components/core_components.ex:548
+#, elixir-autogen, elixir-format
+msgid "Information Technology"
+msgstr "Tecnología de la Información"
+
+#: lib/pulse_web/components/core_components.ex:549
+#, elixir-autogen, elixir-format
+msgid "Materials"
+msgstr "Materiales"
+
+#: lib/pulse_web/components/core_components.ex:550
+#, elixir-autogen, elixir-format
+msgid "Real Estate"
+msgstr "Inmobiliario"
+
+#: lib/pulse_web/components/core_components.ex:551
+#, elixir-autogen, elixir-format
+msgid "Technology"
+msgstr "Tecnología"
+
+#: lib/pulse_web/components/core_components.ex:553
+#, elixir-autogen, elixir-format
+msgid "Unknown"
+msgstr "Desconocido"
+
+#: lib/pulse_web/components/core_components.ex:552
+#, elixir-autogen, elixir-format
+msgid "Utilities"
+msgstr "Servicios Públicos"
diff --git a/test/pulse/radar_aggregator_test.exs b/test/pulse/radar_aggregator_test.exs
new file mode 100644
index 0000000..6e316d1
--- /dev/null
+++ b/test/pulse/radar_aggregator_test.exs
@@ -0,0 +1,58 @@
+defmodule Pulse.RadarAggregatorTest do
+ use ExUnit.Case, async: false
+
+ setup do
+ for {_, pid, _, _} <- DynamicSupervisor.which_children(Pulse.RadarSupervisor) do
+ DynamicSupervisor.terminate_child(Pulse.RadarSupervisor, pid)
+ end
+
+ Pulse.RadarAggregator.refresh()
+ :ok
+ end
+
+ test "returns zero state when no radars are shared" do
+ stats = Pulse.RadarAggregator.refresh()
+ assert stats.radar_count == 0
+ assert stats.most_watched == []
+ assert stats.below_community_target == []
+ end
+
+ test "aggregates watchers across radars and suppresses avg below MIN_COHORT" do
+ # 2 radars include AAPL with a target — below the cohort threshold (3),
+ # so avg_target_price should stay nil. Symbol still appears in
+ # most_watched with watchers=2.
+ seed_radar("alice", [%{"symbol" => "AAPL", "price" => 180.0, "target_price" => 160.0}])
+ seed_radar("bob", [%{"symbol" => "AAPL", "price" => 180.0, "target_price" => 170.0}])
+ Process.sleep(50)
+
+ stats = Pulse.RadarAggregator.refresh()
+ aapl = Enum.find(stats.most_watched, fn s -> s.symbol == "AAPL" end)
+ assert aapl != nil
+ assert aapl.watchers == 2
+ # below MIN_COHORT = 3
+ assert aapl.avg_target_price == nil
+ end
+
+ test "computes avg_target_price + below_community_target when cohort >= 3" do
+ # 3 users target AAPL: 160, 170, 180 → avg 170. Current 150 → below target.
+ seed_radar("alice", [%{"symbol" => "AAPL", "price" => 150.0, "target_price" => 160.0}])
+ seed_radar("bob", [%{"symbol" => "AAPL", "price" => 150.0, "target_price" => 170.0}])
+ seed_radar("carol", [%{"symbol" => "AAPL", "price" => 150.0, "target_price" => 180.0}])
+ Process.sleep(50)
+
+ stats = Pulse.RadarAggregator.refresh()
+ aapl = Enum.find(stats.most_watched, fn s -> s.symbol == "AAPL" end)
+ assert aapl.avg_target_price == 170.0
+ assert aapl.current_price == 150.0
+
+ below = Enum.find(stats.below_community_target, fn s -> s.symbol == "AAPL" end)
+ assert below != nil
+ # (170 - 150) / 170 * 100 ≈ 11.76 → rounded to 11.8
+ assert below.percent_below == 11.8
+ end
+
+ defp seed_radar(slug, stocks) do
+ {:ok, _} = Pulse.RadarSupervisor.start_worker(slug)
+ Pulse.RadarWorker.update_stocks(slug, %{"stocks" => stocks})
+ end
+end
diff --git a/test/pulse/radar_worker_test.exs b/test/pulse/radar_worker_test.exs
new file mode 100644
index 0000000..8bd0ab2
--- /dev/null
+++ b/test/pulse/radar_worker_test.exs
@@ -0,0 +1,81 @@
+defmodule Pulse.RadarWorkerTest do
+ use ExUnit.Case, async: false
+
+ setup do
+ for {_, pid, _, _} <- DynamicSupervisor.which_children(Pulse.RadarSupervisor) do
+ DynamicSupervisor.terminate_child(Pulse.RadarSupervisor, pid)
+ end
+
+ :ok
+ end
+
+ test "starts with empty state" do
+ {:ok, _pid} = Pulse.RadarSupervisor.start_worker("test-empty-radar")
+
+ radar = Pulse.RadarWorker.get_radar("test-empty-radar")
+ assert radar.slug == "test-empty-radar"
+ assert radar.stocks == []
+ assert radar.metrics == %{}
+ end
+
+ test "update_stocks stores stocks and computes basic metrics" do
+ {:ok, _pid} = Pulse.RadarSupervisor.start_worker("test-radar-metrics")
+
+ Pulse.RadarWorker.update_stocks("test-radar-metrics", %{
+ "stocks" => [
+ %{"symbol" => "AAPL", "price" => 178.50, "target_price" => 160.0},
+ %{"symbol" => "KO", "price" => 55.0, "target_price" => 60.0},
+ %{"symbol" => "WATCH", "price" => 100.0}
+ ]
+ })
+
+ Process.sleep(50)
+
+ radar = Pulse.RadarWorker.get_radar("test-radar-metrics")
+ assert radar.metrics.stock_count == 3
+ assert radar.metrics.targeted_count == 2
+ # KO is below its target (55 < 60); AAPL is above (178.5 > 160).
+ assert radar.metrics.below_target_count == 1
+ end
+
+ test "updated_at advances only on material changes (not on stock price refresh)" do
+ {:ok, _pid} = Pulse.RadarSupervisor.start_worker("test-radar-mtime")
+
+ Pulse.RadarWorker.update_stocks("test-radar-mtime", %{
+ "stocks" => [%{"symbol" => "AAPL", "price" => 178.50, "target_price" => 160.0}]
+ })
+
+ Process.sleep(50)
+ first = Pulse.RadarWorker.get_radar("test-radar-mtime").updated_at
+ assert first != nil
+
+ # Stock-price refresh only: same symbol + same target, different price.
+ Pulse.RadarWorker.update_stocks("test-radar-mtime", %{
+ "stocks" => [%{"symbol" => "AAPL", "price" => 200.00, "target_price" => 160.0}]
+ })
+
+ Process.sleep(50)
+ second = Pulse.RadarWorker.get_radar("test-radar-mtime").updated_at
+ assert second == first, "stock-price refresh shouldn't bump updated_at"
+
+ # User actually edited a target → updated_at should advance.
+ Pulse.RadarWorker.update_stocks("test-radar-mtime", %{
+ "stocks" => [%{"symbol" => "AAPL", "price" => 200.00, "target_price" => 150.0}]
+ })
+
+ Process.sleep(50)
+ third = Pulse.RadarWorker.get_radar("test-radar-mtime").updated_at
+ assert DateTime.compare(third, first) == :gt
+ end
+
+ test "broadcasts radar:
on PubSub when state changes" do
+ Phoenix.PubSub.subscribe(Pulse.PubSub, "radar:test-broadcast")
+ {:ok, _pid} = Pulse.RadarSupervisor.start_worker("test-broadcast")
+
+ Pulse.RadarWorker.update_stocks("test-broadcast", %{
+ "stocks" => [%{"symbol" => "AAPL", "price" => 150.0, "target_price" => 140.0}]
+ })
+
+ assert_receive {:radar_updated, %Pulse.RadarWorker{slug: "test-broadcast"}}, 1_000
+ end
+end