Files
tessera-ctl/.planning/phases/04-marketplace-portal-navigation/04-01-PLAN.md
T

20 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
04-marketplace-portal-navigation 01 execute 1
apps/web/package.json
apps/web/vitest.config.ts
apps/web/src/test/setup.ts
apps/web/src/messages/de.json
apps/web/src/messages/en.json
apps/web/src/lib/stores/marketplace-store.ts
apps/web/src/app/(portal)/marketplace/page.tsx
apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx
apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx
apps/web/src/app/(portal)/marketplace/marketplace.test.tsx
true
MRKT-01
MRKT-02
MRKT-04
truths artifacts key_links
User can navigate to /marketplace and see a card grid of all available modules with name, description and category badge
Each card shows whether the module is activated (green status badge) or available for the current tenant
Admin can click the activate button on an available module card and it becomes activated (POST /modules/:id/activate)
Modules are grouped/labelled by category via a category badge on each card
path provides min_lines
apps/web/src/app/(portal)/marketplace/page.tsx Marketplace grid page with parallel fetch of /modules + /modules/active, activation map, role gate 60
path provides min_lines
apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx Module card with icon, name, localized description, category badge, status badge, activate button 40
path provides contains
apps/web/src/lib/stores/marketplace-store.ts Zustand store with sidebarRefreshKey + bumpSidebarRefresh() to signal sidebar re-fetch after activation bumpSidebarRefresh
path provides contains
apps/web/vitest.config.ts Vitest config for Next.js with jsdom environment jsdom
path provides contains
apps/web/src/messages/de.json marketplace + sidebar.search i18n namespace (DE) marketplace
from to via pattern
apps/web/src/app/(portal)/marketplace/page.tsx /modules and /modules/active Promise.all parallel fetch with credentials: include modules/active
from to via pattern
apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx /modules/:id/activate POST fetch on activate click modules/${?.*}?/(activate|deactivate)
Stand up the test infrastructure (Vitest), the marketplace i18n namespace, and the first end-to-end marketplace slice: a user opens `/marketplace`, sees a responsive card grid of all modules with category and activation status, and an admin can activate an available module.

Purpose: Delivers the core marketplace browsing + activation capability (MRKT-01, MRKT-02 base, MRKT-04). This is the foundational vertical slice that Plan 02 (filters/detail/tenant-context) and Plan 03 (sidebar) build on. Output: Vitest config + setup, marketplace + sidebar.search translations, marketplace-store (sidebar refresh signal), /marketplace page, MarketplaceCard component, and unit tests.

Phase Goal

As a Tessera user (admin), I want to browse all available modules in a categorized marketplace and activate them for my tenant, so that activated modules become usable in the portal.

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-marketplace-portal-navigation/04-CONTEXT.md @.planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md @.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md @.planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md Task 1: Install and configure Vitest test infrastructure for apps/web - apps/web/package.json (current scripts + dependencies — no test runner exists yet) - package.json (root turbo scripts — note no `test` pipeline yet) - apps/web/src/messages/de.json (existing namespace structure for mocking next-intl in tests) - .planning/phases/04-marketplace-portal-navigation/04-RESEARCH.md (Validation Architecture section: Wave 0 gaps, quick run command) Install dev dependencies in `apps/web` via `pnpm --filter @tessera/web add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @vitejs/plugin-react` (use the actual package name from apps/web/package.json `name` field for the --filter target). All five packages are mainstream, high-download dev tooling — see Package Legitimacy Audit in 04-RESEARCH.md which records zero new runtime packages; these are dev-only test tooling. Create `apps/web/vitest.config.ts` configuring the `@vitejs/plugin-react` plugin, `test.environment` set to `jsdom`, `test.globals` true, `test.setupFiles` pointing to `./src/test/setup.ts`, and a resolve alias mapping `@` to `./src` (matching the tsconfig path alias used across the app). Create `apps/web/src/test/setup.ts` importing `@testing-library/jest-dom/vitest` so DOM matchers are registered. Add a `"test": "vitest run"` script to `apps/web/package.json` scripts. Add a `"test": "turbo test"` pipeline entry to root `package.json` scripts and a `test` task to turbo.json if a turbo config exists. Do NOT add any runtime dependencies — this phase ships zero new runtime packages. cd apps/web && pnpm vitest run --reporter=verbose 2>&1 | grep -qiE "no test files|passed|0 passed" && echo VITEST_OK - apps/web/package.json devDependencies contains `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom`, `@vitejs/plugin-react` - apps/web/vitest.config.ts exists and contains the string `jsdom` - apps/web/src/test/setup.ts contains `@testing-library/jest-dom` - apps/web/package.json scripts contains `"test"` - `cd apps/web && pnpm vitest run` exits 0 (no failing tests; "no test files" is acceptable at this point) Vitest runs in apps/web with jsdom + jest-dom matchers; `pnpm vitest run` exits 0. Task 2: Add marketplace i18n namespace and build MarketplaceCard component - apps/web/src/messages/de.json (existing namespaces: common, sidebar, modules, adminModules — add new `marketplace` namespace and `sidebar.search` key) - apps/web/src/messages/en.json (mirror structure for EN) - apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx (analog: card structure, ModuleIcon inline SVG, locale extraction `description[locale] || description.en || description.de`) - .planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md (MarketplaceCard Anatomy section: padding p-5, status badge classes, activate/deactivate button classes; Copywriting Contract for all DE/EN strings) - .planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md (MarketplaceCard pattern, CSS Token Convention, status color exception) - Test 1: MarketplaceCard renders the module name and the localized description (de locale) as text content - Test 2: MarketplaceCard renders a category badge showing the module category - Test 3: When `isActive` is false, the card shows the activate button with the "Modul aktivieren" label and an "available" status badge - Test 4: When `isActive` is true, the card shows a green "Aktiviert" status badge and the deactivate label "Deaktivieren" - Test 5: Clicking the activate button calls the `onToggle` prop with the module id and the current active state Add a `marketplace` namespace to both `apps/web/src/messages/de.json` and `en.json` containing every key from the 04-UI-SPEC Copywriting Contract: `title`, `subtitle`, `activate`, `deactivate`, `searchPlaceholder`, status tab keys (`statusAll`, `statusActive`, `statusAvailable`), `categoryAll`, empty-state keys (`emptyTitle`, `emptyBody`, `filteredEmptyTitle`, `filteredEmptyBody`), `errorLoad`, toast keys (`toastActivated`, `toastDeactivated`, `toastActivateError`, `toastDeactivateError`), deactivation dialog keys (`deactivateDialogTitle`, `deactivateDialogBody` with `{moduleName}` placeholder, `deactivateDialogConfirm`, `deactivateDialogCancel`), `tenantContextLabel`, `backToMarketplace`, detail keys (`detailVersion` with `{version}`, `detailStatusActive`, `detailStatusInactive`). Use the exact DE/EN strings from the Copywriting Contract. Also add a `search` key under the existing `sidebar` namespace (DE "Module suchen...", EN "Search modules...") and `sidebar.noResults` (DE "Keine Module gefunden", EN "No modules found"). Create `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx` as a `'use client'` component. Props: `{ id: string; name: string; slug: string; description: Record; icon?: string; category: string; isActive: boolean; isToggling: boolean; onToggle: (id: string, currentlyActive: boolean) => void }`. Reuse the ModuleIcon inline-SVG pattern and locale extraction from ModuleCard.tsx (`const locale = (typeof window !== 'undefined' && document.documentElement.lang) || 'de'`). Render: icon container `rounded-md bg-muted p-2.5`, module name `font-semibold text-base`, a category badge `rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground`, a status badge (active: `bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400`; available: `bg-muted text-muted-foreground`) with `role="status"`, the localized description with `line-clamp-2`, the version text, and an action button. Active card shows the deactivate button (`border border-border text-foreground hover:bg-muted`), inactive shows the activate button (`bg-primary text-primary-foreground`). When `isToggling`, disable the button and show a spinner. Button click calls `onToggle(id, isActive)`. Use `useTranslations('marketplace')`. Card container classes per UI-SPEC: `rounded-lg border border-border bg-card p-5 shadow-sm transition-all hover:shadow-md hover:border-primary/30`, min-height 180px. Create `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx` implementing the five tests in . Mock `next-intl`'s `useTranslations` to return the key (or a minimal lookup) so labels are assertable. Do NOT inline-render `description` directly (it is a JSON object — would render `[object Object]`); always extract the locale value. cd apps/web && pnpm vitest run "src/app/(portal)/marketplace/components/MarketplaceCard.test.tsx" 2>&1 | grep -qiE "passed" && echo CARD_TESTS_OK - apps/web/src/messages/de.json contains a `marketplace` object with key `activate` equal to "Modul aktivieren" - apps/web/src/messages/en.json contains a `marketplace` object with key `activate` equal to "Activate Module" - apps/web/src/messages/de.json `sidebar` object contains key `search` equal to "Module suchen..." - apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx contains `role="status"` and `line-clamp-2` - MarketplaceCard.tsx extracts localized description (contains `description.en` or `description[locale]` fallback chain) - `pnpm vitest run` on MarketplaceCard.test.tsx reports all 5 tests passed MarketplaceCard renders name, localized description, category + status badges, and activate/deactivate button; clicking calls onToggle; all 5 unit tests pass. Task 3: Build the /marketplace page and marketplace-store, wiring browse + activate end-to-end - apps/web/src/app/(portal)/admin/modules/page.tsx (analog: parallel Promise.all fetch of /modules + /modules/active, activation Map, toggleModule, role gate `isAdmin`, error banner) - apps/web/src/lib/stores/sidebar-store.ts (Zustand persist pattern) and apps/web/src/lib/stores/auth-store.ts (non-persisted store pattern, AuthUser.role) - apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx (created Task 2 — the card this page renders) - apps/web/src/app/(portal)/modules/[category]/page.tsx (loading spinner + empty-state pattern, responsive grid `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`) - .planning/phases/04-marketplace-portal-navigation/04-UI-SPEC.md (Marketplace Page layout, grid breakpoints 1/2/3/4 cols, States Matrix) - .planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md (marketplace/page.tsx pattern, API Fetch Convention) - Test 1: When fetch returns a list of modules, the page renders one MarketplaceCard per module (assert by module name text) - Test 2: When /modules/active returns one of the modules, that module's card shows the activated status (green badge / deactivate label) - Test 3: When a non-admin user is in the auth store, the page renders the access-denied message and no cards - Test 4: When the modules list is empty, the page renders the empty-state heading from the marketplace namespace Create `apps/web/src/lib/stores/marketplace-store.ts` as a non-persisted Zustand store: `{ sidebarRefreshKey: number; bumpSidebarRefresh: () => void; selectedTenantId: string | null; setSelectedTenantId: (id: string | null) => void }`. `bumpSidebarRefresh` increments `sidebarRefreshKey`. This store is the signal Plan 03's sidebar subscribes to for re-fetch after activation (avoids the race-condition pitfall in 04-RESEARCH.md Pitfall 2), and holds the Super-Admin tenant context used by Plan 02. Export `useMarketplaceStore`. Create `apps/web/src/app/(portal)/marketplace/page.tsx` as a `'use client'` component following the admin/modules/page.tsx pattern: `const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'`. State: `modules`, `activations` (Map), `loading`, `toggling`, `error`. `fetchModules` does `Promise.all([fetch('/modules'), fetch('/modules/active')])` with `credentials: 'include'`; read `selectedTenantId` from useMarketplaceStore and pass `{ 'x-tenant-id': selectedTenantId }` header on the `/modules/active` call when set (header omitted when null). Build the activation Map from the active response. `toggleModule(id, currentlyActive)` POSTs to `/modules/${id}/${currentlyActive ? 'deactivate' : 'activate'}` with `credentials: 'include'` and the same optional x-tenant-id header; on success update the activation Map optimistically and call `bumpSidebarRefresh()` so the sidebar re-fetches. Role-gate with `isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN'` (from auth-store) — non-admin renders the common `accessDenied` message. Render page header (h1 `text-2xl font-bold` title, subtitle), error banner (`border-destructive/50 bg-destructive/10`), loading spinner, empty-state (illustration + `emptyTitle`/`emptyBody` from marketplace namespace), and the responsive card grid `grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4` mapping modules to `` passing `isActive={activations.has(mod.id)}` and `onToggle={toggleModule}`. Filters and tenant selector are added in Plan 02 — keep this page rendering all modules for now. Create `apps/web/src/app/(portal)/marketplace/marketplace.test.tsx` implementing the four tests in . Mock global `fetch`, the auth-store (`useAuthStore`), and `next-intl`. Do NOT fetch activation status per-card; use one parallel fetch and a client-side Map (04-RESEARCH.md anti-pattern). cd apps/web && pnpm vitest run "src/app/(portal)/marketplace/marketplace.test.tsx" 2>&1 | grep -qiE "passed" && echo PAGE_TESTS_OK - apps/web/src/lib/stores/marketplace-store.ts contains `bumpSidebarRefresh` and `selectedTenantId` - apps/web/src/app/(portal)/marketplace/page.tsx contains `Promise.all` and `modules/active` - page.tsx calls `bumpSidebarRefresh` after a successful toggle - page.tsx renders the access-denied message for non-admin (`role` check against ADMIN/SUPER_ADMIN) - `pnpm vitest run` on marketplace.test.tsx reports all 4 tests passed - `cd apps/web && pnpm type-check` exits 0 Visiting /marketplace as an admin shows a card grid of all modules with correct activation status; activating a module POSTs to the activate endpoint, updates the card, and bumps the sidebar refresh signal; non-admins see access denied; all 4 page tests pass.

<artifacts_this_phase_produces> Symbols created by this plan (excluded from drift verification by downstream review):

  • MarketplaceCard (React component) — apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx
  • MarketplacePage default export — apps/web/src/app/(portal)/marketplace/page.tsx
  • useMarketplaceStore (Zustand) with fields sidebarRefreshKey, bumpSidebarRefresh, selectedTenantId, setSelectedTenantId — apps/web/src/lib/stores/marketplace-store.ts
  • i18n namespace marketplace and keys sidebar.search, sidebar.noResults in de.json / en.json
  • New file paths: apps/web/vitest.config.ts, apps/web/src/test/setup.ts, marketplace.test.tsx, MarketplaceCard.test.tsx
  • New npm dev scripts: test in apps/web/package.json and root package.json </artifacts_this_phase_produces>

<threat_model>

Trust Boundaries

Boundary Description
Browser → NestJS API Authenticated user actions (browse, activate) cross from untrusted client to backend

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-04-01 Elevation of Privilege POST /modules/:id/activate mitigate Backend RolesGuard already requires ADMIN/SUPER_ADMIN (Phase 3, VERIFIED in module-registry.controller.ts). Frontend additionally role-gates the page with isAdmin check; non-admins get accessDenied. Defense-in-depth — backend is authoritative.
T-04-02 Tampering (XSS) MarketplaceCard description mitigate Module description rendered as React text content (not dangerouslySetInnerHTML); React auto-escapes. Locale value extracted as string.
T-04-03 Spoofing x-tenant-id header on activate mitigate TenantMiddleware only honors x-tenant-id when user.role === 'SUPER_ADMIN' (VERIFIED tenant.middleware.ts L33). Regular admin's header is ignored; their own JWT tenantId is used.
T-04-SC Tampering npm dev-dependency installs (vitest et al.) mitigate All five installs are mainstream test tooling with millions of weekly downloads; 04-RESEARCH Package Legitimacy Audit records zero new runtime packages. Dev-only, not shipped to production bundle. No [ASSUMED]/[SUS] packages introduced.
</threat_model>
- `cd apps/web && pnpm vitest run` exits 0 with all marketplace tests passing - `cd apps/web && pnpm type-check` exits 0 - Manual: visit `/marketplace` logged in as admin — card grid renders, activation toggles status and persists across page reload

<success_criteria>

  • Vitest test infra runs in apps/web
  • /marketplace route shows all modules as cards with category + status badges (MRKT-01, MRKT-04)
  • Admin can activate a module from a card (MRKT-02 base)
  • marketplace-store signals sidebar refresh and holds tenant context for downstream plans
  • All unit tests green, type-check clean </success_criteria>
Create `.planning/phases/04-marketplace-portal-navigation/04-01-SUMMARY.md` when done