/** * /api/admin/investigate/test — admin-only trigger for a synthetic * investigation job. Gated by middleware (W0-F1: /api/admin/* → 404 unless * profile.role==='admin'). * * POST { kind?: 'evidence_chain', doc_id, chunks?: string[] } — INSERT a row * in public.investigation_jobs; the disclosure-investigator container's * LISTEN handler picks it up, dispatches Locard, and updates status. * GET ?job_id=... — read the latest state for polling. */ import { NextResponse } from "next/server"; import { pgQuery } from "@/lib/retrieval/db"; import { withRequest } from "@/lib/logger"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; interface JobRow { job_id: string; kind: string; payload: unknown; status: string; worker_id: string | null; started_at: string | null; finished_at: string | null; outputs: unknown; error: string | null; created_at: string; } export async function POST(request: Request) { const log = withRequest(request); let body: { kind?: string; doc_id?: string; chunks?: string[]; claim?: string }; try { body = await request.json(); } catch { return NextResponse.json({ error: "invalid_json" }, { status: 400 }); } const kind = body.kind ?? "evidence_chain"; const doc_id = body.doc_id; if (!doc_id) return NextResponse.json({ error: "doc_id_required" }, { status: 400 }); const payload: Record = { doc_id }; if (body.chunks?.length) payload.chunks = body.chunks; if (body.claim) payload.claim = body.claim; // Triggered_by carries the admin email so the audit ties back to a person. // Middleware already validated the admin role; we just label here. const triggered_by = `user:${request.headers.get("x-user-email") ?? "admin"}`; try { const rows = await pgQuery<{ job_id: string }>( `INSERT INTO public.investigation_jobs (kind, payload, triggered_by, status) VALUES ($1, $2::jsonb, $3, 'queued') RETURNING job_id`, [kind, JSON.stringify(payload), triggered_by], ); const job_id = rows[0]?.job_id; log.info({ event: "investigation_job_created", kind, doc_id, job_id }, "investigation job queued"); return NextResponse.json({ job_id, kind, status: "queued", poll_url: `/api/admin/investigate/test?job_id=${job_id}`, }); } catch (e) { return NextResponse.json({ error: "db_unavailable", message: (e as Error).message }, { status: 503 }); } } export async function GET(request: Request) { const url = new URL(request.url); const job_id = url.searchParams.get("job_id"); if (!job_id) return NextResponse.json({ error: "job_id_required" }, { status: 400 }); try { const rows = await pgQuery( `SELECT job_id, kind, payload, status, worker_id, started_at, finished_at, outputs, error, created_at FROM public.investigation_jobs WHERE job_id = $1`, [job_id], ); if (rows.length === 0) return NextResponse.json({ error: "not_found" }, { status: 404 }); return NextResponse.json(rows[0]); } catch (e) { return NextResponse.json({ error: "db_unavailable", message: (e as Error).message }, { status: 503 }); } }