--- phase: 03-module-system-domaincheck plan: 03 type: execute wave: 2 depends_on: ["03-01"] files_modified: - apps/web/src/app/(portal)/modules/[category]/page.tsx - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx - apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx - apps/web/src/lib/module-loader.ts - apps/web/src/lib/api.ts - apps/web/messages/de.json - apps/web/messages/en.json autonomous: true requirements: - MOD-04 must_haves: truths: - "Module UIs load on demand via dynamic imports — inactive modules are never bundled" - "Category page shows module cards in a grid layout" - "Module can be opened in expanded view from category page" artifacts: - path: "apps/web/src/lib/module-loader.ts" provides: "Dynamic import registry mapping slugs to lazy components" exports: ["loadModuleComponent", "loadModuleCard", "MODULE_REGISTRY"] - path: "apps/web/src/app/(portal)/modules/[category]/page.tsx" provides: "Category page showing activated module cards" - path: "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx" provides: "Expanded module view using lazy-loaded component" key_links: - from: "apps/web/src/lib/module-loader.ts" to: "next/dynamic" via: "dynamic import with ssr:false" pattern: "dynamic.*import" - from: "apps/web/src/app/(portal)/modules/[category]/page.tsx" to: "/modules/active" via: "API fetch for tenant's active modules" pattern: "fetch.*modules/active" --- Deliver lazy-loaded module UI rendering — category pages show module cards from the registry, and modules load on demand via Next.js dynamic imports. No bundle bloat from inactive modules. Purpose: Per D-04, D-05a, D-05b and MOD-04 — modules appear as cards within category pages and can expand to full view, all lazy-loaded. Output: Module loader utility, category page with dynamic routing, module card component. @/home/vicolab/.claude/gsd-core/workflows/execute-plan.md @/home/vicolab/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/03-module-system-domaincheck/03-CONTEXT.md @.planning/phases/03-module-system-domaincheck/03-RESEARCH.md @.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md @apps/web/src/app/(portal)/layout.tsx @apps/web/src/app/(portal)/page.tsx ## Phase Goal **As a** user, **I want to** browse module categories and open modules without page bloat, **so that** only active modules load their UI code on demand. Task 1: Module Loader Utility + Category Page with Lazy Cards apps/web/src/lib/module-loader.ts, apps/web/src/app/(portal)/modules/[category]/page.tsx, apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx, apps/web/messages/de.json, apps/web/messages/en.json apps/web/src/app/(portal)/layout.tsx, apps/web/src/app/(portal)/page.tsx, apps/web/messages/de.json 1. Create apps/web/src/lib/module-loader.ts: - MODULE_REGISTRY: Record mapping module slugs to { component: () => import(...), cardComponent: () => import(...) } - For domaincheck: component maps to dynamic(() => import('@/app/(portal)/modules/domaincheck/page'), { ssr: false }), cardComponent maps to a DomaincheckCard component - Export loadModuleComponent(slug: string): returns the dynamic component or null if not in registry - Export loadModuleCard(slug: string): returns the card component or a generic fallback card - This is the lazy loading mechanism — components are only imported when rendered (per MOD-04) 2. Add i18n keys to both message files under "modules" namespace: - DE: categoryTitle: "Module: {category}", openModule: "Oeffnen", noModules: "Keine aktiven Module in dieser Kategorie" - EN: categoryTitle: "Modules: {category}", openModule: "Open", noModules: "No active modules in this category" 3. Create apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx: - Client component ("use client") - Props: module object (name, slug, description, icon, category) - Renders a shadcn/ui Card with: icon area, module name, description (from current locale), "Open" link to /modules/{category}/{slug} - Per D-05a: uniform card appearance, compact, looks good even with single module - Uses Tailwind for consistent spacing and theming 4. Create apps/web/src/app/(portal)/modules/[category]/page.tsx: - Server component that receives params.category - Fetches active modules for current tenant from API (GET /modules/active), filters by category matching params.category - Renders a responsive grid of ModuleCard components (grid-cols-1 sm:grid-cols-2 lg:grid-cols-3) - Empty state shows "noModules" i18n message - Title shows category name formatted (capitalize, replace hyphens with spaces) - Per D-04: category page groups all modules of that category cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5 - Web build succeeds with dynamic route [category] - /modules/domain-tools page fetches and displays active modules filtered by "domain-tools" category - ModuleCard renders module info with link to expanded view - module-loader.ts maps 'domaincheck' slug to lazy-imported components - Inactive modules never trigger an import (verified by MODULE_REGISTRY only containing active slug mappings at build time) Category page at /modules/domain-tools shows domaincheck as a card, lazy loading is wired via module-loader registry Task 2: Expanded Module View with Dynamic Routing apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx, apps/web/src/lib/api.ts apps/web/src/lib/module-loader.ts, apps/web/src/app/(portal)/modules/[category]/page.tsx 1. Create or extend apps/web/src/lib/api.ts (if not existing): - Export getActiveModules(cookie: string): fetches GET /modules/active with auth cookie, returns module array - Export getModuleBySlug(slug: string, cookie: string): fetches GET /modules, finds by slug, returns module or null - Base URL from NEXT_PUBLIC_API_URL env var 2. Create apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx: - Per D-05b: expanded/full view when user opens a specific module - Server component wrapper that validates moduleSlug exists in registry - Uses loadModuleComponent(params.moduleSlug) from module-loader.ts to get the lazy component - If module not found in registry: show 404/not-found state - If found: render the dynamically imported component within a container - The loaded component for 'domaincheck' is the DomaincheckPage from Plan 02 - Wrapped in shadcn/ui Card with back-link to category page 3. Ensure the domaincheck page.tsx from Plan 02 can function both as standalone (/modules/domaincheck direct access) AND as the loaded component in expanded view. The [moduleSlug] route provides the canonical expanded path per D-05b: /modules/domain-tools/domaincheck cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5 - /modules/domain-tools/domaincheck route renders the domaincheck module component - Module component loads lazily (not in initial bundle of category page) - Unknown module slugs show appropriate not-found state - Back-link navigates to category page /modules/domain-tools User can navigate from category card grid to expanded module view. Module UI code loads on demand only when opened (MOD-04). ## Trust Boundaries | Boundary | Description | |----------|-------------| | URL params -> module loader | User can craft arbitrary slugs in URL | | API -> frontend | Module list from API determines what renders | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-03-09 | Tampering | [moduleSlug] param | mitigate | Only load from MODULE_REGISTRY whitelist — arbitrary slugs get not-found, no arbitrary imports | | T-03-10 | Elevation of Privilege | category page | mitigate | API filters by tenant's active modules — inactive modules never returned, cards never rendered | | T-03-11 | Information Disclosure | module metadata | accept | Module names/descriptions are non-sensitive catalog data | - pnpm --filter web build succeeds - /modules/domain-tools renders category page with module cards - /modules/domain-tools/domaincheck loads domaincheck UI lazily - Bundle analysis confirms domaincheck code not in category page chunk - Module UIs load on demand via next/dynamic with ssr:false (MOD-04) - Category page displays modules as cards per D-04, D-05a - Expanded view available per D-05b - No bundle impact from inactive/unregistered modules ## Artifacts this phase produces | Symbol | Location | Type | |--------|----------|------| | MODULE_REGISTRY | apps/web/src/lib/module-loader.ts | const | | loadModuleComponent | apps/web/src/lib/module-loader.ts | function | | loadModuleCard | apps/web/src/lib/module-loader.ts | function | | CategoryPage | apps/web/src/app/(portal)/modules/[category]/page.tsx | React component | | ModuleCard | apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx | React component | | ExpandedModulePage | apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx | React component | | getActiveModules | apps/web/src/lib/api.ts | function | | getModuleBySlug | apps/web/src/lib/api.ts | function | Create `.planning/phases/03-module-system-domaincheck/03-03-SUMMARY.md` when done