import { prisma } from "@/lib/db"; import type { EpisodeStatus } from "@prisma/client"; /** Update an episode's pipeline status (and optional human-readable stage / error). */ export async function setEpisodeStatus( episodeId: string, status: EpisodeStatus, opts: { stage?: string; errorMessage?: string | null } = {} ): Promise { await prisma.episode.update({ where: { id: episodeId }, data: { status, stage: opts.stage ?? null, ...(opts.errorMessage !== undefined ? { errorMessage: opts.errorMessage } : {}), }, }); } /** Mark a GenerationJob as running. */ export async function startJob(jobId: string, pgBossJobId?: string): Promise { await prisma.generationJob.update({ where: { id: jobId }, data: { status: "running", startedAt: new Date(), pgBossJobId: pgBossJobId ?? undefined }, }); } /** Mark a GenerationJob as completed or failed. */ export async function finishJob( jobId: string, result: { status: "completed" | "failed"; error?: string; stage?: string } ): Promise { await prisma.generationJob.update({ where: { id: jobId }, data: { status: result.status, error: result.error ?? null, stage: result.stage, finishedAt: new Date(), }, }); } /** * Generic, user-safe copy for a failed generation. * * Episode.errorMessage is rendered straight to the end user (see * app/(app)/episodes/[id]/page.tsx) and streamed over SSE, so it must never * carry raw pipeline text: ffmpeg stderr leaks internal binary paths, and an * ElevenLabs/OpenAI response body leaks our upstream quota and billing state. */ export const GENERIC_FAILURE = "Generation failed. Our team has been notified — please try again shortly."; /** * Record a terminal pipeline failure. * * The RAW error is preserved on GenerationJob.error, which is only ever surfaced * in the admin UI (lib/admin/ops.ts), so debuggability is unchanged. The episode * itself gets the generic message above. */ export async function failEpisode( episodeId: string, rawError: string, userMessage: string = GENERIC_FAILURE ): Promise { await prisma.generationJob.updateMany({ where: { episodeId, status: { in: ["queued", "running"] } }, data: { status: "failed", error: rawError, finishedAt: new Date() }, }); await setEpisodeStatus(episodeId, "FAILED", { errorMessage: userMessage }); } /** Terminal episode states — used by the UI/SSE to stop polling. */ export function isTerminal(status: EpisodeStatus): boolean { return status === "READY" || status === "FAILED"; }