import { withEditNotes } from "@/lib/report/guard"; import { StaleToken, manifestToken, updateOnscreen } from "@/lib/report/manifest.mjs"; import { postRows, scheduleForPreview } from "@/lib/report/onscreen.mjs"; import { resolveReport } from "@/lib/report/serve.mjs"; import { deckText, isMultiChannel, resolveDeck } from "umtool-report-to-video/deck"; export const dynamic = "force-dynamic"; // The deck's per-entry text: `onscreen: { title?, subtitle? }` on any // timeline entry, clip, still or card -- and its fact-check `claim: { id, // verdict }` (factcheck.mjs), which a row carries beside its text. // // PUT is the On-screen table's one save: a map of entry id → the row, or null // to clear it. A row REPLACES the entry's whole `onscreen` (`{ title }` alone // clears a subtitle override), blanks are dropped, and an entry left with // nothing loses the key. A row's `claim` sets the entry's claim (null removes // it); a row without one leaves it. One unknown id or one bad value -- a // claim on a teaser, one claim id given two verdicts -- refuses the WHOLE // batch -- nothing is written -- because a table that saved all but one row // reads as saved. type Claim = { id: string; verdict: string }; type Entry = Record & { id: string; type?: string; onscreen?: Record; claim?: Claim }; const noStore = { "cache-control": "no-store" }; /** * The table's rows for one cut: each entry's saved `onscreen` and what the * deck says with no override at all -- the AUTO title and subtitle the form * shows as placeholders. Auto text comes from the archive's cue files (the * clip bench's source), so before a build it can differ from the fetched * file's metadata the build will use. * * `posts` rides along, for the Posts table under it: every post the manifest * carries with the clip the date rule picks for it (`auto`), the clip it rides * on as saved (`effective`, null when hidden), and its slot in the schedule * the preview draws -- the build's when it still matches the cut, else the * estimate (`postsEstimated`). `clips` is the override select's list. */ export async function GET(request: Request) { const url = new URL(request.url); const r = await resolveReport(url.searchParams.get("project") ?? "", url.searchParams.get("variant")); if ("error" in r) return Response.json({ error: r.error }, { status: r.status }); const { variantManifest: cut, metas, schedule } = await scheduleForPreview(r.project, r.manifest, r.variant); const entries = (cut.timeline ?? []) as Entry[]; const render = cut.render ?? {}; const deck = resolveDeck(render); const provenance = cut.provenance ?? {}; const multi = isMultiChannel(entries, provenance); const posts = postRows({ variantManifest: cut, metas, schedule }); const rows = entries.map((e, i) => { const { onscreen, ...bare } = e; return { id: e.id, type: e.type ?? "entry", onscreen: onscreen ?? null, claim: e.claim ?? null, auto: deckText(bare, metas[i] ?? null, provenance, deck, multi), }; }); return Response.json( { variant: r.variant, rows, maxChars: deck.title.maxChars, multiChannel: multi, posts: posts.posts, clips: posts.clips, postsShown: deck.posts.show, postsEstimated: schedule.estimated === true, token: await manifestToken(r.project.dir), }, { headers: noStore }, ); } export async function PUT(request: Request) { let body: Record; try { body = await request.json(); } catch { return Response.json({ error: "expected JSON" }, { status: 400 }); } const r = await resolveReport(String(body.project ?? "")); if ("error" in r) return Response.json({ error: r.error }, { status: r.status }); try { const { result: res, editNotes } = await withEditNotes(r.project, () => updateOnscreen( r.project.dir, body.onscreen as Record, { token: body.token === undefined ? null : String(body.token) }, ), ); return Response.json({ ok: true, onscreen: res.onscreen, claims: res.claims, token: res.token, editNotes }, { headers: noStore }); } catch (e) { if (e instanceof StaleToken) { return Response.json( { error: e.message, expected: e.expected, got: e.got, stale: true }, { status: 409 }, ); } return Response.json({ error: e instanceof Error ? e.message : String(e) }, { status: 400 }); } }