feat: ST-001–ST-005 foundation — scaffolding, Prisma, schema, seed, env

- ST-001: Next.js 16 + React 19 + TypeScript strict + Tailwind + shadcn/ui + all deps
- ST-002: Prisma v7 setup with better-sqlite3 adapter (local) and pg adapter (cloud)
- ST-003: Full schema migration (users, pbis, stories, sprints, tasks, todos, api_tokens)
- ST-004: Seed with 9 PBIs, ~40 stories, demo user (demo/demo1234), lars user
- ST-005: Zod-validated env vars, .env.example, lib/session, lib/auth, lib/api-auth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-22 21:04:48 +02:00
parent 4cf5833c1d
commit 7f94bb6359
32 changed files with 8653 additions and 183 deletions

View file

@ -0,0 +1,171 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"is_demo" BOOLEAN NOT NULL DEFAULT false,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "user_roles" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"role" TEXT NOT NULL,
CONSTRAINT "user_roles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "api_tokens" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"token_hash" TEXT NOT NULL,
"label" TEXT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"revoked_at" DATETIME,
CONSTRAINT "api_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "products" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"repo_url" TEXT,
"definition_of_done" TEXT NOT NULL,
"archived" BOOLEAN NOT NULL DEFAULT false,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL,
CONSTRAINT "products_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "pbis" (
"id" TEXT NOT NULL PRIMARY KEY,
"product_id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"priority" INTEGER NOT NULL,
"sort_order" REAL NOT NULL,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL,
CONSTRAINT "pbis_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "stories" (
"id" TEXT NOT NULL PRIMARY KEY,
"pbi_id" TEXT NOT NULL,
"product_id" TEXT NOT NULL,
"sprint_id" TEXT,
"title" TEXT NOT NULL,
"description" TEXT,
"acceptance_criteria" TEXT,
"priority" INTEGER NOT NULL,
"sort_order" REAL NOT NULL,
"status" TEXT NOT NULL DEFAULT 'OPEN',
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL,
CONSTRAINT "stories_pbi_id_fkey" FOREIGN KEY ("pbi_id") REFERENCES "pbis" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "stories_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "stories_sprint_id_fkey" FOREIGN KEY ("sprint_id") REFERENCES "sprints" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "story_logs" (
"id" TEXT NOT NULL PRIMARY KEY,
"story_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"content" TEXT NOT NULL,
"status" TEXT,
"commit_hash" TEXT,
"commit_message" TEXT,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "story_logs_story_id_fkey" FOREIGN KEY ("story_id") REFERENCES "stories" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "sprints" (
"id" TEXT NOT NULL PRIMARY KEY,
"product_id" TEXT NOT NULL,
"sprint_goal" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completed_at" DATETIME,
CONSTRAINT "sprints_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "products" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "tasks" (
"id" TEXT NOT NULL PRIMARY KEY,
"story_id" TEXT NOT NULL,
"sprint_id" TEXT,
"title" TEXT NOT NULL,
"description" TEXT,
"priority" INTEGER NOT NULL,
"sort_order" REAL NOT NULL,
"status" TEXT NOT NULL DEFAULT 'TO_DO',
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL,
CONSTRAINT "tasks_story_id_fkey" FOREIGN KEY ("story_id") REFERENCES "stories" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "tasks_sprint_id_fkey" FOREIGN KEY ("sprint_id") REFERENCES "sprints" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "todos" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"done" BOOLEAN NOT NULL DEFAULT false,
"archived" BOOLEAN NOT NULL DEFAULT false,
"created_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" DATETIME NOT NULL,
CONSTRAINT "todos_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "user_roles_user_id_role_key" ON "user_roles"("user_id", "role");
-- CreateIndex
CREATE UNIQUE INDEX "api_tokens_token_hash_key" ON "api_tokens"("token_hash");
-- CreateIndex
CREATE INDEX "api_tokens_token_hash_idx" ON "api_tokens"("token_hash");
-- CreateIndex
CREATE INDEX "products_user_id_archived_idx" ON "products"("user_id", "archived");
-- CreateIndex
CREATE UNIQUE INDEX "products_user_id_name_key" ON "products"("user_id", "name");
-- CreateIndex
CREATE INDEX "pbis_product_id_priority_sort_order_idx" ON "pbis"("product_id", "priority", "sort_order");
-- CreateIndex
CREATE INDEX "stories_pbi_id_priority_sort_order_idx" ON "stories"("pbi_id", "priority", "sort_order");
-- CreateIndex
CREATE INDEX "stories_sprint_id_sort_order_idx" ON "stories"("sprint_id", "sort_order");
-- CreateIndex
CREATE INDEX "stories_product_id_status_idx" ON "stories"("product_id", "status");
-- CreateIndex
CREATE INDEX "story_logs_story_id_created_at_idx" ON "story_logs"("story_id", "created_at");
-- CreateIndex
CREATE INDEX "sprints_product_id_status_idx" ON "sprints"("product_id", "status");
-- CreateIndex
CREATE INDEX "tasks_story_id_priority_sort_order_idx" ON "tasks"("story_id", "priority", "sort_order");
-- CreateIndex
CREATE INDEX "tasks_sprint_id_status_idx" ON "tasks"("sprint_id", "status");
-- CreateIndex
CREATE INDEX "todos_user_id_done_archived_idx" ON "todos"("user_id", "done", "archived");

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

203
prisma/schema.prisma Normal file
View file

@ -0,0 +1,203 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
}
enum Role {
PRODUCT_OWNER
SCRUM_MASTER
DEVELOPER
}
enum StoryStatus {
OPEN
IN_SPRINT
DONE
}
enum TaskStatus {
TO_DO
IN_PROGRESS
DONE
}
enum LogType {
IMPLEMENTATION_PLAN
TEST_RESULT
COMMIT
}
enum TestStatus {
PASSED
FAILED
}
enum SprintStatus {
ACTIVE
COMPLETED
}
model User {
id String @id @default(cuid())
username String @unique
password_hash String
is_demo Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
roles UserRole[]
api_tokens ApiToken[]
products Product[]
todos Todo[]
@@map("users")
}
model UserRole {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
role Role
@@unique([user_id, role])
@@map("user_roles")
}
model ApiToken {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
token_hash String @unique
label String?
created_at DateTime @default(now())
revoked_at DateTime?
@@index([token_hash])
@@map("api_tokens")
}
model Product {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
name String
description String?
repo_url String?
definition_of_done String
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
pbis Pbi[]
sprints Sprint[]
stories Story[]
@@unique([user_id, name])
@@index([user_id, archived])
@@map("products")
}
model Pbi {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
title String
description String?
priority Int
sort_order Float
created_at DateTime @default(now())
updated_at DateTime @updatedAt
stories Story[]
@@index([product_id, priority, sort_order])
@@map("pbis")
}
model Story {
id String @id @default(cuid())
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
pbi_id String
product Product @relation(fields: [product_id], references: [id])
product_id String
sprint Sprint? @relation(fields: [sprint_id], references: [id])
sprint_id String?
title String
description String?
acceptance_criteria String?
priority Int
sort_order Float
status StoryStatus @default(OPEN)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
logs StoryLog[]
tasks Task[]
@@index([pbi_id, priority, sort_order])
@@index([sprint_id, sort_order])
@@index([product_id, status])
@@map("stories")
}
model StoryLog {
id String @id @default(cuid())
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
story_id String
type LogType
content String
status TestStatus?
commit_hash String?
commit_message String?
created_at DateTime @default(now())
@@index([story_id, created_at])
@@map("story_logs")
}
model Sprint {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
sprint_goal String
status SprintStatus @default(ACTIVE)
created_at DateTime @default(now())
completed_at DateTime?
stories Story[]
tasks Task[]
@@index([product_id, status])
@@map("sprints")
}
model Task {
id String @id @default(cuid())
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
story_id String
sprint Sprint? @relation(fields: [sprint_id], references: [id])
sprint_id String?
title String
description String?
priority Int
sort_order Float
status TaskStatus @default(TO_DO)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([story_id, priority, sort_order])
@@index([sprint_id, status])
@@map("tasks")
}
model Todo {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
title String
done Boolean @default(false)
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([user_id, done, archived])
@@map("todos")
}

238
prisma/seed.ts Normal file
View file

@ -0,0 +1,238 @@
import { PrismaClient } from '@prisma/client'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import * as dotenv from 'dotenv'
import * as path from 'path'
import * as bcrypt from 'bcryptjs'
// Load env from project root
const root = path.resolve(__dirname, '..')
dotenv.config({ path: path.join(root, '.env.local'), override: true })
dotenv.config({ path: path.join(root, '.env') })
let prisma: PrismaClient
async function main() {
const url = process.env.DATABASE_URL
if (!url) throw new Error('DATABASE_URL is not set. Check .env.local')
// For SQLite: adapter takes a config object with url
const adapter = new PrismaBetterSqlite3({ url })
prisma = new PrismaClient({ adapter })
console.log('Seeding database...')
// Create main demo user
const demoHash = await bcrypt.hash('demo1234', 12)
const demo = await prisma.user.upsert({
where: { username: 'demo' },
update: {},
create: {
username: 'demo',
password_hash: demoHash,
is_demo: true,
roles: {
create: [
{ role: 'PRODUCT_OWNER' },
{ role: 'DEVELOPER' },
],
},
},
})
console.log(`Demo user: ${demo.username} (id: ${demo.id})`)
// Create seed user for the product
const userHash = await bcrypt.hash('scrum4me123', 12)
const user = await prisma.user.upsert({
where: { username: 'lars' },
update: {},
create: {
username: 'lars',
password_hash: userHash,
is_demo: false,
roles: {
create: [
{ role: 'PRODUCT_OWNER' },
{ role: 'SCRUM_MASTER' },
{ role: 'DEVELOPER' },
],
},
},
})
console.log(`Main user: ${user.username} (id: ${user.id})`)
// Create the Scrum4Me product (using the product backlog doc data)
const product = await prisma.product.upsert({
where: { user_id_name: { user_id: demo.id, name: 'DevPlanner' } },
update: {},
create: {
user_id: demo.id,
name: 'DevPlanner',
description: 'Een lichtgewicht Scrum-gebaseerde projectplanner voor solo developers en kleine Scrum Teams.',
repo_url: 'https://github.com/devplanner/devplanner',
definition_of_done: 'Feature is geïmplementeerd, getest (unit + integratie), gedocumenteerd in code, en gedeployed naar de staging-omgeving zonder regressies.',
archived: false,
},
})
console.log(`Product created: ${product.name} (id: ${product.id})`)
// PBI data from the product backlog document
const pbis = [
{
title: 'Authenticatie & gebruikersbeheer',
description: 'Het Scrum Team kan een account aanmaken en inloggen met gebruikersnaam en wachtwoord. Een demo-gebruiker heeft alleen leesrechten. Gebruikers kunnen één of meerdere Scrum-rollen aannemen.',
priority: 1,
stories: [
{ title: 'Account aanmaken', description: 'Als bezoeker wil ik een account aanmaken met gebruikersnaam en wachtwoord, zodat ik toegang krijg tot de app.', acceptance_criteria: '- Gebruikersnaam en wachtwoord zijn verplicht\n- Gebruikersnaam is uniek; dubbele aanmelding geeft foutmelding\n- Wachtwoord heeft minimaal 8 tekens\n- Na aanmaken wordt de gebruiker direct ingelogd\n- Geen e-mailverificatie vereist in v1', priority: 1 },
{ title: 'Inloggen', description: 'Als geregistreerde gebruiker wil ik inloggen met gebruikersnaam en wachtwoord, zodat ik mijn projecten kan beheren.', acceptance_criteria: '- Incorrecte combinatie geeft generieke foutmelding\n- Na inloggen wordt de gebruiker doorgestuurd naar het dashboard\n- Sessie blijft actief totdat de gebruiker uitlogt', priority: 1 },
{ title: 'Uitloggen', description: 'Als ingelogde gebruiker wil ik kunnen uitloggen, zodat mijn sessie veilig afgesloten wordt.', acceptance_criteria: '- Uitlogknop altijd zichtbaar in de navigatie\n- Na uitloggen wordt de gebruiker naar de loginpagina gestuurd\n- Sessiedata wordt gewist', priority: 1 },
{ title: 'Demo-gebruiker (read-only)', description: 'Als bezoeker wil ik kunnen inloggen als demo-gebruiker, zodat ik de app kan verkennen zonder een account aan te maken.', acceptance_criteria: '- Vaste inloggegevens voor de demo-gebruiker zijn beschikbaar op de loginpagina\n- Demo-gebruiker ziet alle data maar kan niets aanmaken, aanpassen of verwijderen\n- Alle actieknoppen zijn zichtbaar maar uitgeschakeld', priority: 2 },
{ title: 'Roltoewijzing', description: 'Als gebruiker wil ik één of meerdere Scrum-rollen kunnen aannemen (Product Owner, Scrum Master, Developer).', acceptance_criteria: '- Gebruiker kan bij registratie of in instellingen rollen selecteren\n- Minimaal één rol is verplicht\n- Alle drie de rollen tegelijk zijn toegestaan', priority: 3 },
],
},
{
title: 'Productbeheer',
description: 'Het Scrum Team kan producten aanmaken, bekijken, bewerken en archiveren.',
priority: 1,
stories: [
{ title: 'Product aanmaken', description: 'Als Product Owner wil ik een nieuw product aanmaken met naam, beschrijving en git-repo URL.', acceptance_criteria: '- Naam is verplicht en uniek per gebruiker\n- Beschrijving is optioneel\n- Git-repo URL is optioneel maar wordt gevalideerd als geldige URL', priority: 1 },
{ title: 'Product bewerken', description: 'Als Product Owner wil ik de naam, beschrijving en git-repo URL van een product kunnen aanpassen.', acceptance_criteria: '- Alle velden zijn bewerkbaar\n- Wijzigingen worden opgeslagen zonder de pagina te verlaten', priority: 2 },
{ title: 'Product archiveren', description: 'Als Product Owner wil ik een product kunnen archiveren.', acceptance_criteria: '- Gearchiveerde producten verschijnen niet in de standaardlijst\n- Archiveren is omkeerbaar', priority: 2 },
{ title: 'Productenlijst bekijken', description: 'Als gebruiker wil ik een overzicht zien van alle actieve producten.', acceptance_criteria: '- Lijst toont naam, beschrijving (ingekort) en git-repo link\n- Klikken op een product opent de Product Backlog', priority: 1 },
],
},
{
title: 'Product Backlog',
description: 'Het Scrum Team kan de Product Backlog beheren via een gesplitst scherm: links de PBIs, rechts de bijbehorende stories.',
priority: 1,
stories: [
{ title: 'PBI aanmaken', description: 'Als Product Owner wil ik een PBI aanmaken in de Product Backlog.', acceptance_criteria: '- PBI heeft een titel (verplicht) en omschrijving (optioneel)\n- PBI krijgt een prioriteit (1 t/m 4)\n- Nieuw PBI verschijnt onderaan de lijst', priority: 1 },
{ title: 'PBI bewerken', description: 'Als Product Owner wil ik de titel, omschrijving en prioriteit van een PBI kunnen aanpassen.', acceptance_criteria: '- Dubbelklikken of via contextmenu opent bewerkingsmodus\n- Alle velden zijn inline bewerkbaar', priority: 2 },
{ title: 'PBI verwijderen', description: 'Als Product Owner wil ik een PBI kunnen verwijderen.', acceptance_criteria: '- Verwijderen vereist bevestiging\n- Cascade verwijdering van bijbehorende stories', priority: 2 },
{ title: 'PBI prioriteit instellen', description: 'Als Product Owner wil ik per PBI een prioriteit kunnen instellen (1 t/m 4).', acceptance_criteria: '- Prioriteit is instelbaar via dropdown of inline label\n- PBIs worden gegroepeerd per prioriteit', priority: 1 },
{ title: 'PBI volgorde aanpassen via drag-and-drop', description: 'Als Product Owner wil ik de volgorde van PBIs binnen dezelfde prioriteit kunnen aanpassen via drag-and-drop.', acceptance_criteria: '- Drag-and-drop werkt vloeiend via dnd-kit\n- Volgorde wordt direct opgeslagen na loslaten', priority: 2 },
{ title: 'PBI filteren', description: 'Als gebruiker wil ik PBIs kunnen filteren op prioriteit.', acceptance_criteria: '- Filteropties beschikbaar in de navigatiebar\n- Filter werkt realtime', priority: 3 },
{ title: 'Gesplitst scherm Product Backlog', description: 'Als gebruiker wil ik de Product Backlog bekijken als gesplitst scherm.', acceptance_criteria: '- Scherm is standaard 50/50 verdeeld\n- Splitter is horizontaal versleepbaar', priority: 1 },
],
},
{
title: 'Story-beheer',
description: 'Stories kunnen worden aangemaakt, bewerkt, geprioriteerd en gerangschikt binnen een PBI.',
priority: 1,
stories: [
{ title: 'Story aanmaken', description: 'Als Product Owner wil ik een story aanmaken binnen een PBI.', acceptance_criteria: '- Story heeft een titel (verplicht), omschrijving (optioneel) en prioriteit\n- Nieuwe story verschijnt als blok rechts', priority: 1 },
{ title: 'Story weergave als blokken', description: 'Als gebruiker wil ik stories zien als compacte blokken (~10% schermbreedte).', acceptance_criteria: '- Elk blok toont: storytitel, prioriteit, status\n- Blokken zijn gerangschikt op prioriteit', priority: 1 },
{ title: 'Story prioriteit instellen', description: 'Als Product Owner wil ik per story een prioriteit instellen.', acceptance_criteria: '- Prioriteit instelbaar via het storyblok\n- Prioriteitswijziging herplaatst het blok in de juiste groep', priority: 2 },
{ title: 'Story volgorde aanpassen via drag-and-drop', description: 'Als Product Owner wil ik de volgorde van stories aanpassen via drag-and-drop.', acceptance_criteria: '- Drag-and-drop werkt via dnd-kit\n- Volgorde wordt direct opgeslagen', priority: 2 },
{ title: 'Story bewerken', description: 'Als Product Owner wil ik de titel, omschrijving en prioriteit van een story kunnen aanpassen.', acceptance_criteria: '- Bewerkbaar via klikken op het storyblok\n- Wijzigingen opgeslagen zonder paginaverversing', priority: 2 },
{ title: 'Story verwijderen', description: 'Als Product Owner wil ik een story kunnen verwijderen.', acceptance_criteria: '- Verwijderen vereist bevestiging\n- Cascade verwijdering van gekoppelde taken', priority: 2 },
],
},
{
title: 'Todo-lijst',
description: 'Gebruikers kunnen een snelle todo-lijst bijhouden voor ongeplande of kortstondige taken.',
priority: 2,
stories: [
{ title: 'Todo-item aanmaken', description: 'Als gebruiker wil ik snel een todo-item aanmaken zonder het aan een product te koppelen.', acceptance_criteria: '- Todo heeft alleen een titel (verplicht)\n- Aanmaken via een snel-invoerveld (Enter om op te slaan)', priority: 1 },
{ title: 'Todo-item afvinken', description: 'Als gebruiker wil ik een todo-item kunnen afvinken.', acceptance_criteria: '- Afgevinkte items zijn visueel doorgestreept\n- Afgevinkte items kunnen worden gearchiveerd', priority: 1 },
{ title: 'Todo promoveren naar PBI', description: 'Als Product Owner wil ik een todo-item promoveren naar een PBI.', acceptance_criteria: '- Promoten opent een dialoog om product en prioriteit te kiezen\n- Todo wordt omgezet naar een PBI', priority: 2 },
{ title: 'Todo promoveren naar story', description: 'Als Product Owner wil ik een todo-item promoveren naar een story.', acceptance_criteria: '- Promoten opent een dialoog om product, PBI en prioriteit te kiezen\n- Todo wordt omgezet naar een story', priority: 2 },
],
},
{
title: 'Sprint Backlog & Sprint Planning',
description: 'Het Scrum Team kan een Sprint aanmaken met een Sprint Goal, stories slepen en de volgorde bepalen.',
priority: 2,
stories: [
{ title: 'Sprint aanmaken', description: 'Als Scrum Master wil ik een nieuwe Sprint aanmaken met een Sprint Goal.', acceptance_criteria: '- Sprint heeft een Sprint Goal (verplicht)\n- Sprint is gekoppeld aan een product\n- Er kan maar één actieve Sprint per product zijn', priority: 1 },
{ title: 'Sprint Backlog scherm (gesplitst)', description: 'Als gebruiker wil ik de Sprint Backlog kunnen beheren via een gesplitst scherm.', acceptance_criteria: '- Links: Sprint Backlog met geselecteerde stories\n- Rechts: stories uit de Product Backlog', priority: 1 },
{ title: 'Story naar Sprint slepen', description: 'Als Developer wil ik een story vanuit de Product Backlog naar de Sprint Backlog kunnen slepen.', acceptance_criteria: '- Drag-and-drop werkt via dnd-kit\n- Story verschijnt in de Sprint Backlog op de gesleepte positie', priority: 1 },
{ title: 'Volgorde stories in Sprint bepalen', description: 'Als Developer wil ik de volgorde van stories in de Sprint Backlog kunnen aanpassen.', acceptance_criteria: '- Drag-and-drop werkt binnen de Sprint Backlog\n- Volgorde wordt direct opgeslagen', priority: 2 },
{ title: 'Story uit Sprint verwijderen', description: 'Als Developer wil ik een story uit de Sprint Backlog kunnen verwijderen.', acceptance_criteria: '- Story verdwijnt uit de Sprint Backlog\n- Story is weer beschikbaar in de Product Backlog', priority: 2 },
],
},
{
title: 'Sprint Planning (taken per story)',
description: 'Tijdens Sprint Planning worden stories opgedeeld in taken via een gesplitst scherm.',
priority: 2,
stories: [
{ title: 'Sprint Planning scherm', description: 'Als Developer wil ik een Sprint Planning scherm zien met stories links en taken rechts.', acceptance_criteria: '- Links: stories in de Sprint Backlog\n- Rechts: taken van de geselecteerde story', priority: 1 },
{ title: 'Taak aanmaken', description: 'Als Developer wil ik een taak aanmaken onder een story.', acceptance_criteria: '- Taak heeft een titel (verplicht), omschrijving (optioneel) en prioriteit\n- Nieuwe taak verschijnt onderaan de takenlijst', priority: 1 },
{ title: 'Taak prioriteit instellen', description: 'Als Developer wil ik per taak een prioriteit instellen.', acceptance_criteria: '- Prioriteit instelbaar via taakregel\n- Taken gegroepeerd op prioriteit', priority: 2 },
{ title: 'Taak volgorde aanpassen via drag-and-drop', description: 'Als Developer wil ik de volgorde van taken kunnen aanpassen via drag-and-drop.', acceptance_criteria: '- Drag-and-drop via dnd-kit binnen de takenlijst\n- Volgorde direct opgeslagen na loslaten', priority: 2 },
{ title: 'Taakstatus bijhouden', description: 'Als Developer wil ik de status van een taak kunnen bijhouden (To Do, In Progress, Done).', acceptance_criteria: '- Status is instelbaar via de UI\n- Story toont een voortgangsindicator op basis van taakstatussen', priority: 1 },
],
},
{
title: 'Claude Code integratie',
description: 'Claude Code kan via een REST API stories en taken ophalen, implementatieplannen vastleggen en commits registreren.',
priority: 2,
stories: [
{ title: 'REST API — story ophalen', description: 'Als Developer (via Claude Code) wil ik de hoogst geprioriteerde open story ophalen via een API-endpoint.', acceptance_criteria: '- Endpoint: GET /api/products/:id/next-story\n- Authentiseerd via API-token\n- Geeft 404 als er geen open stories zijn', priority: 1 },
{ title: 'REST API — eerste 10 taken ophalen', description: 'Als Developer (via Claude Code) wil ik de eerste 10 taken van de Sprint Backlog kunnen ophalen.', acceptance_criteria: '- Endpoint: GET /api/sprints/:id/tasks?limit=10\n- Retourneert taken in huidige volgorde', priority: 1 },
{ title: 'REST API — taakvolgorde aanpassen', description: 'Als Developer (via Claude Code) wil ik de volgorde van taken kunnen aanpassen via de API.', acceptance_criteria: '- Endpoint: PATCH /api/stories/:id/tasks/reorder\n- Accepteert een geordende lijst van taak-ids', priority: 2 },
{ title: 'Implementatieplan vastleggen', description: 'Als Developer (via Claude Code) wil ik een implementatieplan kunnen schrijven naar een story.', acceptance_criteria: '- Endpoint: POST /api/stories/:id/log\n- type: "IMPLEMENTATION_PLAN"', priority: 1 },
{ title: 'Teststatus vastleggen', description: 'Als Developer (via Claude Code) wil ik de uitkomst van testruns kunnen vastleggen in een story.', acceptance_criteria: '- Endpoint: POST /api/stories/:id/log\n- type: "TEST_RESULT", status: "PASSED" | "FAILED"', priority: 1 },
{ title: 'Commit-hash vastleggen', description: 'Als Developer (via Claude Code) wil ik de commit-hash na een succesvolle commit kunnen vastleggen.', acceptance_criteria: '- Endpoint: POST /api/stories/:id/log\n- type: "COMMIT", commit_hash, commit_message', priority: 1 },
{ title: 'Story activiteitenlog in UI', description: 'Als gebruiker wil ik per story een activiteitenlog zien met alle vastgelegde plannen, testresultaten en commits.', acceptance_criteria: '- Log toont alle entries in chronologische volgorde\n- Elk type entry heeft een eigen visuele stijl', priority: 2 },
],
},
{
title: 'Infrastructuur & deployment',
description: 'De app is deployable op Vercel + Neon en volledig lokaal draaibaar zonder externe afhankelijkheden.',
priority: 1,
stories: [
{ title: 'Cloud deployment (Vercel + Neon)', description: 'Als Developer wil ik de app deployen op Vercel met een Neon PostgreSQL-database.', acceptance_criteria: '- next build slaagt zonder fouten\n- Database-migraties worden uitgevoerd via Prisma', priority: 1 },
{ title: 'Lokale modus', description: 'Als Developer wil ik de app volledig lokaal kunnen draaien met een lokale SQLite-database.', acceptance_criteria: '- npm run dev start de app lokaal\n- Database wordt aangemaakt via prisma db push', priority: 1 },
{ title: 'API-token authenticatie', description: 'Als Developer wil ik een API-token kunnen genereren in de app.', acceptance_criteria: '- Gebruiker kan een API-token aanmaken\n- Token wordt eenmalig getoond\n- Alle API-endpoints vereisen een geldig token', priority: 1 },
],
},
]
// Create all PBIs and their stories
for (let pbiIdx = 0; pbiIdx < pbis.length; pbiIdx++) {
const pbiData = pbis[pbiIdx]
const pbi = await prisma.pbi.create({
data: {
product_id: product.id,
title: pbiData.title,
description: pbiData.description,
priority: pbiData.priority,
sort_order: (pbiIdx + 1) * 1.0,
},
})
console.log(` PBI: ${pbi.title} (priority ${pbi.priority})`)
for (let storyIdx = 0; storyIdx < pbiData.stories.length; storyIdx++) {
const storyData = pbiData.stories[storyIdx]
await prisma.story.create({
data: {
pbi_id: pbi.id,
product_id: product.id,
title: storyData.title,
description: storyData.description,
acceptance_criteria: storyData.acceptance_criteria,
priority: storyData.priority,
sort_order: (storyIdx + 1) * 1.0,
status: 'OPEN',
},
})
}
}
console.log('\nSeeding complete!')
console.log('Demo user: username=demo password=demo1234')
console.log('Main user: username=lars password=scrum4me123')
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma?.$disconnect()
})