← back to Marketing Command Center
add reels module — embed dw-marketing-reels console via same-origin proxy (Range passthrough for video seek); add fix-connie-creds.sh
0f129eb770648c767088faa54594648b355fcb5b · 2026-07-15 12:01:15 -0700 · Steve Abrams
Files touched
A modules/reels/index.jsM modules/registry.jsA public/panels/reels.htmlA public/panels/reels.js
Diff
commit 0f129eb770648c767088faa54594648b355fcb5b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 15 12:01:15 2026 -0700
add reels module — embed dw-marketing-reels console via same-origin proxy (Range passthrough for video seek); add fix-connie-creds.sh
---
modules/reels/index.js | 78 ++++++++++++++++++++++++++++++++++++++++++++++++
modules/registry.js | 1 +
public/panels/reels.html | 12 ++++++++
public/panels/reels.js | 28 +++++++++++++++++
4 files changed, 119 insertions(+)
diff --git a/modules/reels/index.js b/modules/reels/index.js
new file mode 100644
index 0000000..b0cbade
--- /dev/null
+++ b/modules/reels/index.js
@@ -0,0 +1,78 @@
+// Reels — embeds the DW New Arrivals Reels console (the standalone
+// dw-marketing-reels app) as an MCC panel via a same-origin reverse proxy.
+// The reels app keeps running as its own pm2 process (:9848); this module
+// proxies /api/reels/ui/* → REELS_BASE, injecting the upstream Basic auth
+// server-side so the operator logs into the MCC once. Streams pass through
+// untouched (Range headers forwarded → video scrubbing works in the iframe).
+// If the upstream is down the panel degrades to a clear banner, never a hang.
+const http = require('http');
+const https = require('https');
+const { URL } = require('url');
+
+const BASE = process.env.REELS_BASE || 'http://127.0.0.1:9848';
+const UP_USER = process.env.REELS_USER || 'admin';
+const UP_PASS = process.env.REELS_PASS || 'DW2024!';
+const UP_AUTH = 'Basic ' + Buffer.from(`${UP_USER}:${UP_PASS}`).toString('base64');
+
+function upstream(pathname, req, res) {
+ let u;
+ try { u = new URL(pathname, BASE.endsWith('/') ? BASE : BASE + '/'); }
+ catch (e) { return res.status(502).json({ error: `bad REELS_BASE: ${e.message}` }); }
+ const lib = u.protocol === 'https:' ? https : http;
+ const headers = {
+ Authorization: UP_AUTH,
+ Accept: req.headers.accept || '*/*',
+ };
+ // Range passthrough is what makes <video> seek work through the proxy.
+ if (req.headers.range) headers.Range = req.headers.range;
+ if (req.headers['content-type']) headers['Content-Type'] = req.headers['content-type'];
+
+ const up = lib.request({
+ method: req.method,
+ hostname: u.hostname,
+ port: u.port || (u.protocol === 'https:' ? 443 : 80),
+ path: u.pathname + u.search,
+ headers,
+ timeout: 15000,
+ }, (r) => {
+ const h = { ...r.headers };
+ delete h['www-authenticate']; // never leak the upstream auth challenge to the browser
+ res.writeHead(r.statusCode || 502, h);
+ r.pipe(res);
+ });
+ up.on('timeout', () => { up.destroy(new Error('upstream timeout')); });
+ up.on('error', (e) => {
+ if (!res.headersSent) res.status(502).json({ error: `reels app unreachable: ${e.message}`, base: BASE });
+ else res.end();
+ });
+ req.pipe(up);
+}
+
+module.exports = {
+ id: 'reels',
+ title: 'Reels',
+ icon: '🎬',
+ mount(router) {
+ // Fast reachability probe for the panel banner.
+ router.get('/status', (_req, res) => {
+ const u = new URL('api/reels', BASE.endsWith('/') ? BASE : BASE + '/');
+ const lib = u.protocol === 'https:' ? https : http;
+ const p = lib.get({
+ hostname: u.hostname, port: u.port || 80, path: u.pathname,
+ headers: { Authorization: UP_AUTH }, timeout: 4000,
+ }, (r) => {
+ let body = '';
+ r.on('data', (d) => { body += d; if (body.length > 1e6) r.destroy(); });
+ r.on('end', () => {
+ let count = null;
+ try { count = JSON.parse(body).length; } catch { /* non-JSON = still up */ }
+ res.json({ ok: r.statusCode === 200, status: r.statusCode, reels: count, base: BASE });
+ });
+ });
+ p.on('timeout', () => p.destroy(new Error('timeout')));
+ p.on('error', (e) => res.json({ ok: false, error: e.message, base: BASE }));
+ });
+ // Everything under /api/reels/ui/* is the embedded console itself.
+ router.use('/ui', (req, res) => upstream(req.url.replace(/^\//, ''), req, res));
+ },
+};
diff --git a/modules/registry.js b/modules/registry.js
index 2581245..8923b9b 100644
--- a/modules/registry.js
+++ b/modules/registry.js
@@ -18,6 +18,7 @@ module.exports = [
'performance', // performance dashboard: email + GA4 KPIs, charts, top campaigns
'social', // social scheduler: IG/TikTok queue board, gated publish
'follow-counts', // Followers/Following: per-DW-IG-account followers_count + follows_count (COUNTS ONLY, official Graph API via Norma :9810) + daily-snapshot growth chart
+ 'reels', // New Arrivals Reels console (dw-marketing-reels :9848) embedded via same-origin proxy — the app that previously owned marketing.designerwallcoverings.com
'linkedin', // LinkedIn composer + drafts + manual post (deep-link) + gated API publish (Phase 2)
'segments', // audience segments: rule builder + live preview over contacts
'vendors', // vendor IG reporting: DW + all vendor Instagram accounts roster
diff --git a/public/panels/reels.html b/public/panels/reels.html
new file mode 100644
index 0000000..b34db06
--- /dev/null
+++ b/public/panels/reels.html
@@ -0,0 +1,12 @@
+<div class="muted-banner" id="reels-banner">Checking the reels console…</div>
+
+<div class="card" id="reels-toolbar" style="display:none;margin-bottom:10px">
+ <div class="row" style="gap:10px;align-items:center;flex-wrap:wrap">
+ <span class="muted" id="reels-count"></span>
+ <span style="flex:1"></span>
+ <a id="reels-open-direct" href="#" target="_blank" rel="noopener" class="muted" style="font-size:12px">open full console ↗</a>
+ </div>
+</div>
+
+<iframe id="reels-frame" title="DW New Arrivals Reels"
+ style="display:none;width:100%;height:calc(100vh - 190px);min-height:520px;border:1px solid var(--line);border-radius:12px;background:#fff"></iframe>
diff --git a/public/panels/reels.js b/public/panels/reels.js
new file mode 100644
index 0000000..34a10dd
--- /dev/null
+++ b/public/panels/reels.js
@@ -0,0 +1,28 @@
+// Reels panel — embeds the dw-marketing-reels console through the module's
+// same-origin proxy (/api/reels/ui/). The probe runs first so a down upstream
+// shows a diagnosis banner instead of a grey dead iframe.
+window.MCC_PANELS = window.MCC_PANELS || {};
+window.MCC_PANELS['reels'] = {
+ async init(root) {
+ const $ = (sel) => root.querySelector(sel);
+ const banner = $('#reels-banner');
+ let st = null;
+ try {
+ const r = await fetch(location.origin + '/api/reels/status', { credentials: 'same-origin' });
+ st = await r.json();
+ } catch (e) { st = { ok: false, error: String(e) }; }
+
+ if (!st.ok) {
+ banner.innerHTML = `The reels console isn’t reachable (<code>${st.base || ''}</code> — ${st.error || 'HTTP ' + st.status}).
+ It runs as its own pm2 process (<code>dw-marketing-reels</code>); restart it and reload this panel.`;
+ return;
+ }
+ banner.style.display = 'none';
+ $('#reels-toolbar').style.display = '';
+ $('#reels-count').textContent = st.reels != null ? `${st.reels} reel${st.reels === 1 ? '' : 's'} in the gallery` : 'gallery live';
+ $('#reels-open-direct').href = location.origin + '/api/reels/ui/';
+ const frame = $('#reels-frame');
+ frame.src = location.origin + '/api/reels/ui/';
+ frame.style.display = '';
+ },
+};
← 0f46bdd auto-save: 2026-07-15T11:36:14 (1 files) — scripts/fix-conni
·
back to Marketing Command Center
·
grasscloth campaign draft — Zoffany Kensington hero + Design b904d73 →