Voegt een verplicht code-veld toe aan Sprint, sequentieel per product (consistent met PBI-N, ST-NNN, T-N). - **Schema** — `Sprint.code String @db.VarChar(30)` + `@@unique([product_id, code])` - **Migratie** — voegt kolom toe als nullable, backfillt bestaande sprints via `ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY created_at)` als `SP-N`, en zet daarna NOT NULL + UNIQUE. - **Generator** — `generateNextSprintCode(productId)` in lib/code-server.ts volgt het patroon van story/pbi/task; createSprintAction gebruikt `createWithCodeRetry` voor race-bescherming. - **Seed** — sprint-counter per product (`SP-1`, `SP-2`, ...). Zichtbaar in: - Sprint-header (`Product › Sprint actief · SP-3`) - JobCard + JobDetailPane voor SPRINT_IMPLEMENTATION jobs - Insights: VelocityChart x-axis (compacter dan goal-truncated), AlignmentTrend tooltip, SprintInfoStrip - actions/jobs-page.ts: `sprintCode` is weer een echte code i.p.v. null Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
243 lines
7.4 KiB
TypeScript
243 lines
7.4 KiB
TypeScript
// Seed bron: docs/scrum4me-backlog.md → prisma/seed-data/parse-backlog.ts.
|
|
// Pas alléén de backlog-markdown aan als je seed-data wilt wijzigen, niet deze file.
|
|
import { PrismaClient } from '@prisma/client'
|
|
import * as dotenv from 'dotenv'
|
|
import * as path from 'path'
|
|
import * as bcrypt from 'bcryptjs'
|
|
import { loadBacklog } from './seed-data/parse-backlog'
|
|
|
|
// 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')
|
|
|
|
const { Pool } = await import('pg')
|
|
const { PrismaPg } = await import('@prisma/adapter-pg')
|
|
const pool = new Pool({ connectionString: url })
|
|
const adapter = new PrismaPg(pool)
|
|
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 tester user for cross-user isolation tests (no products)
|
|
const testerHash = await bcrypt.hash('tester123', 12)
|
|
const tester = await prisma.user.upsert({
|
|
where: { username: 'tester' },
|
|
update: {},
|
|
create: {
|
|
username: 'tester',
|
|
password_hash: testerHash,
|
|
is_demo: false,
|
|
roles: {
|
|
create: [
|
|
{ role: 'PRODUCT_OWNER' },
|
|
{ role: 'DEVELOPER' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
console.log(`Tester user: ${tester.username} (id: ${tester.id})`)
|
|
|
|
// Reset demo product data — delete in dependency order to avoid FK violations
|
|
const existingProducts = await prisma.product.findMany({ where: { user_id: demo.id }, select: { id: true } })
|
|
for (const p of existingProducts) {
|
|
// Stories reference product_id directly (no cascade), so delete PBIs first (cascades to stories via pbi_id)
|
|
const existingPbis = await prisma.pbi.findMany({ where: { product_id: p.id }, select: { id: true } })
|
|
for (const pbi of existingPbis) {
|
|
await prisma.story.deleteMany({ where: { pbi_id: pbi.id } })
|
|
}
|
|
await prisma.pbi.deleteMany({ where: { product_id: p.id } })
|
|
await prisma.sprint.deleteMany({ where: { product_id: p.id } })
|
|
}
|
|
await prisma.product.deleteMany({ where: { user_id: demo.id } })
|
|
|
|
const product = await prisma.product.create({
|
|
data: {
|
|
user_id: demo.id,
|
|
name: 'Scrum4Me',
|
|
code: 'SCRUM4ME',
|
|
description:
|
|
'Een desktop-first fullstack webapplicatie voor solo developers en kleine Scrum Teams die meerdere softwareprojecten parallel beheren.',
|
|
repo_url: 'https://github.com/madhura68/Scrum4Me',
|
|
definition_of_done:
|
|
'Code is geïmplementeerd, type-checked, getest, gedocumenteerd in code en docs, en gedeployed naar Vercel zonder regressies.',
|
|
archived: false,
|
|
},
|
|
})
|
|
|
|
console.log(`Product created: ${product.name} (id: ${product.id}, owner: demo)`)
|
|
|
|
await prisma.productMember.create({
|
|
data: { product_id: product.id, user_id: user.id },
|
|
})
|
|
console.log(` Added ${user.username} as member of ${product.name}`)
|
|
|
|
const milestones = await loadBacklog(root)
|
|
console.log(`Loaded backlog: ${milestones.length} milestones, ${milestones.reduce((acc, m) => acc + m.stories.length, 0)} stories`)
|
|
|
|
let productTaskCounter = 0
|
|
let sprintCounter = 0
|
|
for (const ms of milestones) {
|
|
const pbi = await prisma.pbi.create({
|
|
data: {
|
|
product_id: product.id,
|
|
code: ms.key,
|
|
title: ms.title,
|
|
description: ms.goal,
|
|
priority: ms.priority,
|
|
sort_order: ms.sort_order,
|
|
},
|
|
})
|
|
|
|
const sprint = await prisma.sprint.create({
|
|
data: {
|
|
product_id: product.id,
|
|
code: `SP-${++sprintCounter}`,
|
|
sprint_goal: `${ms.key} — ${ms.goal}`,
|
|
status: ms.sprint_status,
|
|
},
|
|
})
|
|
|
|
console.log(
|
|
` PBI ${pbi.title} (priority ${pbi.priority}) + sprint ${ms.sprint_status}`,
|
|
)
|
|
|
|
// M3.5 = de huidige sprint die nog moet beginnen — alle stories en taken
|
|
// worden geforceerd op niet-uitgevoerd, ongeacht de checkbox in de backlog.
|
|
const forceOpen = ms.key === 'M3.5'
|
|
|
|
for (const s of ms.stories) {
|
|
const isActive = ms.sprint_status === 'ACTIVE'
|
|
const effectivelyDone = !forceOpen && s.status === 'DONE'
|
|
const inSprint = isActive || effectivelyDone
|
|
const storyStatus = effectivelyDone ? 'DONE' : isActive ? 'IN_SPRINT' : 'OPEN'
|
|
const storySummary = s.tasks.map((t) => t.title).join('; ')
|
|
|
|
const story = await prisma.story.create({
|
|
data: {
|
|
pbi_id: pbi.id,
|
|
product_id: product.id,
|
|
sprint_id: inSprint ? sprint.id : null,
|
|
code: s.ref,
|
|
title: s.title,
|
|
description: storySummary,
|
|
acceptance_criteria: s.acceptance_criteria,
|
|
priority: ms.priority,
|
|
sort_order: s.sort_order,
|
|
status: storyStatus,
|
|
},
|
|
})
|
|
|
|
for (const t of s.tasks) {
|
|
productTaskCounter += 1
|
|
await prisma.task.create({
|
|
data: {
|
|
story_id: story.id,
|
|
product_id: product.id,
|
|
sprint_id: inSprint ? sprint.id : null,
|
|
code: `T-${productTaskCounter}`,
|
|
title: t.title,
|
|
description: t.description,
|
|
priority: ms.priority,
|
|
sort_order: t.sort_order,
|
|
status: effectivelyDone ? 'DONE' : 'TO_DO',
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
const modelPrices = [
|
|
{
|
|
model_id: 'claude-opus-4-7',
|
|
input_price_per_1m: 15.0,
|
|
output_price_per_1m: 75.0,
|
|
cache_read_price_per_1m: 1.5,
|
|
cache_write_price_per_1m: 18.75,
|
|
},
|
|
{
|
|
model_id: 'claude-sonnet-4-6',
|
|
input_price_per_1m: 3.0,
|
|
output_price_per_1m: 15.0,
|
|
cache_read_price_per_1m: 0.3,
|
|
cache_write_price_per_1m: 3.75,
|
|
},
|
|
{
|
|
model_id: 'claude-haiku-4-5-20251001',
|
|
input_price_per_1m: 0.8,
|
|
output_price_per_1m: 4.0,
|
|
cache_read_price_per_1m: 0.08,
|
|
cache_write_price_per_1m: 1.0,
|
|
},
|
|
]
|
|
|
|
for (const mp of modelPrices) {
|
|
await prisma.modelPrice.upsert({
|
|
where: { model_id: mp.model_id },
|
|
update: mp,
|
|
create: mp,
|
|
})
|
|
console.log(` ModelPrice upserted: ${mp.model_id}`)
|
|
}
|
|
|
|
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()
|
|
})
|