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:
parent
4cf5833c1d
commit
7f94bb6359
32 changed files with 8653 additions and 183 deletions
171
prisma/migrations/20260422184304_init/migration.sql
Normal file
171
prisma/migrations/20260422184304_init/migration.sql
Normal 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");
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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
203
prisma/schema.prisma
Normal 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
238
prisma/seed.ts
Normal 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()
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue