Duplicate and Replace per-item actions, both leaning on the normalized
playlist_items schema (only content_id/widget_id/zone_id/sort_order/
duration_sec; type-specific fields are JOINed at snapshot time).
- Replace: extend PUT /:id/items/:itemId to accept a content_id/widget_id
swap. Clean FK swap across ANY content type (image<->video<->youtube<->
widget) — sets one, nulls the other, preserving zone_id/duration/
sort_order/schedule rows. Only acts when content_id|widget_id is present,
so partial PUTs are unaffected. Workspace-validated; markDraft.
- Duplicate: new POST /:id/items/:itemId/duplicate — copies the row +
its schedule blocks (new ids) in one transaction, appended (sort_order
MAX+1). markDraft.
- Frontend: Replace + Duplicate icon buttons per item; Replace reuses the
add-item picker in a replaceItemId mode (PUT instead of POST). i18n x6.
Validated end-to-end against the live API: duplicate (incl. schedule copy
with distinct ids), replace same-type and cross-type both directions,
preservation of duration/schedule/zone, and validation (both->400,
missing->404). 149 server tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>