[object Object]

← back to Marketing Command Center

auto-save: 2026-07-22T10:13:11 (5 files) — scripts/repro-run3.js screenrecord/DEBUG-REPORT.md scripts/probe-403.js scripts/verify-fix.js scripts/verify-fix2.js

5a92b9035f0e6d96f6b5b3104bd09eecb32885c3 · 2026-07-22 10:13:14 -0700 · Steve Abrams

Files touched

Diff

commit 5a92b9035f0e6d96f6b5b3104bd09eecb32885c3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 10:13:14 2026 -0700

    auto-save: 2026-07-22T10:13:11 (5 files) — scripts/repro-run3.js screenrecord/DEBUG-REPORT.md scripts/probe-403.js scripts/verify-fix.js scripts/verify-fix2.js
---
 screenrecord/DEBUG-REPORT.md | 72 ++++++++++++++++++++++++++++++++++++++++++++
 scripts/probe-403.js         | 16 ++++++++++
 scripts/repro-run3.js        |  2 +-
 scripts/verify-fix.js        | 33 ++++++++++++++++++++
 scripts/verify-fix2.js       | 28 +++++++++++++++++
 5 files changed, 150 insertions(+), 1 deletion(-)

diff --git a/screenrecord/DEBUG-REPORT.md b/screenrecord/DEBUG-REPORT.md
new file mode 100644
index 0000000..e78cb76
--- /dev/null
+++ b/screenrecord/DEBUG-REPORT.md
@@ -0,0 +1,72 @@
+# MCC "posts are all blank" — screen-record debug report
+
+Target: https://marketing.designerwallcoverings.com (Marketing Command Center, Basic-auth admin)
+Method: /screenrecord — 5 recorded runs, each a different panel-visit order, + follow-up
+deterministic reproducers. Artifacts: `screenrecord/rec/run{0..4}/*.webm`, `screenrecord/debug-log.jsonl`.
+
+## Highest-leverage finding first
+
+There are **two independent causes** of blank posts. They need different fixes.
+
+---
+
+### 1) INTERMITTENT: whole panels render blank — panel-router race (client JS)
+
+**Symptom captured:** In run3 (one specific click order), after visiting Composer, *every*
+subsequent panel (Calendar, Streams Board, Copy, Engine, Templates) showed its **title only** —
+**zero cards, zero `/api/*` calls, and zero console errors**. Reload fixes it. Order-dependent:
+runs 0/1/2/4 (different orders) all rendered fine. A single happy-path pass would never catch this.
+
+**Root cause:** `public/app.js` → `route()` (runs on every `hashchange`) has **two `await`
+points** (fetch panel HTML, then load panel JS) but **no concurrency guard**. Rapid panel
+switching interleaves two `route()` calls that both write the single shared `#panel` element; a
+stale invocation lands after the fresh render and either wipes the DOM or calls
+`MCC_PANELS[id].init()` against the wrong panel's DOM — `init()` finds its container `null`, bails
+silently, so the panel keeps its title but never fetches or renders. No error, no network → blank.
+
+Smoking gun: at 250 ms inter-click spacing, Streams Board landed with **`cards=0` while its 6
+API calls DID fire** — proof the render happened and was clobbered, not that data failed to load.
+
+**Fix (applied locally, NOT deployed):** latest-wins token in `route()` — stamp each invocation,
+and after each `await` bail if a newer navigation superseded it. `public/app.js`, ~6 lines.
+
+**Verification status:** The defect is real and the fix is the correct class. The *user-visible*
+blank is timing-rare — under clean poll-based tests (10 s) board always renders within ~800 ms on
+both unfixed-live and fixed-local, and an isolated 2-panel hash race never blanks. So I could not
+produce a 100%-deterministic A/B that proves the fix eliminates it. Treat the fix as correct
+hardening of a genuine race, pending a deploy + real-use confirmation.
+
+---
+
+### 2) CONSISTENT: post *images* blank — expired Instagram CDN hotlinks (server data)
+
+**Symptom captured:** On **every** run, Streams Board cards had broken `<img>`s → **HTTP 403**.
+Cards have text but no image = "posts look blank," and this scales: if the board hasn't re-synced
+recently, many/all Instagram-sourced thumbnails expire at once → an all-blank-images board.
+
+**Root cause:** Board cards embed **raw Instagram CDN URLs** directly, e.g.
+`https://scontent-lax3-1.cdninstagram.com/v/t51...&oe=6A6027AE`. Those URLs are **hotlink-protected
+and time-limited** (the `oe=` param is an expiry stamp). Once expired / cross-origin, IG returns
+**403** and the tile renders blank.
+
+**Fix (proposed, not applied — bigger + server-side + gated):** stop embedding raw expiring IG
+CDN URLs. Either (a) at sync time, download the thumbnail and store it locally, or (b) add a
+server-side image proxy endpoint (`/api/img?u=…`) that fetches IG media with proper headers and
+caches it, and rewrite card image src to that. This makes post images durable regardless of IG URL
+expiry.
+
+---
+
+## Which one is "posts are all blank"?
+- If the **whole panel** is empty (no cards at all, fixed by reload) → cause #1 (router race).
+- If cards show **text but no images / broken image tiles** → cause #2 (expired IG hotlinks).
+
+## Deploy note
+Both live fixes require a Kamatera deploy — **gated, not performed**. Cause-#1 fix is staged
+locally in `public/app.js`. Cause-#2 fix is a proposal pending Steve's go.
+
+## Repro scripts (kept under scripts/)
+- `screenrecord-posts.js` — the 5-run recorder (label-text nav + blank-card + network capture)
+- `repro-run3.js` — replays the all-blank order at 3 timings
+- `verify-proxy.js` + `verify-fix2.js` — serve fixed app.js locally over live data, race A/B
+- `probe-403.js` — isolates the expired-IG-CDN 403
diff --git a/scripts/probe-403.js b/scripts/probe-403.js
new file mode 100644
index 0000000..25b0f80
--- /dev/null
+++ b/scripts/probe-403.js
@@ -0,0 +1,16 @@
+const { chromium } = require('playwright');
+const BASE='https://marketing.designerwallcoverings.com';
+(async()=>{
+  const b=await chromium.launch();
+  const ctx=await b.newContext({viewport:{width:1600,height:900},httpCredentials:{username:'admin',password:'DW2024!'}});
+  const page=await ctx.newPage();
+  const bad=[];
+  page.on('response',r=>{if(r.status()>=400)bad.push(r.status()+'  '+r.url());});
+  await page.goto(BASE+'/',{waitUntil:'domcontentloaded'});
+  await page.evaluate(()=>location.hash='board'); await page.waitForTimeout(5000);
+  // list broken img srcs directly from DOM
+  const broken=await page.evaluate(()=>[...document.querySelectorAll('.bd-card img')].filter(i=>i.complete&&i.naturalWidth===0&&i.getAttribute('src')).map(i=>i.getAttribute('src')).slice(0,6));
+  console.log('BROKEN <img> srcs on Streams Board:'); broken.forEach(s=>console.log('   '+s));
+  console.log('\nALL >=400 responses:'); [...new Set(bad)].forEach(x=>console.log('   '+x));
+  await ctx.close(); await b.close();
+})().catch(e=>{console.error(e);process.exit(1);});
diff --git a/scripts/repro-run3.js b/scripts/repro-run3.js
index 8c83af9..a155296 100644
--- a/scripts/repro-run3.js
+++ b/scripts/repro-run3.js
@@ -1,7 +1,7 @@
 // Replay run3's exact order, inspect each panel right after its single click,
 // across several inter-click delays, to surface the blank-on-land cascade.
 const { chromium } = require('playwright');
-const BASE = 'https://marketing.designerwallcoverings.com';
+const BASE = process.env.MCC_BASE || 'https://marketing.designerwallcoverings.com';
 const SEQ = [['Social Scheduler','social'],['Composer','composer'],['Marketing Calendar','calendar'],['Streams Board','board'],['Suggested Copy','copy'],['Engine','engine'],['Campaign Templates','templates']];
 async function cardCount(page){return await page.evaluate(()=>{const c=document.querySelectorAll('.bd-card,[class*="card"],article');let real=0;c.forEach(e=>{const r=e.getBoundingClientRect();if(r.width>40&&r.height>20)real++;});return {n:real,title:(document.querySelector('#paneltitle')?.innerText||'').trim(),loading:/loading/i.test((document.querySelector('#panel')?.innerText||'').slice(0,30))};});}
 (async () => {
diff --git a/scripts/verify-fix.js b/scripts/verify-fix.js
new file mode 100644
index 0000000..8f53c1d
--- /dev/null
+++ b/scripts/verify-fix.js
@@ -0,0 +1,33 @@
+// Timing-robust race test. Rapid-nav a sequence, land on a target panel, then POLL
+// up to 10s for it to render. "Blank" = still 0 cards after 10s (genuine bug, not slow).
+// Compare UNFIXED live vs FIXED proxy on identical logic.
+const { chromium } = require('playwright');
+const BASE = process.env.MCC_BASE || 'https://marketing.designerwallcoverings.com';
+const LABEL = 'Streams Board';
+const SEQ = ['Social Scheduler', 'Composer', 'Marketing Calendar', 'Streams Board', 'Suggested Copy', 'Engine', 'Campaign Templates'];
+async function cards(page){return await page.evaluate(()=>{let n=0;document.querySelectorAll('.bd-card,[class*="card"],article').forEach(e=>{const r=e.getBoundingClientRect();if(r.width>40&&r.height>20)n++;});return n;});}
+async function landAndPoll(page){
+  // rapid fire whole sequence at 180ms (forces interleave), no settle
+  for(const l of SEQ){ try{const el=page.locator('a',{hasText:new RegExp(l,'i')}).first();if(await el.count())await el.click({timeout:3000});}catch{} await page.waitForTimeout(180); }
+  // land firmly on target once more, then POLL up to 10s
+  try{await page.locator('a',{hasText:new RegExp(LABEL,'i')}).first().click();}catch{}
+  const title=await page.evaluate(()=>document.querySelector('#paneltitle')?.innerText||'');
+  let n=0,t0=Date.now();
+  while(Date.now()-t0<10000){ n=await cards(page); if(n>0)break; await page.waitForTimeout(400); }
+  return {title:title.trim(),cards:n,ms:Date.now()-t0};
+}
+(async()=>{
+  let blank=0;
+  for(let trial=1;trial<=4;trial++){
+    const b=await chromium.launch();
+    const ctx=await b.newContext({viewport:{width:1600,height:900},httpCredentials:{username:'admin',password:'DW2024!'}});
+    const page=await ctx.newPage();
+    await page.goto(BASE+'/',{waitUntil:'domcontentloaded'}); await page.waitForTimeout(2500);
+    const r=await landAndPoll(page);
+    const isBlank=r.cards===0;
+    if(isBlank)blank++;
+    console.log(`trial${trial}: land="${r.title}" cards=${r.cards} settledIn=${r.ms}ms ${isBlank?'BLANK (stayed 0 after 10s)':'rendered'}`);
+    await ctx.close(); await b.close();
+  }
+  console.log(`\n${BASE}\n=> blank ${blank}/4 trials`);
+})().catch(e=>{console.error('FATAL',e);process.exit(1);});
diff --git a/scripts/verify-fix2.js b/scripts/verify-fix2.js
new file mode 100644
index 0000000..ed9beef
--- /dev/null
+++ b/scripts/verify-fix2.js
@@ -0,0 +1,28 @@
+// Deterministic race test via location.hash. Fire calendar->board with a tiny gap so
+// board's route() starts while calendar's is mid-fetch. Poll board up to 10s.
+// Blank after 10s = race left it dead. Compare unfixed-live vs fixed-proxy.
+const { chromium } = require('playwright');
+const BASE = process.env.MCC_BASE || 'https://marketing.designerwallcoverings.com';
+async function boardCards(page){return await page.evaluate(()=>{const t=(document.querySelector('#paneltitle')?.innerText||'').trim();let n=0;document.querySelectorAll('.bd-card').forEach(e=>{const r=e.getBoundingClientRect();if(r.width>40&&r.height>20)n++;});return {t,n};});}
+(async()=>{
+  let blank=0;const TRIALS=6;
+  for(let i=1;i<=TRIALS;i++){
+    const b=await chromium.launch();
+    const ctx=await b.newContext({viewport:{width:1600,height:900},httpCredentials:{username:'admin',password:'DW2024!'}});
+    const page=await ctx.newPage();
+    await page.goto(BASE+'/',{waitUntil:'domcontentloaded'}); await page.waitForTimeout(2500);
+    // vary the mid-flight gap across trials to hit the interleave window
+    const gap=[20,40,60,90,130,200][i-1];
+    await page.evaluate(()=>{location.hash='calendar';});
+    await page.waitForTimeout(gap);
+    await page.evaluate(()=>{location.hash='board';});
+    // poll board specifically
+    let r={t:'',n:0},t0=Date.now();
+    while(Date.now()-t0<10000){ r=await boardCards(page); if(r.t==='Streams Board'&&r.n>0)break; await page.waitForTimeout(400); }
+    const isBlank=(r.t==='Streams Board'&&r.n===0);
+    if(isBlank)blank++;
+    console.log(`trial${i} gap=${gap}ms: title="${r.t}" boardCards=${r.n} in=${Date.now()-t0}ms ${isBlank?'  <-- BLANK after 10s':''}`);
+    await ctx.close(); await b.close();
+  }
+  console.log(`\n${BASE}\n=> board BLANK ${blank}/${TRIALS}`);
+})().catch(e=>{console.error('FATAL',e);process.exit(1);});

← c073e61 auto-save: 2026-07-22T09:43:01 (8 files) — public/app.js scr  ·  back to Marketing Command Center  ·  fix(mcc): guard panel-router race that blanked panels + untr 243d9c5 →