Consume radar.* events: RadarWorker + LiveView + community insights#35
Merged
Conversation
Second of two coordinated PRs. The Quantic side (dividend-portfolio
#171) ships the publishing flow + Settings UI. This is the Pulse
mirror: a new GenServer-per-slug RadarWorker tree, a /r/:slug
LiveView, and a "Community watchlist" card on the dashboard.
## OTP architecture (mirrors the portfolio tree exactly)
- `Pulse.RadarRegistry` — Registry (keys: :unique) for slug → pid
lookup, separate namespace from PortfolioRegistry.
- `Pulse.RadarSupervisor` — DynamicSupervisor managing
RadarWorker children.
- `Pulse.RadarWorker` — GenServer per shared slug. State is
stocks + base_currency + computed metrics (stock_count,
targeted_count, below_target_count). Recomputes on each
`radar.updated`; broadcasts on `radar:#{slug}` and `radars` PubSub.
- `Pulse.RadarStore` — DETS-backed persistence (priv/data/radars.dets,
separate from portfolios.dets), with `restore_all` to rehydrate
workers on app boot.
- `Pulse.RadarAggregator` — community stats GenServer mirroring
DashboardAggregator: top-N most-watched stocks, average target prices
(suppressed below MIN_COHORT = 3 to avoid exposing a single user's
preference), and "below community target" derived view sorted by
largest gap.
## Consumer + Application
- `Pulse.Nats.Consumer.@subjects` extends with `radar.{updated,
opted_in,opted_out}`. Each handler mirrors the corresponding
portfolio handler: opted_in starts the worker (and fills stocks
if present), opted_out tears it down and clears DETS, updated
ensures a worker exists then forwards.
- `Pulse.Application` adds the radar registry, supervisor, store,
aggregator, and a second restore Task. The two restore tasks
use `Supervisor.child_spec` with explicit IDs to avoid the
default-id collision.
## Frontend
- New `PulseWeb.RadarLive` at `/r/:slug`. Subscribes to
`radar:#{slug}` PubSub so target-price edits push without a
refresh. Renders a stocks table with Symbol / Sector / Price /
Target / Delta (color-coded) / Yield. Cross-links to `/p/:slug`
when the same slug also has a PortfolioWorker.
- `PulseWeb.DashboardLive` gets a new "Community watchlist" card
with the most-watched table + a "below community target"
sub-section. Subscribes to the existing `dashboard` topic; the
RadarAggregator broadcasts `{:radar_dashboard_updated, stats}`
there.
- Router: `live "/r/:slug", RadarLive, :show`.
## Tests
44 total, 0 failures (7 new across RadarWorker + RadarAggregator
covering the worker lifecycle, metric computation, material-change
gating on updated_at, PubSub broadcast, and the aggregator's
MIN_COHORT threshold + below-target derivation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…shboard reorg
Addresses the round-of-feedback on the radar PR:
1. **Bidirectional cross-link**. Portfolio page now shows a "View
radar" link when a RadarWorker exists for the same slug (mirror of
the existing "View portfolio" link from RadarLive).
2. **RadarLive visual overhaul**. Same capturable card / branding
footer / button row as PortfolioLive (Share Image + Share Link
hooks both wired). Differentiated where it matters: indigo avatar
with an eye icon vs the portfolio's primary-colored initial, "·
Radar" suffix on the title, three summary stat cards (stocks
tracked / with target / below target). Stocks table includes
per-stock logos like PortfolioLive's grid.
3. **Visit tracking** via new `Pulse.RadarAnalytics` — parallel of
`Pulse.Analytics` (separate ETS + DETS tables so portfolio and
radar slugs don't conflate counts). Wired into RadarLive.mount and
the radar.opted_out NATS handler for cleanup.
4. **Dashboard reorganization**:
- "Portfolios" section heading + the existing trio (stats cards /
three-column lists / yield cards / sectors) grouped together
- New "Radars" section below: a fresh trio of stat cards (Shared
Radars / Stocks Tracked / Below Community Target) + a three-column
grid (Community Watchlist / Recently Updated / Trending This
Week) + a "Below Community Target" strip across the bottom
- "How It Works" moved to the very end (was previously sandwiched
between the portfolio and community-watchlist content because of
a misplaced nested div from the earlier commit)
- `RadarAggregator.stats` extended with `recent_radars`
(sorted by updated_at, top-N).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the radar's stocks table with a portfolio-style mini-card grid (same `capture-grid` columns, same logo placement, same card structure) so the radar page feels at home next to the portfolio. Each card surfaces what matters on a radar: - The stock logo - The symbol - A color-coded delta (green when below target, red when above, neutral at target, muted when no target set) with a matching dot - Current price / target price line underneath, monospace Sort order is what makes the page actionable: most-below-target first (biggest opportunity → green stripe up top), then at-target, then above-target, then no-target last. Within each band sorted by absolute gap so the most striking signal leads. Dropped the sector column entirely — it's not actionable on a radar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Radars can easily run 50+ stocks (way longer than a portfolio's typical 5-15 holdings), so the portfolio-equivalent card density was too tall. Trim: - Card padding p-4 → p-2 - Logo size: default → 28px - Symbol text-sm → text-xs - Status dot w-2.5 → w-1.5 - Delta text-lg → text-xs - Drop the price/target line entirely from card body; surface it via the card's `title` attribute (hover tooltip) so the info is still one mouse-over away when wanted. - Grid: 2/3/4/5 cols → 3/4/6/8 cols at base/sm/md/lg Net result: ~half the previous card height. A 60-stock radar now fits in a couple of screens instead of needing a scroll marathon. Sort order and color scheme unchanged — still most-below-target first with green/red dots. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous version put price + target in the card's `title` attribute, which is invisible on touch devices and undiscoverable even on desktop. Replace with: - A small information-circle icon in the top-right corner of each card — visible signal that more is available. - Tapping/clicking the icon toggles a detail overlay covering the card with Price / Target labels and values. Works identically on mobile (tap) and desktop (click). - A close ✕ in the overlay so the dismiss target is obvious. Tapping the info icon again also closes it (it's the same `JS.toggle_class` binding). Uses `Phoenix.LiveView.JS.toggle_class`/`add_class` so the whole interaction is client-side — no server roundtrip per tap. Symbols may contain dots (e.g. `REP.MC`); we replace `.` with `-` in the DOM id so the `to: "#..."` selector doesn't trip on CSS escaping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without `.btn` classes the plain <button> elements default to the arrow cursor, making the info / close icons feel non-interactive on hover. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`compute_stats/0` was returning a map without a `:recent_radars`
key, so the dashboard's "Recently Updated" panel rendered empty
even when shared radars existed. DashboardLive accessed the key
via `Map.get(..., :recent_radars, [])` and silently fell back to
the empty list.
Add the missing field: sort all live workers by `updated_at`
desc (nil-safe), take top-N, and emit `%{slug, updated_at,
stock_count, below_target_count}` so the panel can show what's
been touched most recently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-visible: 1. **Share images** now carry a YYYYMMDD-stamped filename (`<slug>-portfolio-20260525.png` / `<slug>-radar-20260525.png`) and a localized date in the header so the screenshot itself is self-dating. The SaveImage / ShareLink JS hooks read a new `data-filename` attribute and append the stamp. 2. **Sector names** are now localized via a `translate_sector/1` helper in CoreComponents covering the 17 GICS / Yahoo Finance sector names + `Unknown`; unknown names pass through verbatim so a new sector never blanks the UI. Wired into the four render sites (portfolio sector legend + tooltip, dashboard community-sectors legend + tooltip). 3. **Below Community Target** card on the dashboard is now always visible — empty state explains the 3-user cohort requirement. Previously the whole card was hidden until consensus existed, so users never knew the feature was there. 4. **Localized date helper** (`format_date/1`) used by the share images. Full Spanish month names (`enero` / `mayo` / `diciembre`) — abbreviations like `may` are visually indistinguishable from English `May`, so we use full forms. 5. **Spanish translation pass**: filled in all empty `msgstr` entries and corrected fuzzy ones that had been auto-merged from the portfolio strings (e.g. `Community Watchlist` was `Carteras de la comunidad`, the radar not-found message said `cartera`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of two coordinated PRs. The Quantic side ships the publishing flow + Settings UI (fleveque/dividend-portfolio#171). This is the Pulse mirror: a new GenServer-per-slug RadarWorker tree, a
/r/:slugLiveView, and a "Community watchlist" card on the dashboard.What landed
OTP architecture (clean mirror of the portfolio tree):
Pulse.RadarRegistry— separate Registry namespace from PortfolioRegistryPulse.RadarSupervisor— DynamicSupervisor of RadarWorker childrenPulse.RadarWorker— GenServer per slug; recomputes basic metrics on eachradar.updated; broadcasts onradar:{slug}(per-page) andradars(aggregator) PubSubPulse.RadarStore— DETS-backed (priv/data/radars.dets, separate from portfolios.dets);restore_allrehydrates workers on bootPulse.RadarAggregator— community stats: top-N most-watched stocks, average target prices (suppressed when cohort < MIN_COHORT=3, mirror ofStocks::CommunityTargetPriceon the Quantic side), and a "below community target" derived view sorted by largest gapPulse.RadarAnalytics— visit-tracking parallel toPulse.Analytics(separate ETS + DETS tables so portfolio and radar slugs don't conflate counts)Consumer + Application:
@subjectsextends withradar.{updated,opted_in,opted_out}. Each handler mirrors the portfolio path.Applicationregisters the new tree + a second restore Task. The two tasks need explicitSupervisor.child_specids — without them the default Task id collides and supervisor refuses to boot.LiveView —
/r/:slug(PulseWeb.RadarLive):PortfolioLive— differentiated by an indigo avatar (eye icon) and a "· Radar" title suffix so screenshots are distinguishable from portfolios.titletooltip so touch devices have a real affordance.Dashboard polish:
updated_atdesc.Share images carry a YYYYMMDD-stamped filename (
<slug>-radar-20260525.png) and a localized date in the header so screenshots are self-dating.Localization:
format_date/1helper using gettext'd month names (full Spanish month names —mayo, notmay, to avoid visual collision with English).translate_sector/1helper covering the 17 GICS / Yahoo sector names +Unknown. Unknown sectors pass through verbatim.msgstrentries and corrected fuzzy ones that had been auto-merged from portfolio strings (e.g.Community Watchlist→Radar de la Comunidad, the radar not-found message saidcartera).Deploy this PR before the Quantic PR
NATS here is plain pub/sub (no JetStream queue) — messages with no live subscriber are dropped at the broker. If Quantic ships first, every
radar.opted_inandradar.updatedfrom the moment the toggle goes live is lost on the floor until Pulse restarts with the new subjects subscribed.Shipping this PR first means Pulse subscribes to
radar.*immediately and idles harmlessly (no publishers yet, nothing arrives). Then when fleveque/dividend-portfolio#171 deploys, Pulse catches event #1 cleanly.Test plan
mix test— 44 / 44mix format --check-formatted— cleanmix compile --warnings-as-errors— cleanportfolio_slug+share_radar=true) → RadarWorker spawns;localhost:4000/r/<slug>renders the gridRadarStore.restore_allre-spawns the workers from DETS<slug>-radar-<YYYYMMDD>.pngwith the date visible in the headerRollback
Single revert. The DETS file in
priv/data/radars.detsis harmless if left behind (Pulse just ignores radar.* events without the code).Sister PR
Quantic side (schema + publishing + Settings UI): fleveque/dividend-portfolio#171.
🤖 Generated with Claude Code