← all branches

feat/discovery

needs attentionviewing older commit
b686d58 · incrementalPR #305reviewed 2026-07-16 14:24 UTC0H · 2M · 2L · 2I
The branch
Purpose
Feature Discovery v1 — contextual spotlights, Descubre hub, and first-run tour for existing Batu platform users
Goal
Ship flag-gated ambient coachmarks, a discovery hub, and a first-run tour — all behind feature-discovery-spotlights PostHog flag
Sub-goals
  • SG-1: Ambient spotlights with per-user pacing and retire-on-dismiss
  • SG-2: Descubre hub with badge + revisit tour link
  • SG-3: First-run linear product tour (5 steps, dimmed scrim, spotlight cutout)
  • SG-4: discovery_state table (user-scoped RLS, migration 0058)
  • SG-5: FCIS throughout — DiscoveryFCIS namespace, JSend API, i18n, PostHog analytics
The changes (whole branch)
What
This incremental commit fixes the ambient coachmark anchor positioning: (1) adds ResizeObserver on the anchor element and document.body so the card re-measures after table column reflow (data load), and (2) center-aligns the 320px card on the anchor midpoint instead of left-aligning off the anchor's left edge.
Why
Live-observed bug: the contratos table re-lays out after rows arrive, shifting column widths. The coachmark was measured at mount (~101px right of its real anchor post-reflow) and never updated. Center-alignment was also wrong — a 320px card left-anchored to a ~107px column header trailed ~270px to the right, reading as unattached.
Areas
apps/platform+209349domains/cross-domain+15410domains/core+4980packages/secrets+15212packages/database+1381packages/api+1030packages/analytics+920e2e/platform+560
Blast
58 files across platform, cross-domain, core, secrets, api, analytics, e2e. Incremental window: 1 file (+27/-6).
feature-discovery-spotlights (PostHog, OFF by default — not live until flipped)
CI· No checks reported (runner not wired or checks pending)CodeRabbit· No .coderabbit.yaml in repo

Findings · 6

correctness2

low

Stale observer element if anchor re-mounts while anchorRect remains non-null

apps/platform/src/components/discovery/FeatureSpotlight.tsx:284

Dependency `[anchorRect !== null]` means the effect only re-runs on null↔non-null transitions. If `anchorRef.current` changes to a different DOM node while `anchorRect` stays non-null (anchor re-mounts due to key change, etc.), the ResizeObserver keeps watching the old `el`. The `update` callback reads `anchorRef.current` fresh so position updates are correct, but the observer fires only for the stale element. Edge case for a one-shot coachmark, but worth noting.

info

Arrow math simplification is algebraically correct

apps/platform/src/components/discovery/FeatureSpotlight.tsx:321

`anchorCenter - left` is identical to the previous `anchorRect.left + anchorRect.width/2 - left` since `anchorCenter := anchorRect.left + anchorRect.width/2`. Clean simplification, no correctness issue.

conventions1

medium

Variable name inconsistency: outer `el` and inner `node` both alias anchorRef.current

apps/platform/src/components/discovery/FeatureSpotlight.tsx:276

The effect captures `anchorRef.current` into `el` at setup time (used for `observer.observe(el)`) and then re-reads it as `node` inside the `update` closure. Both refer to the same ref — the dual naming adds confusion and leaves a subtle stale-observer edge: if the anchor element ever re-mounts while `anchorRect` stays non-null the observer watches the old `el` while `update` reads the new `node`. Simplify by using one name (`el`) throughout: `const el = anchorRef.current` at the top, then `if (el && el.isConnected)` in `update` (fresh-ref-read not needed here since the observer fires while the element is connected).

tests1

low

No unit test for the recentered position math

apps/platform/src/components/discovery/FeatureSpotlight.tsx:291

The `position` useMemo is pure arithmetic that could be extracted and unit-tested in isolation (anchor rect + viewport dimensions → {left, arrowLeft, …}). The prior bug was exactly this kind of formula error (~101px displacement). A test would guard against regression. Not blocking — the fix is live-confirmed and E2E covers the outcome — but the gap is real.

improvement2

medium

`observer.observe(document.body)` fires on every global layout change

apps/platform/src/components/discovery/FeatureSpotlight.tsx:285

Observing `document.body` fires `update()` (getBoundingClientRect read + setAnchorRect state write) on any layout change anywhere on the page — infinite scroll, toasts, sidebar collapse, etc. For the coachmark's short visible window this is harmless, but it deviates from minimal-observer-scope. The stated motivation (contratos table column reflow) is fully covered by `observer.observe(el)` on the anchor itself — when its column header resizes, the anchor's own bounding box changes. Dropping `observer.observe(document.body)` and keeping only `observer.observe(el)` should be sufficient. Also: `window.addEventListener('resize', update)` is now redundant since a viewport resize changes body dimensions and would trigger via the body observer (if kept); conversely, if body is dropped the window resize listener should stay.

info

`window.resize` listener may be redundant if body observer is retained

apps/platform/src/components/discovery/FeatureSpotlight.tsx:281

A viewport resize changes `document.body` dimensions, so if body is observed via ResizeObserver the `window.addEventListener('resize', update)` fires a duplicate update. If the body observer is dropped (see medium finding above), the resize listener is load-bearing and must stay.

History · 36 commits

  1. c1fe7b7needs attentionincremental3H · 4M · 3L2026-07-27 06:02
  2. 414db56safeincremental0H · 0M · 2L2026-07-23 00:52
  3. ea68968needs attentionincremental0H · 4M · 7L2026-07-22 23:01
  4. 4bdb2c9needs attentionincremental1H · 3M · 5L2026-07-21 23:22
  5. 6e3f255safeincremental0H · 0M · 0L2026-07-20 16:46
  6. cc41079blockedincremental5H · 5M · 3L2026-07-20 16:25
  7. 2451ee0needs attentionincremental0H · 5M · 6L2026-07-20 16:09
  8. ab7c103needs attentionincremental0H · 1M · 6L2026-07-20 15:52
  9. 7eff611needs attentionincremental0H · 1M · 2L2026-07-18 00:41
  10. 246c67csafeincremental0H · 0M · 0L2026-07-17 23:58
  11. 7521b17safeincremental0H · 0M · 0L2026-07-17 23:35
  12. 1847ec8needs attentionincremental0H · 2M · 4L2026-07-17 23:28
  13. 5d3175cneeds attentionincremental3H · 6M · 4L2026-07-17 19:31
  14. d3f875dneeds attentionincremental1H · 2M · 4L2026-07-17 18:01
  15. 47d25faneeds attentionincremental1H · 1M · 2L2026-07-17 00:46
  16. 440d838needs attentionincremental4H · 9M · 9L2026-07-17 00:27
  17. 7466f97needs attentionincremental0H · 1M · 5L2026-07-16 23:20
  18. b686d58needs attentionincremental0H · 2M · 2L2026-07-16 14:24current
  19. 9ea1446blockedincremental2H · 7M · 10L2026-07-16 13:44
  20. 6f39bb9needs attentionincremental0H · 2M · 7L2026-07-14 21:53
  21. 2eadf2aneeds attentionincremental0H · 1M · 2L2026-07-14 20:12
  22. 97bd08fneeds attentionincremental0H · 2M · 5L2026-07-14 19:40
  23. a48e3ffneeds attentionincremental5H · 8M · 6L2026-07-14 18:22
  24. 69473b7needs attentionincremental2H · 5M · 3L2026-07-14 01:34
  25. 92e4235needs attentionincremental2H · 1M · 3L2026-07-14 01:04
  26. 6be8018safeincremental0H · 0M · 3L2026-07-14 00:15
  27. 090b4daneeds attentionincremental1H · 3M · 5L2026-07-13 23:50
  28. fa7683bneeds attentionincremental1H · 4M · 3L2026-07-13 18:55
  29. d64cd72needs attentionincremental2H · 3M · 2L2026-07-13 16:27
  30. c1f337bneeds attentionincremental3H · 7M · 14L2026-07-13 13:30
  31. 46e32b2safeincremental0H · 0M · 3L2026-07-11 02:54
  32. 3b30949needs attentionincremental0H · 2M · 3L2026-07-11 02:39
  33. 9f8dbdfneeds attentionincremental3H · 5M · 5L2026-07-11 02:32
  34. 23191eeneeds attentionincremental5H · 12M · 7L2026-07-11 02:18
  35. fa9b88fneeds attentionincremental0H · 1M · 2L2026-07-11 01:30
  36. 6a8bf42blockedfull8H · 11M · 8L2026-07-11 01:08