Skip to content

feat([issue-4188]): mood boards gain gallery/upload pickers and playable video items - #4203

Merged
atomantic merged 3 commits into
mainfrom
next/issue-4188
Aug 14, 2026
Merged

feat([issue-4188]): mood boards gain gallery/upload pickers and playable video items#4203
atomantic merged 3 commits into
mainfrom
next/issue-4188

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Phase 2 of #4188 (mood boards as a universe style-building tool): board items can now come from the shared galleries, videos are first-class board items, and board-referenced uploads land in peer-syncable storage.

  • Gallery + upload pickers — the board detail page gains "Pick from gallery" (GalleryImagePicker allowUpload) and "Pick video" (GalleryVideoPicker) alongside the existing URL/Note tabs. Picks store mediaKey + a directly-renderable preview, mirroring PinToMoodBoardMenu's shape.
  • Real type:'video' items — added to MOOD_BOARD_ITEM_TYPES, normalizeItem, the Zod schemas, and the item-PATCH invariants. A video item's mediaKey ref is the on-disk filename (video:<file>.mp4), so playback (/data/videos/<ref>) and the peer-sync asset manifest (collectionVideoRefToFilename passes an extensioned ref through untouched) both resolve without an id→filename guess — including non-mp4 containers. The card renders a poster with a play affordance and plays inline; posters fall back to the derived <stem>.jpg on error (a downloaded video's sender thumbnail is <id>.jpg, but a receiving peer regenerates <stem>.jpg when it pulls the video).
  • Peer-syncable video uploads — new POST /api/video-gen/uploadsaveUploadedGalleryVideo: container-sniffed (mp4/mov/webm), lands upload-<id>.<ext> under PATHS.videos with a thumbnail and a source:'upload' video-history entry (the same tail as downloadVideoIntoLibrary), and emits the media-index completed event. GalleryVideoPicker gains an opt-in uploadToGallery mode; its existing /api/uploads path is unchanged for prompt-from-media consumers. Board-referenced media never lands in the non-federating data/uploads/ scratch dir. Upload caps derive from server/lib/uploadLimits.js (route schema = the JSON body-parser limit; saver = MAX_BASE64_UPLOAD_BYTES).
  • moodBoards sync gate bumped 1 → 2 — a v1 peer renders a video item as a blank note and its updateItem gates a video item onto the text editable-keys, so an older receiver that edits the board could mangle video items and LWW the damage back; the bump makes v1 receivers 412-reject the ahead-version push until they upgrade. buildBoardAssetManifest now ships video bytes for video items (poster thumbnails deliberately not shipped — receivers regenerate them on pull, per the existing deterministic-name design).

A codex review pass surfaced four P2s, fixed in the second commit: filename-with-extension enforcement on video mediaKeys (create and PATCH — a bare video:<id> would 404 locally and mis-guess .mp4 on the wire; a PATCH could swap a video item onto an image key), the derived-poster fallback for synced download pins, and unifying the hand-rolled upload caps onto uploadLimits.js (the schema previously advertised a cap above the body-parser limit).

Remaining phases of #4188 (per-item prompt-from-media analysis, board→style synthesis) are not in this PR — see the reconciliation comment on the issue.

Refs #4188

Test plan

  • server/lib/moodBoardValidation.test.js — video item create-schema cases (accept filename keys ± poster, reject missing/wrong-kind/extension-less keys) + isVideoItemMediaKey unit cases (traversal, extension, kind).
  • server/services/moodBoard/logic.test.js — video item normalize (mediaKey + poster, text nulled), media-style PATCH keys, and merged-key invariants (cannot clear, cannot swap to image:/extension-less).
  • server/services/videoUpload.test.js — container sniff (mp4/mov/webm/reject), history-entry shape, pre-write validation errors (empty/oversize/non-video).
  • server/services/sharing/peerSyncAssets.test.jsbuildBoardAssetManifest bundles video items by filename-as-ref (extension preserved, no .mp4 guess), skips missing bytes and poster thumbnails, mixes with image items.
  • client/src/lib/moodBoardItemSrc.test.js — derived stem poster, stored-poster preference, moodBoardItemVideoSrc playback resolution and legacy-pin null cases.
  • client/src/components/videoGen/GalleryVideoPicker.test.jsxuploadToGallery uploads via the new endpoint and selects the normalized entry; legacy /api/uploads path untouched.
  • client/src/pages/MoodBoardDetail.test.jsx — video item renders poster + play affordance, plays inline with the right src, and the poster onError fallback swaps to the derived stem name.
  • Full server suite (28,671 passed) and full client suite (7,554 passed) green locally; biome check clean.

…video items

Phase 2 of #4188. Board items can now be picked from the shared image
gallery (with upload) and the video gallery; a new type:'video' item plays
inline with a poster thumbnail. Video items store their mediaKey as
video:<filename> so playback and peer-sync asset transfer both resolve
without an id-to-filename guess.

Uploads that a board will reference must federate, so video uploads land in
the shared gallery via a new POST /api/video-gen/upload
(saveUploadedGalleryVideo: PATHS.videos + thumbnail + a source:'upload'
video-history entry, mirroring the video-download tail) instead of the
non-syncing data/uploads scratch dir. GalleryVideoPicker gains an opt-in
uploadToGallery mode; its existing /api/uploads path is unchanged for
prompt-from-media consumers.

moodBoards sync schema bumped 1 -> 2: a v1 peer renders a video item as a
blank note and its updateItem would mangle one on edit, so older receivers
now 412-reject the ahead-version push until they upgrade. The board asset
manifest ships video bytes for video items (poster thumbnails regenerate on
the receiving peer).
… fallback, one upload cap

- Video item mediaKeys must be a filename with extension (isVideoItemMediaKey,
  shared by the create schema and the item-PATCH invariant): a bare
  video:<id> would 404 as /data/videos/<id> and mis-guess .mp4 on the peer
  asset manifest, and a PATCH could previously swap a video item onto an
  image key.
- Board posters fall back to the derived <stem>.jpg on error: a downloaded
  video's sender thumbnail is <id>.jpg but a receiving peer regenerates
  <filename-stem>.jpg on pull, so the stored URL 404s there.
- Upload caps derive from server/lib/uploadLimits.js: the route schema's
  string max is the JSON body-parser limit (anything longer 413s before the
  route) and the saver's binary cap is MAX_BASE64_UPLOAD_BYTES, replacing two
  hand-rolled literals that straddled the parser limit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant