Files
tessera-ctl/.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md
T
2026-06-22 14:06:42 +02:00

569 lines
34 KiB
Markdown

# 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>
## 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.
</user_constraints>
<phase_requirements>
## 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. |
</phase_requirements>
## 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:
<a href={href} className={cn(
'flex items-center gap-3 rounded-md px-2 py-2 text-sm transition-colors',
isActive(href)
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
: 'text-sidebar-foreground hover:bg-muted'
)}>
```
### 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<ToastState>()((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 `<a>` tags instead of Next.js `<Link>`:** The current sidebar uses raw `<a>` tags, which causes full page reloads. Phase 4 should migrate sidebar links to Next.js `<Link>` 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 `<a>` Tags (Full Page Reloads)
**What goes wrong:** The current sidebar.tsx uses raw HTML `<a>` 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 `<a href>` with Next.js `<Link href>` 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<string, boolean>();
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<Tenant[]> => {
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 `<a>` tags | Should use Next.js `<Link>` | 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 `<a>` tags in sidebar should be replaced with `<Link>` 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 `<a>` tags, missing usePathname, race conditions)
**Research date:** 2026-06-22
**Valid until:** 2026-07-22 (stable -- no external API changes, pure frontend work)