← back to Marketing Command Center
Composer: 'Build this reel' handoff to Reel Studio + fix reels-proxy POST body
7186df003b9fd1752035dfeba30c7e3cdd2ce8c1 · 2026-08-26 08:34:50 -0700 · Steve
Finish the create flow's last link — a composed photo + song + caption now actually
RENDERS to a reel. The composer's photo+song+reel hint became a 'Build this reel ->' button
that POSTs the recipe to /api/reels/ui/api/build-photo-reel (proxied to the reels app) and
opens Reel Studio to watch the render.
Fix (found in verification): the reels reverse-proxy did req.pipe(up), but express.json had
ALREADY consumed the POST body upstream — so every proxied POST forwarded an EMPTY body and
the reel endpoint 400'd on a valid request. Now re-serializes the parsed body + sets
Content-Length; GET/video streams still pipe raw.
Verified: proxied POST → 202 (body arrives); the button appears on photo+song+Reel; a real
mp4 with the song comes out the far end. Note: render only runs on the studio host
(ALLOW_BUILD=1); prod serves reels, so it reports that honestly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M modules/reels/index.jsM public/panels/composer.js
Diff
commit 7186df003b9fd1752035dfeba30c7e3cdd2ce8c1
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 26 08:34:50 2026 -0700
Composer: 'Build this reel' handoff to Reel Studio + fix reels-proxy POST body
Finish the create flow's last link — a composed photo + song + caption now actually
RENDERS to a reel. The composer's photo+song+reel hint became a 'Build this reel ->' button
that POSTs the recipe to /api/reels/ui/api/build-photo-reel (proxied to the reels app) and
opens Reel Studio to watch the render.
Fix (found in verification): the reels reverse-proxy did req.pipe(up), but express.json had
ALREADY consumed the POST body upstream — so every proxied POST forwarded an EMPTY body and
the reel endpoint 400'd on a valid request. Now re-serializes the parsed body + sets
Content-Length; GET/video streams still pipe raw.
Verified: proxied POST → 202 (body arrives); the button appears on photo+song+Reel; a real
mp4 with the song comes out the far end. Note: render only runs on the studio host
(ALLOW_BUILD=1); prod serves reels, so it reports that honestly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
modules/reels/index.js | 14 +++++++++++++-
public/panels/composer.js | 26 +++++++++++++++++++++++++-
2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/modules/reels/index.js b/modules/reels/index.js
index b0cbade..8f4f276 100644
--- a/modules/reels/index.js
+++ b/modules/reels/index.js
@@ -27,6 +27,18 @@ function upstream(pathname, req, res) {
if (req.headers.range) headers.Range = req.headers.range;
if (req.headers['content-type']) headers['Content-Type'] = req.headers['content-type'];
+ // Express's json body-parser has ALREADY consumed the request stream on POSTs, so
+ // req.pipe(up) would forward an EMPTY body (the upstream endpoint then sees no data).
+ // If a parsed body is present, re-serialize it and send it explicitly; otherwise
+ // pipe the raw stream (GET / video / anything without a parsed body).
+ const parsed = req.body && typeof req.body === 'object' && Object.keys(req.body).length ? req.body : null;
+ let payload = null;
+ if (parsed) {
+ payload = JSON.stringify(req.body);
+ headers['Content-Type'] = 'application/json';
+ headers['Content-Length'] = Buffer.byteLength(payload);
+ }
+
const up = lib.request({
method: req.method,
hostname: u.hostname,
@@ -45,7 +57,7 @@ function upstream(pathname, req, res) {
if (!res.headersSent) res.status(502).json({ error: `reels app unreachable: ${e.message}`, base: BASE });
else res.end();
});
- req.pipe(up);
+ if (payload !== null) up.end(payload); else req.pipe(up);
}
module.exports = {
diff --git a/public/panels/composer.js b/public/panels/composer.js
index 9f51b82..bfbcd35 100644
--- a/public/panels/composer.js
+++ b/public/panels/composer.js
@@ -257,7 +257,9 @@ window.MCC_PANELS['composer'] = {
// A still photo + a song = a reel that has to be RENDERED (photo over music →
// mp4). The composer assembles the recipe; Reel Studio produces the video.
if (state.song && (state.format === 'reel' || state.format === 'story') && state.mediaUrl && !state.videoUrl) {
- h.innerHTML = `🎬 Photo + song → this is a video. Assemble it here, then <a href="#reels">build it in Reel Studio</a> (renders the photo over the track).`;
+ h.innerHTML = `🎬 Photo + song → this renders to a video. ` +
+ `<button type="button" id="cmp-buildreel" class="btn gold" style="font-size:11.5px;padding:4px 11px;margin-left:2px">Build this reel →</button> ` +
+ `<span class="muted" id="cmp-buildreel-msg" style="font-size:11px"></span>`;
} else if (state.song && state.format === 'post') {
h.innerHTML = `🎵 A feed photo can’t carry audio — a song makes it a <b>Reel</b> or <b>Story</b>. Switched the format for you.`;
} else if (state.format === 'reel' && !state.song && !state.videoUrl) {
@@ -318,6 +320,28 @@ window.MCC_PANELS['composer'] = {
});
});
+ // "Build this reel →" — hand the photo + song + caption to Reel Studio, which
+ // renders the still over the track to an mp4 (POST proxied to the reels app;
+ // the render only runs on the studio host, so on prod it reports that honestly).
+ // Delegated on #cmp-make-hint because updateMakeHint() rebuilds its innerHTML.
+ $('#cmp-make-hint').addEventListener('click', async (e) => {
+ const btn = e.target.closest('#cmp-buildreel'); if (!btn) return;
+ if (!state.mediaUrl || !state.song) return;
+ const msg = $('#cmp-buildreel-msg');
+ btn.disabled = true; btn.textContent = 'Building…';
+ const r = await jpost('/api/reels/ui/api/build-photo-reel', {
+ photo: state.mediaUrl, song: state.song.url, songName: state.song.name,
+ caption: cap.value.trim(), seconds: 15,
+ });
+ if (r && r.started) {
+ if (msg) msg.textContent = '✓ rendering in Reel Studio — opening…';
+ setTimeout(() => { location.hash = 'reels'; }, 800);
+ } else {
+ btn.disabled = false; btn.textContent = 'Build this reel →';
+ if (msg) msg.textContent = '⚠ ' + ((r && r.error) || 'could not start — reel rendering runs on the studio host');
+ }
+ });
+
// ── PUBLISH ────────────────────────────────────────────────────────────────
$('#cmp-publish').addEventListener('click', async () => {
const out = $('#cmp-result'); out.innerHTML = '';
← fb74fe6 auto-data-snapshot: 2026-08-25T20:06:24 (1 data files) — pub
·
back to Marketing Command Center
·
auto-data-snapshot: 2026-08-26T09:01:00 (1 data files) — pub 6bb98c4 →