feat/discovery
needs attentionviewing older commit3b30949 · incrementalpre-PRreviewed 2026-07-11 02:39 UTC0H · 2M · 3L · 1I- Purpose
- Feature Discovery (Descubre) — surface unused Batu platform capabilities to operators via contextual spotlights and a persistent gap badge so they adopt more of the product without needing a guided tour
- Goal
- Descubre hub: a persistent sparkles button in the header with a count badge showing undiscovered features; clicking deep-links to the feature page and triggers a self-initiated spotlight. Disappears at gap = 0.
- Sub-goals
- SG-1: discovery domain FCIS entity — discovery-state schema, decisions, queries, shells, mapper
- SG-2: cross-domain feature-discovery coordinator — gap evaluation spanning core + utility domains
- SG-3: DescubreMenu UI — persistent header badge with dropdown deep-links
- SG-4: FeatureSpotlight UI — coachmark cards triggered by ?descubre= param or budget-based scheduling
- SG-5: spotlight-catalog — typed registry of feature keys → page paths → anchor selectors
- SG-6: analytics events — discovery_gap_viewed, spotlight_shown, spotlight_completed
- SG-7: E2E anchors spec — validates spotlight anchor presence on relevant pages
- SG-8: cache freshness fix — invalidate gap query on route change (this commit)
- What
- Single-commit incremental: adds useEffect in DescubreMenu to invalidate the ['discovery'] query cache on every pathname change, and sets staleTime to 0, fixing a live bug where the gap badge remained stale after the user completed an action (e.g. saving CFE credentials) because the component never unmounts during a session.
- Why
- The DescubreMenu lives in the dashboard header and is never unmounted. With staleTime: 60s the badge could read as 'tour stuck' for up to a minute after completing a setup step — found live post-deploy with the CFE credential step.
- Areas
- apps/platform/src/api+278−0apps/platform/src/components/discovery+452−0apps/platform/src/app+33−2domains/core/src/discovery+576−0domains/cross-domain/src+1158−0e2e/platform+48−0packages/api/src/schemas+73−0packages/database/src/schema+95−0
- Blast
- ~2700 lines net-new across 8 areas: full FCIS discovery domain + cross-domain coordinator + platform API surface + header UI + E2E anchors + analytics events + DB schema. The incremental being reviewed is 18 lines in one file.
Findings · 6
correctness3
staleTime: 0 and invalidateQueries on pathname overlap — double fetch on every navigation
apps/platform/src/components/discovery/DescubreMenu.tsx:64
staleTime: 0 makes TanStack Query treat every read as stale and refetch on focus/mount automatically. The useEffect invalidateQueries on pathname then triggers a second concurrent refetch on navigation. The two mechanisms serve the same purpose but compound: on initial mount and on every route change the component fires two requests instead of one. Recommended: restore staleTime to 60_000 and keep only the invalidateQueries effect, which is the surgical fix for the mid-session stale-badge bug.
['discovery'] prefix invalidation is broader than the specific gap query
apps/platform/src/components/discovery/DescubreMenu.tsx:65
TanStack Query prefix-matches on queryKey, so invalidateQueries({ queryKey: ['discovery'] }) will bust any future query registered under ['discovery', ...]. The actual query key is ['discovery', 'gap', orgId]. Today there is only one discovery query, so the behaviour is correct. As new discovery sub-queries are added the invalidation will over-bust them on every route change. Consider targeting ['discovery', 'gap', orgId] to match intent exactly.
queryClient in useEffect dependency array is a stable singleton
apps/platform/src/components/discovery/DescubreMenu.tsx:66
queryClient from useQueryClient() is the same object for the lifetime of the provider and will never change between renders. Listing it as a dep is correct for lint exhaustive-deps but it is the kind of dependency that misleads future readers into thinking it could vary. No functional impact.
conventions1
Block comment recounts bug history — belongs in commit message, not source
apps/platform/src/components/discovery/DescubreMenu.tsx:57
The 4-line comment includes 'found live: after connecting CFE the badge kept showing the CFE step' — a good commit-message line, not a durable source invariant. The invariant worth keeping is one sentence: why route-change invalidation is the right scope (this component has no sight of where actions happen). Trim to that.
tests1
No component test for the pathname → invalidateQueries pathway
apps/platform/src/components/discovery/DescubreMenu.tsx:64
The live bug this commit fixes (stale badge after completing an action) is not guarded by any test. Domain unit tests cover pure decision logic; the E2E spec navigates once per test and never crosses routes to verify badge refresh. A React Testing Library test that mocks useQueryClient, simulates a pathname change, and asserts invalidateQueries was called is ~20 lines and directly pins the regression.
improvement1
void on invalidateQueries silently swallows errors
apps/platform/src/components/discovery/DescubreMenu.tsx:65
void queryClient.invalidateQueries(...) discards the returned Promise. If invalidation throws during an unmount race nothing surfaces. Either await inside the effect, or add an explicit .catch(() => {}) no-op to signal the fire-and-forget intent to reviewers.
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:54
- 3b30949needs attentionincremental0H · 2M · 3L2026-07-11 02:39current
- 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