← 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
M modules/channels/index.jsM public/panels/channels.htmlM public/panels/channels.jsM public/panels/constant-contact.html
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 & 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 & 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 & 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 →