Skip to content

Publish radar to Pulse — single slug, two share toggles#171

Merged
fleveque merged 2 commits into
mainfrom
feat/share-radar-on-pulse
May 25, 2026
Merged

Publish radar to Pulse — single slug, two share toggles#171
fleveque merged 2 commits into
mainfrom
feat/share-radar-on-pulse

Conversation

@fleveque

@fleveque fleveque commented May 25, 2026

Copy link
Copy Markdown
Owner

First of two coordinated PRs to add "share your radar on Pulse" alongside the existing portfolio sharing. This PR is the Quantic side (schema + NATS publishing + Settings UI). Pulse-side consumer + worker + LiveView + dashboard aggregates ship next as a separate PR.

Sharing model

One slug per user (existing portfolio_slug) with two booleans:

  • share_portfolio — default true (back-compat, current sharers keep sharing)
  • share_radar — default false (opt-in for the new surface)

When both are enabled: pulse.quantic.es/p/<slug> AND pulse.quantic.es/r/<slug>.

Publish flow (mirrors portfolio path)

  • User#publish_pulse_changes (renamed from publish_portfolio_slug_change) — fires on save when slug, share_portfolio, or share_radar changed. Diffs the effective shared state per surface and emits {portfolio,radar}.opted_in/out correctly across all 6 transitions.
  • RadarStock#after_commit :publish_radar_updated — re-emits radar.updated on any radar change (add / remove / target_price edit). Mirror of Holding#publish_portfolio_updated.
  • RadarPayloadBuilder (new) — v1 payload with enough stock metadata (price, dividend_yield, 52w range, MA200) that Pulse can render a standalone radar page without portfolio events.

Settings UI

Existing Share on Pulse card gets a "What to share publicly" subsection (visible only when a slug is set) with two checkbox rows. New ShareToggle component shows the live public URL per surface when enabled.

Frontend polish (added after initial scope)

  • Localized stock sectors via new translateSector(t, sector) helper + sectors namespace in both en.ts / es.ts. Covers the 17 GICS / Yahoo Finance names + Unknown; unknown sectors pass through verbatim. Wired into PortfolioStatsCard's sector legend and tooltip.
  • Pulse share button on the radar page. PulseShareButton parameterised with kind: 'portfolio' | 'radar' — gates on the matching toggle (sharePortfolio / shareRadar) and links to the right URL. Also fixed: if a slug exists but the relevant toggle is off, show the "opt in from Settings" hint instead of a dead public link.
  • Radar AI insights title said "AI Portfolio Insights" because RadarInsights and PortfolioInsights shared the insights.title key. Split into insights.portfolioTitle and insights.radarTitle.

Deploy order: ship the Pulse PR first

NATS is plain pub/sub (no JetStream queue) — messages sent to a subject with no live subscriber are dropped at the broker. If this PR deploys before fleveque/pulse#35:

  • radar.opted_in events for users toggling Share Radar are dropped — their radar never registers in Pulse until they next edit it.
  • radar.updated events from price refreshes are also dropped during the window.

Order to ship: fleveque/pulse#35 first (Pulse starts subscribing to radar.* and idles harmlessly with no publishers), then this PR (Quantic starts publishing into a Pulse that's ready to consume from event #1).

Test plan

  • bundle exec rspec — 676 examples / 0 failures (22 new across payload builder, user transitions, RadarStock callback, profile controller)
  • bin/rubocop on touched Ruby — clean
  • npx tsc --noEmit — clean
  • npx vite build — succeeds
  • Manual: Settings → toggle Share radar → publish appears in NATS monitoring (localhost:8222)
  • Manual: edit a target price → radar.updated fires
  • Manual: clear portfolio_slug → both opt-out events fire
  • Manual: switch language to ES → sector names render in Spanish; radar page shows "Compartir en Pulse" + "Análisis IA del Radar"

Rollback

Single revert. Migration adds two columns with safe defaults — leaving them in place is harmless if code reverts.

Sister PR

Pulse-side (RadarWorker + RadarLive + Dashboard "Community watchlist"): fleveque/pulse#35.

🤖 Generated with Claude Code

First of two coordinated PRs to add "share your radar on Pulse"
alongside the existing portfolio sharing. This PR is the Quantic side:
schema, NATS publishing, Settings UI. Pulse side (consumer + worker +
LiveView + dashboard aggregates) comes next.

Sharing model: one slug per user (the existing `portfolio_slug`) with
two independent booleans `share_portfolio` (default true, back-compat)
and `share_radar` (default false, opt-in). When both are set, the
user gets `pulse.quantic.es/p/<slug>` AND `pulse.quantic.es/r/<slug>`.

Publish flow mirrors the portfolio pipeline:

- `User#publish_pulse_changes` (renamed from publish_portfolio_slug_change)
  detects all transitions across slug + the two toggles and emits the
  right opted_in / opted_out events per surface. Six cases covered in
  spec.
- `RadarStock#after_commit :publish_radar_updated` re-emits `radar.updated`
  on any radar change (add / remove / target_price edit) — same pattern
  as `Holding#publish_portfolio_updated`.
- New `RadarPayloadBuilder` builds a v1 payload with enough stock
  metadata (price, dividend_yield, 52w range, MA200) that Pulse can
  render a standalone radar page without needing the portfolio events.

Settings UI: existing "Share on Pulse" card grows two checkbox rows
inside the slug field. New `ShareToggle` component renders each
toggle with a description and (when enabled) the live public URL.

Pulse-side preview: Pulse already ignores unknown NATS subjects
gracefully (Consumer.ex line 126), so deploying this Quantic PR first
is safe — `radar.*` events fire but Pulse just logs warnings until
PR 2 ships.

Specs: 22 new examples covering the payload builder, all 6 publish
transitions, RadarStock callback gating, and the controller's new
fields. Full suite 676 examples / 0 failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small frontend fixes on the radar surface to address feedback
during the share-radar-on-pulse round:

1. **Localize stock sectors**. New `translateSector(t, sector)` helper
   in `lib/sectors.ts` plus a `sectors` namespace in both locales
   covering the 17 GICS / Yahoo Finance names + `Unknown`. Unknown
   sectors pass through verbatim so a new provider sector never
   blanks the UI. Wired into `PortfolioStatsCard`'s sector legend
   and tooltip; matches the equivalent helper that just landed in
   Pulse for visual parity across both apps.

2. **Share-on-Pulse button on the radar page**. `PulseShareButton`
   is now parameterised with a `kind: 'portfolio' | 'radar'` prop —
   gates on the matching toggle (`sharePortfolio` / `shareRadar`)
   and links to the right URL. RadarPage gets the button under the
   title (gated on `radarStocks.length > 0`); PortfolioPage now
   passes `kind="portfolio"` explicitly. Also fixed the gate: if a
   slug exists but the relevant toggle is off, show the
   "opt-in from Settings" hint instead of a dead public link.

3. **Radar AI insights title**. `insights.title` was used by both
   `PortfolioInsights` and `RadarInsights` but said "AI Portfolio
   Insights" in both languages. Split into `insights.portfolioTitle`
   and `insights.radarTitle` so each component renders correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fleveque fleveque merged commit 1c47df4 into main May 25, 2026
5 checks passed
@fleveque fleveque deleted the feat/share-radar-on-pulse branch May 25, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant