plan-module · supabase_json
KnowledgeVault AI plan-module M-02
plan-module artifact · for KnowledgeVault AI · phase M-02 · status draft
tasks
| id | role | brief | title | completion | depends on |
|---|---|---|---|---|---|
| 1 | developer | Create apps/kv-app/supabase/migrations/002_products_catalog.sql. This migration runs after 001_kv_phase1_foundation.sql (which creates users and expert_profiles). Apply with supabase db push from inside apps/kv-app/.
products table — use CREATE TABLE IF NOT EXISTS products ( with columns: id uuid PRIMARY KEY DEFAULT gen_random_uuid(), sku text NOT NULL UNIQUE, name text NOT NULL, manufacturer text NOT NULL, category text -- nullable: matched against expert_profiles.specializations[], description text, spec_sheet_url text, image_url text, `has_surge_badge bool | Create products and product_pre_seeds migration | - type: file - evaluate: SQL file creates products and product_pre_seeds tables with all specified columns, correct NOT NULL constraints, UNIQUE on sku and product_id, FK from product_pre_seeds.product_id to products.id with ON DELETE CASCADE, RLS enabled with correct SELECT policies on both tables (POL-SEC-001), three partial indexes on products, one index on product_pre_seeds, and uses CREATE TABLE IF NOT EXISTS for idempotency (POL-ARCH-001) - min length: 800 | |
| 2 | developer | Create apps/kv-app/app/api/products/queue/route.ts. Export an async GET function (Next.js 14 App Router format).
Auth (POL-SEC-002 — mandatory): Import createClient from @/lib/supabase/server. Call const supabase = createClient() then const { data: { user }, error: authError } = await supabase.auth.getUser(). If authError or no user → return NextResponse.json({ error: 'Unauthorized', code: 'unauthorized', status: 401 }, { status: 401 }).
Role check: const { data: userRow } = await supabase.from('users').select('id, role').eq('supabase_auth_id', user.id).single(). | Implement GET /api/products/queue API route | - type: file - evaluate: TypeScript file exports async GET handler that calls supabase.auth.getUser() (POL-SEC-002), checks users.role='expert', looks up expert_profiles.specializations, filters products by category and is_fully_captured=false ordered by has_surge_badge DESC, maps each product with coverage_pct:0.0 and correct earnings_potential values, and returns correct 401/403/500 error response shapes - min length: 400 | - 1 |
| 3 | developer | Create three files in apps/kv-app/components/products/:
CoverageBadge.tsx — Props: { coverage_pct: number }. Render a <span> pill with Tailwind classes. Color thresholds: 0–30 → apply bg-red-500; 31–70 → apply bg-yellow-400; 71–99 → apply bg-green-500. Must set aria-label={\Coverage: ${coverage_pct}%\} (exactly this format — e.g. aria-label="Coverage: 25%" when coverage_pct=25). Display {coverage_pct}% as text inside.
ProductCard.tsx — Props interface:
```ts
interface ProductCardProps {
product: {
id: string; sku: string; name: string; manufacturer: string | Build CoverageBadge and ProductCard components | - type: file - evaluate: index.ts re-exports CoverageBadge and ProductCard; CoverageBadge applies bg-red-500/bg-yellow-400/bg-green-500 per threshold and has correct aria-label format; ProductCard renders Surge chip only when has_surge_badge=true, earnings text using max.toFixed(2), animate-pulse skeleton when loading=true, and fires onClick when clicked; npm run build in apps/kv-app/ succeeds (POL-STYLE-001) - min length: 50 | |
| 4 | developer | Create two files:
apps/kv-app/app/(expert)/products/page.tsx (Server Component — no 'use client'):
- Import createClient from @/lib/supabase/server and headers from next/headers
- const supabase = createClient(). Call supabase.auth.getUser() — if no user or auth error, redirect('/login')
- Get session via supabase.auth.getSession() to extract access_token for the API call
- Fetch expert_profiles.specializations: await supabase.from('expert_profiles').select('specializations').eq('user_id', userRow.id).single()
- Call the products queue API server-side with timeout: | Build Product Queue Screen S-02-a | - type: file - evaluate: Server component SSR-fetches /api/products/queue with user JWT and 10s timeout, passes products and specializations to ProductQueueClient which renders h1 'Products to capture', specialty filter chips including 'All', ProductCard grid with onClick navigating to /products/[id], empty state 1 with 'Set your specialties' and 'Update profile' CTA when specializations empty, empty state 2 with 'All caught up' when products empty but specializations non-empty, and npm run build succeeds (POL-STYLE-001) - min length: 300 | - 2 - 3 |
| 5 | developer | Create two files at apps/kv-app/app/(expert)/products/[id]/:
page.tsx (Server Component — no 'use client'):
Signature: export default async function ProductPreSessionPage({ params }: { params: { id: string } }).
- Import createClient from @/lib/supabase/server, notFound, redirect from next/navigation
- const supabase = createClient(). supabase.auth.getUser() — if no user → redirect('/login')
- Query: `const { data: product, error } = await supabase.from('products').select('id, sku, name, manufacturer, category, description, image_url, has_surge_badge, target_session_ | Build Product Pre-Session Screen S-02-b | - type: file - evaluate: Server component queries Supabase products table by params.id, calls notFound() when product absent, renders product image with loading='lazy' and correct alt text, earnings badge with correct min/max values per has_surge_badge, 'Surge bonus active' conditionally rendered, 'First capture bonus — $5.00' always rendered, and PreSessionClient sticky button with correct aria-label navigating to /capture/new?product_id=[id]; npm run build succeeds (POL-STYLE-001) - min length: 300 | - 1 |
Agent Handoff
Start Here
| id | role | brief | title | completion | depends on |
|---|---|---|---|---|---|
| 1 | developer | Create apps/kv-app/supabase/migrations/002_products_catalog.sql. This migration runs after 001_kv_phase1_foundation.sql (which creates users and expert_profiles). Apply with supabase db push from inside apps/kv-app/.
products table — use CREATE TABLE IF NOT EXISTS products ( with columns: id uuid PRIMARY KEY DEFAULT gen_random_uuid(), sku text NOT NULL UNIQUE, name text NOT NULL, manufacturer text NOT NULL, category text -- nullable: matched against expert_profiles.specializations[], description text, spec_sheet_url text, image_url text, `has_surge_badge bool | Create products and product_pre_seeds migration | - type: file - evaluate: SQL file creates products and product_pre_seeds tables with all specified columns, correct NOT NULL constraints, UNIQUE on sku and product_id, FK from product_pre_seeds.product_id to products.id with ON DELETE CASCADE, RLS enabled with correct SELECT policies on both tables (POL-SEC-001), three partial indexes on products, one index on product_pre_seeds, and uses CREATE TABLE IF NOT EXISTS for idempotency (POL-ARCH-001) - min length: 800 | |
| 2 | developer | Create apps/kv-app/app/api/products/queue/route.ts. Export an async GET function (Next.js 14 App Router format).
Auth (POL-SEC-002 — mandatory): Import createClient from @/lib/supabase/server. Call const supabase = createClient() then const { data: { user }, error: authError } = await supabase.auth.getUser(). If authError or no user → return NextResponse.json({ error: 'Unauthorized', code: 'unauthorized', status: 401 }, { status: 401 }).
Role check: const { data: userRow } = await supabase.from('users').select('id, role').eq('supabase_auth_id', user.id).single(). | Implement GET /api/products/queue API route | - type: file - evaluate: TypeScript file exports async GET handler that calls supabase.auth.getUser() (POL-SEC-002), checks users.role='expert', looks up expert_profiles.specializations, filters products by category and is_fully_captured=false ordered by has_surge_badge DESC, maps each product with coverage_pct:0.0 and correct earnings_potential values, and returns correct 401/403/500 error response shapes - min length: 400 | - 1 |
| 3 | developer | Create three files in apps/kv-app/components/products/:
CoverageBadge.tsx — Props: { coverage_pct: number }. Render a <span> pill with Tailwind classes. Color thresholds: 0–30 → apply bg-red-500; 31–70 → apply bg-yellow-400; 71–99 → apply bg-green-500. Must set aria-label={\Coverage: ${coverage_pct}%\} (exactly this format — e.g. aria-label="Coverage: 25%" when coverage_pct=25). Display {coverage_pct}% as text inside.
ProductCard.tsx — Props interface:
```ts
interface ProductCardProps {
product: {
id: string; sku: string; name: string; manufacturer: string | Build CoverageBadge and ProductCard components | - type: file - evaluate: index.ts re-exports CoverageBadge and ProductCard; CoverageBadge applies bg-red-500/bg-yellow-400/bg-green-500 per threshold and has correct aria-label format; ProductCard renders Surge chip only when has_surge_badge=true, earnings text using max.toFixed(2), animate-pulse skeleton when loading=true, and fires onClick when clicked; npm run build in apps/kv-app/ succeeds (POL-STYLE-001) - min length: 50 | |
| 4 | developer | Create two files:
apps/kv-app/app/(expert)/products/page.tsx (Server Component — no 'use client'):
- Import createClient from @/lib/supabase/server and headers from next/headers
- const supabase = createClient(). Call supabase.auth.getUser() — if no user or auth error, redirect('/login')
- Get session via supabase.auth.getSession() to extract access_token for the API call
- Fetch expert_profiles.specializations: await supabase.from('expert_profiles').select('specializations').eq('user_id', userRow.id).single()
- Call the products queue API server-side with timeout: | Build Product Queue Screen S-02-a | - type: file - evaluate: Server component SSR-fetches /api/products/queue with user JWT and 10s timeout, passes products and specializations to ProductQueueClient which renders h1 'Products to capture', specialty filter chips including 'All', ProductCard grid with onClick navigating to /products/[id], empty state 1 with 'Set your specialties' and 'Update profile' CTA when specializations empty, empty state 2 with 'All caught up' when products empty but specializations non-empty, and npm run build succeeds (POL-STYLE-001) - min length: 300 | - 2 - 3 |
| 5 | developer | Create two files at apps/kv-app/app/(expert)/products/[id]/:
page.tsx (Server Component — no 'use client'):
Signature: export default async function ProductPreSessionPage({ params }: { params: { id: string } }).
- Import createClient from @/lib/supabase/server, notFound, redirect from next/navigation
- const supabase = createClient(). supabase.auth.getUser() — if no user → redirect('/login')
- Query: `const { data: product, error } = await supabase.from('products').select('id, sku, name, manufacturer, category, description, image_url, has_surge_badge, target_session_ | Build Product Pre-Session Screen S-02-b | - type: file - evaluate: Server component queries Supabase products table by params.id, calls notFound() when product absent, renders product image with loading='lazy' and correct alt text, earnings badge with correct min/max values per has_surge_badge, 'Surge bonus active' conditionally rendered, 'First capture bonus — $5.00' always rendered, and PreSessionClient sticky button with correct aria-label navigating to /capture/new?product_id=[id]; npm run build succeeds (POL-STYLE-001) - min length: 300 | - 1 |
Completion Evidence
No explicit evidence field yet. Require tests, screenshots, linked PRs, or reviewed outputs before marking complete.
Artifact Shape
- tasks: 5 items
- module id: M-02
- module name: Product Catalog & Queue
- schema version: 1.0
- depends on modules: 1 item
Structured Payload
Machine-readable source fields
tasks
| id | role | brief | title | completion | depends on |
|---|---|---|---|---|---|
| 1 | developer | Create apps/kv-app/supabase/migrations/002_products_catalog.sql. This migration runs after 001_kv_phase1_foundation.sql (which creates users and expert_profiles). Apply with supabase db push from inside apps/kv-app/.
products table — use CREATE TABLE IF NOT EXISTS products ( with columns: id uuid PRIMARY KEY DEFAULT gen_random_uuid(), sku text NOT NULL UNIQUE, name text NOT NULL, manufacturer text NOT NULL, category text -- nullable: matched against expert_profiles.specializations[], description text, spec_sheet_url text, image_url text, `has_surge_badge bool | Create products and product_pre_seeds migration | - type: file - evaluate: SQL file creates products and product_pre_seeds tables with all specified columns, correct NOT NULL constraints, UNIQUE on sku and product_id, FK from product_pre_seeds.product_id to products.id with ON DELETE CASCADE, RLS enabled with correct SELECT policies on both tables (POL-SEC-001), three partial indexes on products, one index on product_pre_seeds, and uses CREATE TABLE IF NOT EXISTS for idempotency (POL-ARCH-001) - min length: 800 | |
| 2 | developer | Create apps/kv-app/app/api/products/queue/route.ts. Export an async GET function (Next.js 14 App Router format).
Auth (POL-SEC-002 — mandatory): Import createClient from @/lib/supabase/server. Call const supabase = createClient() then const { data: { user }, error: authError } = await supabase.auth.getUser(). If authError or no user → return NextResponse.json({ error: 'Unauthorized', code: 'unauthorized', status: 401 }, { status: 401 }).
Role check: const { data: userRow } = await supabase.from('users').select('id, role').eq('supabase_auth_id', user.id).single(). | Implement GET /api/products/queue API route | - type: file - evaluate: TypeScript file exports async GET handler that calls supabase.auth.getUser() (POL-SEC-002), checks users.role='expert', looks up expert_profiles.specializations, filters products by category and is_fully_captured=false ordered by has_surge_badge DESC, maps each product with coverage_pct:0.0 and correct earnings_potential values, and returns correct 401/403/500 error response shapes - min length: 400 | - 1 |
| 3 | developer | Create three files in apps/kv-app/components/products/:
CoverageBadge.tsx — Props: { coverage_pct: number }. Render a <span> pill with Tailwind classes. Color thresholds: 0–30 → apply bg-red-500; 31–70 → apply bg-yellow-400; 71–99 → apply bg-green-500. Must set aria-label={\Coverage: ${coverage_pct}%\} (exactly this format — e.g. aria-label="Coverage: 25%" when coverage_pct=25). Display {coverage_pct}% as text inside.
ProductCard.tsx — Props interface:
```ts
interface ProductCardProps {
product: {
id: string; sku: string; name: string; manufacturer: string | Build CoverageBadge and ProductCard components | - type: file - evaluate: index.ts re-exports CoverageBadge and ProductCard; CoverageBadge applies bg-red-500/bg-yellow-400/bg-green-500 per threshold and has correct aria-label format; ProductCard renders Surge chip only when has_surge_badge=true, earnings text using max.toFixed(2), animate-pulse skeleton when loading=true, and fires onClick when clicked; npm run build in apps/kv-app/ succeeds (POL-STYLE-001) - min length: 50 | |
| 4 | developer | Create two files:
apps/kv-app/app/(expert)/products/page.tsx (Server Component — no 'use client'):
- Import createClient from @/lib/supabase/server and headers from next/headers
- const supabase = createClient(). Call supabase.auth.getUser() — if no user or auth error, redirect('/login')
- Get session via supabase.auth.getSession() to extract access_token for the API call
- Fetch expert_profiles.specializations: await supabase.from('expert_profiles').select('specializations').eq('user_id', userRow.id).single()
- Call the products queue API server-side with timeout: | Build Product Queue Screen S-02-a | - type: file - evaluate: Server component SSR-fetches /api/products/queue with user JWT and 10s timeout, passes products and specializations to ProductQueueClient which renders h1 'Products to capture', specialty filter chips including 'All', ProductCard grid with onClick navigating to /products/[id], empty state 1 with 'Set your specialties' and 'Update profile' CTA when specializations empty, empty state 2 with 'All caught up' when products empty but specializations non-empty, and npm run build succeeds (POL-STYLE-001) - min length: 300 | - 2 - 3 |
| 5 | developer | Create two files at apps/kv-app/app/(expert)/products/[id]/:
page.tsx (Server Component — no 'use client'):
Signature: export default async function ProductPreSessionPage({ params }: { params: { id: string } }).
- Import createClient from @/lib/supabase/server, notFound, redirect from next/navigation
- const supabase = createClient(). supabase.auth.getUser() — if no user → redirect('/login')
- Query: `const { data: product, error } = await supabase.from('products').select('id, sku, name, manufacturer, category, description, image_url, has_surge_badge, target_session_ | Build Product Pre-Session Screen S-02-b | - type: file - evaluate: Server component queries Supabase products table by params.id, calls notFound() when product absent, renders product image with loading='lazy' and correct alt text, earnings badge with correct min/max values per has_surge_badge, 'Surge bonus active' conditionally rendered, 'First capture bonus — $5.00' always rendered, and PreSessionClient sticky button with correct aria-label navigating to /capture/new?product_id=[id]; npm run build succeeds (POL-STYLE-001) - min length: 300 | - 1 |
module id
M-02
module name
Product Catalog & Queue
schema version
1.0
depends on modules
- M-01