feat/discovery
needs attentionviewing older commitb686d58 · incrementalPR #305reviewed 2026-07-16 14:24 UTC0H · 2M · 2L · 2I- 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
- 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+2093−49domains/cross-domain+1541−0domains/core+498−0packages/secrets+152−12packages/database+138−1packages/api+103−0packages/analytics+92−0e2e/platform+56−0
- Blast
- 58 files across platform, cross-domain, core, secrets, api, analytics, e2e. Incremental window: 1 file (+27/-6).
Findings · 6
correctness2
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.
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
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
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
`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.
`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
- c1fe7b7needs attentionincremental3H · 4M · 3L2026-07-27 06:02
- 414db56safeincremental0H · 0M · 2L2026-07-23 00:52
- ea68968needs attentionincremental0H · 4M · 7L2026-07-22 23:01
- 4bdb2c9needs attentionincremental1H · 3M · 5L2026-07-21 23:22
- 6e3f255safeincremental0H · 0M · 0L2026-07-20 16:46
- cc41079blockedincremental5H · 5M · 3L2026-07-20 16:25
- 2451ee0needs attentionincremental0H · 5M · 6L2026-07-20 16:09
- ab7c103needs attentionincremental0H · 1M · 6L2026-07-20 15:52
- 7eff611needs attentionincremental0H · 1M · 2L2026-07-18 00:41
- 246c67csafeincremental0H · 0M · 0L2026-07-17 23:58
- 7521b17safeincremental0H · 0M · 0L2026-07-17 23:35
- 1847ec8needs attentionincremental0H · 2M · 4L2026-07-17 23:28
- 5d3175cneeds attentionincremental3H · 6M · 4L2026-07-17 19:31
- d3f875dneeds attentionincremental1H · 2M · 4L2026-07-17 18:01
- 47d25faneeds attentionincremental1H · 1M · 2L2026-07-17 00:46
- 440d838needs attentionincremental4H · 9M · 9L2026-07-17 00:27
- 7466f97needs attentionincremental0H · 1M · 5L2026-07-16 23:20
- b686d58needs attentionincremental0H · 2M · 2L2026-07-16 14:24current
- 9ea1446blockedincremental2H · 7M · 10L2026-07-16 13:44
- 6f39bb9needs attentionincremental0H · 2M · 7L2026-07-14 21:53
- 2eadf2aneeds attentionincremental0H · 1M · 2L2026-07-14 20:12
- 97bd08fneeds attentionincremental0H · 2M · 5L2026-07-14 19:40
- a48e3ffneeds attentionincremental5H · 8M · 6L2026-07-14 18:22
- 69473b7needs attentionincremental2H · 5M · 3L2026-07-14 01:34
- 92e4235needs attentionincremental2H · 1M · 3L2026-07-14 01:04
- 6be8018safeincremental0H · 0M · 3L2026-07-14 00:15
- 090b4daneeds attentionincremental1H · 3M · 5L2026-07-13 23:50
- fa7683bneeds attentionincremental1H · 4M · 3L2026-07-13 18:55
- d64cd72needs attentionincremental2H · 3M · 2L2026-07-13 16:27
- c1f337bneeds attentionincremental3H · 7M · 14L2026-07-13 13:30
- 46e32b2safeincremental0H · 0M · 3L2026-07-11 02:54
- 3b30949needs attentionincremental0H · 2M · 3L2026-07-11 02:39
- 9f8dbdfneeds attentionincremental3H · 5M · 5L2026-07-11 02:32
- 23191eeneeds attentionincremental5H · 12M · 7L2026-07-11 02:18
- fa9b88fneeds attentionincremental0H · 1M · 2L2026-07-11 01:30
- 6a8bf42blockedfull8H · 11M · 8L2026-07-11 01:08