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.
This commit is contained in:
Janpeter Visser 2026-04-24 22:01:20 +02:00 committed by GitHub
parent aea278eed7
commit 990fca792f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

592
CLAUDE.md
View file

@ -6,7 +6,7 @@ Dit is het centrale instructiedocument voor Claude Code. Lees dit volledig voord
## Wat is Scrum4Me? ## 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 | | Document | Gebruik voor |
|---|---| |---|---|
| `scrum4me-functional-spec.md` | Acceptatiecriteria, randgevallen, user flows per feature | | `scrum4me-functional-spec.md` | Acceptatiecriteria, randgevallen, user flows |
| `scrum4me-architecture.md` | Stack, datamodel, Prisma schema, Zustand stores, projectstructuur | | `scrum4me-architecture.md` | Stack, datamodel, Prisma schema, Zustand stores |
| `scrum4me-backlog.md` | Welke task bouwen, in welke volgorde, "done when"-criteria | | `scrum4me-backlog.md` | Welke task bouwen, volgorde, "done when"-criteria |
| `scrum4me-personas.md` | Lars (primaire gebruiker), Dina, Remi — gebruik bij UI-beslissingen | | `scrum4me-personas.md` | Lars (primair), Dina, Remi — gebruik bij UI-beslissingen |
| `scrum4me-product-backlog.md` | Testdata voor de seed — PBI's en stories van Scrum4Me zelf | | `scrum4me-product-backlog.md` | Testdata voor de seed |
| `scrum4me-styling.md` | **Lees dit voor elk component** — MD3-kleuren, shadcn gebruik, component-patronen | | `scrum4me-styling.md` | **Lees dit voor elk component** — MD3-kleuren, shadcn patronen |
| `theme.css` | Bronbestand — kopieer naar `styles/theme.css`, importeer in `app/globals.css` |
| `MD3_Color_Scheme_Documentation.md` | Volledige MD3-kleurendocumentatie als referentie |
--- ---
@ -39,570 +37,92 @@ M0 (ST-001008) → M1 (ST-101110) → M2 (ST-201210)
Per task: Per task:
1. Lees de task in `scrum4me-backlog.md` 1. Lees de task in `scrum4me-backlog.md`
2. Zoek de bijbehorende feature-spec op in `scrum4me-functional-spec.md` 2. Zoek de bijbehorende feature-spec in `scrum4me-functional-spec.md`
3. Bouw — test — verifieer de "Done when"-criteria 3. Lees het relevante patroon in `docs/patterns/` als dat van toepassing is
4. Commit met de task-ID in het commit-bericht: `feat: ST-001 project scaffolding` 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 TypeScript strict
Tailwind CSS + shadcn/ui ← UI-primitieven (Button, Dialog, Sheet, Badge, etc.) Tailwind CSS + shadcn/ui
MD3 kleurensysteem via theme.css ← semantische tokens, nooit willekeurige Tailwind-kleuren MD3 kleurensysteem via theme.css
Zustand (client state) Zustand (client state)
dnd-kit (drag-and-drop) dnd-kit (drag-and-drop)
Prisma v7 (ORM) Prisma v7 + PostgreSQL (Neon) | SQLite (lokaal)
PostgreSQL via Neon (cloud) | SQLite (lokaal)
iron-session (auth cookies) iron-session (auth cookies)
bcrypt (wachtwoord hashing) bcryptjs + Zod + Sonner
Zod (validatie)
Sonner (toasts)
``` ```
> **Stylingregel:** Gebruik **nooit** `bg-blue-500`, `bg-green-600` of andere willekeurige Tailwind-kleuren. > ⚠️ **Stylingregel:** Gebruik **nooit** `bg-blue-500` of willekeurige Tailwind-kleuren.
> Gebruik altijd semantische MD3-tokens: `bg-primary`, `bg-status-done`, `bg-priority-critical`, etc. > Gebruik altijd semantische MD3-tokens: `bg-primary`, `bg-status-done`, `bg-priority-critical`.
> Zie `scrum4me-styling.md` voor alle patronen en regels. > 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 Lees het relevante patroon vóór je begint. Nooit uit het hoofd schrijven.
{
"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"
}
}
```
--- | Patroon | Bestand |
## 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 `<html>`. 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<SessionData>(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<SessionData>(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<SessionData>(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 | | iron-session (auth cookies) | `docs/patterns/iron-session.md` |
| Story | User Story, Ticket | | Prisma Client singleton | `docs/patterns/prisma-client.md` |
| Sprint Goal | Sprint Objective | | Server Action (met auth + Zod) | `docs/patterns/server-action.md` |
| Sprint Planning | Sprint Meeting | | Route Handler (REST API) | `docs/patterns/route-handler.md` |
| Scrum Team | Team | | Zustand optimistische update + rollback | `docs/patterns/zustand-optimistic.md` |
| Definition of Done | DoD criteria | | Float sort_order drag-and-drop | `docs/patterns/sort-order.md` |
| Middleware (route protection) | `docs/patterns/middleware.md` |
--- ---
## Env vars ## Env vars
```bash ```bash
# .env.local DATABASE_URL="" # postgresql://... of file:./dev.db
DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require" DIRECT_URL="" # alleen bij Neon/cloud
DIRECT_URL="postgresql://user:password@host/dbname?sslmode=require" SESSION_SECRET="" # openssl rand -base64 32
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 <repo>
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 <token>`
### 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 ## Conventies
- **Commit-berichten:** `feat: ST-XXX beschrijving` / `fix: ST-XXX beschrijving` - **Commits:** `feat: ST-XXX beschrijving` / `fix: ST-XXX beschrijving`
- **Branch-namen:** `feat/ST-001-scaffolding` - **Branches:** `feat/ST-001-scaffolding`
- **Server Actions:** altijd in `actions/[domein].ts`, nooit inline in page.tsx - **Server Actions:** altijd in `actions/[domein].ts`, nooit inline in page.tsx
- **Validatie:** altijd Zod, nooit handmatige checks - **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 - **Demo-check:** elke Server Action controleert `session.isDemo` vóór schrijven
- **Foutberichten:** altijd in het Nederlands voor eindgebruikers - **Foutberichten:** Nederlands voor eindgebruikers — comments in code: Engels
- **Comments in code:** Engels
--- ---
## Definitie of Done (project) ## Scrum-terminologie
De MVP is klaar wanneer: | Correct | Niet gebruiken |
- [ ] Alle 62 tasks (ST-001 t/m ST-612) zijn afgerond |---|---|
- [ ] Volledige Lars-flow doorlopen zonder fouten (ST-612) | 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 - [ ] Alle 7 API-endpoints werken via curl
- [ ] Demo-gebruiker heeft geen schrijfrechten - [ ] 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 - [ ] CI/CD actief — falende build blokkeert merge
- [ ] Beveiligingsreview API geslaagd (cross-user toegang onmogelijk) - [ ] Beveiligingsreview API geslaagd (cross-user toegang onmogelijk)