docs(04): create phase plan
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ milestone: v1.0
|
|||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: verifying
|
status: verifying
|
||||||
stopped_at: Phase 4 UI-SPEC approved
|
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
|
last_activity: 2026-06-20 -- Phase 03 human verification passed
|
||||||
progress:
|
progress:
|
||||||
total_phases: 6
|
total_phases: 6
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ autonomous: true
|
|||||||
requirements: [MRKT-01, MRKT-02, MRKT-04]
|
requirements: [MRKT-01, MRKT-02, MRKT-04]
|
||||||
must_haves:
|
must_haves:
|
||||||
truths:
|
truths:
|
||||||
- "User can navigate to /marketplace and see a card grid of all available modules with name, description and category badge"
|
- "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"
|
- "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)"
|
- "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:
|
artifacts:
|
||||||
- path: "apps/web/src/app/(portal)/marketplace/page.tsx"
|
- path: "apps/web/src/app/(portal)/marketplace/page.tsx"
|
||||||
provides: "Marketplace grid page with parallel fetch of /modules + /modules/active, activation map, role gate"
|
provides: "Marketplace grid page with parallel fetch of /modules + /modules/active, activation map, role gate"
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ autonomous: true
|
|||||||
requirements: [MRKT-01, MRKT-02, MRKT-04]
|
requirements: [MRKT-01, MRKT-02, MRKT-04]
|
||||||
must_haves:
|
must_haves:
|
||||||
truths:
|
truths:
|
||||||
- "User can type in the marketplace search box and the card grid live-filters by module name and description (debounced)"
|
- "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)"
|
- "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)"
|
- "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)"
|
- "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"
|
- "User can open a module detail page at /marketplace/[slug] showing the full description, status, and an activation button"
|
||||||
|
|||||||
@@ -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<string, boolean>();
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<p className="text-lg text-muted-foreground">{tCommon('accessDenied')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error display pattern** (lines 126-129):
|
||||||
|
```typescript
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Page layout pattern** (lines 118-124):
|
||||||
|
```typescript
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-foreground">{t('title')}</h1>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
|
||||||
|
</div>
|
||||||
|
{/* content */}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `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 && (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && modules.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||||
|
<div className="rounded-lg bg-muted p-4 mb-4">
|
||||||
|
{/* SVG icon */}
|
||||||
|
</div>
|
||||||
|
<p className="text-lg text-muted-foreground">{t('noModules')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `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 (
|
||||||
|
<Link href={`/modules/${category}/${slug}`} className="group block">
|
||||||
|
<div className="rounded-lg border border-border bg-card p-5 shadow-sm transition-all hover:shadow-md hover:border-primary/30 group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<div className="rounded-md bg-muted p-2.5 shrink-0">
|
||||||
|
<ModuleIcon icon={icon} />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h3 className="font-semibold text-base leading-tight truncate">{name}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2 mb-4">{localizedDescription}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `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 && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||||
|
<div className="w-full max-w-sm rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||||
|
<p className="text-sm text-foreground mb-4">{t('deleteConfirm')}</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
{tCommon('cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(deleteConfirm)}
|
||||||
|
className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity"
|
||||||
|
>
|
||||||
|
{tCommon('delete')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `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<SidebarState>()(
|
||||||
|
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<AuthState>()((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 `<a>` with `<Link>`** -- current pattern (lines 54-56, 81-82, etc.):
|
||||||
|
```typescript
|
||||||
|
// CURRENT (replace all instances):
|
||||||
|
<a href="/" className="...">
|
||||||
|
// REPLACE WITH:
|
||||||
|
<Link href="/" className="...">
|
||||||
|
```
|
||||||
|
|
||||||
|
**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 `<a>` tags need `<Link>` 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 `<input>` 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
|
||||||
@@ -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 `<automated>` 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
|
||||||
Reference in New Issue
Block a user