[object Object]

← back to Marketing Command Center

Quick Post live path: + Threads/Bluesky, 'Publish live' hands off to Composer (prefill + targeted account + dry-run+confirm) — no direct live post

311ff853d7828f21c110f2f1a7995ac523d71dc7 · 2026-08-12 16:33:07 -0700 · Steve

Files touched

Diff

commit 311ff853d7828f21c110f2f1a7995ac523d71dc7
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 12 16:33:07 2026 -0700

    Quick Post live path: + Threads/Bluesky, 'Publish live' hands off to Composer (prefill + targeted account + dry-run+confirm) — no direct live post
---
 package.json               |  2 +-
 public/panels/assets.js    |  2 +-
 public/panels/compose.js   | 12 +++++++++++-
 public/panels/composer.js  | 35 +++++++++++++++++++++++++++++++++++
 public/panels/quickpost.js |  3 ++-
 public/quickpost.js        | 32 +++++++++++++++++++++++++++++++-
 6 files changed, 81 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 6331c2f..3b5054e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "marketing-command-center",
-  "version": "1.10.0",
+  "version": "1.10.1",
   "description": "DW Marketing Command Center — Constant Contact, marketing calendar, suggested copy, on-demand layouts",
   "main": "server.js",
   "scripts": {
diff --git a/public/panels/assets.js b/public/panels/assets.js
index ac5d6af..a0a72f3 100644
--- a/public/panels/assets.js
+++ b/public/panels/assets.js
@@ -84,7 +84,7 @@ window.MCC_PANELS['assets'] = {
       if (window.MCCQuickPost) libGrid.querySelectorAll('.qp-mount').forEach(m => {
         if (m.dataset.wired) return; m.dataset.wired = '1';
         window.MCCQuickPost.attach(m, () => ({ caption: m.dataset.name, mediaUrl: m.dataset.url, source: 'asset-card' }),
-          { mini: true, includeAll: false });
+          { mini: true, includeAll: false, live: true });
       });
     }
     async function loadLibrary() {
diff --git a/public/panels/compose.js b/public/panels/compose.js
index 5036758..5bae0ad 100644
--- a/public/panels/compose.js
+++ b/public/panels/compose.js
@@ -75,6 +75,16 @@ window.MCC_PANELS['compose'] = {
       if (q.length && badge) { badge.hidden = false; badge.textContent = q.length; }
     } catch (_) {}
 
-    initSuggested(); // Suggested is the default tab
+    // If Quick Post handed off a "Publish live" prefill, open the Manual tab
+    // directly (composer.js reads + applies the prefill on its init).
+    let hasPrefill = false;
+    try { hasPrefill = !!sessionStorage.getItem('mccComposePrefill'); } catch (_) {}
+    if (hasPrefill) {
+      root.querySelectorAll('.cmp2-tab').forEach(b => b.classList.toggle('active', b.dataset.tab === 'manual'));
+      paneS.hidden = true; paneM.hidden = false;
+      initManual();
+    } else {
+      initSuggested(); // Suggested is the default tab
+    }
   },
 };
diff --git a/public/panels/composer.js b/public/panels/composer.js
index d57af9c..eca5788 100644
--- a/public/panels/composer.js
+++ b/public/panels/composer.js
@@ -240,5 +240,40 @@ window.MCC_PANELS['composer'] = {
         () => ({ caption: cap.value.trim(), mediaUrl: state.videoUrl || state.mediaUrl, source: 'composer' }),
         { mini: true });
     }
+
+    // Prefill from Quick Post's "Publish live" handoff — apply caption + media +
+    // preselect channels, then the human picks the exact Page/IG account, dry-runs,
+    // and confirms here. This IS the gated live path.
+    try {
+      const raw = sessionStorage.getItem('mccComposePrefill');
+      if (raw) {
+        sessionStorage.removeItem('mccComposePrefill');
+        const pf = JSON.parse(raw);
+        if (pf && Date.now() - (pf.ts || 0) < 120000) {
+          if (pf.caption) { cap.value = pf.caption; countIt(); }
+          const u = pf.mediaUrl || '';
+          if (u) {
+            const isVid = /\.(mp4|mov|webm|m4v)(\?|$)/i.test(u);
+            const ph = $('#cmp-preview').querySelector('.cmp-ph'); if (ph) ph.style.display = 'none';
+            if (isVid) {
+              state.videoUrl = u; state.mediaUrl = '';
+              const v = $('#cmp-video'); if (v) { v.src = u; v.style.display = 'block'; }
+              $('#cmp-img').style.display = 'none';
+            } else {
+              state.mediaUrl = u; state.videoUrl = '';
+              const img = $('#cmp-img'); img.src = u; img.style.display = 'block';
+              const v = $('#cmp-video'); if (v) { v.style.display = 'none'; v.removeAttribute('src'); }
+            }
+          }
+          (pf.channels || []).forEach(ch => {
+            const cb = root.querySelector('#cmp-targets input[data-ch="' + ch + '"]');
+            if (cb && !cb.disabled) { cb.checked = true; state.selected.add(ch); }
+          });
+          toggleFbPages();
+          const note = $('#cmp-bannernote');
+          if (note) note.textContent = '↗ Prefilled from Quick Post — pick your Page/IG account below, then Publish (leave Dry-run on for the first pass).';
+        }
+      }
+    } catch (_) {}
   },
 };
diff --git a/public/panels/quickpost.js b/public/panels/quickpost.js
index cc1d299..329f9cf 100644
--- a/public/panels/quickpost.js
+++ b/public/panels/quickpost.js
@@ -22,7 +22,8 @@ window.MCC_PANELS['quickpost'] = {
     // wire the shared button row — payload is read live at click time
     if (window.MCCQuickPost) {
       window.MCCQuickPost.attach($('#qp-buttons'),
-        () => ({ caption: cap.value.trim(), mediaUrl: media.value.trim(), source: 'quickpost-panel' }));
+        () => ({ caption: cap.value.trim(), mediaUrl: media.value.trim(), source: 'quickpost-panel' }),
+        { live: true });
     } else {
       $('#qp-buttons').innerHTML = '<div class="muted" style="font-size:12px">Quick-post component not loaded.</div>';
     }
diff --git a/public/quickpost.js b/public/quickpost.js
index fe8463e..96bfa0e 100644
--- a/public/quickpost.js
+++ b/public/quickpost.js
@@ -10,7 +10,13 @@
     { id: 'tiktok',    label: 'TikTok',    icon: '🎵' },
     { id: 'facebook',  label: 'Facebook',  icon: '📘' },
     { id: 'linkedin',  label: 'LinkedIn',  icon: '💼' },
+    { id: 'threads',   label: 'Threads',   icon: '🧵' },
+    { id: 'bluesky',   label: 'Bluesky',   icon: '🦋' },
   ];
+  // Live posting hands off to the Composer (targeted account picker + dry-run +
+  // confirm) — Quick Post never fires a live post itself. This is the set of
+  // platforms enabled for the live handoff (Steve, per-platform go).
+  const LIVE_ENABLED = ['instagram', 'tiktok', 'facebook', 'linkedin', 'threads', 'bluesky'];
 
   // one-time CSS
   if (!document.getElementById('qp-css')) {
@@ -26,6 +32,8 @@
       .qp-btn .qp-i{font-size:14px}
       .qp-btn.mini{padding:5px 8px;font-size:10.5px;border-radius:7px}
       .qp-all{border-style:dashed;color:var(--accent,#8a5a44)}
+      .qp-btn.qp-live{border-color:var(--accent,#8a5a44);color:var(--accent,#8a5a44);font-weight:700}
+      .qp-btn.qp-live:hover{background:var(--accent,#8a5a44);color:#fff}
       .qp-hint{font-size:11px;color:var(--mut,#8a8275)}
       #qp-toast{position:fixed;right:18px;bottom:18px;z-index:9999;display:flex;flex-direction:column;gap:8px}
       #qp-toast .t{background:var(--ink,#14110f);color:#fff;border-radius:10px;padding:11px 15px;font:600 12.5px/1.3 Inter,sans-serif;
@@ -62,12 +70,34 @@
       `<span class="qp-i">${p.icon}</span>${opts.mini ? '' : p.label}</button>`).join('');
     if (opts.includeAll !== false && !opts.mini)
       html += `<button type="button" class="qp-btn qp-all" data-qp="__all" title="Stage a draft to all platforms">+ All</button>`;
+    // Live handoff → opens the Composer prefilled (targeted account + confirm live there)
+    if (opts.live) {
+      const live = opts.mini
+        ? `<button type="button" class="qp-btn mini qp-live" data-qp="__live" title="Publish live in Composer (pick account + confirm there)">↗ Live</button>`
+        : `<button type="button" class="qp-btn qp-live" data-qp="__live" title="Publish live in Composer — pick the exact account + confirm there">↗ Publish live in Composer…</button>`;
+      html += live;
+    }
     mount.insertAdjacentHTML('beforeend', `<div class="qp-row">${html}</div>`);
 
     mount.querySelectorAll('[data-qp]').forEach(btn => btn.addEventListener('click', async () => {
       const which = btn.getAttribute('data-qp');
-      const targets = which === '__all' ? PLATFORMS.map(p => p.id) : [which];
       const payload = (typeof getPayload === 'function' ? getPayload() : getPayload) || {};
+      // LIVE handoff — hand media+caption to the Composer, preselect the enabled
+      // platforms, switch there. The human picks the exact Page/IG account, does a
+      // dry-run, and confirms in the Composer. Quick Post never posts live itself.
+      if (which === '__live') {
+        if (!payload.mediaUrl && !payload.caption) { toast('Nothing to hand off — add a caption or pick media first.'); return; }
+        try {
+          sessionStorage.setItem('mccComposePrefill', JSON.stringify({
+            caption: payload.caption || '', mediaUrl: payload.mediaUrl || '',
+            channels: LIVE_ENABLED, source: payload.source || 'quickpost', ts: Date.now(),
+          }));
+        } catch (_) {}
+        toast('↗ Opening Composer — pick the account, then <span class="g">dry-run &amp; confirm</span> to post live.');
+        location.hash = 'compose';
+        return;
+      }
+      const targets = which === '__all' ? PLATFORMS.map(p => p.id) : [which];
       if (!payload.mediaUrl && !payload.caption) { toast('Nothing to post — add a caption or pick media first.'); return; }
       btn.classList.add('busy'); const orig = btn.innerHTML;
       let ok = 0;

← a03f7e3 deploy: Easy Post buttons live on marketing.dw (v1.10.0)  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-12T16:33:05 (2 data files) — dat 941504a →