feat/discovery
safeviewing older commit46e32b2 · incrementalpre-PRreviewed 2026-07-11 02:54 UTC0H · 0M · 3L · 3I- Purpose
- Feature Discovery v3 — contextual spotlights that guide users to undiscovered platform capabilities, converting recognition over recall into onboarding momentum
- Goal
- Ship the Descubre discovery hub end-to-end: gap badge in header, self-initiated spotlights via ?descubre=, completion acknowledgement, and mutation-driven gap freshness
- Sub-goals
- SG-1: Contextual feature spotlights v1 — FeatureSpotlight component, spotlight catalog, PostHog flag gate
- SG-2: Descubre hub — persistent gap badge + self-initiated spotlights via ?descubre= deep-link
- SG-3: Retirement fix — failed spotlight actions must be retryable, not auto-retired
- SG-4: Gap freshness on navigation — invalidate on route change so badge reflects completed actions
- SG-5 (this commit): Completion hand-off — next-step toast + mutation-driven gap invalidation
- What
- Two orthogonal additions: (a) DescubreMenu now fires a toast when a gap item departs — pointing to the next step or showing all-done — using a prevKeysRef + itemsKey effect; (b) QueryProvider wraps QueryClient in a MutationCache.onSuccess that blanket-invalidates ['discovery'] so same-page mutations refresh the gap without requiring navigation. Plus 3 i18n strings (en/es).
- Why
- Without (a) users who complete a step get no acknowledgement. Without (b) same-page mutations (credential save, RPU add, monitoring toggle) didn't refresh the gap — only navigation triggered invalidation — leaving the badge stale until the user navigated elsewhere.
- Areas
- apps/platform/src/components/discovery+40−0apps/platform/src/lib/providers+28−17apps/platform/src/messages+10−0
- Blast
- Incremental: 4 files, +83/−17 lines. Cumulative branch: ~9 areas, +15k/−18 lines (bulk is DB meta snapshot). Core logic: discovery domain + cross-domain shells + platform API + UI components.
Findings · 6
correctness2
prevKeysRef not reset when `enabled` goes false — spurious toast on re-enable
apps/platform/src/components/discovery/DescubreMenu.tsx:90
When `enabled` is false the effect's early-return at L90 skips the `prevKeysRef.current = current` update. So the ref holds the last enabled-session's keys. If `enabled` later flips true with a changed item set (e.g. orgId changes, PostHog flag briefly toggles), `left = prev − current` can be non-empty and fires a spurious 'next step' toast — even though the user didn't complete anything. Fix: `if (!enabled) { prevKeysRef.current = null; return; }` so the guard resets the ref when going dark, making the next enable behave like a fresh mount.
eslint-disable suppresses toast/t/router from deps — stable in practice, undocumented
apps/platform/src/components/discovery/DescubreMenu.tsx:114
toast, t, and router are used inside the effect but omitted from [itemsKey, enabled]. They are stable refs in practice (useToast/useTranslations/useRouter all memoise their returns). The suppression comment gives no indication of this assumption, so a future reader or upgrade could miss a stale-closure regression silently.
conventions1
Multi-line comment blocks violate 'one short line max'
apps/platform/src/components/discovery/DescubreMenu.tsx:81
CLAUDE.md: 'Never write multi-line comment blocks — one short line max.' The 5-line block at L81-85 in DescubreMenu.tsx and the 5-line block at L22-25 in QueryProvider.tsx both violate this. Each could be a single sentence: '// toast the next step when a gap item is retired — skip initial load' and '// any mutation may change the gap — blanket-invalidate instead of per-hook tracking'.
tests1
Toast-trigger logic (prev-key tracking) has no unit test
apps/platform/src/components/discovery/DescubreMenu.tsx:89
The prevKeysRef + itemsKey effect has three non-obvious edge cases — toast on departure, skip on first load, skip on spurious enabled-flip — none of which are exercised by the existing integration tests (which validate gap computation, not the toast trigger). A React Testing Library test rendering with items=[a,b] → [b] would cover the core path and the fix for finding #1 above.
improvement2
prevKeysRef typed as string[] — could be SpotlightFeatureKey[] to avoid cast
apps/platform/src/components/discovery/DescubreMenu.tsx:87
useRef<readonly string[] | null> forces the `as readonly string[]` widening cast at L95 (because Array<SpotlightFeatureKey>.includes(string) doesn't type-check). Typing the ref as useRef<readonly SpotlightFeatureKey[] | null>(null) keeps the union type end-to-end and removes the cast. Minor type hygiene.
itemsKey join(',') fragile if SpotlightFeatureKey ever contains a comma
apps/platform/src/components/discovery/DescubreMenu.tsx:88
keys.join(',') is unambiguous today (catalog keys are simple identifiers). A future key with a comma would produce hash collisions. Safer sentinel: keys.join('|'). Low practical risk — the catalog is controlled — but worth noting for future key additions.
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:24
- 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:54current
- 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