Files
tessera-ctl/.planning/phases/04-marketplace-portal-navigation/04-03-PLAN.md
T

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-marketplace-portal-navigation 03 execute 2
04-01
apps/web/src/components/layout/sidebar.tsx
apps/web/src/components/layout/sidebar-search.tsx
apps/web/src/components/layout/sidebar.test.tsx
apps/web/src/components/layout/sidebar-search.test.tsx
true
MRKT-03
PRTAL-02
PRTAL-03
PRTAL-05
truths artifacts key_links
Sidebar shows only the tenant's activated modules, grouped by category, with an individual clickable link per module (PRTAL-02, MRKT-03)
Clicking a module link in the sidebar navigates client-side to /modules/{category}/{slug} and the module opens in the main content area (PRTAL-03)
The currently open page/module is highlighted active in the sidebar via usePathname (PRTAL-03)
A sidebar search field filters visible modules and categories by name in real time; non-matching categories hide (PRTAL-05)
After a module is activated/deactivated in the marketplace, the sidebar refreshes its active-module list without a full page reload
path provides min_lines
apps/web/src/components/layout/sidebar-search.tsx Sidebar search input filtering modules + categories 20
path provides contains
apps/web/src/components/layout/sidebar.tsx Sidebar using Next.js Link + usePathname active state + per-module links + search + marketplace-store refresh subscription usePathname
from to via pattern
apps/web/src/components/layout/sidebar.tsx /modules/active fetch on mount and on marketplace-store sidebarRefreshKey change modules/active
from to via pattern
apps/web/src/components/layout/sidebar.tsx marketplace-store sidebarRefreshKey useEffect dependency triggers re-fetch sidebarRefreshKey
from to via pattern
apps/web/src/components/layout/sidebar.tsx next/navigation usePathname active-state comparison against link href usePathname
Enhance the existing sidebar so it becomes the live navigation surface for activated modules: migrate all links from raw `` to Next.js ``, add `usePathname`-based active highlighting, render an individual link per activated module under each category, add a sidebar search field, and subscribe to the marketplace-store refresh signal so newly activated/deactivated modules appear without a page reload.

Purpose: Delivers PRTAL-02 (sidebar shows categories + activated modules), PRTAL-03 (selected module opens in main area + active highlight), PRTAL-05 (sidebar search/filter), and MRKT-03 (only activated modules appear). Output: Modified sidebar.tsx, new sidebar-search.tsx, and unit tests.

Phase Goal

As a Tessera user, I want to see my tenant's activated modules in the sidebar, search them, and click one to open it, so that I can navigate directly to the tools I use without leaving the portal.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md @.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md @.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md @.planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md @.planning/phases/04-marketplace-portal-navigation/04-01-SUMMARY.md Task 1: Build SidebarSearch and migrate sidebar to Next.js Link + usePathname active state - apps/web/src/components/layout/sidebar.tsx (the file being modified — current raw `` links, hardcoded Dashboard active state, categories accordion with per-module `` links at lines 144-169, admin section) - apps/web/src/app/(portal)/admin/tenants/page.tsx (input styling for the search field) - .planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md (sidebar.tsx MODIFY section: Link/usePathname insertion points, active CSS classes; sidebar-search no-analog guidance) - .planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md (Sidebar Enhancement: SidebarSearch 36px height, placement between main nav and categories, collapsed-mode rule, active classes) - .planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md (Pitfall 1 raw `` reloads, Pitfall 3 active state, Pitfall 5 accordion state reset) - apps/web/src/messages/de.json (sidebar.search + sidebar.noResults added in Plan 01) - Test 1: Sidebar renders the Dashboard and Marketplace links as Next.js Link elements (anchor with client-side href, no full reload semantics) — assert hrefs "/" and "/marketplace" - Test 2: Given pathname "/marketplace", the Marketplace link has the active class (`bg-sidebar-accent`) and Dashboard does not - Test 3: SidebarSearch renders an input with the `sidebar.search` placeholder and calls onChange with typed value - Test 4: With a search term that matches no module, the sidebar renders the `sidebar.noResults` text Create `apps/web/src/components/layout/sidebar-search.tsx`: `'use client'` controlled ``, props `{ value: string; onChange: (v: string) => void }`, placeholder from `useTranslations('sidebar')('search')`, height 36px, `border border-border rounded-md`, focus `focus:ring-2 focus:ring-ring focus:border-transparent`, `px-3` within the sidebar nav padding. Include an accessible label (`aria-label` = placeholder). No debounce needed (client-side, tiny dataset) — filter immediately. Modify `apps/web/src/components/layout/sidebar.tsx`: 1. Add imports `import Link from 'next/link'` and `import { usePathname } from 'next/navigation'`. Add `const pathname = usePathname()` and an `isActive(href)` helper: for "/" return `pathname === '/'`, otherwise `pathname.startsWith(href)`. 2. Replace EVERY `` (Dashboard, Marketplace, category links, per-module links, all admin links) with ``. Remove the hardcoded Dashboard active class; instead apply active classes conditionally via `isActive(href)`: active = `bg-sidebar-accent text-sidebar-accent-foreground font-medium`, inactive = `text-sidebar-foreground hover:bg-muted` (for top-level) / muted variants for nested module links. Per-module links use `/modules/{category}/{slug}` and get active highlight when `isActive` matches that path. 3. Insert `` between the main-nav `
    ` (Dashboard/Marketplace) and the categories accordion, only when `!isCollapsed` (collapsed mode hides search per UI-SPEC). Add `searchQuery` state. 4. Filter the activated-module list and categories by `searchQuery` (case-insensitive match on module name and category name). Hide categories whose modules all filter out. When the search yields zero modules, render `t('noResults')` instead of the accordion list. Keep accordion expand/collapse state stable across search changes (track expanded categories in a Set keyed by category name so clearing search does not collapse them — Pitfall 5). Create `apps/web/src/components/layout/sidebar-search.test.tsx` and `apps/web/src/components/layout/sidebar.test.tsx` implementing the four tests. Mock `next/navigation` `usePathname`, `next-intl`, the auth-store, and `fetch` for `/modules/active`. Do NOT leave any raw `` in the sidebar (causes full reloads — Pitfall 1). Do NOT show the search field or module list in collapsed (64px) mode. 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.

    <artifacts_this_phase_produces> 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 </artifacts_this_phase_produces>

    <threat_model>

    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).
    </threat_model>
    - `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

    <success_criteria>

    • 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 <a> links migrated to Next.js <Link> </success_criteria>
    Create `.planning/phases/04-marketplace-portal-navigation/04-03-SUMMARY.md` when done