import { withEditNotes } from "@/lib/report/guard"; import { StaleToken, updateClip } from "@/lib/report/manifest.mjs"; import { resolveClip } from "@/lib/report/serve.mjs"; export const dynamic = "force-dynamic"; // Saving a window, and what the header says about it. // // The client sends a project id, a clip id, numbers, strings, and the TOKEN it // was given when it opened the clip. It never sends a path, and it cannot ask for an // entry to move: re-ordering recomputes `sectionEnter` and changes the cut, so // it is a different operation with a different button. // // A stale token is a 409 with both values, not a silent overwrite. The other // writer is usually `resolve-windows --write` or an agent running `umtool`, and // what it wrote is somebody's judgement. export async function PUT(request: Request) { let body: Record; try { body = await request.json(); } catch { return Response.json({ error: "expected JSON" }, { status: 400 }); } const projectId = String(body.project ?? ""); const clipId = String(body.clip ?? ""); const r = await resolveClip(projectId, clipId); if ("error" in r) return Response.json({ error: r.error }, { status: r.status }); // The whitelist, mirrored from lib/report/manifest.mjs so a field nobody // meant to expose cannot arrive through a JSON body. The second group is the // attribution: what the burned-in header and the QR will say. const patch: Record = {}; for (const k of [ "start", "end", "lock", "lockStart", "lockEnd", "note", "title", "date", "cite", "citeUrl", "quote", "correction", // The cut inside the extent, and the pin that stops the resolver moving it. "cutStart", "cutEnd", "lockCut", // Where the sound fades out for the rest of the clip while the picture // plays on, in source seconds; empty or null clears it. Checked by the // writer against the clip's window, like the cut. "muteFrom", // Whether the walk has looked at this clip: "confirmed", or empty to clear // it. A non-empty `correction` is the other answer and needs no value. "verdict", // What the deck says over this clip: { title?, subtitle? }, or null to // clear it. Normalised and checked by the writer, like the date. "onscreen", ]) { if (body[k] !== undefined) patch[k] = body[k]; } if (!Object.keys(patch).length) return Response.json({ error: "nothing to change" }, { status: 400 }); try { const { result: res, editNotes } = await withEditNotes(r.project, () => updateClip(r.project.dir, clipId, patch, { token: body.token === undefined ? null : String(body.token), }), ); return Response.json( { ok: true, entry: res.entry, before: res.before, token: res.token, editNotes }, { headers: { "cache-control": "no-store" } }, ); } 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 }); } }