Scrum4Me/prisma/schema.prisma
Janpeter Visser 9794a9baef
M13: Veilige Claude-agent-workflow (Scrum4Me-side) (#26)
* feat: add pushed_at field to ClaudeJob schema

Nullable DateTime column to record when the agent's feature branch was
pushed to origin. Enables the UI to show a 'pushed' state independently
of DONE status.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: GitHub-link op DONE-card + pushed_at doorvoer

- lib/job-status-url.ts: getBranchUrl(repoUrl, branch) → GitHub tree URL
- JobState + ClaudeJobEvent: pushed_at? veld toegevoegd
- realtime/solo/route.ts: pushed_at in Prisma-select, JobPayload en mapping
- SoloBoardProps + TaskDetailDialog: repoUrl prop doorgevoerd
- task-detail-dialog: "Open op GitHub"-link als done + pushed_at + branch + repoUrl
- 3 unit-tests voor getBranchUrl; totaal 261 tests groen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add VerifyResult enum, verify_only on Task, verify_result on ClaudeJob

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add verify_result+pushed_at to JobState, VerifyResultApi type, SSE payload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: verify_only field on SoloTask, PATCH route saves verify_only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: TaskDetailDialog — verify_result display + verify_only checkbox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: verify_only PATCH + verify_result dialog render + store fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: document VerifyResult enum, verify_only task field, pushed_at in architecture

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(M13): cron /api/cron/cleanup-agent-artifacts — hard-delete FAILED/CANCELLED jobs >7 days

* feat(M13): add auto_pr field to Product schema + migration

* feat(M13): auto_pr toggle in product settings — server action + UI component + tests

* feat(M13): add pr_url to ClaudeJob schema + migration

* feat(M13): UI — 'Open PR' link on DONE-card; pr_url in JobState + SSE + task-dialog

* feat(M13): add retry_count migration + regen erd

- Migration ALTER TABLE claude_jobs ADD COLUMN retry_count INT DEFAULT 0
  (schema.prisma was reeds bijgewerkt in eerdere commits)
- docs/erd.svg geregenereerd voor de complete M13-schema-wijzigingen
  (verify_result, verify_only, pushed_at, pr_url, auto_pr, retry_count)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 13:42:18 +02:00

378 lines
11 KiB
Text

generator client {
provider = "prisma-client-js"
}
generator erd {
provider = "prisma-erd-generator"
output = "../docs/erd.svg"
}
datasource db {
provider = "postgresql"
}
enum Role {
PRODUCT_OWNER
SCRUM_MASTER
DEVELOPER
}
enum StoryStatus {
OPEN
IN_SPRINT
DONE
}
enum PbiStatus {
READY
BLOCKED
DONE
}
enum ClaudeJobStatus {
QUEUED
CLAIMED
RUNNING
DONE
FAILED
CANCELLED
}
enum VerifyResult {
ALIGNED
PARTIAL
EMPTY
DIVERGENT
}
enum TaskStatus {
TO_DO
IN_PROGRESS
REVIEW
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
email String? @unique
password_hash String
is_demo Boolean @default(false)
bio String? @db.VarChar(160)
bio_detail String? @db.VarChar(2000)
avatar_data Bytes?
active_product_id String?
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
roles UserRole[]
api_tokens ApiToken[]
products Product[]
todos Todo[]
product_members ProductMember[]
assigned_stories Story[] @relation("StoryAssignee")
login_pairings LoginPairing[]
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
claude_jobs ClaudeJob[]
claude_workers ClaudeWorker[]
@@index([active_product_id])
@@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?
claimed_jobs ClaudeJob[]
claude_worker ClaudeWorker?
@@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
code String? @db.VarChar(30)
description String?
repo_url String?
definition_of_done String
auto_pr Boolean @default(false)
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
pbis Pbi[]
sprints Sprint[]
stories Story[]
todos Todo[]
members ProductMember[]
active_for_users User[] @relation("UserActiveProduct")
claude_questions ClaudeQuestion[]
claude_jobs ClaudeJob[]
@@unique([user_id, name])
@@unique([user_id, code])
@@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
code String? @db.VarChar(30)
title String
description String?
priority Int
sort_order Float
status PbiStatus @default(READY)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
stories Story[]
@@unique([product_id, code])
@@index([product_id, priority, sort_order])
@@index([product_id, status])
@@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?
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
assignee_id String?
code String? @db.VarChar(30)
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[]
claude_questions ClaudeQuestion[]
@@unique([product_id, code])
@@index([pbi_id, priority, sort_order])
@@index([sprint_id, sort_order])
@@index([product_id, status])
@@index([sprint_id, assignee_id])
@@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?
metadata Json?
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?
implementation_plan String?
priority Int
sort_order Float
status TaskStatus @default(TO_DO)
verify_only Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
claude_questions ClaudeQuestion[]
claude_jobs ClaudeJob[]
@@index([story_id, priority, sort_order])
@@index([sprint_id, status])
@@map("tasks")
}
model ClaudeJob {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
task Task @relation(fields: [task_id], references: [id], onDelete: Cascade)
task_id String
status ClaudeJobStatus @default(QUEUED)
claimed_by_token ApiToken? @relation(fields: [claimed_by_token_id], references: [id], onDelete: SetNull)
claimed_by_token_id String?
claimed_at DateTime?
started_at DateTime?
finished_at DateTime?
pushed_at DateTime?
verify_result VerifyResult?
plan_snapshot String?
branch String?
pr_url String?
summary String?
error String?
retry_count Int @default(0)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([user_id, status])
@@index([task_id, status])
@@index([status, claimed_at])
@@map("claude_jobs")
}
model ClaudeWorker {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
token ApiToken @relation(fields: [token_id], references: [id], onDelete: Cascade)
token_id String
product_id String?
started_at DateTime @default(now())
last_seen_at DateTime @default(now())
@@unique([token_id])
@@index([user_id, last_seen_at])
@@map("claude_workers")
}
model ProductMember {
id String @id @default(cuid())
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
created_at DateTime @default(now())
@@unique([product_id, user_id])
@@index([user_id])
@@map("product_members")
}
model Todo {
id String @id @default(cuid())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
user_id String
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
product_id String?
title String
description String? @db.VarChar(2000)
done Boolean @default(false)
archived Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
@@index([user_id, done, archived])
@@index([user_id, product_id])
@@map("todos")
}
model LoginPairing {
id String @id @default(cuid())
secret_hash String
desktop_token_hash String
status String
user_id String?
user User? @relation(fields: [user_id], references: [id], onDelete: SetNull)
desktop_ua String? @db.VarChar(255)
desktop_ip String? @db.VarChar(45)
created_at DateTime @default(now())
expires_at DateTime
approved_at DateTime?
consumed_at DateTime?
@@index([expires_at])
@@index([status, expires_at])
@@map("login_pairings")
}
model ClaudeQuestion {
id String @id @default(cuid())
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
story_id String
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
task_id String?
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
product_id String // gedenormaliseerd uit story.product_id voor SSE-filter
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
asked_by String // user_id van token-houder (= Claude-token)
question String @db.Text
options Json? // string[] voor multi-choice; null voor free-text
status String // 'open' | 'answered' | 'cancelled' | 'expired'
answer String? @db.Text
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
answered_by String?
answered_at DateTime?
created_at DateTime @default(now())
expires_at DateTime // ingesteld door MCP-tool, default now() + 24h
@@index([story_id, status])
@@index([product_id, status])
@@index([status, expires_at])
@@map("claude_questions")
}