From 990fca792f0df1f9c9d0ebba52b0747fd3a6d126 Mon Sep 17 00:00:00 2001 From: Janpeter Visser Date: Fri, 24 Apr 2026 22:01:20 +0200 Subject: [PATCH] Refine CLAUDE.md content and task instructions Removed references to automatic logging of implementation plans, test results, and commits in stories. Updated task instructions and conventions for clarity. --- CLAUDE.md | 592 ++++++------------------------------------------------ 1 file changed, 56 insertions(+), 536 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7fffd2b..1605c80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Dit is het centrale instructiedocument voor Claude Code. Lees dit volledig voord ## 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. +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. --- @@ -16,14 +16,12 @@ Lees het relevante document voordat je aan een feature begint. Nooit gokken over | 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 | +| `scrum4me-functional-spec.md` | Acceptatiecriteria, randgevallen, user flows | +| `scrum4me-architecture.md` | Stack, datamodel, Prisma schema, Zustand stores | +| `scrum4me-backlog.md` | Welke task bouwen, volgorde, "done when"-criteria | +| `scrum4me-personas.md` | Lars (primair), Dina, Remi — gebruik bij UI-beslissingen | +| `scrum4me-product-backlog.md` | Testdata voor de seed | +| `scrum4me-styling.md` | **Lees dit voor elk component** — MD3-kleuren, shadcn patronen | --- @@ -39,570 +37,92 @@ M0 (ST-001–008) → M1 (ST-101–110) → M2 (ST-201–210) 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` +2. Zoek de bijbehorende feature-spec in `scrum4me-functional-spec.md` +3. Lees het relevante patroon in `docs/patterns/` als dat van toepassing is +4. Bouw — test — verifieer de "Done when"-criteria +5. vraag of code juiste is +6. Commit: `feat: ST-001 project scaffolding` +7. vraag of volgende taak moet worden gedaan --- -## Tech stack (samenvatting) +## Tech stack ``` -Next.js 15 (App Router) + React 19 +Next.js 16 (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 +Tailwind CSS + shadcn/ui +MD3 kleurensysteem via theme.css Zustand (client state) dnd-kit (drag-and-drop) -Prisma v7 (ORM) -PostgreSQL via Neon (cloud) | SQLite (lokaal) +Prisma v7 + PostgreSQL (Neon) | SQLite (lokaal) iron-session (auth cookies) -bcrypt (wachtwoord hashing) -Zod (validatie) -Sonner (toasts) +bcryptjs + Zod + Sonner ``` -> **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. +> ⚠️ **Stylingregel:** Gebruik **nooit** `bg-blue-500` of willekeurige Tailwind-kleuren. +> Gebruik altijd semantische MD3-tokens: `bg-primary`, `bg-status-done`, `bg-priority-critical`. +> Zie `scrum4me-styling.md` voor alle patronen. + +> ⚠️ **Next.js-versie:** Lees `node_modules/next/dist/docs/` bij twijfel — API's kunnen afwijken van trainingsdata. --- -## Exacte dependencies (package.json) +## Implementatiepatronen -```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" - } -} -``` +Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven. ---- - -## 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 | +| Patroon | Bestand | |---|---| -| 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 | +| iron-session (auth cookies) | `docs/patterns/iron-session.md` | +| Prisma Client singleton | `docs/patterns/prisma-client.md` | +| Server Action (met auth + Zod) | `docs/patterns/server-action.md` | +| Route Handler (REST API) | `docs/patterns/route-handler.md` | +| Zustand optimistische update + rollback | `docs/patterns/zustand-optimistic.md` | +| Float sort_order drag-and-drop | `docs/patterns/sort-order.md` | +| Middleware (route protection) | `docs/patterns/middleware.md` | --- ## 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" } +DATABASE_URL="" # postgresql://... of file:./dev.db +DIRECT_URL="" # alleen bij Neon/cloud +SESSION_SECRET="" # openssl rand -base64 32 ``` --- ## Conventies -- **Commit-berichten:** `feat: ST-XXX beschrijving` / `fix: ST-XXX beschrijving` -- **Branch-namen:** `feat/ST-001-scaffolding` +- **Commits:** `feat: ST-XXX beschrijving` / `fix: ST-XXX beschrijving` +- **Branches:** `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 +- **Eigenaarschap:** elke Server Action en Route Handler controleert dat de resource bij de ingelogde gebruiker hoort - **Demo-check:** elke Server Action controleert `session.isDemo` vóór schrijven -- **Foutberichten:** altijd in het Nederlands voor eindgebruikers -- **Comments in code:** Engels +- **Foutberichten:** Nederlands voor eindgebruikers — comments in code: Engels --- -## Definitie of Done (project) +## Scrum-terminologie -De MVP is klaar wanneer: -- [ ] Alle 62 tasks (ST-001 t/m ST-612) zijn afgerond -- [ ] Volledige Lars-flow doorlopen zonder fouten (ST-612) +| Correct | Niet gebruiken | +|---|---| +| Product Backlog Item (PBI) | Feature, Epic, Issue | +| Story | User Story, Ticket | +| Sprint Goal | Sprint Objective | +| Scrum Team | Team | + +--- + +## Definition of Done + +- [ ] Alle 62 tasks (ST-001 t/m ST-612) afgerond +- [ ] Volledige Lars-flow zonder fouten (ST-612) - [ ] Alle 7 API-endpoints werken via curl - [ ] Demo-gebruiker heeft geen schrijfrechten -- [ ] App lokaal opzetbaar via README zonder extra hulp +- [ ] App opzetbaar via README zonder extra hulp - [ ] CI/CD actief — falende build blokkeert merge - [ ] Beveiligingsreview API geslaagd (cross-user toegang onmogelijk)