Files
schalli 89f559ce62 docs(03): create phase plan for Module System & Domaincheck
4 plans across 3 waves: SDK + registry (W1), Domaincheck module +
lazy loading (W2), visual verification (W3). Covers MOD-01..04,
DCHK-01..03.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:13:20 +02:00

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-module-system-domaincheck 03 execute 2
03-01
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
true
MOD-04
truths artifacts key_links
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
path provides exports
apps/web/src/lib/module-loader.ts Dynamic import registry mapping slugs to lazy components
loadModuleComponent
loadModuleCard
MODULE_REGISTRY
path provides
apps/web/src/app/(portal)/modules/[category]/page.tsx Category page showing activated module cards
path provides
apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx Expanded module view using lazy-loaded component
from to via pattern
apps/web/src/lib/module-loader.ts next/dynamic dynamic import with ssr:false dynamic.*import
from to via pattern
apps/web/src/app/(portal)/modules/[category]/page.tsx /modules/active API fetch for tenant's active modules 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.

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

@.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).

<threat_model>

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

<success_criteria>

  • 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 </success_criteria>

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