← back to Marketing Command Center
modules/reels/index.js
79 lines
// 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));
},
};