'use server' import { revalidatePath } from 'next/cache' import { prisma } from '@/lib/prisma' import { getSession } from '@/lib/auth' import { productAccessFilter } from '@/lib/product-access' import { ACTIVE_JOB_STATUSES, jobStatusToApi } from '@/lib/job-status' type EnqueueResult = | { success: true; jobId: string } | { error: string; jobId?: string } type EnqueueAllResult = | { success: true; count: number } | { error: string } type CancelResult = { success: true } | { error: string } export async function enqueueClaudeJobAction(taskId: string): Promise { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } if (!taskId) return { error: 'task_id is verplicht' } // Resolve task + product access in one query const task = await prisma.task.findFirst({ where: { id: taskId, story: { product: productAccessFilter(session.userId) }, }, select: { id: true, story: { select: { product_id: true } } }, }) if (!task) return { error: 'Task niet gevonden' } const productId = task.story.product_id // Idempotency: weiger als er al een actieve job voor deze task bestaat const existing = await prisma.claudeJob.findFirst({ where: { task_id: taskId, status: { in: ACTIVE_JOB_STATUSES } }, select: { id: true }, }) if (existing) { return { error: 'Er loopt al een agent voor deze task', jobId: existing.id } } const job = await prisma.claudeJob.create({ data: { user_id: session.userId, product_id: productId, task_id: taskId, status: 'QUEUED' }, }) await prisma.$executeRaw` SELECT pg_notify('scrum4me_changes', ${JSON.stringify({ type: 'claude_job_enqueued', job_id: job.id, task_id: taskId, user_id: session.userId, product_id: productId, status: 'queued', })}::text) ` revalidatePath(`/products/${productId}/solo`) return { success: true, jobId: job.id } } export async function enqueueAllTodoJobsAction(productId: string): Promise { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } if (!productId) return { error: 'product_id is verplicht' } const product = await prisma.product.findFirst({ where: { id: productId, ...productAccessFilter(session.userId) }, select: { id: true }, }) if (!product) return { error: 'Geen toegang tot dit product' } const userId = session.userId // Match het scope dat de gebruiker op het Solo Paneel ziet: // alleen TO_DO-taken in de actieve sprint, in stories die aan deze // gebruiker zijn toegewezen. Anders queue je per ongeluk taken die // niet in de huidige sprint zitten of aan iemand anders toebehoren. const sprint = await prisma.sprint.findFirst({ where: { product_id: productId, status: 'ACTIVE' }, select: { id: true }, }) if (!sprint) return { success: true, count: 0 } const tasks = await prisma.task.findMany({ where: { status: 'TO_DO', story: { sprint_id: sprint.id, assignee_id: userId }, claude_jobs: { none: { status: { in: ACTIVE_JOB_STATUSES } } }, }, select: { id: true }, }) if (tasks.length === 0) return { success: true, count: 0 } const created = await prisma.$transaction( tasks.map(t => prisma.claudeJob.create({ data: { user_id: userId, product_id: productId, task_id: t.id, status: 'QUEUED' }, select: { id: true, task_id: true }, }) ) ) for (const job of created) { await prisma.$executeRaw` SELECT pg_notify('scrum4me_changes', ${JSON.stringify({ type: 'claude_job_enqueued', job_id: job.id, task_id: job.task_id, user_id: userId, product_id: productId, status: 'queued', })}::text) ` } revalidatePath(`/products/${productId}/solo`) return { success: true, count: created.length } } export async function cancelClaudeJobAction(jobId: string): Promise { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } if (!jobId) return { error: 'job_id is verplicht' } const job = await prisma.claudeJob.findFirst({ where: { id: jobId, user_id: session.userId }, select: { id: true, status: true, task_id: true, product_id: true }, }) if (!job) return { error: 'Job niet gevonden' } if (!ACTIVE_JOB_STATUSES.includes(job.status)) { return { error: 'Alleen actieve jobs kunnen geannuleerd worden' } } await prisma.claudeJob.update({ where: { id: jobId }, data: { status: 'CANCELLED', finished_at: new Date() }, }) await prisma.$executeRaw` SELECT pg_notify('scrum4me_changes', ${JSON.stringify({ type: 'claude_job_status', job_id: jobId, task_id: job.task_id, user_id: session.userId, product_id: job.product_id, status: jobStatusToApi('CANCELLED'), })}::text) ` revalidatePath(`/products/${job.product_id}/solo`) return { success: true } }