[object Object]

← back to Marketing Command Center

MCC polish: publish-time caveats for half-wired channels (TikTok private-only, YouTube uploads-not-wired) on chips + confirm dialog, created date+time + sort on Channels outbox, float CC import+ranked-opens above mock live cards

535807b80003825c85e6186b6f0b69b81d72a5f8 · 2026-06-15 06:48:35 -0700 · Steve Abrams

Files touched

Diff

commit 535807b80003825c85e6186b6f0b69b81d72a5f8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 15 06:48:35 2026 -0700

    MCC polish: publish-time caveats for half-wired channels (TikTok private-only, YouTube uploads-not-wired) on chips + confirm dialog, created date+time + sort on Channels outbox, float CC import+ranked-opens above mock live cards
---
 modules/channels/index.js           |  5 +++++
 public/panels/channels.html         | 11 +++++++++--
 public/panels/channels.js           | 33 +++++++++++++++++++++++++++------
 public/panels/constant-contact.html | 32 ++++++++++++++++----------------
 4 files changed, 57 insertions(+), 24 deletions(-)

diff --git a/modules/channels/index.js b/modules/channels/index.js
index 1397cbb..70a140d 100644
--- a/modules/channels/index.js
+++ b/modules/channels/index.js
@@ -166,6 +166,8 @@ function platformStatus() {
       accounts: [],
       needs: 'TikTok for Developers app + Content Posting API access (audited) → user OAuth → access token. Direct-post needs app review; sandbox posts to private only.',
       scopes: ['video.publish', 'video.upload'],
+      // Surfaced at publish time so a "live" post isn't mistaken for public.
+      caveat: 'Posts are private-only (SELF_ONLY) until TikTok approves the app audit.',
     },
     youtube: {
       label: 'YouTube', icon: '▶️',
@@ -173,6 +175,8 @@ function platformStatus() {
       accounts: [],
       needs: 'Google Cloud project + YouTube Data API v3 enabled + OAuth consent (youtube.upload scope) → channel refresh token.',
       scopes: ['https://www.googleapis.com/auth/youtube.upload'],
+      // Connecting works, but uploads aren't built — a "live" post will stage.
+      caveat: 'Connecting works, but the video upload pipeline isn’t wired yet — posts stage instead of going live.',
     },
   };
   // Merge in OAuth-flow state: a platform is `connected` if env creds OR a minted
@@ -256,6 +260,7 @@ module.exports = {
         platforms: p.platforms.map(pl => ({
           id: pl, label: st[pl].label, connected: st[pl].connected,
           configured: st[pl].configured, connectUrl: st[pl].connectUrl,
+          caveat: st[pl].caveat || null,
         })),
       }));
       res.json({ redirectBase: redirectBase(), providers });
diff --git a/public/panels/channels.html b/public/panels/channels.html
index 812fab1..0af3813 100644
--- a/public/panels/channels.html
+++ b/public/panels/channels.html
@@ -29,8 +29,15 @@
 </div>
 
 <div class="card">
-  <h2>Outbox</h2>
-  <div id="ch-outbox" class="muted">No posts yet.</div>
+  <div class="row" style="justify-content:space-between;align-items:center">
+    <h2 style="margin:0">Outbox</h2>
+    <select id="ch-outbox-sort" style="width:auto;font-size:12px">
+      <option value="newest">Newest</option>
+      <option value="oldest">Oldest</option>
+      <option value="channel">Channel</option>
+    </select>
+  </div>
+  <div id="ch-outbox" class="muted" style="margin-top:8px">No posts yet.</div>
 </div>
 
 <style>
diff --git a/public/panels/channels.js b/public/panels/channels.js
index c10c40d..c04ccfd 100644
--- a/public/panels/channels.js
+++ b/public/panels/channels.js
@@ -6,6 +6,8 @@ window.MCC_PANELS['channels'] = {
     const $ = s => root.querySelector(s);
     const jget = async u => (await fetch(ORIGIN + u, { credentials: 'same-origin' })).json();
     let status = {};
+    // created date + time in the admin's local tz (admin-card convention)
+    const fmtWhen = iso => { if (!iso) return ''; const d = new Date(iso); return isNaN(d) ? '' : d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); };
 
     async function loadStatus() {
       status = await jget('/api/channels/status');
@@ -13,7 +15,7 @@ window.MCC_PANELS['channels'] = {
       $('#ch-banner').innerHTML = `${status.connectedCount}/${status.total} channels connected · ${status.outbox} in outbox. ` +
         (status.connectedCount === 0 ? 'No channels connected yet — activate each platform below to enable live posting.' : 'Connected channels post live; the rest stage.');
       $('#ch-targets').innerHTML = Object.entries(ps).map(([k, p]) =>
-        `<label style="display:flex;align-items:center;gap:6px;font-weight:600;color:var(--ink)"><input type="checkbox" class="ch-t" value="${k}" style="width:auto"> ${p.icon} ${esc(p.label)}${p.connected ? '' : ' <span class="muted" style="font-weight:400">(stages)</span>'}</label>`).join('');
+        `<label style="display:flex;align-items:center;gap:6px;font-weight:600;color:var(--ink)" ${p.caveat ? `title="${esc(p.caveat)}"` : ''}><input type="checkbox" class="ch-t" value="${k}" style="width:auto"> ${p.icon} ${esc(p.label)}${p.connected ? '' : ' <span class="muted" style="font-weight:400">(stages)</span>'}${p.caveat ? ' <span class="muted" style="font-weight:400" title="' + esc(p.caveat) + '">⚠</span>' : ''}</label>`).join('');
     }
 
     // ── ① Activate page ──────────────────────────────────────────────────────
@@ -100,20 +102,39 @@ window.MCC_PANELS['channels'] = {
       });
     }
 
+    let outboxItems = [];
+    function renderOutbox() {
+      const sort = ($('#ch-outbox-sort') && $('#ch-outbox-sort').value) || 'newest';
+      const rows = outboxItems.slice();
+      rows.sort((a, b) => sort === 'oldest' ? (a.at || '').localeCompare(b.at || '')
+        : sort === 'channel' ? (a.channel || '').localeCompare(b.channel || '') || (b.at || '').localeCompare(a.at || '')
+        : (b.at || '').localeCompare(a.at || '')); // newest
+      $('#ch-outbox').innerHTML = rows.length ? rows.map(it => `
+        <div class="row" style="justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:7px 2px">
+          <div style="min-width:0">
+            <div>${esc(it.channel)} · <span class="muted">${esc((it.caption || '').slice(0, 50))}</span></div>
+            <div class="muted" style="font-size:11px" title="${esc(it.at || '')}">🕓 ${esc(fmtWhen(it.at))}</div>
+          </div>
+          <span class="pill">${esc(it.status)}</span></div>`).join('') : '<div class="muted">No posts yet.</div>';
+    }
     async function loadOutbox() {
       const d = await jget('/api/channels/outbox');
-      $('#ch-outbox').innerHTML = (d.items || []).length ? d.items.map(it => `
-        <div class="row" style="justify-content:space-between;border-bottom:1px solid var(--line);padding:7px 2px">
-          <span>${esc(it.channel)} · <span class="muted">${esc((it.caption || '').slice(0, 50))}</span></span>
-          <span class="pill">${esc(it.status)}</span></div>`).join('') : '<div class="muted">No posts yet.</div>';
+      outboxItems = d.items || [];
+      renderOutbox();
     }
+    if ($('#ch-outbox-sort')) $('#ch-outbox-sort').addEventListener('change', renderOutbox);
 
     async function send(live) {
       const channels = [...root.querySelectorAll('.ch-t:checked')].map(c => c.value);
       const caption = $('#ch-caption').value.trim(); const mediaUrl = $('#ch-media').value.trim();
       if (!channels.length) { $('#ch-msg').textContent = 'pick at least one channel'; return; }
       if (!caption && !mediaUrl) { $('#ch-msg').textContent = 'write a caption or add media'; return; }
-      if (live && !confirm(`PUBLISH LIVE to: ${channels.join(', ')}?\n\nConnected channels post immediately; unconnected ones stage. Proceed?`)) return;
+      if (live) {
+        const ps = status.platforms || {};
+        const caveats = channels.filter(c => ps[c] && ps[c].caveat).map(c => `• ${ps[c].label}: ${ps[c].caveat}`);
+        const warn = caveats.length ? `\n\n⚠ Heads up:\n${caveats.join('\n')}` : '';
+        if (!confirm(`PUBLISH LIVE to: ${channels.join(', ')}?\n\nConnected channels post immediately; unconnected ones stage.${warn}\n\nProceed?`)) return;
+      }
       $('#ch-msg').textContent = live ? 'publishing…' : 'staging…';
       const body = { channels, caption, mediaUrl, confirm: live, dryRun: !live };
       const r = await fetch(ORIGIN + '/api/channels/publish', { method: 'POST', headers: { 'content-type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(body) });
diff --git a/public/panels/constant-contact.html b/public/panels/constant-contact.html
index c508173..a9e3278 100644
--- a/public/panels/constant-contact.html
+++ b/public/panels/constant-contact.html
@@ -1,21 +1,5 @@
 <div id="cc-banner"></div>
 
-<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(280px,1fr))">
-
-  <div class="card" id="cc-contacts">
-    <h2>Contacts</h2>
-    <div class="muted" style="font-size:12px">List membership &amp; recent signups</div>
-    <div id="cc-contacts-body" class="loading">Loading…</div>
-  </div>
-
-  <div class="card" id="cc-stats">
-    <h2>Stats summary</h2>
-    <div class="muted" style="font-size:12px">Aggregate sends / opens / clicks</div>
-    <div id="cc-stats-body" class="loading">Loading…</div>
-  </div>
-
-</div>
-
 <div class="card" id="cc-import">
   <h2>Import old emails &amp; campaigns (CSV)</h2>
   <div class="muted" style="font-size:12px">No API token needed — export from Constant Contact (Contacts → Export, or Reporting → Export) and drop the CSV here. <b id="cc-store-meta"></b></div>
@@ -49,6 +33,22 @@
   </div>
 </div>
 
+<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(280px,1fr))">
+
+  <div class="card" id="cc-contacts">
+    <h2>Contacts</h2>
+    <div class="muted" style="font-size:12px">List membership &amp; recent signups</div>
+    <div id="cc-contacts-body" class="loading">Loading…</div>
+  </div>
+
+  <div class="card" id="cc-stats">
+    <h2>Stats summary</h2>
+    <div class="muted" style="font-size:12px">Aggregate sends / opens / clicks</div>
+    <div id="cc-stats-body" class="loading">Loading…</div>
+  </div>
+
+</div>
+
 <div class="card" id="cc-campaigns">
   <h2>Recent Campaigns</h2>
   <div class="muted" style="font-size:12px">Name · status · sends · open rate</div>

← 9968bde MCC should-fix sweep: unify Kravet block (lib/kravet.js, ser  ·  back to Marketing Command Center  ·  Add Streams Board — Hootsuite-style column view (social chan a32a889 →