'use server' import { revalidatePath } from 'next/cache' import { type ClaudeJobStatus } from '@prisma/client' import { prisma } from '@/lib/prisma' import { getSession } from '@/lib/auth' 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 } type RestartResult = { success: true } | { error: string } const RESTARTABLE_STATUSES: ClaudeJobStatus[] = ['FAILED', 'CANCELLED', 'SKIPPED'] export type PreviewTask = { id: string title: string status: string story_title: string pbi_id: string pbi_status: string } type PreflightResult = | { error: string } | { tasks: PreviewTask[]; blockerIndex: number | null; blockerReason: 'task-review' | 'pbi-blocked' | null } /** * @deprecated Vervangen door startSprintRunAction in actions/sprint-runs.ts. * Per-task starts zijn niet meer toegestaan — een sprint draait nu als geheel. * Wordt verwijderd zodra de UI is omgebouwd (F4). */ export async function enqueueClaudeJobAction(_taskId: string): Promise { return { error: 'Per-task starten is niet meer mogelijk. Gebruik "Start Sprint" voor de hele actieve sprint.', } } /** * @deprecated Vervangen door startSprintRunAction in actions/sprint-runs.ts. */ export async function enqueueAllTodoJobsAction(_productId: string): Promise { return { error: '"Alle TO_DO als jobs queueen" is vervangen door "Start Sprint". Gebruik startSprintRunAction.', } } /** * @deprecated Vervangen door pre-flight in startSprintRunAction (actions/sprint-runs.ts). */ export async function previewEnqueueAllAction(_productId: string): Promise { return { error: 'Per-product preview is vervangen door de pre-flight check in startSprintRunAction.', } } /** * @deprecated Vervangen door startSprintRunAction in actions/sprint-runs.ts. */ export async function enqueueClaudeJobsBatchAction( _productId: string, _taskIds: string[] ): Promise { return { error: 'Batch-queue per task is vervangen door "Start Sprint". Gebruik startSprintRunAction.', } } 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 } } export async function restartClaudeJobAction(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, kind: true, task_id: true, idea_id: true, sprint_run_id: true, product_id: true }, }) if (!job) return { error: 'Job niet gevonden' } if (!RESTARTABLE_STATUSES.includes(job.status)) { return { error: 'Alleen mislukte, geannuleerde of overgeslagen jobs kunnen opnieuw gestart worden' } } const updated = await prisma.$transaction(async (tx) => { const result = await tx.claudeJob.updateMany({ where: { id: jobId, status: { in: RESTARTABLE_STATUSES } }, data: { status: 'QUEUED', retry_count: { increment: 1 }, claimed_by_token_id: null, claimed_at: null, started_at: null, finished_at: null, pushed_at: null, verify_result: null, error: null, summary: null, branch: null, head_sha: null, lease_until: null, }, }) if (result.count === 0) return 0 if (job.kind === 'SPRINT_IMPLEMENTATION') { await tx.sprintTaskExecution.updateMany({ where: { sprint_job_id: jobId }, data: { status: 'PENDING', verify_result: null, verify_summary: null, skip_reason: null, head_sha: null, started_at: null, finished_at: null, }, }) } return result.count }) if (updated === 0) { return { error: 'Job-status is gewijzigd; herlaad en probeer opnieuw' } } await prisma.$executeRaw` SELECT pg_notify('scrum4me_changes', ${JSON.stringify({ type: 'claude_job_status', job_id: jobId, kind: job.kind, task_id: job.task_id, idea_id: job.idea_id, sprint_run_id: job.sprint_run_id, user_id: session.userId, product_id: job.product_id, status: jobStatusToApi('QUEUED'), })}::text) ` revalidatePath('/jobs') return { success: true } }