docs(04): research phase domain — marketplace & portal navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
## 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)
|
||||||
Reference in New Issue
Block a user