# CLAUDE.md — Scrum4Me Dit is het centrale instructiedocument voor Claude Code. Lees dit volledig voordat je iets bouwt. --- ## Wat is Scrum4Me? Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren. De app organiseert werk hiërarchisch (product → PBI → story → taak), biedt gesplitste planningsschermen met drag-and-drop, en integreert met Claude Code via een REST API zodat implementatieplannen, testresultaten en commits automatisch vastgelegd worden in stories. --- ## Specificatiedocumenten Lees het relevante document voordat je aan een feature begint. Nooit gokken over requirements. | Document | Gebruik voor | |---|---| | `scrum4me-functional-spec.md` | Acceptatiecriteria, randgevallen, user flows per feature | | `scrum4me-architecture.md` | Stack, datamodel, Prisma schema, Zustand stores, projectstructuur | | `scrum4me-backlog.md` | Welke task bouwen, in welke volgorde, "done when"-criteria | | `scrum4me-personas.md` | Lars (primaire gebruiker), Dina, Remi — gebruik bij UI-beslissingen | | `scrum4me-product-backlog.md` | Testdata voor de seed — PBI's en stories van Scrum4Me zelf | | `scrum4me-styling.md` | **Lees dit voor elk component** — MD3-kleuren, shadcn gebruik, component-patronen | | `theme.css` | Bronbestand — kopieer naar `styles/theme.css`, importeer in `app/globals.css` | | `MD3_Color_Scheme_Documentation.md` | Volledige MD3-kleurendocumentatie als referentie | --- ## Waar te beginnen Volg de backlog strikt op volgorde. Start bij **ST-001**. Sla geen milestone over. ``` M0 (ST-001–008) → M1 (ST-101–110) → M2 (ST-201–210) → M3 (ST-301–312) → M4 (ST-401–410) → M5 (ST-501–506) → M6 (ST-601–612) ``` Per task: 1. Lees de task in `scrum4me-backlog.md` 2. Zoek de bijbehorende feature-spec op in `scrum4me-functional-spec.md` 3. Bouw — test — verifieer de "Done when"-criteria 4. Commit met de task-ID in het commit-bericht: `feat: ST-001 project scaffolding` --- ## Tech stack (samenvatting) ``` Next.js 15 (App Router) + React 19 TypeScript strict Tailwind CSS + shadcn/ui ← UI-primitieven (Button, Dialog, Sheet, Badge, etc.) MD3 kleurensysteem via theme.css ← semantische tokens, nooit willekeurige Tailwind-kleuren Zustand (client state) dnd-kit (drag-and-drop) Prisma v7 (ORM) PostgreSQL via Neon (cloud) | SQLite (lokaal) iron-session (auth cookies) bcrypt (wachtwoord hashing) Zod (validatie) Sonner (toasts) ``` > **Stylingregel:** Gebruik **nooit** `bg-blue-500`, `bg-green-600` of andere willekeurige Tailwind-kleuren. > Gebruik altijd semantische MD3-tokens: `bg-primary`, `bg-status-done`, `bg-priority-critical`, etc. > Zie `scrum4me-styling.md` voor alle patronen en regels. --- ## Exacte dependencies (package.json) ```json { "dependencies": { "next": "^15.0.0", "react": "^19.0.0", "react-dom": "^19.0.0", "typescript": "^5.0.0", "tailwindcss": "^3.4.0", "zustand": "^5.0.0", "@dnd-kit/core": "^6.3.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.0", "prisma": "^7.0.0", "@prisma/client": "^7.0.0", "@prisma/adapter-pg": "^7.0.0", "iron-session": "^8.0.0", "bcryptjs": "^2.4.3", "zod": "^3.22.0", "sonner": "^1.5.0", "pg": "^8.11.0" }, "devDependencies": { "@types/bcryptjs": "^2.4.6", "@types/pg": "^8.11.0", "@types/node": "^20.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "eslint": "^8.0.0", "eslint-config-next": "^15.0.0" } } ``` --- ## theme.css installeren ```bash # Kopieer theme.css naar de project root of styles map cp theme.css app/styles/theme.css # Importeer bovenaan app/globals.css: # @import './styles/theme.css'; ``` Dark mode werkt via `.dark` class op ``. Zie `scrum4me-styling.md` voor het ThemeToggle component. ## shadcn/ui componenten om te installeren Voer deze uit na `npx shadcn@latest init`: ```bash npx shadcn@latest add button npx shadcn@latest add input npx shadcn@latest add textarea npx shadcn@latest add dialog npx shadcn@latest add dropdown-menu npx shadcn@latest add badge npx shadcn@latest add tooltip npx shadcn@latest add separator npx shadcn@latest add sheet # voor story slide-over npx shadcn@latest add select npx shadcn@latest add alert-dialog # voor bevestigingsdialogen npx shadcn@latest add skeleton npx shadcn@latest add toast ``` --- ## Projectstructuur ``` scrum4me/ ├── app/ │ ├── (auth)/ │ │ ├── login/page.tsx │ │ └── register/page.tsx │ ├── (app)/ │ │ ├── layout.tsx # Auth-check + navigatie │ │ ├── dashboard/page.tsx │ │ ├── products/ │ │ │ ├── new/page.tsx │ │ │ └── [id]/ │ │ │ ├── page.tsx # Product Backlog │ │ │ └── sprint/ │ │ │ ├── page.tsx # Sprint Backlog │ │ │ └── planning/page.tsx │ │ ├── todos/page.tsx │ │ └── settings/ │ │ ├── page.tsx │ │ └── tokens/page.tsx │ └── api/ │ ├── products/[id]/next-story/route.ts │ ├── sprints/[id]/tasks/route.ts │ ├── stories/[id]/ │ │ ├── log/route.ts │ │ └── tasks/reorder/route.ts │ ├── tasks/[id]/route.ts │ └── todos/route.ts ├── components/ │ ├── ui/ # shadcn/ui (auto-gegenereerd) │ ├── split-pane/ │ │ └── split-pane.tsx │ ├── backlog/ │ │ ├── pbi-list.tsx │ │ ├── pbi-item.tsx │ │ ├── story-grid.tsx │ │ └── story-block.tsx │ ├── sprint/ │ │ ├── sprint-backlog.tsx │ │ └── sprint-story-item.tsx │ ├── planning/ │ │ ├── task-list.tsx │ │ └── task-item.tsx │ └── shared/ │ ├── panel-nav-bar.tsx │ ├── confirm-dialog.tsx │ └── story-log.tsx ├── stores/ │ ├── planner-store.ts │ ├── selection-store.ts │ └── sprint-store.ts ├── lib/ │ ├── prisma.ts │ ├── session.ts │ ├── auth.ts │ ├── api-auth.ts │ └── env.ts ├── actions/ │ ├── products.ts │ ├── pbis.ts │ ├── stories.ts │ ├── sprints.ts │ ├── tasks.ts │ └── todos.ts ├── prisma/ │ ├── schema.prisma │ ├── migrations/ │ └── seed.ts ├── middleware.ts ├── prisma.config.ts └── .env.example ``` --- ## Kritieke implementatiepatronen ### 1. iron-session configuratie ```ts // lib/session.ts import { SessionOptions } from 'iron-session' export interface SessionData { userId: string isDemo: boolean } export const sessionOptions: SessionOptions = { password: process.env.SESSION_SECRET!, cookieName: 'scrum4me-session', cookieOptions: { secure: process.env.NODE_ENV === 'production', httpOnly: true, sameSite: 'lax', }, } ``` ```ts // Gebruik in Server Action of Route Handler: import { getIronSession } from 'iron-session' import { cookies } from 'next/headers' import { SessionData, sessionOptions } from '@/lib/session' const session = await getIronSession(await cookies(), sessionOptions) if (!session.userId) redirect('/login') ``` ### 2. Prisma Client singleton ```ts // lib/prisma.ts import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }) if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma ``` ### 3. prisma.config.ts (Prisma v7 vereiste) ```ts // prisma.config.ts import 'dotenv/config' import { defineConfig } from 'prisma/config' export default defineConfig({ schema: 'prisma/schema.prisma', migrations: { path: 'prisma/migrations' }, }) ``` ### 4. API Bearer token authenticatie ```ts // lib/api-auth.ts import { createHash } from 'crypto' import { prisma } from '@/lib/prisma' export async function authenticateApiRequest(request: Request) { const authHeader = request.headers.get('Authorization') if (!authHeader?.startsWith('Bearer ')) { return { error: 'Unauthorized', status: 401 } } const token = authHeader.slice(7) const tokenHash = createHash('sha256').update(token).digest('hex') const apiToken = await prisma.apiToken.findUnique({ where: { token_hash: tokenHash }, include: { user: true }, }) if (!apiToken || apiToken.revoked_at) { return { error: 'Unauthorized', status: 401 } } return { userId: apiToken.user_id, isDemo: apiToken.user.is_demo } } // Gebruik in Route Handler: export async function GET(request: Request) { const auth = await authenticateApiRequest(request) if ('error' in auth) { return Response.json({ error: auth.error }, { status: auth.status }) } // auth.userId beschikbaar } ``` ### 5. Float sort_order — drag-and-drop volgorde ```ts // Bereken nieuwe sort_order bij tussenvoeging: function getSortOrder(before: number | null, after: number | null): number { if (before === null && after === null) return 1.0 if (before === null) return after! / 2 if (after === null) return before + 1.0 return (before + after) / 2 } // Herindexeer als precisie opraakt (< 0.001 verschil): async function reindexIfNeeded(items: { id: string; sort_order: number }[]) { const minGap = Math.min(...items.slice(1).map((item, i) => item.sort_order - items[i].sort_order )) if (minGap < 0.001) { // Herindexeer: 1.0, 2.0, 3.0, ... await Promise.all(items.map((item, i) => prisma.pbi.update({ where: { id: item.id }, data: { sort_order: i + 1.0 } }) )) } } ``` ### 6. Zustand store patroon (optimistische update + rollback) ```ts // Gebruik in dnd-kit onDragEnd: const { pbiOrder, reorderPbis, rollbackPbis } = usePlannerStore() async function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id) return const prevOrder = [...pbiOrder[productId]] const newOrder = arrayMove(prevOrder, oldIndex, newIndex) // 1. Optimistisch updaten reorderPbis(productId, newOrder) // 2. Server Action aanroepen const result = await reorderPbisAction(productId, newOrder) // 3. Rollback bij fout if (!result.success) { rollbackPbis(productId, prevOrder) toast.error('Volgorde opslaan mislukt') } } ``` ### 7. Server Action patroon ```ts // actions/pbis.ts 'use server' import { revalidatePath } from 'next/cache' import { getIronSession } from 'iron-session' import { cookies } from 'next/headers' import { z } from 'zod' import { prisma } from '@/lib/prisma' import { SessionData, sessionOptions } from '@/lib/session' const createPbiSchema = z.object({ productId: z.string().cuid(), title: z.string().min(1).max(200), priority: z.number().int().min(1).max(4), }) export async function createPbi(formData: FormData) { const session = await getIronSession(await cookies(), sessionOptions) if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const parsed = createPbiSchema.safeParse({ productId: formData.get('productId'), title: formData.get('title'), priority: Number(formData.get('priority')), }) if (!parsed.success) return { error: parsed.error.flatten().fieldErrors } // Valideer eigenaarschap const product = await prisma.product.findFirst({ where: { id: parsed.data.productId, user_id: session.userId } }) if (!product) return { error: 'Product niet gevonden' } // Bepaal sort_order (onderaan de prioriteitsgroep) const last = await prisma.pbi.findFirst({ where: { product_id: parsed.data.productId, priority: parsed.data.priority }, orderBy: { sort_order: 'desc' }, }) const sort_order = (last?.sort_order ?? 0) + 1.0 const pbi = await prisma.pbi.create({ data: { ...parsed.data, product_id: parsed.data.productId, sort_order }, }) revalidatePath(`/products/${parsed.data.productId}`) return { success: true, pbi } } ``` ### 8. Route Handler patroon (REST API) ```ts // app/api/products/[id]/next-story/route.ts import { authenticateApiRequest } from '@/lib/api-auth' import { prisma } from '@/lib/prisma' export async function GET( request: Request, { params }: { params: Promise<{ id: string }> } ) { const auth = await authenticateApiRequest(request) if ('error' in auth) { return Response.json({ error: auth.error }, { status: auth.status }) } const { id } = await params // Valideer eigenaarschap const sprint = await prisma.sprint.findFirst({ where: { product_id: id, status: 'ACTIVE', product: { user_id: auth.userId } }, }) if (!sprint) { return Response.json({ error: 'Geen actieve Sprint gevonden' }, { status: 404 }) } const story = await prisma.story.findFirst({ where: { sprint_id: sprint.id, status: 'IN_SPRINT' }, orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], include: { tasks: { orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }] } }, }) if (!story) { return Response.json({ error: 'Geen open stories in de Sprint' }, { status: 404 }) } return Response.json(story) } ``` --- ## Middleware patroon ```ts // middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' import { getIronSession } from 'iron-session' import { SessionData, sessionOptions } from '@/lib/session' const protectedRoutes = ['/dashboard', '/products', '/todos', '/settings'] const authRoutes = ['/login', '/register'] export async function middleware(request: NextRequest) { const response = NextResponse.next() const session = await getIronSession(request.cookies, sessionOptions) const isProtected = protectedRoutes.some(r => request.nextUrl.pathname.startsWith(r)) const isAuthRoute = authRoutes.some(r => request.nextUrl.pathname.startsWith(r)) if (isProtected && !session.userId) { return NextResponse.redirect(new URL('/login', request.url)) } if (isAuthRoute && session.userId) { return NextResponse.redirect(new URL('/dashboard', request.url)) } return response } export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], } ``` --- ## Scrum-terminologie (gebruik consistent) | Correct | Niet gebruiken | |---|---| | Product Backlog Item (PBI) | Feature, Epic, Issue | | Story | User Story, Ticket | | Sprint Goal | Sprint Objective | | Sprint Planning | Sprint Meeting | | Scrum Team | Team | | Definition of Done | DoD criteria | --- ## Env vars ```bash # .env.local DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require" DIRECT_URL="postgresql://user:password@host/dbname?sslmode=require" SESSION_SECRET="genereer-met-openssl-rand-base64-32" # Lokaal (SQLite): # DATABASE_URL="file:./dev.db" # DIRECT_URL niet nodig bij SQLite ``` --- ## Lokale setup (quickstart) ```bash git clone cd scrum4me npm install cp .env.example .env.local # Vul SESSION_SECRET in .env.local # SQLite lokaal: npx prisma db push npx prisma db seed npm run dev ``` --- ## Demo-gebruiker credentials Na seeding: - **Gebruikersnaam:** `demo` - **Wachtwoord:** `demo1234` De demo-gebruiker heeft read-only rechten. Alle schrijfacties geven een 403 of zijn uitgeschakeld in de UI. --- ## REST API — alle endpoints | Methode | Endpoint | Doel | |---|---|---| | GET | `/api/products` | Actieve producten ophalen | | GET | `/api/products/:id/next-story` | Hoogst geprioriteerde open story | | GET | `/api/sprints/:id/tasks?limit=10` | Eerste N taken van de Sprint | | PATCH | `/api/stories/:id/tasks/reorder` | Taakvolgorde aanpassen | | POST | `/api/stories/:id/log` | Plan / testresultaat / commit vastleggen | | PATCH | `/api/tasks/:id` | Taakstatus bijwerken | | POST | `/api/todos` | Todo aanmaken | Alle endpoints vereisen: `Authorization: Bearer ` ### POST /api/stories/:id/log — body schema ```json // Implementatieplan: { "type": "IMPLEMENTATION_PLAN", "content": "string" } // Testresultaat: { "type": "TEST_RESULT", "content": "string", "status": "PASSED" | "FAILED" } // Commit: { "type": "COMMIT", "content": "string", "commit_hash": "string", "commit_message": "string" } ``` --- ## Conventies - **Commit-berichten:** `feat: ST-XXX beschrijving` / `fix: ST-XXX beschrijving` - **Branch-namen:** `feat/ST-001-scaffolding` - **Server Actions:** altijd in `actions/[domein].ts`, nooit inline in page.tsx - **Validatie:** altijd Zod, nooit handmatige checks - **Eigenaarschap:** elke Server Action en Route Handler controleert dat de resource bij de geverifieerde gebruiker hoort - **Demo-check:** elke Server Action controleert `session.isDemo` vóór schrijven - **Foutberichten:** altijd in het Nederlands voor eindgebruikers - **Comments in code:** Engels --- ## Definitie of Done (project) De MVP is klaar wanneer: - [ ] Alle 62 tasks (ST-001 t/m ST-612) zijn afgerond - [ ] Volledige Lars-flow doorlopen zonder fouten (ST-612) - [ ] Alle 7 API-endpoints werken via curl - [ ] Demo-gebruiker heeft geen schrijfrechten - [ ] App lokaal opzetbaar via README zonder extra hulp - [ ] CI/CD actief — falende build blokkeert merge - [ ] Beveiligingsreview API geslaagd (cross-user toegang onmogelijk)