diff --git a/.planning/STATE.md b/.planning/STATE.md index 771d6d1..8f087d5 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,7 +4,7 @@ milestone: v1.0 milestone_name: milestone status: verifying stopped_at: Phase 4 UI-SPEC approved -last_updated: "2026-06-22T11:55:16.375Z" +last_updated: "2026-06-22T12:19:54.125Z" last_activity: 2026-06-20 -- Phase 03 human verification passed progress: total_phases: 6 diff --git a/.planning/phases/04-marketplace-portal-navigation/04-01-PLAN.md b/.planning/phases/04-marketplace-portal-navigation/04-01-PLAN.md index b0f5efc..452c102 100644 --- a/.planning/phases/04-marketplace-portal-navigation/04-01-PLAN.md +++ b/.planning/phases/04-marketplace-portal-navigation/04-01-PLAN.md @@ -19,10 +19,11 @@ autonomous: true requirements: [MRKT-01, MRKT-02, MRKT-04] must_haves: truths: - - "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" + - "User can navigate to /marketplace and see a card grid of all available modules with name, description and category badge (D-01)" + - "Each card shows whether the module is activated (green status badge) or available for the current tenant (D-01)" - "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" + - "Modules are grouped/labelled by category via a category badge on each card (D-01)" + - "Empty state shows friendly illustration + text when no modules available (D-03)" artifacts: - path: "apps/web/src/app/(portal)/marketplace/page.tsx" provides: "Marketplace grid page with parallel fetch of /modules + /modules/active, activation map, role gate" diff --git a/.planning/phases/04-marketplace-portal-navigation/04-02-PLAN.md b/.planning/phases/04-marketplace-portal-navigation/04-02-PLAN.md index 7c80980..87260ce 100644 --- a/.planning/phases/04-marketplace-portal-navigation/04-02-PLAN.md +++ b/.planning/phases/04-marketplace-portal-navigation/04-02-PLAN.md @@ -18,8 +18,8 @@ autonomous: true requirements: [MRKT-01, MRKT-02, MRKT-04] must_haves: truths: - - "User can type in the marketplace search box and the card grid live-filters by module name and description (debounced)" - - "User can filter the grid by category chip and by status tab (Alle / Aktiviert / Verfuegbar)" + - "User can type in the marketplace search box and the card grid live-filters by module name and description (debounced) (D-02)" + - "User can filter the grid by category chip and by status tab (Alle / Aktiviert / Verfuegbar) (D-02)" - "Super-Admin sees a tenant-context dropdown; switching tenant re-fetches activation status for that tenant and activation targets that tenant (D-04)" - "Deactivating a module shows a confirmation dialog before the deactivate call; activation shows a success toast (no dialog)" - "User can open a module detail page at /marketplace/[slug] showing the full description, status, and an activation button" diff --git a/.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md b/.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md new file mode 100644 index 0000000..f7f471a --- /dev/null +++ b/.planning/phases/04-marketplace-portal-navigation/04-PATTERNS.md @@ -0,0 +1,422 @@ +# Phase 4: Marketplace & Portal Navigation - Pattern Map + +**Mapped:** 2026-06-22 +**Files analyzed:** 12 new/modified files +**Analogs found:** 12 / 12 + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|-------------------|------|-----------|----------------|---------------| +| `apps/web/src/app/(portal)/marketplace/page.tsx` | component (page) | request-response | `apps/web/src/app/(portal)/admin/modules/page.tsx` | exact | +| `apps/web/src/app/(portal)/marketplace/[slug]/page.tsx` | component (page) | request-response | `apps/web/src/app/(portal)/modules/[category]/page.tsx` | role-match | +| `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx` | component | CRUD | `apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx` | exact | +| `apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx` | component | event-driven | (no direct analog) | -- | +| `apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx` | component | event-driven | (no direct analog) | -- | +| `apps/web/src/app/(portal)/marketplace/components/StatusFilter.tsx` | component | event-driven | (no direct analog) | -- | +| `apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx` | component | request-response | `apps/web/src/app/(portal)/admin/tenants/page.tsx` | partial | +| `apps/web/src/app/(portal)/marketplace/components/ActivationDialog.tsx` | component | event-driven | `apps/web/src/app/(portal)/admin/tenants/page.tsx` (delete confirm modal) | role-match | +| `apps/web/src/app/(portal)/marketplace/components/Toast.tsx` | component | event-driven | (no direct analog) | -- | +| `apps/web/src/lib/stores/marketplace-store.ts` | store | event-driven | `apps/web/src/lib/stores/sidebar-store.ts` | role-match | +| `apps/web/src/components/layout/sidebar.tsx` | component (MODIFY) | request-response | self | exact | +| `apps/web/src/components/layout/sidebar-search.tsx` | component | event-driven | (no direct analog) | -- | + +## Pattern Assignments + +### `apps/web/src/app/(portal)/marketplace/page.tsx` (page, request-response) + +**Analog:** `apps/web/src/app/(portal)/admin/modules/page.tsx` + +**Imports pattern** (lines 1-6): +```typescript +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { useAuthStore } from '@/lib/stores/auth-store'; +``` + +**API fetch pattern** (lines 39-64) -- parallel fetch of all modules + active modules, building activation map: +```typescript +const fetchModules = useCallback(async () => { + try { + const [allRes, activeRes] = await Promise.all([ + fetch(`${API_URL}/modules`, { credentials: 'include' }), + fetch(`${API_URL}/modules/active`, { credentials: 'include' }), + ]); + + if (allRes.ok) { + const allModules: Module[] = await allRes.json(); + setModules(allModules); + } + + if (activeRes.ok) { + const activeModules: (Module & { activations?: TenantActivation[] })[] = await activeRes.json(); + const map = new Map(); + for (const mod of activeModules) { + map.set(mod.id, true); + } + setActivations(map); + } + } catch { + // silently fail + } finally { + setLoading(false); + } +}, []); +``` + +**Toggle activation pattern** (lines 74-103): +```typescript +const toggleModule = async (moduleId: string, currentlyActive: boolean) => { + setToggling(moduleId); + setError(null); + try { + const action = currentlyActive ? 'deactivate' : 'activate'; + const res = await fetch(`${API_URL}/modules/${moduleId}/${action}`, { + method: 'POST', + credentials: 'include', + }); + + if (res.ok) { + setActivations((prev) => { + const next = new Map(prev); + if (currentlyActive) { + next.delete(moduleId); + } else { + next.set(moduleId, true); + } + return next; + }); + } else { + const body = await res.text().catch(() => ''); + setError(`${res.status}: ${body}`); + } + } catch (err) { + setError(String(err)); + } finally { + setToggling(null); + } +}; +``` + +**Role-gated access pattern** (lines 37, 110-116): +```typescript +const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN'; + +if (!isAdmin) { + return ( +
+

{tCommon('accessDenied')}

+
+ ); +} +``` + +**Error display pattern** (lines 126-129): +```typescript +{error && ( +
+ {error} +
+)} +``` + +**Page layout pattern** (lines 118-124): +```typescript +
+
+

{t('title')}

+

{t('description')}

+
+ {/* content */} +
+``` + +--- + +### `apps/web/src/app/(portal)/marketplace/[slug]/page.tsx` (detail page, request-response) + +**Analog:** `apps/web/src/app/(portal)/modules/[category]/page.tsx` + +**Dynamic params pattern** (lines 1-5): +```typescript +'use client'; + +import { useTranslations } from 'next-intl'; +import { useParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +``` + +**Loading + empty state pattern** (lines 104-143): +```typescript +{isLoading && ( +
+
+
+)} + +{!isLoading && modules.length === 0 && ( +
+
+ {/* SVG icon */} +
+

{t('noModules')}

+
+)} +``` + +--- + +### `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx` (component, CRUD) + +**Analog:** `apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx` + +**Card structure pattern** (lines 70-107): +```typescript +export function ModuleCard({ name, slug, description, icon, category }: ModuleCardProps) { + const t = useTranslations('modules'); + const locale = (typeof window !== 'undefined' && document.documentElement.lang) || 'de'; + const localizedDescription = description[locale] || description.en || description.de || ''; + + return ( + +
+
+
+ +
+
+

{name}

+
+
+

{localizedDescription}

+
+ + ); +} +``` + +**Locale extraction pattern** (line 72-73): +```typescript +const locale = (typeof window !== 'undefined' && document.documentElement.lang) || 'de'; +const localizedDescription = description[locale] || description.en || description.de || ''; +``` + +**Responsive grid pattern** (from category page, line 112): +```typescript +
+``` + +--- + +### `apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx` (component, request-response) + +**Analog:** `apps/web/src/app/(portal)/admin/tenants/page.tsx` + +**Tenant fetch pattern** (lines 44-57): +```typescript +const fetchTenants = useCallback(async () => { + try { + const res = await fetch(`${API_URL}/tenants`, { + credentials: 'include', + }); + if (res.ok) { + setTenants(await res.json()); + } + } catch { + // silently fail + } finally { + setLoading(false); + } +}, []); +``` + +**Tenant interface** (lines 9-16): +```typescript +interface Tenant { + id: string; + name: string; + slug: string; + isActive: boolean; + createdAt: string; + userCount: number; +} +``` + +**Super-Admin guard pattern** (line 42): +```typescript +const hasAccess = currentUser?.role === 'SUPER_ADMIN'; +``` + +--- + +### `apps/web/src/app/(portal)/marketplace/components/ActivationDialog.tsx` (component, event-driven) + +**Analog:** `apps/web/src/app/(portal)/admin/tenants/page.tsx` (delete confirm modal) + +**Modal pattern** (lines 301-323): +```typescript +{deleteConfirm && ( +
+
+

{t('deleteConfirm')}

+
+ + +
+
+
+)} +``` + +--- + +### `apps/web/src/lib/stores/marketplace-store.ts` (store, event-driven) + +**Analog:** `apps/web/src/lib/stores/sidebar-store.ts` + +**Zustand store pattern** (lines 1-21): +```typescript +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface SidebarState { + isCollapsed: boolean; + isMobileOpen: boolean; + toggle: () => void; + setMobileOpen: (open: boolean) => void; +} + +export const useSidebarStore = create()( + persist( + (set) => ({ + isCollapsed: false, + isMobileOpen: false, + toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })), + setMobileOpen: (open) => set({ isMobileOpen: open }), + }), + { name: 'tessera-sidebar' } + ) +); +``` + +**Non-persisted store pattern** (from auth-store, lines 22-26): +```typescript +export const useAuthStore = create()((set) => ({ + user: null, + setUser: (user) => set({ user }), + clearUser: () => set({ user: null }), +})); +``` + +--- + +### `apps/web/src/components/layout/sidebar.tsx` (MODIFY -- add search, Link, usePathname) + +**Self-analog.** Key modification points: + +**Replace `` with ``** -- current pattern (lines 54-56, 81-82, etc.): +```typescript +// CURRENT (replace all instances): + +// REPLACE WITH: + +``` + +**Add usePathname for active state** -- insert after existing imports: +```typescript +import { usePathname } from 'next/navigation'; +import Link from 'next/link'; + +// Inside component: +const pathname = usePathname(); +const isActive = (href: string) => { + if (href === '/') return pathname === '/'; + return pathname.startsWith(href); +}; +``` + +**Sidebar link CSS classes** -- current active pattern (line 57): +```typescript +// Active: 'bg-sidebar-accent text-sidebar-accent-foreground font-medium' +// Inactive: 'text-sidebar-foreground hover:bg-muted' +``` + +**Categories accordion structure** (lines 106-176) -- extend with SidebarSearch component insertion point before the accordion. + +**Module items in categories** (lines 156-165) -- already renders per-module links under each category. These `` tags need `` replacement. + +--- + +## Shared Patterns + +### API Fetch Convention +**Source:** All page components +**Apply to:** All new page/component files that fetch data +```typescript +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + +// Always use credentials: 'include' for cookie-based auth +fetch(`${API_URL}/endpoint`, { credentials: 'include' }); + +// For Super-Admin tenant context, add x-tenant-id header: +headers: selectedTenantId ? { 'x-tenant-id': selectedTenantId } : {} +``` + +### Translations Convention +**Source:** All components +**Apply to:** All new components +```typescript +import { useTranslations } from 'next-intl'; +const t = useTranslations('marketplace'); // namespace key +const tCommon = useTranslations('common'); +``` + +### CSS Token Convention +**Source:** All components +**Apply to:** All new components +``` +// Semantic color tokens (NOT raw colors): +text-foreground, text-muted-foreground, bg-card, bg-muted, border-border, +bg-primary, text-primary-foreground, bg-destructive, text-destructive, +bg-sidebar, text-sidebar-foreground, bg-sidebar-accent, text-sidebar-accent-foreground + +// Status colors (only exception to semantic tokens): +// Active: 'text-green-600 dark:text-green-400' or 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' +``` + +### Role Check Convention +**Source:** `auth-store.ts` + all admin pages +**Apply to:** TenantContextSelector, marketplace activation controls +```typescript +const currentUser = useAuthStore((s) => s.user); +const isAdmin = currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN'; +const isSuperAdmin = currentUser?.role === 'SUPER_ADMIN'; +``` + +## No Analog Found + +| File | Role | Data Flow | Reason | +|------|------|-----------|--------| +| `MarketplaceSearch.tsx` | component | event-driven | No search input components exist yet. Use standard `` with debounced `onChange` + project CSS conventions from form inputs in tenants page (line 259: `flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm`). | +| `CategoryFilter.tsx` | component | event-driven | No chip/filter-row components exist. Build as horizontal button row using badge styling from admin/modules (line 157-159: `rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground`). | +| `StatusFilter.tsx` | component | event-driven | No tab-bar components exist. Build as segmented button row following same badge/button conventions. | +| `Toast.tsx` | component | event-driven | No toast system exists. Build with Zustand store pattern (see RESEARCH.md Pattern 4). | +| `sidebar-search.tsx` | component | event-driven | No sidebar search exists. Use input styling from tenants page form fields. | + +## Metadata + +**Analog search scope:** `apps/web/src/` +**Files scanned:** 30 source files (excluding node_modules, .next, dist) +**Pattern extraction date:** 2026-06-22 diff --git a/.planning/phases/04-marketplace-portal-navigation/04-VALIDATION.md b/.planning/phases/04-marketplace-portal-navigation/04-VALIDATION.md new file mode 100644 index 0000000..8b48ca4 --- /dev/null +++ b/.planning/phases/04-marketplace-portal-navigation/04-VALIDATION.md @@ -0,0 +1,81 @@ +--- +phase: 04 +slug: marketplace-portal-navigation +status: draft +nyquist_compliant: false +wave_0_complete: false +created: 2026-06-22 +--- + +# Phase 04 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | vitest | +| **Config file** | apps/web/vitest.config.ts, apps/api/vitest.config.ts | +| **Quick run command** | `pnpm --filter @tessera/api test -- --run` | +| **Full suite command** | `pnpm test` | +| **Estimated runtime** | ~30 seconds | + +--- + +## Sampling Rate + +- **After every task commit:** Run `pnpm --filter @tessera/api test -- --run` +- **After every plan wave:** Run `pnpm test` +- **Before `/gsd-verify-work`:** Full suite must be green +- **Max feedback latency:** 30 seconds + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| 04-01-01 | 01 | 1 | MRKT-01 | — | N/A | integration | `pnpm --filter @tessera/api test` | ❌ W0 | ⬜ pending | +| 04-01-02 | 01 | 1 | MRKT-02 | T-04-01 | Admin-only activation | integration | `pnpm --filter @tessera/api test` | ❌ W0 | ⬜ pending | +| 04-02-01 | 02 | 1 | PRTAL-02 | — | N/A | e2e | browser check | ❌ W0 | ⬜ pending | +| 04-02-02 | 02 | 1 | PRTAL-03 | — | N/A | e2e | browser check | ❌ W0 | ⬜ pending | +| 04-02-03 | 02 | 1 | PRTAL-05 | — | N/A | e2e | browser check | ❌ W0 | ⬜ pending | +| 04-03-01 | 03 | 2 | MRKT-03 | — | N/A | integration | `pnpm --filter @tessera/api test` | ❌ W0 | ⬜ pending | +| 04-03-02 | 03 | 2 | MRKT-04 | — | N/A | e2e | browser check | ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] Test stubs for marketplace API endpoints +- [ ] Test stubs for sidebar navigation behavior + +*Existing infrastructure covers framework setup — only test files needed.* + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Marketplace grid layout responsive | MRKT-01 | Visual layout check | Open marketplace at 320px, 768px, 1024px widths | +| Sidebar module grouping by category | PRTAL-02 | Visual grouping check | Activate modules from different categories, verify sidebar groups | +| Module opens in main content area | PRTAL-03 | Navigation flow | Click module in sidebar, verify content renders in main area | + +--- + +## Validation Sign-Off + +- [ ] All tasks have `` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < 30s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** pending