← back to Dw Marketing Reels
Safety-when-on: ARMED gate (no live post until SOCIAL_LIVE_ARMED=1) + private-label leak filter in captions (contrarian fixes)
2f74ee0f41df171174146080b4346099312f53bc · 2026-07-12 12:00:19 -0700 · Steve Abrams
Files touched
M data/reels.jsonM public/index.htmlM scripts/build-reel.mjsM scripts/publish-social.mjs
Diff
commit 2f74ee0f41df171174146080b4346099312f53bc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 12:00:19 2026 -0700
Safety-when-on: ARMED gate (no live post until SOCIAL_LIVE_ARMED=1) + private-label leak filter in captions (contrarian fixes)
---
data/reels.json | 39 +++++++++++++++++++++++++++++++++++++++
public/index.html | 3 ++-
scripts/build-reel.mjs | 13 ++++++++++---
scripts/publish-social.mjs | 6 ++++++
4 files changed, 57 insertions(+), 4 deletions(-)
diff --git a/data/reels.json b/data/reels.json
index eecfa15..65a1e2b 100644
--- a/data/reels.json
+++ b/data/reels.json
@@ -1,4 +1,43 @@
[
+ {
+ "file": "new-arrivals-2026-07-12-19-00.mp4",
+ "created_at": "2026-07-12T19:00:19.205Z",
+ "products": 6,
+ "seconds": 19,
+ "titles": [
+ "Katamari / Pewter",
+ "Tenaya / Beige",
+ "Katamari / Dark Gold",
+ "Tenaya / Cream",
+ "Tenaya / Spa Blue",
+ "Tenaya / Sage"
+ ],
+ "caption": "New arrivals just landed ✨ Katamari, Tenaya from Thibaut. Discover the latest wallcoverings — shop the collection at designerwallcoverings.com 🔗\n\n#DesignerWallcoverings #NewArrivals #Wallpaper #Wallcovering #InteriorDesign #InteriorDecor #HomeDesign #LuxuryInteriors #DesignInspo #Thibaut",
+ "hashtags": [
+ "#DesignerWallcoverings",
+ "#NewArrivals",
+ "#Wallpaper",
+ "#Wallcovering",
+ "#InteriorDesign",
+ "#InteriorDecor",
+ "#HomeDesign",
+ "#LuxuryInteriors",
+ "#DesignInspo",
+ "#Thibaut"
+ ],
+ "publish": {
+ "instagram": {
+ "status": "ready-pending-arm",
+ "note": "IG creds present; set SOCIAL_LIVE_ARMED=1 (Steve arm) to enable the first live post",
+ "at": "2026-07-12T19:00:19.278Z"
+ },
+ "tiktok": {
+ "status": "pending-reconnect",
+ "note": "TikTok channel not connected",
+ "at": "2026-07-12T19:00:19.278Z"
+ }
+ }
+ },
{
"file": "new-arrivals-2026-07-12-17-17.mp4",
"created_at": "2026-07-12T17:17:27.415Z",
diff --git a/public/index.html b/public/index.html
index 2dd836b..7d7f8e3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -87,7 +87,8 @@ const fmt = iso => { try { return new Date(iso).toLocaleString(undefined,{year:'
const escapeHtml = s => String(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
const escapeAttr = s => String(s||'').replace(/"/g,'"').replace(/\n/g,' ');
const STMAP = { posted:['✓ posted','ok'], simulated:['◐ simulated','sim'], 'pending-creds':['◷ needs creds','wait'],
- 'pending-reconnect':['◷ reconnect','wait'], pending:['◷ pending','wait'], error:['✗ error','err'], skipped:['– skipped','wait'] };
+ 'pending-reconnect':['◷ reconnect','wait'], 'ready-pending-arm':['🔒 armed?','sim'], 'pending-review':['👁 review','sim'],
+ pending:['◷ pending','wait'], error:['✗ error','err'], skipped:['– skipped','wait'] };
function pubBadges(pub){
if(!pub) return '';
const ch = { instagram:'IG', tiktok:'TT' };
diff --git a/scripts/build-reel.mjs b/scripts/build-reel.mjs
index 5e4b583..86175fc 100644
--- a/scripts/build-reel.mjs
+++ b/scripts/build-reel.mjs
@@ -15,10 +15,15 @@ const INTRO = 2.0, OUTRO = 2.6;
const esc = s => String(s || '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+// Private-label / upstream names that must NEVER surface publicly (mirrors dw-leak-scanner).
+// Captions pull from the LIVE storefront, so defend against any leak reaching IG/TikTok.
+const LEAK_DENY = /greenland|wallquest|chesapeake|nextwall|seabrook|brewster|command\s*54|momentum|versa|desima|carlsten|nicolette\s*mayer|york\s*wall/i;
+const safe = s => s && !LEAK_DENY.test(s);
+
// On-brand social caption + hashtags from the reel's products ($0 local, deterministic).
function captionFor(items) {
- const pats = [...new Set(items.map(i => i.pattern).filter(Boolean))];
- const vendors = [...new Set(items.map(i => i.vendor).filter(Boolean))];
+ const pats = [...new Set(items.map(i => i.pattern).filter(safe))];
+ const vendors = [...new Set(items.map(i => i.vendor).filter(safe))];
const lead = pats.slice(0, 3).join(', ');
const body = `New arrivals just landed ✨ ${lead}${pats.length > 3 ? ' & more' : ''}`
+ `${vendors.length ? ` from ${vendors.slice(0, 3).join(', ')}` : ''}.`
@@ -29,7 +34,9 @@ function captionFor(items) {
'#InteriorDesign', '#InteriorDecor', '#HomeDesign', '#LuxuryInteriors', '#DesignInspo',
...vendors.slice(0, 3).map(tagify),
])].slice(0, 14);
- return { text: `${body}\n\n${tags.join(' ')}`, hashtags: tags };
+ // final safety net: never emit a denylisted token even if one slipped through a field
+ const clean = s => s.split(/\s+/).filter(w => !LEAK_DENY.test(w)).join(' ');
+ return { text: `${clean(body)}\n\n${tags.join(' ')}`, hashtags: tags };
}
async function dl(url, dest) {
diff --git a/scripts/publish-social.mjs b/scripts/publish-social.mjs
index 630d21a..94e37f6 100644
--- a/scripts/publish-social.mjs
+++ b/scripts/publish-social.mjs
@@ -18,6 +18,10 @@ import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const MAN = join(ROOT, 'data', 'reels.json');
const AUTOPOST = process.env.SOCIAL_AUTOPOST === '1';
+// ARMED gate (Steve's standing pattern): even WITH valid creds, no real post fires until
+// SOCIAL_LIVE_ARMED=1 — so the first live post is a deliberate, supervised act, never an
+// accident of cred-landing. Until armed, a cred'd channel reports 'ready-pending-arm'.
+const ARMED = process.env.SOCIAL_LIVE_ARMED === '1';
const CHANNELS = (process.env.SOCIAL_CHANNELS || 'instagram,tiktok').split(',').map(s => s.trim());
const IG_URL = process.env.IG_AGENT_URL || 'http://127.0.0.1:9810';
const IG_AUTH = process.env.IG_AGENT_AUTH || '';
@@ -30,6 +34,7 @@ async function withTimeout(p, ms = 12000) {
async function postInstagram(reel) {
if (!IG_AUTH) return { status: 'pending-creds', note: 'IG_AGENT_AUTH not set (route via secrets skill)', at: now() };
+ if (!ARMED) return { status: 'ready-pending-arm', note: 'IG creds present; set SOCIAL_LIVE_ARMED=1 (Steve arm) to enable the first live post', at: now() };
const mediaUrl = `${PUBLIC_BASE}/reels/${encodeURIComponent(reel.file)}`;
try {
const res = await withTimeout(fetch(`${IG_URL}/api/skill/reel`, {
@@ -49,6 +54,7 @@ async function postTikTok(reel) {
// The tiktok skill needs the DW TikTok channel reconnected (OAuth) before the
// Content Posting API will accept uploads. Until TIKTOK_ACCESS_TOKEN is present, queue.
if (!process.env.TIKTOK_ACCESS_TOKEN) return { status: 'pending-reconnect', note: 'TikTok channel not connected', at: now() };
+ if (!ARMED) return { status: 'ready-pending-arm', note: 'TikTok token present; set SOCIAL_LIVE_ARMED=1 to enable the first live post', at: now() };
return { status: 'pending-reconnect', note: 'connected-token present but uploader wiring is a follow-up', at: now() };
}
← 1584e5c Public media exemption: GET /reels/*.mp4 unauthenticated (co
·
back to Dw Marketing Reels
·
Themeable reels + Phillipe Romano cork reel (env-driven copy 78da8fd →