schema: add Idea + IdeaLog models, extend ClaudeJob/Question for ideas (M12 T-491)
- new enums IdeaStatus, ClaudeJobKind, IdeaLogType
- new models Idea (with @@unique([user_id, code]) + pbi_id @unique) and IdeaLog
- User.idea_code_counter Int @default(0) for IDEA-{nnn} code generation
- ClaudeJob.task_id nullable; new idea_id + kind fields + index
- ClaudeQuestion.story_id nullable; new idea_id field + index
- existing call sites narrowed to story-questions / task-jobs (idea-paths come in T-502+)
- includes the M12 plan doc copied from /Users/janpetervisser/.claude/plans
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90343573f3
commit
300e426a4e
6 changed files with 508 additions and 120 deletions
|
|
@ -132,6 +132,7 @@ export async function GET(request: NextRequest) {
|
||||||
status: 'open',
|
status: 'open',
|
||||||
expires_at: { gt: new Date() },
|
expires_at: { gt: new Date() },
|
||||||
product_id: { in: products.map((p) => p.id) },
|
product_id: { in: products.map((p) => p.id) },
|
||||||
|
story_id: { not: null },
|
||||||
},
|
},
|
||||||
orderBy: { created_at: 'desc' },
|
orderBy: { created_at: 'desc' },
|
||||||
take: 100,
|
take: 100,
|
||||||
|
|
@ -150,7 +151,9 @@ export async function GET(request: NextRequest) {
|
||||||
|
|
||||||
enqueue(
|
enqueue(
|
||||||
`event: state\ndata: ${JSON.stringify({
|
`event: state\ndata: ${JSON.stringify({
|
||||||
questions: openQuestions.map((q) => ({
|
questions: openQuestions.flatMap((q) => {
|
||||||
|
if (!q.story || q.story_id === null) return []
|
||||||
|
return [{
|
||||||
id: q.id,
|
id: q.id,
|
||||||
product_id: q.product_id,
|
product_id: q.product_id,
|
||||||
story_id: q.story_id,
|
story_id: q.story_id,
|
||||||
|
|
@ -162,7 +165,8 @@ export async function GET(request: NextRequest) {
|
||||||
options: q.options,
|
options: q.options,
|
||||||
created_at: q.created_at.toISOString(),
|
created_at: q.created_at.toISOString(),
|
||||||
expires_at: q.expires_at.toISOString(),
|
expires_at: q.expires_at.toISOString(),
|
||||||
})),
|
}]
|
||||||
|
}),
|
||||||
})}\n\n`,
|
})}\n\n`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export async function NotificationsBridge({ userId }: NotificationsBridgeProps)
|
||||||
status: 'open',
|
status: 'open',
|
||||||
expires_at: { gt: new Date() },
|
expires_at: { gt: new Date() },
|
||||||
product_id: { in: productIds },
|
product_id: { in: productIds },
|
||||||
|
story_id: { not: null },
|
||||||
},
|
},
|
||||||
orderBy: { created_at: 'desc' },
|
orderBy: { created_at: 'desc' },
|
||||||
take: 100,
|
take: 100,
|
||||||
|
|
@ -44,19 +45,22 @@ export async function NotificationsBridge({ userId }: NotificationsBridgeProps)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const initial: NotificationQuestion[] = openQuestions.map((q) => ({
|
const initial: NotificationQuestion[] = openQuestions.flatMap((q) => {
|
||||||
id: q.id,
|
if (!q.story || q.story_id === null) return []
|
||||||
product_id: q.product_id,
|
return [{
|
||||||
story_id: q.story_id,
|
id: q.id,
|
||||||
task_id: q.task_id,
|
product_id: q.product_id,
|
||||||
story_code: q.story.code,
|
story_id: q.story_id,
|
||||||
story_title: q.story.title,
|
task_id: q.task_id,
|
||||||
assignee_id: q.story.assignee_id,
|
story_code: q.story.code,
|
||||||
question: q.question,
|
story_title: q.story.title,
|
||||||
options: Array.isArray(q.options) ? (q.options as string[]) : null,
|
assignee_id: q.story.assignee_id,
|
||||||
created_at: q.created_at.toISOString(),
|
question: q.question,
|
||||||
expires_at: q.expires_at.toISOString(),
|
options: Array.isArray(q.options) ? (q.options as string[]) : null,
|
||||||
}))
|
created_at: q.created_at.toISOString(),
|
||||||
|
expires_at: q.expires_at.toISOString(),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
return <NotificationsRealtimeMount initial={initial} />
|
return <NotificationsRealtimeMount initial={initial} />
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ Auto-generated on 2026-05-04 from front-matter and headings.
|
||||||
| [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 |
|
| [Landing v2 — lokaal & veilig + architectuurdiagram](./plans/landing-local-first.md) | active | 2026-05-03 |
|
||||||
| [M10 — Password-loze inlog via QR-pairing](./plans/M10-qr-pairing-login.md) | active | 2026-05-03 |
|
| [M10 — Password-loze inlog via QR-pairing](./plans/M10-qr-pairing-login.md) | active | 2026-05-03 |
|
||||||
| [M11 — Claude vraagt, gebruiker antwoordt](./plans/M11-claude-questions.md) | active | 2026-05-03 |
|
| [M11 — Claude vraagt, gebruiker antwoordt](./plans/M11-claude-questions.md) | active | 2026-05-03 |
|
||||||
|
| [M12 — Idea entity + Grill/Plan Claude jobs](./plans/M12-ideas.md) | planned | — |
|
||||||
| [M9 — Actief Product Backlog](./plans/M9-active-product-backlog.md) | active | 2026-05-03 |
|
| [M9 — Actief Product Backlog](./plans/M9-active-product-backlog.md) | active | 2026-05-03 |
|
||||||
| [PBI-11 — Mobile-shell met landscape-lock (settings + backlog + solo)](./plans/PBI-11-mobile-shell.md) | — | — |
|
| [PBI-11 — Mobile-shell met landscape-lock (settings + backlog + solo)](./plans/PBI-11-mobile-shell.md) | — | — |
|
||||||
| [ST-1109 — PBI krijgt een status (Ready / Blocked / Done)](./plans/ST-1109-pbi-status.md) | active | 2026-05-03 |
|
| [ST-1109 — PBI krijgt een status (Ready / Blocked / Done)](./plans/ST-1109-pbi-status.md) | active | 2026-05-03 |
|
||||||
|
|
|
||||||
299
docs/plans/M12-ideas.md
Normal file
299
docs/plans/M12-ideas.md
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
---
|
||||||
|
title: "M12 — Idea entity + Grill/Plan Claude jobs"
|
||||||
|
status: planned
|
||||||
|
audience: implementation
|
||||||
|
language: nl
|
||||||
|
---
|
||||||
|
|
||||||
|
# M12 — Idea entity + Grill/Plan Claude jobs
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Scrum4Me ondersteunt `Todo` als lichtgewicht voorstel-laag, en kan dat handmatig promoveren naar PBI/Story. Dat slaat het *denkproces* niet vast: waarom werd iets een PBI, welke alternatieven zijn afgewogen, welke randvoorwaarden waren er.
|
||||||
|
|
||||||
|
Doel: een nieuw concept **Idee** dat:
|
||||||
|
- werkt als een Todo (top-level lijst, privé per gebruiker), met een **Grill Me**- en **Make Plan**-knop;
|
||||||
|
- via de bestaande Claude-job/worker-infrastructuur een gestructureerd plan oplevert;
|
||||||
|
- het hele planningsproces vastlegt (Q&A, beslissingen, grill-md, plan-md, link naar PBI);
|
||||||
|
- na goedkeuring deterministisch materialiseert tot PBI + stories + taken (incl. `implementation_plan`).
|
||||||
|
|
||||||
|
## Vastgelegde keuzes (uit grill-sessie)
|
||||||
|
|
||||||
|
1. **UI-plek**: top-level `/ideas`, naast `/todos`.
|
||||||
|
2. **Auth-scope**: strikt `user_id`-only (privé, ook ná `PLANNED`). Geen `productAccessFilter` op idea-acties; geen `pbi.idea_id`-veld nodig.
|
||||||
|
3. **Product-binding**: een idee mag bestaan zonder product, maar **Grill Me** én **Make Plan** vereisen een product met `repo_url` (de worker leest sources/docs uit de repo). `claude_jobs.product_id` blijft NOT NULL.
|
||||||
|
4. **Executie-model**: bestaand worker-model. `ClaudeJob{kind:IDEA_*}` QUEUED → lokale Claude-CLI claimt via `wait_for_job`. Knoppen zijn **disabled als `connectedWorkers === 0`** (exact zoals `solo/task-detail-dialog.tsx`).
|
||||||
|
5. **Skill-afhankelijkheid**: **embedded prompts** in `lib/idea-prompts/{grill,make-plan}.md`; meegestuurd in payload. Geen externe `anthropic-skills:grill-me`-plugin-vereiste op de worker.
|
||||||
|
6. **Make-Plan flow**: preview-en-bevestigen. Job produceert `Idea.plan_md`, status → `PLAN_READY`. Aparte knop **"Materialiseer plan"** parseert md → entiteiten in één Prisma-transactie, status → `PLANNED`.
|
||||||
|
7. **Plan-md formaat**: YAML-frontmatter (structuur) + markdown-body (overwegingen, alternatieven, vrije reasoning).
|
||||||
|
8. **Make-Plan-job**: single-pass (geen `ask_user_question`). Twijfels → terug naar grill (append-context).
|
||||||
|
9. **Backward transitions**:
|
||||||
|
- Re-grill vanuit `GRILLED`/`PLAN_READY`: nieuwe `IDEA_GRILL`-job met **append-context** (oude `grill_md` als input); oude versie naar `IdeaLog{type:GRILL_RESULT}` als history.
|
||||||
|
- Re-plan vanuit `PLAN_READY`: idem voor `plan_md`.
|
||||||
|
- PBI-verwijdering vanuit `PLANNED`: **expliciete user-actie "Re-link plan"** (geen DB-trigger). Zet `pbi_id=null`, status `PLAN_READY`.
|
||||||
|
- Failed grill/plan: dedicated states **`GRILL_FAILED` / `PLAN_FAILED`** (zichtbaar voor user), niet stilzwijgend resetten.
|
||||||
|
10. **Logging-model**: `IdeaLog` smal (`DECISION | NOTE | GRILL_RESULT | PLAN_RESULT | STATUS_CHANGE | JOB_EVENT`). Q&A blijft uitsluitend in `claude_questions`. Timeline-tab in UI doet `UNION ALL` over beide bronnen.
|
||||||
|
11. **Opslag md-bestanden**: alleen DB (`Idea.grill_md`, `Idea.plan_md`). Geen auto-commit naar repo (zou strict-private auth-keuze ondergraven). UI biedt **"Download .md"**.
|
||||||
|
12. **Editability**: beide md's bewerkbaar door user in hun ready-states (`GRILLED` voor grill_md, `PLAN_READY` voor plan_md). Bij `PLANNED`: read-only. Yaml-frontmatter wordt zod-gevalideerd-on-save voor `plan_md`.
|
||||||
|
13. **Promotie vanuit Todo**: nieuwe `promoteTodoToIdeaAction` (Todo → DRAFT-Idea + Todo wordt `archived=true`). Bestaande Todo→PBI/Story-acties blijven onaangetast.
|
||||||
|
14. **Demo-policy** (3-laag, zoals Todo): create/edit/archive **mag**; Grill / Make Plan / Materialiseer / promote-from-Todo zijn **geblokkeerd** (proxy.ts 403 + `session.isDemo`-guard + `<DemoTooltip>`).
|
||||||
|
15. **Idea-code**: `Idea.code = "IDEA-{nnn}"`, `@@unique([user_id, code])`, counter op `User.idea_code_counter`.
|
||||||
|
16. **Realtime-store**: nieuwe `stores/idea-store.ts`. `connectedWorkers` direct selecten via `useSoloStore(s => s.connectedWorkers)` (lift naar shared store is opvolg-refactor).
|
||||||
|
17. **Sidebar**: nieuwe entry **Ideeën** (`Lightbulb`-icon) direct boven Todo's.
|
||||||
|
18. **Q&A-expiry**: 24h aanhouden (consistent met bestaand). Verlopen → re-grill (append-context).
|
||||||
|
|
||||||
|
## State machine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──── re-grill ────┐
|
||||||
|
▼ │
|
||||||
|
DRAFT ──Grill Me──▶ GRILLING ─done──▶ GRILLED ─Make Plan─▶ PLANNING ─done──▶ PLAN_READY ─Materialiseer─▶ PLANNED
|
||||||
|
│ fail │ fail │ ▲ │
|
||||||
|
▼ ▼ │ │ │
|
||||||
|
GRILL_FAILED PLAN_FAILED └──┘ re-plan │
|
||||||
|
│ │
|
||||||
|
└────── retry/edit ──────────────────────────────────────── PBI verwijderd ──────────┘
|
||||||
|
+ "Re-link plan"
|
||||||
|
```
|
||||||
|
|
||||||
|
`archived: boolean` is orthogonaal en kan vanuit elke status.
|
||||||
|
|
||||||
|
## Datamodel
|
||||||
|
|
||||||
|
### Nieuwe enums
|
||||||
|
```prisma
|
||||||
|
enum IdeaStatus {
|
||||||
|
DRAFT
|
||||||
|
GRILLING
|
||||||
|
GRILL_FAILED
|
||||||
|
GRILLED
|
||||||
|
PLANNING
|
||||||
|
PLAN_FAILED
|
||||||
|
PLAN_READY
|
||||||
|
PLANNED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClaudeJobKind {
|
||||||
|
TASK_IMPLEMENTATION
|
||||||
|
IDEA_GRILL
|
||||||
|
IDEA_MAKE_PLAN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdeaLogType {
|
||||||
|
DECISION
|
||||||
|
NOTE
|
||||||
|
GRILL_RESULT
|
||||||
|
PLAN_RESULT
|
||||||
|
STATUS_CHANGE
|
||||||
|
JOB_EVENT
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `User`
|
||||||
|
- Veld toevoegen: `idea_code_counter Int @default(0)`.
|
||||||
|
|
||||||
|
### Nieuwe tabel `ideas`
|
||||||
|
```prisma
|
||||||
|
model Idea {
|
||||||
|
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?
|
||||||
|
code String @db.VarChar(30)
|
||||||
|
title String
|
||||||
|
description String? @db.VarChar(4000)
|
||||||
|
grill_md String? @db.Text
|
||||||
|
plan_md String? @db.Text
|
||||||
|
pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull)
|
||||||
|
pbi_id String? @unique
|
||||||
|
status IdeaStatus @default(DRAFT)
|
||||||
|
archived Boolean @default(false)
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
|
|
||||||
|
questions ClaudeQuestion[]
|
||||||
|
jobs ClaudeJob[]
|
||||||
|
logs IdeaLog[]
|
||||||
|
|
||||||
|
@@unique([user_id, code])
|
||||||
|
@@index([user_id, archived, status])
|
||||||
|
@@index([user_id, product_id])
|
||||||
|
@@map("ideas")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nieuwe tabel `idea_logs`
|
||||||
|
```prisma
|
||||||
|
model IdeaLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||||
|
idea_id String
|
||||||
|
type IdeaLogType
|
||||||
|
content String @db.Text
|
||||||
|
metadata Json?
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([idea_id, created_at])
|
||||||
|
@@map("idea_logs")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aanpassingen `claude_jobs`
|
||||||
|
- `task_id` → **nullable**.
|
||||||
|
- `idea_id String?` toegevoegd, FK → `Idea`, `onDelete: Cascade`.
|
||||||
|
- `kind ClaudeJobKind @default(TASK_IMPLEMENTATION)` toegevoegd.
|
||||||
|
- `product_id` blijft NOT NULL.
|
||||||
|
- Raw-SQL check-constraint: `(task_id IS NOT NULL) <> (idea_id IS NOT NULL)`.
|
||||||
|
- Index: `@@index([idea_id, status])`.
|
||||||
|
|
||||||
|
### Aanpassingen `claude_questions`
|
||||||
|
- `story_id` → **nullable**.
|
||||||
|
- `idea_id String?` toegevoegd, FK → `Idea`, `onDelete: Cascade`.
|
||||||
|
- Raw-SQL check-constraint: `(story_id IS NOT NULL) <> (idea_id IS NOT NULL)`.
|
||||||
|
- Index: `@@index([idea_id, status])`.
|
||||||
|
- pg_notify-trigger payload uitbreiden met `idea_id` (nullable). SSE-filter laat idea-payloads alleen door naar `idea.user_id === session.user_id`.
|
||||||
|
|
||||||
|
## Server-laag
|
||||||
|
|
||||||
|
### Schemas + helpers
|
||||||
|
- `lib/schemas/idea.ts` — `ideaCreateSchema`, `ideaUpdateSchema`, `ideaPlanMdFrontmatterSchema`.
|
||||||
|
- `lib/idea-status.ts` — DB-enum ↔ API-string mapping.
|
||||||
|
- `lib/idea-plan-parser.ts` — synchroon: `parsePlanMd(md): ParsedPlan | ZodError`. Gebruikt `yaml`-package + zod.
|
||||||
|
- `lib/idea-code.ts` — atomair `nextIdeaCode(userId)` via Prisma-transactie.
|
||||||
|
|
||||||
|
### Embedded prompts
|
||||||
|
- `lib/idea-prompts/grill.md` — eigen scrum4me-versie. Instrueert: gebruik `ask_user_question` MCP, schrijf via `update_idea_grill_md` aan eind.
|
||||||
|
- `lib/idea-prompts/make-plan.md` — strict yaml-frontmatter-format. Instrueert: lees `grill_md`, gebruik repo-files, **geen vragen**, eindig met `update_idea_plan_md`.
|
||||||
|
|
||||||
|
### Server actions — `actions/ideas.ts`
|
||||||
|
Volg `docs/patterns/server-action.md`: auth → demo-check → zod → user-id-scope-check → write → `revalidatePath`.
|
||||||
|
|
||||||
|
- `createIdeaAction(input)` — `nextIdeaCode(userId)`, status `DRAFT`.
|
||||||
|
- `updateIdeaAction(id, input)` — alleen `DRAFT|GRILL_FAILED|GRILLED|PLAN_FAILED|PLAN_READY`.
|
||||||
|
- `archiveIdeaAction(id)` / `unarchiveIdeaAction(id)`.
|
||||||
|
- `deleteIdeaAction(id)` — geweigerd als `pbi_id` gevuld.
|
||||||
|
- `updateGrillMdAction(id, md)` — alleen in `GRILLED|PLAN_READY`. Logt `IdeaLog{NOTE}`.
|
||||||
|
- `updatePlanMdAction(id, md)` — alleen in `PLAN_READY`. Eerst `parsePlanMd(md)`; bij parse-fail → 422 met line-info.
|
||||||
|
- `startGrillJobAction(id)` — vereist product met `repo_url`, `connectedWorkers > 0`. `ClaudeJob{kind:IDEA_GRILL, idea_id, product_id, QUEUED}`. Status → `GRILLING`. Demo: 403.
|
||||||
|
- `startMakePlanJobAction(id)` — vereist `GRILLED|PLAN_FAILED|PLAN_READY` voor re-plan, product met repo, worker. Status → `PLANNING`. Demo: 403.
|
||||||
|
- `cancelIdeaJobAction(id)` — actieve job CANCELLED, idea-status terug naar vorige.
|
||||||
|
- `materializeIdeaPlanAction(id)` — `PLAN_READY` → `parsePlanMd` → Prisma-`$transaction`:
|
||||||
|
1. Counters incrementeren (PBI/Story/Task).
|
||||||
|
2. INSERT PBI + N stories + M tasks (incl. `implementation_plan`).
|
||||||
|
3. UPDATE idea: `pbi_id`, `status:PLANNED`.
|
||||||
|
4. INSERT `IdeaLog{type:PLAN_RESULT, metadata}`.
|
||||||
|
Rollback bij ANY fail. Demo: 403.
|
||||||
|
- `relinkIdeaPlanAction(id)` — alleen als `status===PLANNED && pbi_id===null`. Status → `PLAN_READY`.
|
||||||
|
- `downloadIdeaMdAction(id, kind: 'grill'|'plan')` — server returnt md.
|
||||||
|
|
||||||
|
### Promote van Todo → Idea
|
||||||
|
- `actions/todos.ts`: nieuwe `promoteTodoToIdeaAction(todoId)` — auth + demo + scope. Maakt Idea (DRAFT) met title/description; zet Todo `archived=true`. Demo: 403.
|
||||||
|
|
||||||
|
### REST-routes
|
||||||
|
- `app/api/ideas/route.ts` (GET, POST) en `app/api/ideas/[id]/route.ts` (GET, PATCH).
|
||||||
|
|
||||||
|
### proxy.ts (demo-laag)
|
||||||
|
- 403 op `POST/PATCH/DELETE /api/ideas*` voor demo-token.
|
||||||
|
- 403 op grill/make-plan/materialize-endpoints.
|
||||||
|
|
||||||
|
## MCP-laag (`scrum4me-mcp`-repo)
|
||||||
|
|
||||||
|
### Nieuwe tools
|
||||||
|
- `get_idea_context(idea_id)` — `{idea, product, repo_url, grill_md_so_far, open_questions, prompt_text}`.
|
||||||
|
- `update_idea_grill_md(idea_id, markdown)` — schrijft veld; status → `GRILLED`; logt `IdeaLog{GRILL_RESULT}`.
|
||||||
|
- `update_idea_plan_md(idea_id, markdown)` — schrijft veld; parser draait server-side; status → `PLAN_READY` of `PLAN_FAILED`.
|
||||||
|
- `log_idea_decision(idea_id, type, content, metadata?)` — types: `DECISION | NOTE`.
|
||||||
|
|
||||||
|
### Uitbreiding bestaande tools
|
||||||
|
- `ask_user_question`: contract uitbreiden — exact één van `story_id` of `idea_id`.
|
||||||
|
- `wait_for_job`: response uitbreiden:
|
||||||
|
- `kind: 'TASK_IMPLEMENTATION' | 'IDEA_GRILL' | 'IDEA_MAKE_PLAN'`
|
||||||
|
- bij `IDEA_*`: `idea`, `product`, `repo_url`, `prompt_text`.
|
||||||
|
- `update_job_status`: bij `failed` voor IDEA_*-jobs zet idea-status `GRILL_FAILED` / `PLAN_FAILED`.
|
||||||
|
|
||||||
|
### Schema-drift
|
||||||
|
- `docs/runbooks/mcp-integration.md:62`: schema-drift-watchdog moet groen zijn vóór merge. MCP-server-PR parallel.
|
||||||
|
|
||||||
|
## Realtime-laag
|
||||||
|
|
||||||
|
- `app/api/realtime/notifications/route.ts` — idea-questions alleen aan `idea.user_id === session.user_id`.
|
||||||
|
- `app/api/realtime/solo/route.ts` — `JobPayload` uitbreiden met `kind` en `idea_id`. Idea-jobs op `user_id`.
|
||||||
|
- `stores/idea-store.ts` (nieuw). `connectedWorkers` direct uit `useSoloStore`.
|
||||||
|
|
||||||
|
## UI-laag
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
- `app/(app)/ideas/page.tsx` — top-level lijst.
|
||||||
|
- `app/(app)/ideas/[id]/page.tsx` — detailpagina met tabs **Idee** · **Grill** · **Plan** · **Timeline**.
|
||||||
|
- Sidebar-entry: `Lightbulb`, label "Ideeën", boven Todo's.
|
||||||
|
|
||||||
|
### Componenten — `components/ideas/`
|
||||||
|
- `idea-list.tsx` — TanStack Table; kolommen code/title/product/status/archived. Bulk-archive.
|
||||||
|
- `idea-row-actions.tsx` — Grill Me / Make Plan / Materialiseer / Edit / Archive met disabled-rules.
|
||||||
|
- `idea-dialog.tsx` + `components/dialogs/idea-dialog.tsx` (wrapper) volgens dialog-pattern.
|
||||||
|
- `idea-md-editor.tsx` — markdown editor met yaml-validate voor plan_md.
|
||||||
|
- `idea-timeline.tsx` — UNION-view IdeaLog + claude_questions.
|
||||||
|
- `idea-pbi-link-card.tsx` — incl. "Re-link plan"-banner.
|
||||||
|
- `download-md-button.tsx`.
|
||||||
|
|
||||||
|
### Promote-from-Todo UI
|
||||||
|
- `components/todos/todo-list.tsx`: extra menu-item "Promote naar Idee".
|
||||||
|
|
||||||
|
### Profiel-doc
|
||||||
|
- `docs/specs/dialogs/idea.md` — verplicht volgens dialog-pattern.
|
||||||
|
|
||||||
|
## Te raken / aan te maken bestanden
|
||||||
|
|
||||||
|
| Laag | Bestand |
|
||||||
|
|---|---|
|
||||||
|
| Schema | `prisma/schema.prisma` |
|
||||||
|
| Migratie | `prisma/migrations/<ts>_add_ideas/migration.sql` |
|
||||||
|
| Schemas | `lib/schemas/idea.ts`, `lib/idea-status.ts`, `lib/idea-plan-parser.ts`, `lib/idea-code.ts` |
|
||||||
|
| Prompts | `lib/idea-prompts/grill.md`, `lib/idea-prompts/make-plan.md` |
|
||||||
|
| Actions | `actions/ideas.ts`, uitbreiding `actions/todos.ts` |
|
||||||
|
| API | `app/api/ideas/route.ts`, `app/api/ideas/[id]/route.ts`, `proxy.ts` |
|
||||||
|
| Realtime | `app/api/realtime/notifications/route.ts`, `app/api/realtime/solo/route.ts` |
|
||||||
|
| Pages | `app/(app)/ideas/page.tsx`, `app/(app)/ideas/[id]/page.tsx` |
|
||||||
|
| UI | `components/ideas/*.tsx`, `components/dialogs/idea-dialog.tsx`, sidebar-update |
|
||||||
|
| Store | `stores/idea-store.ts` |
|
||||||
|
| Docs | `docs/specs/dialogs/idea.md`, `docs/runbooks/mcp-integration.md`, `docs/backlog/index.md` |
|
||||||
|
| MCP-server | `madhura68/scrum4me-mcp` (parallel-PR) |
|
||||||
|
|
||||||
|
## Implementatievolgorde
|
||||||
|
|
||||||
|
1. **DB & migratie**
|
||||||
|
2. **Lib + schemas + prompts**
|
||||||
|
3. **Server actions + Todo-promote**
|
||||||
|
4. **REST + proxy demo-laag**
|
||||||
|
5. **Realtime SSE + idea-store**
|
||||||
|
6. **MCP-server tools (extern repo, parallel)**
|
||||||
|
7. **UI lijst + row-actions**
|
||||||
|
8. **UI detail + dialog + tabs**
|
||||||
|
9. **UI promote-from-Todo + sidebar-entry**
|
||||||
|
10. **End-to-end smoke + docs**
|
||||||
|
|
||||||
|
## Verificatie
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run lint && npm test && npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
End-to-end:
|
||||||
|
1. `npm run dev` + lokale Claude-CLI met `wait_for_job`-loop.
|
||||||
|
2. Maak idee, koppel aan product. Status `DRAFT`.
|
||||||
|
3. Grill Me → vragen via answer-modal → `update_idea_grill_md` → `GRILLED`.
|
||||||
|
4. Edit grill_md handmatig → `IdeaLog{NOTE}`.
|
||||||
|
5. Make Plan → `update_idea_plan_md` → `PLAN_READY`.
|
||||||
|
6. Yaml-fout in plan_md → save geblokkeerd.
|
||||||
|
7. Materialiseer → PBI + stories + taken in transactie. `idea.pbi_id` gezet, `PLANNED`.
|
||||||
|
8. PBI verwijderen → "Re-link plan"-banner → `PLAN_READY`.
|
||||||
|
9. Demo-test: knoppen geblokkeerd via DemoTooltip + 403.
|
||||||
|
10. Failure-test: kill worker → `GRILL_FAILED`/`PLAN_FAILED`.
|
||||||
|
11. Promote-test: Todo → "Promote naar Idee" → DRAFT-Idea, Todo archived.
|
||||||
|
|
||||||
|
## Open punten (niet-blokkerend)
|
||||||
|
|
||||||
|
- Concrete copy-finetuning van prompt-md's tijdens implementatie.
|
||||||
|
- Lift `connectedWorkers` naar gedeelde `worker-presence-store` (opvolg-refactor).
|
||||||
|
- Optionele "Commit plan_md naar repo"-knop (buiten v1).
|
||||||
|
|
@ -40,6 +40,7 @@ export async function getVerifyResultStats(
|
||||||
status: 'DONE' as const,
|
status: 'DONE' as const,
|
||||||
verify_result: { not: null as null },
|
verify_result: { not: null as null },
|
||||||
finished_at: { gt: cutoff },
|
finished_at: { gt: cutoff },
|
||||||
|
task_id: { not: null },
|
||||||
}
|
}
|
||||||
|
|
||||||
const [grouped, rawEmpty, rawDivergent] = await Promise.all([
|
const [grouped, rawEmpty, rawDivergent] = await Promise.all([
|
||||||
|
|
@ -82,7 +83,8 @@ export async function getVerifyResultStats(
|
||||||
.filter(r => countMap.has(r))
|
.filter(r => countMap.has(r))
|
||||||
.map(r => ({ result: r, count: countMap.get(r)! }))
|
.map(r => ({ result: r, count: countMap.get(r)! }))
|
||||||
|
|
||||||
function toTopJob(j: { id: string; finished_at: Date | null; task: { id: string; title: string }; product: { id: string; name: string } }): TopJob {
|
function toTopJob(j: { id: string; finished_at: Date | null; task: { id: string; title: string } | null; product: { id: string; name: string } }): TopJob | null {
|
||||||
|
if (!j.task) return null
|
||||||
return {
|
return {
|
||||||
jobId: j.id,
|
jobId: j.id,
|
||||||
taskId: j.task.id,
|
taskId: j.task.id,
|
||||||
|
|
@ -95,8 +97,8 @@ export async function getVerifyResultStats(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
counts,
|
counts,
|
||||||
topEmpty: rawEmpty.map(toTopJob),
|
topEmpty: rawEmpty.map(toTopJob).filter((j): j is TopJob => j !== null),
|
||||||
topDivergent: rawDivergent.map(toTopJob),
|
topDivergent: rawDivergent.map(toTopJob).filter((j): j is TopJob => j !== null),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,30 +74,58 @@ enum SprintStatus {
|
||||||
COMPLETED
|
COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum IdeaStatus {
|
||||||
|
DRAFT
|
||||||
|
GRILLING
|
||||||
|
GRILL_FAILED
|
||||||
|
GRILLED
|
||||||
|
PLANNING
|
||||||
|
PLAN_FAILED
|
||||||
|
PLAN_READY
|
||||||
|
PLANNED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClaudeJobKind {
|
||||||
|
TASK_IMPLEMENTATION
|
||||||
|
IDEA_GRILL
|
||||||
|
IDEA_MAKE_PLAN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdeaLogType {
|
||||||
|
DECISION
|
||||||
|
NOTE
|
||||||
|
GRILL_RESULT
|
||||||
|
PLAN_RESULT
|
||||||
|
STATUS_CHANGE
|
||||||
|
JOB_EVENT
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
username String @unique
|
username String @unique
|
||||||
email String? @unique
|
email String? @unique
|
||||||
password_hash String
|
password_hash String
|
||||||
is_demo Boolean @default(false)
|
is_demo Boolean @default(false)
|
||||||
bio String? @db.VarChar(160)
|
bio String? @db.VarChar(160)
|
||||||
bio_detail String? @db.VarChar(2000)
|
bio_detail String? @db.VarChar(2000)
|
||||||
avatar_data Bytes?
|
avatar_data Bytes?
|
||||||
active_product_id String?
|
active_product_id String?
|
||||||
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)
|
||||||
created_at DateTime @default(now())
|
idea_code_counter Int @default(0)
|
||||||
updated_at DateTime @updatedAt
|
created_at DateTime @default(now())
|
||||||
roles UserRole[]
|
updated_at DateTime @updatedAt
|
||||||
api_tokens ApiToken[]
|
roles UserRole[]
|
||||||
products Product[]
|
api_tokens ApiToken[]
|
||||||
todos Todo[]
|
products Product[]
|
||||||
product_members ProductMember[]
|
todos Todo[]
|
||||||
assigned_stories Story[] @relation("StoryAssignee")
|
ideas Idea[]
|
||||||
login_pairings LoginPairing[]
|
product_members ProductMember[]
|
||||||
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
assigned_stories Story[] @relation("StoryAssignee")
|
||||||
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
login_pairings LoginPairing[]
|
||||||
claude_jobs ClaudeJob[]
|
asked_questions ClaudeQuestion[] @relation("ClaudeQuestionAsker")
|
||||||
claude_workers ClaudeWorker[]
|
answered_questions ClaudeQuestion[] @relation("ClaudeQuestionAnswerer")
|
||||||
|
claude_jobs ClaudeJob[]
|
||||||
|
claude_workers ClaudeWorker[]
|
||||||
|
|
||||||
@@index([active_product_id])
|
@@index([active_product_id])
|
||||||
@@map("users")
|
@@map("users")
|
||||||
|
|
@ -114,33 +142,33 @@ model UserRole {
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiToken {
|
model ApiToken {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||||
user_id String
|
user_id String
|
||||||
token_hash String @unique
|
token_hash String @unique
|
||||||
label String?
|
label String?
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
revoked_at DateTime?
|
revoked_at DateTime?
|
||||||
claimed_jobs ClaudeJob[]
|
claimed_jobs ClaudeJob[]
|
||||||
claude_worker ClaudeWorker?
|
claude_worker ClaudeWorker?
|
||||||
|
|
||||||
@@index([token_hash])
|
@@index([token_hash])
|
||||||
@@map("api_tokens")
|
@@map("api_tokens")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Product {
|
model Product {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||||
user_id String
|
user_id String
|
||||||
name String
|
name String
|
||||||
code String? @db.VarChar(30)
|
code String? @db.VarChar(30)
|
||||||
description String?
|
description String?
|
||||||
repo_url String?
|
repo_url String?
|
||||||
definition_of_done String
|
definition_of_done String
|
||||||
auto_pr Boolean @default(false)
|
auto_pr Boolean @default(false)
|
||||||
archived Boolean @default(false)
|
archived Boolean @default(false)
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
pbis Pbi[]
|
pbis Pbi[]
|
||||||
sprints Sprint[]
|
sprints Sprint[]
|
||||||
stories Story[]
|
stories Story[]
|
||||||
|
|
@ -150,6 +178,7 @@ model Product {
|
||||||
active_for_users User[] @relation("UserActiveProduct")
|
active_for_users User[] @relation("UserActiveProduct")
|
||||||
claude_questions ClaudeQuestion[]
|
claude_questions ClaudeQuestion[]
|
||||||
claude_jobs ClaudeJob[]
|
claude_jobs ClaudeJob[]
|
||||||
|
ideas Idea[]
|
||||||
|
|
||||||
@@unique([user_id, name])
|
@@unique([user_id, name])
|
||||||
@@unique([user_id, code])
|
@@unique([user_id, code])
|
||||||
|
|
@ -158,20 +187,21 @@ model Product {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Pbi {
|
model Pbi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||||
product_id String
|
product_id String
|
||||||
code String @db.VarChar(30)
|
code String @db.VarChar(30)
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
priority Int
|
priority Int
|
||||||
sort_order Float
|
sort_order Float
|
||||||
status PbiStatus @default(READY)
|
status PbiStatus @default(READY)
|
||||||
pr_url String?
|
pr_url String?
|
||||||
pr_merged_at DateTime?
|
pr_merged_at DateTime?
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
stories Story[]
|
stories Story[]
|
||||||
|
idea Idea?
|
||||||
|
|
||||||
@@unique([product_id, code])
|
@@unique([product_id, code])
|
||||||
@@index([product_id, priority, sort_order])
|
@@index([product_id, priority, sort_order])
|
||||||
|
|
@ -180,24 +210,24 @@ model Pbi {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Story {
|
model Story {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
|
pbi Pbi @relation(fields: [pbi_id], references: [id], onDelete: Cascade)
|
||||||
pbi_id String
|
pbi_id String
|
||||||
product Product @relation(fields: [product_id], references: [id])
|
product Product @relation(fields: [product_id], references: [id])
|
||||||
product_id String
|
product_id String
|
||||||
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
||||||
sprint_id String?
|
sprint_id String?
|
||||||
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
|
assignee User? @relation("StoryAssignee", fields: [assignee_id], references: [id], onDelete: SetNull)
|
||||||
assignee_id String?
|
assignee_id String?
|
||||||
code String @db.VarChar(30)
|
code String @db.VarChar(30)
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
acceptance_criteria String?
|
acceptance_criteria String?
|
||||||
priority Int
|
priority Int
|
||||||
sort_order Float
|
sort_order Float
|
||||||
status StoryStatus @default(OPEN)
|
status StoryStatus @default(OPEN)
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
logs StoryLog[]
|
logs StoryLog[]
|
||||||
tasks Task[]
|
tasks Task[]
|
||||||
claude_questions ClaudeQuestion[]
|
claude_questions ClaudeQuestion[]
|
||||||
|
|
@ -244,29 +274,29 @@ model Sprint {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Task {
|
model Task {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
||||||
story_id String
|
story_id String
|
||||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||||
product_id String
|
product_id String
|
||||||
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
sprint Sprint? @relation(fields: [sprint_id], references: [id])
|
||||||
sprint_id String?
|
sprint_id String?
|
||||||
code String @db.VarChar(30)
|
code String @db.VarChar(30)
|
||||||
title String
|
title String
|
||||||
description String?
|
description String?
|
||||||
implementation_plan String?
|
implementation_plan String?
|
||||||
priority Int
|
priority Int
|
||||||
sort_order Float
|
sort_order Float
|
||||||
status TaskStatus @default(TO_DO)
|
status TaskStatus @default(TO_DO)
|
||||||
verify_only Boolean @default(false)
|
verify_only Boolean @default(false)
|
||||||
verify_required VerifyRequired @default(ALIGNED_OR_PARTIAL)
|
verify_required VerifyRequired @default(ALIGNED_OR_PARTIAL)
|
||||||
// Override product.repo_url for branch/worktree/push purposes. Set when
|
// Override product.repo_url for branch/worktree/push purposes. Set when
|
||||||
// a task targets a different repo than its parent product (e.g. an
|
// a task targets a different repo than its parent product (e.g. an
|
||||||
// MCP-server task tracked under the main product's PBI). Falls back to
|
// MCP-server task tracked under the main product's PBI). Falls back to
|
||||||
// product.repo_url when null.
|
// product.repo_url when null.
|
||||||
repo_url String?
|
repo_url String?
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
claude_questions ClaudeQuestion[]
|
claude_questions ClaudeQuestion[]
|
||||||
claude_jobs ClaudeJob[]
|
claude_jobs ClaudeJob[]
|
||||||
|
|
||||||
|
|
@ -283,8 +313,11 @@ model ClaudeJob {
|
||||||
user_id String
|
user_id String
|
||||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||||
product_id String
|
product_id String
|
||||||
task Task @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
task Task? @relation(fields: [task_id], references: [id], onDelete: Cascade)
|
||||||
task_id String
|
task_id String?
|
||||||
|
idea Idea? @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||||
|
idea_id String?
|
||||||
|
kind ClaudeJobKind @default(TASK_IMPLEMENTATION)
|
||||||
status ClaudeJobStatus @default(QUEUED)
|
status ClaudeJobStatus @default(QUEUED)
|
||||||
claimed_by_token ApiToken? @relation(fields: [claimed_by_token_id], references: [id], onDelete: SetNull)
|
claimed_by_token ApiToken? @relation(fields: [claimed_by_token_id], references: [id], onDelete: SetNull)
|
||||||
claimed_by_token_id String?
|
claimed_by_token_id String?
|
||||||
|
|
@ -304,20 +337,21 @@ model ClaudeJob {
|
||||||
|
|
||||||
@@index([user_id, status])
|
@@index([user_id, status])
|
||||||
@@index([task_id, status])
|
@@index([task_id, status])
|
||||||
|
@@index([idea_id, status])
|
||||||
@@index([status, claimed_at])
|
@@index([status, claimed_at])
|
||||||
@@index([status, finished_at])
|
@@index([status, finished_at])
|
||||||
@@map("claude_jobs")
|
@@map("claude_jobs")
|
||||||
}
|
}
|
||||||
|
|
||||||
model ClaudeWorker {
|
model ClaudeWorker {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||||
user_id String
|
user_id String
|
||||||
token ApiToken @relation(fields: [token_id], references: [id], onDelete: Cascade)
|
token ApiToken @relation(fields: [token_id], references: [id], onDelete: Cascade)
|
||||||
token_id String
|
token_id String
|
||||||
product_id String?
|
product_id String?
|
||||||
started_at DateTime @default(now())
|
started_at DateTime @default(now())
|
||||||
last_seen_at DateTime @default(now())
|
last_seen_at DateTime @default(now())
|
||||||
|
|
||||||
@@unique([token_id])
|
@@unique([token_id])
|
||||||
@@index([user_id, last_seen_at])
|
@@index([user_id, last_seen_at])
|
||||||
|
|
@ -338,23 +372,64 @@ model ProductMember {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Todo {
|
model Todo {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||||
user_id String
|
user_id String
|
||||||
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
|
product Product? @relation(fields: [product_id], references: [id], onDelete: SetNull)
|
||||||
product_id String?
|
product_id String?
|
||||||
title String
|
title String
|
||||||
description String? @db.VarChar(2000)
|
description String? @db.VarChar(2000)
|
||||||
done Boolean @default(false)
|
done Boolean @default(false)
|
||||||
archived Boolean @default(false)
|
archived Boolean @default(false)
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime @updatedAt
|
updated_at DateTime @updatedAt
|
||||||
|
|
||||||
@@index([user_id, done, archived])
|
@@index([user_id, done, archived])
|
||||||
@@index([user_id, product_id])
|
@@index([user_id, product_id])
|
||||||
@@map("todos")
|
@@map("todos")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Idea {
|
||||||
|
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?
|
||||||
|
code String @db.VarChar(30)
|
||||||
|
title String
|
||||||
|
description String? @db.VarChar(4000)
|
||||||
|
grill_md String? @db.Text
|
||||||
|
plan_md String? @db.Text
|
||||||
|
pbi Pbi? @relation(fields: [pbi_id], references: [id], onDelete: SetNull)
|
||||||
|
pbi_id String? @unique
|
||||||
|
status IdeaStatus @default(DRAFT)
|
||||||
|
archived Boolean @default(false)
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
updated_at DateTime @updatedAt
|
||||||
|
|
||||||
|
questions ClaudeQuestion[]
|
||||||
|
jobs ClaudeJob[]
|
||||||
|
logs IdeaLog[]
|
||||||
|
|
||||||
|
@@unique([user_id, code])
|
||||||
|
@@index([user_id, archived, status])
|
||||||
|
@@index([user_id, product_id])
|
||||||
|
@@map("ideas")
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdeaLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||||
|
idea_id String
|
||||||
|
type IdeaLogType
|
||||||
|
content String @db.Text
|
||||||
|
metadata Json?
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([idea_id, created_at])
|
||||||
|
@@map("idea_logs")
|
||||||
|
}
|
||||||
|
|
||||||
model LoginPairing {
|
model LoginPairing {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
secret_hash String
|
secret_hash String
|
||||||
|
|
@ -375,26 +450,29 @@ model LoginPairing {
|
||||||
}
|
}
|
||||||
|
|
||||||
model ClaudeQuestion {
|
model ClaudeQuestion {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
story Story @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
story Story? @relation(fields: [story_id], references: [id], onDelete: Cascade)
|
||||||
story_id String
|
story_id String?
|
||||||
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
|
task Task? @relation(fields: [task_id], references: [id], onDelete: SetNull)
|
||||||
task_id String?
|
task_id String?
|
||||||
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
idea Idea? @relation(fields: [idea_id], references: [id], onDelete: Cascade)
|
||||||
product_id String // gedenormaliseerd uit story.product_id voor SSE-filter
|
idea_id String?
|
||||||
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
|
product Product @relation(fields: [product_id], references: [id], onDelete: Cascade)
|
||||||
asked_by String // user_id van token-houder (= Claude-token)
|
product_id String // gedenormaliseerd uit story.product_id voor SSE-filter
|
||||||
question String @db.Text
|
asker User @relation("ClaudeQuestionAsker", fields: [asked_by], references: [id])
|
||||||
options Json? // string[] voor multi-choice; null voor free-text
|
asked_by String // user_id van token-houder (= Claude-token)
|
||||||
status String // 'open' | 'answered' | 'cancelled' | 'expired'
|
question String @db.Text
|
||||||
answer String? @db.Text
|
options Json? // string[] voor multi-choice; null voor free-text
|
||||||
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
|
status String // 'open' | 'answered' | 'cancelled' | 'expired'
|
||||||
answered_by String?
|
answer String? @db.Text
|
||||||
answered_at DateTime?
|
answerer User? @relation("ClaudeQuestionAnswerer", fields: [answered_by], references: [id])
|
||||||
created_at DateTime @default(now())
|
answered_by String?
|
||||||
expires_at DateTime // ingesteld door MCP-tool, default now() + 24h
|
answered_at DateTime?
|
||||||
|
created_at DateTime @default(now())
|
||||||
|
expires_at DateTime // ingesteld door MCP-tool, default now() + 24h
|
||||||
|
|
||||||
@@index([story_id, status])
|
@@index([story_id, status])
|
||||||
|
@@index([idea_id, status])
|
||||||
@@index([product_id, status])
|
@@index([product_id, status])
|
||||||
@@index([status, expires_at])
|
@@index([status, expires_at])
|
||||||
@@map("claude_questions")
|
@@map("claude_questions")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue