import { NextResponse } from "next/server"; import { drainAutoRunner, startAutoRunner, startAutoRunnerBlockedReason, stopAutoRunner, } from "yt-dlp-transcript-common/controller/autoRunner"; import { LANES } from "yt-dlp-transcript-common/lib/autoQueueTypes"; import type { AutoQueueKind } from "yt-dlp-transcript-common/jobs/autoQueueState"; import { opsAuth } from "../../ops/_lib"; export const dynamic = "force-dynamic"; // Start or stop an auto-queue runner without a server restart. The runner only // auto-starts at boot (editor/instrumentation.ts) when enabled, so the save flow // and this endpoint are how a script (or an e2e test) brings a runner up after // flipping the enable toggle. // // BEHIND THE OPS TOKEN SINCE RELEASE 19 (A3). It was the one route that starts // and stops lanes with no gate at all — reachable by anything that could reach // the editor, and by a page in the operator's own browser posting a form at it // (the CSRF reasoning in api/test/_guard.ts). The /operations Start, Drain and // Stop buttons no longer call it: they are server actions now // (operations/actions.ts), which Next checks for origin. `pnpm ops lane // {"lane", "action"}` is the same three verbs for a caller with the token. // // Body: { kind: , action: "start" | "stop" | "drain" }. // "stop" hard-cancels (aborts in-flight); "drain" lets in-flight units finish. // // ALL FOUR LANES SINCE SLICE 1.2. It refused digest and backfill while they had // no executor — a Start button for a lane that would pick a video and not know // what to do with it is worse than no button. They have one now, and the check // is against LANES rather than two names so it cannot be forgotten again. export async function POST(req: Request) { const denied = opsAuth(req); if (denied) return denied; let body: { kind?: unknown; action?: unknown }; try { body = (await req.json()) as typeof body; } catch { return NextResponse.json({ ok: false, error: "invalid JSON" }, { status: 400 }); } const kind = body.kind; if (typeof kind !== "string" || !LANES.includes(kind as AutoQueueKind)) { return NextResponse.json( { ok: false, error: `kind must be one of ${LANES.join(", ")}` }, { status: 400 }, ); } const action = body.action; if (action === "start") { // WHY IT DID NOT START, when it did not. `startAutoRunner` returns null for // a disabled policy AND for a lane whose sweep is armed, and those want // different answers from the operator — so the reason travels with the // refusal rather than leaving a silent no-op on the page. const blocked = startAutoRunnerBlockedReason(kind as AutoQueueKind); const jobId = await startAutoRunner(kind as AutoQueueKind); return NextResponse.json({ ok: true, started: jobId !== null, jobId, ...(jobId === null && blocked ? { blocked } : {}), }); } if (action === "stop") { const stopped = stopAutoRunner(kind as AutoQueueKind); return NextResponse.json({ ok: true, stopped }); } if (action === "drain") { const draining = drainAutoRunner(kind as AutoQueueKind); return NextResponse.json({ ok: true, draining }); } return NextResponse.json( { ok: false, error: "action must be 'start', 'stop', or 'drain'" }, { status: 400 }, ); }