diff --git a/.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md b/.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md new file mode 100644 index 0000000..7ffb45a --- /dev/null +++ b/.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md @@ -0,0 +1,568 @@ +# Phase 4: Marketplace & Portal Navigation - Research + +**Researched:** 2026-06-22 +**Domain:** Frontend Marketplace UI, Sidebar Navigation Enhancement, Tenant Context Switching +**Confidence:** HIGH + +## Summary + +Phase 4 builds a marketplace view for browsing and activating modules, enhances the sidebar with search/filter and individual module links, and adds a Super-Admin tenant context selector. The critical finding is that nearly all backend infrastructure already exists: the module registry API (GET /modules, GET /modules/active, POST activate/deactivate) is complete, the Prisma schema has Module and TenantModuleActivation models, and the TenantMiddleware already supports `x-tenant-id` header for Super-Admin tenant switching. The tenant list API (GET /tenants) is also available and restricted to SUPER_ADMIN role. + +This phase is primarily a frontend implementation phase. The backend needs only minor extensions: an endpoint to fetch modules with their activation status for a specific tenant (combined view for marketplace cards), and potentially a lightweight endpoint to get tenant list for the context selector (the existing GET /tenants already works). The frontend work involves creating the marketplace page with card grid, search, category filters, status tabs, activation/deactivation flows, a module detail page, and enhancing the existing sidebar with search functionality and active-state highlighting via `usePathname`. + +**Primary recommendation:** Leverage existing API endpoints and patterns. Focus frontend work on the marketplace page components (MarketplaceCard, search, filters, tenant selector) and sidebar enhancements (SidebarSearch, usePathname-based active state, individual module links). Use the existing admin/modules page pattern for activation toggle logic. All components are hand-rolled following established shadcn-compatible CSS variable conventions. + + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Marketplace als Karten-Grid (wie VS Code Extensions / App Store). Jede Karte zeigt Icon, Name, Kurzbeschreibung, Kategorie-Badge, Aktivierungs-Status. +- **D-02:** Suchleiste + Kategorie-Filter oben auf Marketplace-Seite. Live-Filter waehrend Tippen. Kategorie-Chips oder Dropdown. +- **D-03:** Empty State: Freundliche Illustration + Text ("Noch keine Module verfuegbar"). +- **D-04:** Super-Admin bekommt leichtgewichtigen Mandanten-Kontext-Selector im Marketplace-Bereich (Dropdown zur Mandanten-Auswahl). Aktivierung/Deaktivierung erfolgt aus Perspektive des gewaehlten Mandanten. Kein volles Impersonation-System — nur Kontext-Wechsel fuer Modul-Management (volle Impersonation bleibt v2: FEAT-V2-04). +- **D-05:** Regulaerer Admin aktiviert Module direkt fuer seinen eigenen Mandanten (kein Kontext-Wechsel noetig). + +### Claude's Discretion +- Marketplace Filter/Status-Unterscheidung: Tab-Ansicht vs. Badge-System vs. Kombination — basierend auf UX Best Practices +- Bestaetigungs-Dialog bei Aktivierung: Dialog vs. sofortige Aktion mit Undo-Toast — UX-Entscheidung +- Visuelles Feedback nach Aktivierung: Toast + Badge-Update vs. Animation — passend zum existierenden Design +- Sidebar Kategorien-Darstellung: Bestehendes Accordion beibehalten vs. flache Liste mit Gruppen-Header — basierend auf Modul-Anzahl und UX +- Sidebar collapsed Modus: Modul-Icons anzeigen vs. nur Haupt-Navigation — basierend auf existierender Sidebar-Logik +- Sidebar Suchfeld: Im Kategorien-Bereich vs. globale Header-Suche vs. beides — basierend auf PRTAL-05 und Komplexitaet +- Modul-Detail-Ansicht Tiefe: Ausfuehrlich (App-Store-Stil) vs. kompakt — passend fuer v1 Modul-Anzahl +- Marketplace-Detail vs. Modul-Nutzung Navigation: Separate Seiten vs. Tabs vs. anderes Pattern — basierend auf App Router Architektur + +### Deferred Ideas (OUT OF SCOPE) +- **FEAT-V2-04: Volle Admin-Impersonation** — Komplettes "Als Mandant agieren"-Feature kommt in v2. Phase 4 baut nur leichtgewichtigen Kontext-Selector fuer Modul-Management. + + + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| MRKT-01 | Uebersicht aller verfuegbaren Module mit Beschreibung und Kategorie | Marketplace page at `/marketplace` with card grid. GET /modules API exists. ModuleCard pattern reusable. UI-SPEC defines card anatomy with icon, name, description, category badge, status badge. | +| MRKT-02 | Admin kann Module pro Mandant aktivieren und deaktivieren | POST /modules/:id/activate and /deactivate APIs exist. Admin/modules page has toggle pattern. Marketplace adds button-based activation on cards. TenantMiddleware supports x-tenant-id for Super-Admin context switch. | +| MRKT-03 | Nur aktivierte Module erscheinen in der Seitenleiste des Mandanten | Sidebar already fetches GET /modules/active and groups by category. Need to add individual module links under each category and search/filter. | +| MRKT-04 | Module sind in Kategorien gruppiert | Category data is part of Module model (category field). Sidebar already groups by category. Marketplace adds CategoryFilter chips. | +| PRTAL-02 | Linke Seitenleiste zeigt Kategorien und aktivierte Module | Sidebar already shows categories and modules. Need to enhance with individual clickable module items under each category (currently links to category page, needs per-module links). | +| PRTAL-03 | Ausgewaehltes Modul oeffnet sich im Hauptbereich (Mitte) | Module route `/modules/{category}/{slug}` exists with lazy-loaded component rendering in main area. Need active-state highlighting in sidebar via usePathname. | +| PRTAL-05 | Module koennen in der Seitenleiste durchsucht und gefiltert werden | New SidebarSearch component needed between main nav and categories section. Client-side filtering of module names and category names. | + + + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Module catalog display | Frontend (Client) | API (Backend) | GET /modules already returns all modules. Client renders grid with filtering. All filtering is client-side (small dataset, all modules loaded). | +| Module activation/deactivation | API (Backend) | Frontend (Client) | Business logic (tenant-module association, upsert) lives in backend service. Frontend sends POST and updates UI optimistically. | +| Tenant context switching (Super-Admin) | API (Backend) | Frontend (Client) | TenantMiddleware reads x-tenant-id header. Frontend dropdown sets header on subsequent requests. | +| Sidebar module navigation | Frontend (Client) | -- | Pure client-side: fetches active modules, groups by category, renders links, handles search/filter. | +| Sidebar active state | Frontend (Client) | -- | usePathname comparison against link hrefs. No backend involvement. | +| Module detail view | Frontend (Client) | API (Backend) | Frontend renders detail page at /marketplace/[slug]. Module data from same GET /modules response. | +| i18n for marketplace copy | Frontend (Client) | -- | next-intl with DE/EN translation files. All strings from UI-SPEC copywriting contract. | + +## Standard Stack + +### Core (Already Installed -- No New Dependencies) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Next.js | 15.3.x | Frontend framework, App Router | Already installed, provides routing, dynamic imports, SSR [VERIFIED: apps/web/package.json] | +| React | 19.x | UI library | Already installed [VERIFIED: apps/web/package.json] | +| Zustand | 5.0.x | Client state management | Already used for sidebar-store, auth-store [VERIFIED: apps/web/src/lib/stores/] | +| next-intl | 4.13.x | Internationalization | Already used for all UI strings [VERIFIED: apps/web/src/messages/] | +| next-themes | 0.4.x | Theme switching | Already installed [VERIFIED: apps/web/package.json] | +| Tailwind CSS | 4.x | Styling | Already configured with OKLCH tokens [VERIFIED: apps/web/src/app/globals.css] | +| NestJS | 11.x | Backend API | Already running with module-registry controller [VERIFIED: apps/api/package.json] | +| Prisma | 6.x | ORM | Schema has Module + TenantModuleActivation [VERIFIED: apps/api/prisma/schema.prisma] | + +### Supporting (No New Libraries Needed) + +This phase requires **zero new npm packages**. All functionality is achievable with existing dependencies: + +- **Search/filter:** Native string matching, `useState` for filter state +- **Debounce:** Simple `setTimeout`/`clearTimeout` pattern (no lodash needed for one debounce) +- **Toast notifications:** Hand-rolled toast component with CSS animations (no sonner/react-hot-toast needed -- project convention is hand-rolled components) +- **Dialog:** Hand-rolled modal with focus trap (project convention, no @radix-ui/react-dialog) +- **URL routing:** Next.js App Router dynamic routes (`/marketplace/[slug]`) +- **Active state:** `usePathname()` from `next/navigation` + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Hand-rolled toast | sonner / react-hot-toast | Would introduce a new dependency. Project convention is hand-rolled components following shadcn CSS variable naming. Toast is simple enough to build. | +| Hand-rolled dialog | @radix-ui/react-dialog | Same reasoning. Only one dialog needed (deactivation confirm). Focus trap can be done with a few lines of JS. | +| Hand-rolled debounce | lodash.debounce | Adding lodash for a single 4-line debounce function is wasteful. | + +**Installation:** +```bash +# No new packages needed for this phase +``` + +## Package Legitimacy Audit + +> No new external packages are installed in this phase. All functionality uses existing project dependencies. + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| (none) | -- | -- | -- | -- | -- | No new packages | + +**Packages removed due to [SLOP] verdict:** none +**Packages flagged as suspicious [SUS]:** none + +## Architecture Patterns + +### System Architecture Diagram + +``` +User Browser + | + v +[Next.js App Router - (portal) route group] + | + +-- /marketplace ---------> MarketplacePage + | | |-- MarketplaceSearch (debounced text input) + | | |-- StatusFilter (tabs: All/Active/Available) + | | |-- CategoryFilter (horizontal chips) + | | |-- TenantContextSelector (Super-Admin only) + | | |-- MarketplaceCard[] (grid) + | | |-- Activate/Deactivate button + | | |-- Link to /marketplace/[slug] + | | + | +-- /marketplace/[slug] -> ModuleDetailView + | |-- Back link + | |-- Full description, status, activation button + | + +-- Sidebar (enhanced) + | |-- Dashboard link + | |-- Marketplace link + | |-- SidebarSearch (filters categories+modules) + | |-- Categories accordion + | | |-- Category header (link to /modules/{cat}) + | | |-- Module items (link to /modules/{cat}/{slug}) + | |-- Admin section (role-gated) + | |-- Active state via usePathname() + | + +-- API calls (fetch with credentials: 'include') + | + v +[NestJS API - existing endpoints] + |-- GET /modules -> all registered modules + |-- GET /modules/active -> tenant's active modules + |-- POST /modules/:id/activate -> activate for tenant + |-- POST /modules/:id/deactivate -> deactivate for tenant + |-- GET /tenants -> all tenants (SUPER_ADMIN) + | + +-- TenantMiddleware + |-- Reads x-tenant-id header for Super-Admin context switch + |-- Falls back to JWT tenantId for regular users +``` + +### Recommended Project Structure + +``` +apps/web/src/ +├── app/(portal)/ +│ ├── marketplace/ +│ │ ├── page.tsx # Marketplace grid page +│ │ ├── [slug]/ +│ │ │ └── page.tsx # Module detail page +│ │ └── components/ +│ │ ├── MarketplaceCard.tsx # Card with activation controls +│ │ ├── MarketplaceSearch.tsx # Debounced search input +│ │ ├── CategoryFilter.tsx # Category chip row +│ │ ├── StatusFilter.tsx # Status tab bar +│ │ ├── TenantContextSelector.tsx # Super-Admin tenant dropdown +│ │ ├── ActivationDialog.tsx # Deactivation confirmation dialog +│ │ └── Toast.tsx # Toast notification component +│ └── ...existing routes... +├── components/layout/ +│ ├── sidebar.tsx # MODIFIED: add SidebarSearch, usePathname, module links +│ ├── sidebar-search.tsx # NEW: search input for sidebar +│ └── ...existing layout components... +├── lib/stores/ +│ └── marketplace-store.ts # NEW: Zustand store for marketplace state (optional) +└── messages/ + ├── de.json # MODIFIED: add marketplace translations + └── en.json # MODIFIED: add marketplace translations +``` + +### Pattern 1: Client-Side Filtering with Multiple Dimensions + +**What:** Marketplace loads all modules once, then filters client-side by search text, status tab, and category chip. Three filter dimensions compose together. + +**When to use:** Small dataset (modules are typically < 100), all data available from initial fetch, instant feedback needed. + +**Example:** +```typescript +// Source: Codebase convention — client-side state filtering +// All three filters compose: search AND status AND category +const filteredModules = useMemo(() => { + return allModules.filter((mod) => { + // Text search: name + description + const matchesSearch = !searchQuery || + mod.name.toLowerCase().includes(searchQuery.toLowerCase()) || + getLocalizedDescription(mod.description).toLowerCase().includes(searchQuery.toLowerCase()); + + // Status filter: 'all' | 'active' | 'available' + const matchesStatus = statusFilter === 'all' || + (statusFilter === 'active' && activationMap.has(mod.id)) || + (statusFilter === 'available' && !activationMap.has(mod.id)); + + // Category filter: 'all' | specific category slug + const matchesCategory = categoryFilter === 'all' || mod.category === categoryFilter; + + return matchesSearch && matchesStatus && matchesCategory; + }); +}, [allModules, searchQuery, statusFilter, categoryFilter, activationMap]); +``` + +### Pattern 2: Tenant Context Header for Super-Admin + +**What:** Super-Admin switches tenant context via dropdown. All subsequent API calls include `x-tenant-id` header. The existing TenantMiddleware already reads this header. + +**When to use:** When Super-Admin needs to manage modules for a specific tenant. + +**Example:** +```typescript +// Source: Existing pattern in apps/api/src/tenant/tenant.middleware.ts +// Frontend sends x-tenant-id header for Super-Admin context switching +const fetchModulesForTenant = async (tenantId: string) => { + const [allRes, activeRes] = await Promise.all([ + fetch(`${API_URL}/modules`, { credentials: 'include' }), + fetch(`${API_URL}/modules/active`, { + credentials: 'include', + headers: tenantId ? { 'x-tenant-id': tenantId } : {}, + }), + ]); + // ...process responses +}; +``` + +### Pattern 3: Sidebar Active State with usePathname + +**What:** Current page URL determines which sidebar item gets the active class. Uses `usePathname()` from `next/navigation`. + +**When to use:** Replace the hardcoded Dashboard active state with dynamic path matching. + +**Example:** +```typescript +// Source: Next.js App Router convention +import { usePathname } from 'next/navigation'; + +const pathname = usePathname(); +const isActive = (href: string) => { + if (href === '/') return pathname === '/'; + return pathname.startsWith(href); +}; + +// Usage in sidebar link: + +``` + +### Pattern 4: Hand-Rolled Toast System + +**What:** Lightweight toast notification system matching project convention (no external library). Uses a Zustand store or React context for state, CSS transitions for animations. + +**When to use:** Activation/deactivation feedback, error notifications. + +**Example:** +```typescript +// Toast store pattern — minimal Zustand approach +import { create } from 'zustand'; + +interface Toast { + id: string; + type: 'success' | 'error'; + message: string; +} + +interface ToastState { + toasts: Toast[]; + addToast: (type: Toast['type'], message: string) => void; + removeToast: (id: string) => void; +} + +export const useToastStore = create()((set) => ({ + toasts: [], + addToast: (type, message) => { + const id = crypto.randomUUID(); + set((s) => ({ toasts: [...s.toasts, { id, type, message }] })); + setTimeout(() => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); + }, 4000); + }, + removeToast: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), +})); +``` + +### Anti-Patterns to Avoid + +- **Fetching activation status per card individually:** All modules and their activation status should be fetched in parallel (one GET /modules + one GET /modules/active), then merged client-side. The admin/modules page already does this pattern with `Promise.all`. [VERIFIED: apps/web/src/app/(portal)/admin/modules/page.tsx] +- **Storing marketplace filter state in URL search params:** For this MVP, local React state is sufficient. URL params add complexity (back/forward, sharing URLs with filters). Keep it simple. +- **Building a full impersonation system:** CONTEXT.md explicitly defers this (FEAT-V2-04). The tenant context selector is ONLY a dropdown that changes which tenant's activation status is displayed/modified. +- **Re-fetching sidebar modules on every navigation:** The sidebar should re-fetch active modules only after an activation/deactivation action, not on every route change. Use a callback or event pattern. +- **Using `` tags instead of Next.js ``:** The current sidebar uses raw `` tags, which causes full page reloads. Phase 4 should migrate sidebar links to Next.js `` for client-side navigation. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Module registry API | New CRUD endpoints | Existing GET /modules, GET /modules/active, POST activate/deactivate | Fully implemented in Phase 3, tested and working [VERIFIED: module-registry.controller.ts] | +| Tenant context switching | Custom auth/session override | Existing x-tenant-id header in TenantMiddleware | Already supports Super-Admin context switching [VERIFIED: tenant.middleware.ts L33] | +| Tenant list for selector | New API endpoint | Existing GET /tenants (SUPER_ADMIN only) | Returns all tenants with user count [VERIFIED: tenant.controller.ts] | +| Module lazy loading | New import system | Existing MODULE_REGISTRY + loadModuleComponent | Whitelist-based dynamic imports [VERIFIED: lib/module-loader.ts] | +| Localized descriptions | Custom i18n for module data | Existing pattern: `description[locale]` fallback chain | Module.description is JSON with locale keys [VERIFIED: ModuleCard.tsx L73] | +| Design token system | New CSS variables | Existing OKLCH tokens in globals.css | Complete light/dark mode token set [VERIFIED: globals.css] | + +**Key insight:** This phase is 90% frontend work. The backend APIs, data models, middleware, and patterns all exist from Phase 2 and Phase 3. The only new code needed is the marketplace UI components, sidebar enhancements, and i18n translations. + +## Common Pitfalls + +### Pitfall 1: Sidebar Uses Raw `` Tags (Full Page Reloads) + +**What goes wrong:** The current sidebar.tsx uses raw HTML `` tags for all navigation links. This causes full-page reloads instead of client-side navigation. +**Why it happens:** Phase 1 implemented the sidebar skeleton without Next.js navigation patterns. +**How to avoid:** Replace all `` with Next.js `` in the sidebar. This is required for the usePathname-based active state to work correctly (no page reload means pathname persists in React state). +**Warning signs:** Page flickers on sidebar clicks; loading spinner appears on every navigation. + +### Pitfall 2: Race Condition in Activation + Sidebar Refresh + +**What goes wrong:** After activating a module, the marketplace card updates optimistically but the sidebar re-fetch may return stale data if the activation hasn't fully propagated. +**Why it happens:** POST activate returns the activation record, but GET /modules/active is a separate query. +**How to avoid:** After successful POST activate/deactivate, wait for the response, then trigger sidebar re-fetch. The optimistic update on the marketplace card provides immediate feedback while sidebar refresh catches up. Consider a shared event bus or callback. +**Warning signs:** Module shows as "Aktiviert" in marketplace but doesn't appear in sidebar. + +### Pitfall 3: Tenant Context Not Applied to All API Calls + +**What goes wrong:** Super-Admin changes tenant in the selector but only the module list updates; activation still targets the Super-Admin's own tenant. +**Why it happens:** The x-tenant-id header must be included in EVERY API call that should respect the selected tenant context (GET /modules/active AND POST activate/deactivate). +**How to avoid:** Create a wrapper function for API calls that automatically includes the x-tenant-id header based on the selected tenant. Store the selected tenant in React state, pass it through to all fetch calls. +**Warning signs:** Super-Admin activates module for "Firma A" but it activates for their own tenant. + +### Pitfall 4: Missing Locale Handling for Module Descriptions + +**What goes wrong:** Module description shows as `[object Object]` or is blank. +**Why it happens:** Module.description is a JSON field containing `{ "de": "...", "en": "..." }`. If rendered directly without locale extraction, it breaks. +**How to avoid:** Follow existing ModuleCard pattern: extract `description[locale] || description.en || description.de || ''`. The locale can be detected from `document.documentElement.lang` (existing pattern). +**Warning signs:** Cards show object notation or empty descriptions. + +### Pitfall 5: Categories Accordion State Reset on Re-render + +**What goes wrong:** Sidebar search filters modules, but when the search is cleared, all category accordions collapse to their default state. +**Why it happens:** The categories accordion state (open/closed) is local component state that resets when the component re-renders with new data. +**How to avoid:** Track which categories are expanded in stable state (set or map keyed by category name). When search filter changes, don't reset the accordion state. +**Warning signs:** User expands a category, types in search, clears search, and the category is collapsed again. + +## Code Examples + +### Verified Pattern: Parallel Module + Activation Fetch + +```typescript +// Source: apps/web/src/app/(portal)/admin/modules/page.tsx (existing pattern) +// Fetch all modules and active modules in parallel, merge into activation map +const [allRes, activeRes] = await Promise.all([ + fetch(`${API_URL}/modules`, { credentials: 'include' }), + fetch(`${API_URL}/modules/active`, { + credentials: 'include', + headers: selectedTenantId ? { 'x-tenant-id': selectedTenantId } : {}, + }), +]); + +const allModules: Module[] = allRes.ok ? await allRes.json() : []; +const activeModules: Module[] = activeRes.ok ? await activeRes.json() : []; + +// Build activation map: moduleId -> boolean +const activationMap = new Map(); +for (const mod of activeModules) { + activationMap.set(mod.id, true); +} +``` + +### Verified Pattern: Module Card with Localized Description + +```typescript +// Source: apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx +const locale = (typeof window !== 'undefined' && document.documentElement.lang) || 'de'; +const localizedDescription = typeof description === 'string' + ? description + : description[locale] || description.en || description.de || ''; +``` + +### Verified Pattern: Toggle Activation API Call + +```typescript +// Source: apps/web/src/app/(portal)/admin/modules/page.tsx +const action = currentlyActive ? 'deactivate' : 'activate'; +const res = await fetch(`${API_URL}/modules/${moduleId}/${action}`, { + method: 'POST', + credentials: 'include', + headers: selectedTenantId ? { 'x-tenant-id': selectedTenantId } : {}, +}); +``` + +### Verified Pattern: Tenant List Fetch (Super-Admin) + +```typescript +// Source: apps/api/src/tenant/tenant.controller.ts — GET /tenants returns: +// [{ id, name, slug, isActive, createdAt, userCount }] +const fetchTenants = async (): Promise => { + const res = await fetch(`${API_URL}/tenants`, { credentials: 'include' }); + if (!res.ok) return []; + return res.json(); +}; +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Sidebar uses `` tags | Should use Next.js `` | Phase 4 (now) | Enables client-side navigation, usePathname active state | +| Sidebar Dashboard always "active" | Dynamic active state via usePathname | Phase 4 (now) | Correct visual feedback for current page | +| Category-only links in sidebar | Per-module links under each category | Phase 4 (now) | Direct module navigation from sidebar | +| Admin modules page for activation | Marketplace page with richer UX | Phase 4 (now) | Better discovery, search, filtering for end users | + +**Deprecated/outdated:** +- Admin modules page remains available but marketplace becomes the primary discovery/activation interface for users +- Raw `` tags in sidebar should be replaced with `` for performance + +## Assumptions Log + +> List all claims tagged `[ASSUMED]` in this research. The planner and discuss-phase use this +> section to identify decisions that need user confirmation before execution. + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | Toast system can be implemented as a simple Zustand store + positioned div, no library needed | Architecture Patterns | Low -- toast is a simple UI pattern, worst case is slightly rougher animations than a library provides | +| A2 | Deactivation confirmation dialog focus trap can be hand-rolled in ~10 lines | Architecture Patterns | Low -- if accessibility requirements demand more robust focus management, could add @radix-ui/react-dialog later | +| A3 | All modules fit on one page without pagination | Architecture Patterns | Low -- v1 will have very few modules. If wrong, add client-side pagination later | + +**If this table is empty:** Most claims in this research were verified directly from the codebase -- minimal assumptions needed. + +## Open Questions + +1. **Sidebar Module Refresh Mechanism** + - What we know: Sidebar fetches `/modules/active` on mount. Admin modules page updates state locally after toggle. + - What's unclear: How to signal sidebar to re-fetch after marketplace activation. Options: (a) poll on interval, (b) event bus, (c) shared Zustand store that both marketplace and sidebar read, (d) callback passed via context. + - Recommendation: Use a shared Zustand store or a simple `useCallback` passed through props. Zustand is already the state management pattern in this project. + +2. **Marketplace Page vs. Admin Modules Page Overlap** + - What we know: Admin modules page (admin/modules) already allows activation toggle. Marketplace adds richer UX. + - What's unclear: Should admin modules page be kept as-is, deprecated, or redirect to marketplace? + - Recommendation: Keep both. Admin modules page is a quick admin-only toggle view. Marketplace is the user-facing discovery interface. They serve different use cases. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Node.js | Next.js, NestJS | yes | 24.16.0 | -- | +| pnpm | Package management | yes | 9.15.0 | -- | +| Docker | Container runtime | yes | 29.5.3 | -- | +| Docker Compose | Service orchestration | yes | 5.1.4 | -- | +| Turborepo | Build orchestration | yes | 2.9.18 | -- | + +**Missing dependencies with no fallback:** none +**Missing dependencies with fallback:** none + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Vitest (specified in CLAUDE.md, not yet installed) | +| Config file | none -- see Wave 0 | +| Quick run command | `pnpm vitest run --reporter=verbose` | +| Full suite command | `pnpm vitest run` | + +### Phase Requirements to Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| MRKT-01 | Marketplace grid renders all modules with descriptions and categories | unit | `pnpm vitest run apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx -x` | -- Wave 0 | +| MRKT-02 | Activation toggle calls correct API endpoint with tenant context | unit | `pnpm vitest run apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx -x` | -- Wave 0 | +| MRKT-03 | Sidebar shows only active modules | unit | `pnpm vitest run apps/web/src/components/layout/sidebar.test.tsx -x` | -- Wave 0 | +| MRKT-04 | Modules grouped by category in grid and sidebar | unit | `pnpm vitest run apps/web/src/app/(portal)/marketplace/marketplace.test.tsx -x` | -- Wave 0 | +| PRTAL-02 | Sidebar displays categories and activated module links | unit | `pnpm vitest run apps/web/src/components/layout/sidebar.test.tsx -x` | -- Wave 0 | +| PRTAL-03 | Module opens in main content area, sidebar active state updates | integration | Manual verification -- requires full Next.js routing | -- Manual | +| PRTAL-05 | Sidebar search filters modules by name | unit | `pnpm vitest run apps/web/src/components/layout/sidebar-search.test.tsx -x` | -- Wave 0 | + +### Sampling Rate + +- **Per task commit:** `pnpm vitest run --reporter=verbose` (if test infra exists) +- **Per wave merge:** Full test suite +- **Phase gate:** Full suite green + manual verification of navigation flows + +### Wave 0 Gaps + +- [ ] Vitest installation: `pnpm add -D vitest @testing-library/react @testing-library/jest-dom jsdom` in apps/web +- [ ] `apps/web/vitest.config.ts` -- Vitest config for Next.js with jsdom environment +- [ ] `apps/web/src/test/setup.ts` -- Test setup with @testing-library/jest-dom matchers +- [ ] Test utilities for mocking fetch, Zustand stores, and next-intl + +## Security Domain + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | yes (existing) | JWT auth via global JwtAuthGuard -- no changes needed | +| V3 Session Management | no | No new session handling in this phase | +| V4 Access Control | yes | RolesGuard on activate/deactivate endpoints (ADMIN/SUPER_ADMIN). TenantMiddleware for tenant isolation. x-tenant-id only respected for SUPER_ADMIN role. [VERIFIED: module-registry.controller.ts, tenant.middleware.ts] | +| V5 Input Validation | yes | Module ID validated as UUID by Prisma parameterized queries. Search input is client-side only (no server-side SQL). Category filter uses module data, not user input for DB queries. | +| V6 Cryptography | no | No cryptographic operations in this phase | + +### Known Threat Patterns for This Phase + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| Tenant context spoofing via x-tenant-id header | Elevation of Privilege | TenantMiddleware checks `user.role === 'SUPER_ADMIN'` before reading header [VERIFIED: tenant.middleware.ts L33] | +| Unauthorized module activation | Elevation of Privilege | RolesGuard requires ADMIN or SUPER_ADMIN [VERIFIED: module-registry.controller.ts] | +| XSS via module description | Tampering | Module descriptions are rendered as text content (not innerHTML). React auto-escapes. | +| CSRF on activation endpoints | Tampering | Cookie-based auth with `credentials: 'include'` + same-origin policy. POST endpoints require valid JWT. | + +## Sources + +### Primary (HIGH confidence) +- `apps/api/src/module-registry/module-registry.controller.ts` -- Full module registry API with activate/deactivate +- `apps/api/src/module-registry/module-registry.service.ts` -- Service with findAll, findActiveForTenant, activateForTenant, deactivateForTenant +- `apps/api/src/tenant/tenant.middleware.ts` -- TenantMiddleware with x-tenant-id header support for SUPER_ADMIN +- `apps/api/src/tenant/tenant.controller.ts` -- GET /tenants for Super-Admin tenant list +- `apps/api/prisma/schema.prisma` -- Module + TenantModuleActivation models +- `apps/web/src/components/layout/sidebar.tsx` -- Current sidebar implementation with category accordion +- `apps/web/src/app/(portal)/admin/modules/page.tsx` -- Existing activation toggle pattern +- `apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx` -- Module card pattern +- `apps/web/src/lib/module-loader.ts` -- MODULE_REGISTRY and lazy loading +- `apps/web/src/app/globals.css` -- OKLCH design tokens, layout variables +- `apps/web/src/lib/stores/auth-store.ts` -- AuthUser with role and tenantId +- `.planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md` -- UI design contract +- `.planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md` -- User decisions + +### Secondary (MEDIUM confidence) +- `.planning/phases/01-foundation-portal-shell/01-CONTEXT.md` -- Phase 1 design decisions (sidebar, header, color scheme) +- `.planning/phases/02-authentication-multi-tenancy/02-CONTEXT.md` -- Auth/RBAC decisions (SUPER_ADMIN, ADMIN, USER roles) +- `.planning/phases/03-module-system-domaincheck/03-CONTEXT.md` -- Module system decisions (SDK, registry, categories) + +### Tertiary (LOW confidence) +- None -- all critical findings verified from codebase + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH -- all libraries verified from package.json, no new dependencies needed +- Architecture: HIGH -- all backend APIs verified from source code, patterns established in previous phases +- Pitfalls: HIGH -- identified from direct codebase analysis (raw `` tags, missing usePathname, race conditions) + +**Research date:** 2026-06-22 +**Valid until:** 2026-07-22 (stable -- no external API changes, pure frontend work)