cd apps/web && pnpm vitest run "src/components/layout/sidebar" 2>&1 | grep -qiE "passed" && grep -vE '^\s*//|^\s*\*' src/components/layout/sidebar.tsx | grep -c '
- apps/web/src/components/layout/sidebar.tsx contains `usePathname` and `import Link from 'next/link'`
- sidebar.tsx contains zero ``
- sidebar.tsx applies `bg-sidebar-accent` via `isActive(...)` (not hardcoded on Dashboard)
- sidebar-search.tsx renders an input with the `sidebar.search` placeholder and an `aria-label`
- SidebarSearch is rendered only when `!isCollapsed`
- sidebar.test.tsx + sidebar-search.test.tsx all pass; `pnpm type-check` exits 0
Sidebar uses Next.js Link with dynamic active highlighting, shows per-module links under categories, and a search field filters modules/categories (hidden when collapsed); all tests pass.Task 2: Subscribe sidebar to marketplace-store refresh signal so activations appear live
- apps/web/src/components/layout/sidebar.tsx (modified in Task 1 — has fetchActiveModules useCallback + useEffect)
- apps/web/src/lib/stores/marketplace-store.ts (created Plan 01 — sidebarRefreshKey, bumpSidebarRefresh)
- .planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md (Pitfall 2 race condition + Open Question 1: shared Zustand store as refresh mechanism; anti-pattern: re-fetch on every navigation)
- Test 1: When `bumpSidebarRefresh()` is called on the marketplace-store, the sidebar re-invokes its fetch of /modules/active (assert fetch call count increases)
- Test 2: The sidebar does NOT re-fetch /modules/active purely because pathname changed (navigation alone must not trigger a re-fetch)
Modify `apps/web/src/components/layout/sidebar.tsx` to read `sidebarRefreshKey` from `useMarketplaceStore` and add it to the dependency array of the existing `useEffect` that calls `fetchActiveModules`. Keep the mount fetch. Do NOT add `pathname` to that effect's dependency array (re-fetching on every navigation is the anti-pattern in 04-RESEARCH). This makes the sidebar re-fetch active modules exactly when the marketplace bumps the signal after a successful activate/deactivate, closing the Pitfall 2 race (marketplace awaits the POST response before bumping).
Update `apps/web/src/components/layout/sidebar.test.tsx` (or add a focused test) implementing the two tests: render the sidebar, capture fetch call count, call `useMarketplaceStore.getState().bumpSidebarRefresh()` (or trigger via the store) and assert the fetch count increased; separately change the mocked pathname and assert the fetch count did not change.
cd apps/web && pnpm vitest run "src/components/layout/sidebar" 2>&1 | grep -qiE "passed" && cd /home/vicolab/projects/tessera-ctl/apps/web && pnpm vitest run 2>&1 | grep -qiE "passed" && echo REFRESH_OK
- sidebar.tsx imports `useMarketplaceStore` and reads `sidebarRefreshKey`
- `sidebarRefreshKey` is in the fetchActiveModules useEffect dependency array
- `pathname` is NOT in that effect's dependency array
- Test proves bumpSidebarRefresh triggers a re-fetch and navigation alone does not
- Full `pnpm vitest run` in apps/web exits 0; `pnpm type-check` exits 0
Activating/deactivating a module in the marketplace causes the sidebar to refresh its module list without a full page reload; navigation alone does not trigger refetch; tests pass.
Symbols created/modified by this plan (excluded from drift verification by downstream review):
- React component `SidebarSearch` — apps/web/src/components/layout/sidebar-search.tsx
- Modified `Sidebar` component: now imports `usePathname`, `Link`, `useMarketplaceStore`; adds `isActive` helper, `searchQuery` state, expanded-categories Set
- New test files: sidebar.test.tsx, sidebar-search.test.tsx
- Consumes (from Plan 01): `useMarketplaceStore.sidebarRefreshKey`, `sidebar.search` / `sidebar.noResults` i18n keys
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser → NestJS API | Sidebar fetches the tenant's active modules (GET /modules/active) |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-08 | Information Disclosure | Sidebar /modules/active list | mitigate | GET /modules/active is tenant-scoped server-side (TenantMiddleware + RLS, Phase 2/3). Sidebar renders whatever the backend returns for the authenticated tenant — no client-side tenant selection here. |
| T-04-09 | Tampering | Module name rendered in sidebar link | mitigate | Rendered as React text content; auto-escaped. Module slug used in href is a registry-controlled slug, not free user input. |
| T-04-10 | Denial of Service | Sidebar re-fetch loop | mitigate | Re-fetch keyed only on mount + explicit sidebarRefreshKey bump; pathname deliberately excluded from deps to avoid per-navigation fetch storms (anti-pattern guarded). |
- `cd apps/web && pnpm vitest run` exits 0 (all sidebar tests green)
- `cd apps/web && pnpm type-check` exits 0
- Manual: activate a module in marketplace → it appears in sidebar without page reload; click it → opens in main area and sidebar highlights it; clear-then-type in sidebar search keeps expanded categories
- Sidebar shows only activated modules grouped by category with per-module links (PRTAL-02, MRKT-03)
- Clicking a module opens it in the main area client-side with active highlight (PRTAL-03)
- Sidebar search filters modules/categories in real time (PRTAL-05)
- Sidebar refreshes on marketplace activation without full reload; no per-navigation re-fetch
- All raw `` links migrated to Next.js ``