[object Object]

← 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

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 →