feat(ideas): upload-plan knop — short-circuit van Make-Plan AI-flow (#197)
Voegt een 'Upload plan' knop toe in idea-row-actions (verschijnt in zowel list als idea-detail). Klik → file picker → kies .md → server-side parse + opslaan; idea-status springt naar PLAN_READY. Vandaaruit de bestaande 'Maak PBI' knop voor materialize. Server (uploadPlanMdAction): - Toegestaan vanuit DRAFT, GRILLED, PLAN_FAILED, PLAN_READY - DRAFT → skip-grill: status gaat direct naar PLAN_READY - PLAN_READY overschrijft het bestaande plan (consistent met updatePlanMdAction, geen confirmation) - Geblokkeerd in GRILLING/PLANNING (job loopt), PLANNED (al gematerialiseerd) - Parse-failure → 422 + details (NIET opslaan, zodat een onparseerbaar plan nooit in de DB belandt) - Empty / >100k chars → 422 - Schrijft IdeaLog NOTE met from_status + length - Rate-limit + demo-guard + ownership-check via loadOwnedIdea (zelfde patroon als updatePlanMdAction) UI (idea-row-actions.tsx): - Hidden <input type=file accept=".md,.markdown,text/markdown,text/plain"> - FileReader → text → action - Toast bij success + router.refresh() - Blocked-tooltip in andere statussen Tests: 10 nieuwe in __tests__/actions/ideas-crud.test.ts dekkend voor: happy paths (DRAFT/GRILLED/PLAN_READY-overwrite/PLAN_FAILED), blocks (PLANNED/GRILLING), validation (empty/oversized/parse-fail), 404. Full suite groen: 849/849. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
551550791e
commit
76c2efd27c
3 changed files with 224 additions and 1 deletions
|
|
@ -311,6 +311,73 @@ export async function updatePlanMdAction(
|
|||
return { success: true }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Upload — gebruiker plakt/uploadt zelf een plan.md in plaats van de Make-Plan
|
||||
// AI-flow. Skipt grill als gewenst. Status springt direct naar PLAN_READY.
|
||||
// Bij parse-failure: NIET opslaan (return 422), zodat een onparseerbaar plan
|
||||
// nooit in de DB belandt. Geen worker nodig — synchrone parser.
|
||||
|
||||
const UPLOAD_PLAN_FROM: IdeaStatus[] = ['DRAFT', 'GRILLED', 'PLAN_FAILED', 'PLAN_READY']
|
||||
const MAX_PLAN_MD_LENGTH = 100_000
|
||||
|
||||
export async function uploadPlanMdAction(
|
||||
id: string,
|
||||
markdown: string,
|
||||
): Promise<ActionResult> {
|
||||
const session = await getSession()
|
||||
if (!session.userId) return { error: 'Niet ingelogd', code: 401 }
|
||||
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 }
|
||||
|
||||
const limited = enforceUserRateLimit('upload-idea-plan', session.userId)
|
||||
if (limited) return limited
|
||||
|
||||
if (typeof markdown !== 'string' || markdown.trim().length === 0) {
|
||||
return { error: 'plan_md is leeg', code: 422 }
|
||||
}
|
||||
if (markdown.length > MAX_PLAN_MD_LENGTH) {
|
||||
return {
|
||||
error: `plan_md is te groot (${markdown.length} > ${MAX_PLAN_MD_LENGTH} chars)`,
|
||||
code: 422,
|
||||
}
|
||||
}
|
||||
|
||||
const idea = await loadOwnedIdea(id, session.userId, ['status'])
|
||||
if (!idea) return { error: 'Idee niet gevonden', code: 404 }
|
||||
if (!UPLOAD_PLAN_FROM.includes(idea.status)) {
|
||||
return {
|
||||
error: `Upload plan alleen toegestaan vanuit ${UPLOAD_PLAN_FROM.join('/')} (huidige status: ${idea.status})`,
|
||||
code: 422,
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parsePlanMd(markdown)
|
||||
if (!parsed.ok) {
|
||||
return {
|
||||
error: 'plan_md is niet parseerbaar',
|
||||
code: 422,
|
||||
details: parsed.errors,
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.$transaction([
|
||||
prisma.idea.update({
|
||||
where: { id },
|
||||
data: { plan_md: markdown, status: 'PLAN_READY' },
|
||||
}),
|
||||
prisma.ideaLog.create({
|
||||
data: {
|
||||
idea_id: id,
|
||||
type: 'NOTE',
|
||||
content: 'User-uploaded plan_md',
|
||||
metadata: { length: markdown.length, from_status: idea.status },
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
revalidatePath(`/ideas/${id}`)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Download — geeft de raw markdown terug; UI bouwt een Blob.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue