← back to Marketing Command Center
auto-save: 2026-07-22T09:43:01 (8 files) — public/app.js screenrecord/ scripts/nav-map.js scripts/probe-403-and-posts.js scripts/repro-race.js
c073e612439258fd18d740e426a1873ecd1f2593 · 2026-07-22 09:43:05 -0700 · Steve Abrams
Files touched
M public/app.jsA screenrecord/debug-log.jsonlA screenrecord/rec/run0/page@2c01d7569b49f94de71e05a9041138ba.webmA screenrecord/rec/run0/page@dc4fb41967627d02042518ad55804ad4.webmA screenrecord/rec/run1/page@69c3c7a42f13d89b8093ee3cbdfb4684.webmA screenrecord/rec/run1/page@8c820e74949b209ee3fb93803ee5fd1b.webmA screenrecord/rec/run2/page@1616cf421912feb6679fb38f1cd1aeaf.webmA screenrecord/rec/run2/page@355fdee8652b5a116b4b1321da3205f3.webmA screenrecord/rec/run3/page@0e731e2683305ede1ccab89a5b156259.webmA screenrecord/rec/run3/page@c60d9473e49baf6ebfdac95e693ec000.webmA screenrecord/rec/run4/page@07f63bbb75a5b265add060fc2fa13ead.webmA screenrecord/rec/run4/page@9b7d861794ba0b63f83535678a98fd0f.webmA scripts/nav-map.jsA scripts/probe-403-and-posts.jsA scripts/repro-race.jsA scripts/repro-run3.jsA scripts/screenrecord-posts.jsA scripts/verify-proxy.js
Diff
commit c073e612439258fd18d740e426a1873ecd1f2593
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 09:43:05 2026 -0700
auto-save: 2026-07-22T09:43:01 (8 files) — public/app.js screenrecord/ scripts/nav-map.js scripts/probe-403-and-posts.js scripts/repro-race.js
---
public/app.js | 10 +++
screenrecord/debug-log.jsonl | 35 ++++++++
.../page@2c01d7569b49f94de71e05a9041138ba.webm | Bin 0 -> 721402 bytes
.../page@dc4fb41967627d02042518ad55804ad4.webm | Bin 0 -> 983290 bytes
.../page@69c3c7a42f13d89b8093ee3cbdfb4684.webm | Bin 0 -> 973328 bytes
.../page@8c820e74949b209ee3fb93803ee5fd1b.webm | Bin 0 -> 714611 bytes
.../page@1616cf421912feb6679fb38f1cd1aeaf.webm | Bin 0 -> 651074 bytes
.../page@355fdee8652b5a116b4b1321da3205f3.webm | Bin 0 -> 979439 bytes
.../page@0e731e2683305ede1ccab89a5b156259.webm | Bin 0 -> 763778 bytes
.../page@c60d9473e49baf6ebfdac95e693ec000.webm | Bin 0 -> 495490 bytes
.../page@07f63bbb75a5b265add060fc2fa13ead.webm | Bin 0 -> 663779 bytes
.../page@9b7d861794ba0b63f83535678a98fd0f.webm | Bin 0 -> 936511 bytes
scripts/nav-map.js | 22 +++++
scripts/probe-403-and-posts.js | 35 ++++++++
scripts/repro-race.js | 36 ++++++++
scripts/repro-run3.js | 29 +++++++
scripts/screenrecord-posts.js | 92 +++++++++++++++++++++
scripts/verify-proxy.js | 25 ++++++
18 files changed, 284 insertions(+)
diff --git a/public/app.js b/public/app.js
index 517dca5..80cccf7 100644
--- a/public/app.js
+++ b/public/app.js
@@ -89,7 +89,15 @@ window.boot = boot;
window.addEventListener('hashchange', route);
const loaded = new Set();
+// Monotonic token: every route() call claims the next value. After each await we
+// re-check that we're still the newest navigation; if a later click superseded us
+// we abort BEFORE touching #panel. Without this, rapid panel-switching interleaves
+// two route() calls that both write the single shared #panel — a stale one lands
+// after the fresh render and wipes it, leaving the panel blank even though its API
+// calls fired (the "all posts blank" bug). Latest-wins is the fix.
+let routeToken = 0;
async function route() {
+ const myToken = ++routeToken;
const id = location.hash.replace('#', '') || (PANELS[0] && PANELS[0].id);
if (!id) return;
const meta = PANELS.find(p => p.id === id) || { id, title: id };
@@ -100,6 +108,7 @@ async function route() {
panel.innerHTML = '<div class="loading">Loading…</div>';
let html = '';
try { const r = await fetch(`${ORIGIN}/panels/${id}.html`, { credentials: 'same-origin' }); html = r.ok ? await r.text() : ''; } catch {}
+ if (myToken !== routeToken) return; // superseded during HTML fetch — don't clobber the newer panel
panel.innerHTML = html || `<div class="muted-banner">The “${meta.title}” panel isn’t built yet.</div>`;
if (!loaded.has(id)) {
await new Promise(res => {
@@ -108,6 +117,7 @@ async function route() {
});
loaded.add(id);
}
+ if (myToken !== routeToken) return; // superseded during panel-JS load — don't init a stale panel
try { window.MCC_PANELS[id] && window.MCC_PANELS[id].init && window.MCC_PANELS[id].init(panel); } catch (e) { console.error(e); }
}
diff --git a/screenrecord/debug-log.jsonl b/screenrecord/debug-log.jsonl
new file mode 100644
index 0000000..0cf6dd0
--- /dev/null
+++ b/screenrecord/debug-log.jsonl
@@ -0,0 +1,35 @@
+{"run":0,"id":"board","label":"Streams Board","clicked":true,"title":"Streams Board","cards":{"title":"Streams Board","total":69,"blank":0,"withText":69,"withImg":21,"brokenImg":2,"samples":[{"textLen":252,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"},{"textLen":250,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"}]},"api":[{"url":"/api/board/columns","status":200,"len":560},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/channels/outbox","status":200,"len":14799},{"url":"/api/linkedin/drafts","status":200,"len":1385},{"url":"/api/constant-contact/calendar","status":200,"len":15236},{"url":"/api/constant-contact/campaigns","status":200,"len":11689}],"bad":["403 https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410630935506340_n.jpg?s"],"errors":["Failed to load resource: the server responded with a status of 403 ()"]}
+{"run":0,"id":"social","label":"Social Scheduler","clicked":true,"title":"Social Scheduler","cards":{"title":"Social Scheduler","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/social/status","status":200,"len":211},{"url":"/api/social/posts","status":200,"len":7372},{"url":"/api/social/best-times","status":200,"len":979}],"bad":[],"errors":[]}
+{"run":0,"id":"engine","label":"Engine","clicked":true,"title":"Engine","cards":{"title":"Engine","total":6,"blank":0,"withText":6,"withImg":2,"brokenImg":0,"samples":[]},"api":[{"url":"/api/engine/status","status":200,"len":162},{"url":"/api/engine/today?generate=0","status":200,"len":24579},{"url":"/api/engine/queue?days=7","status":200,"len":49067}],"bad":[],"errors":[]}
+{"run":0,"id":"composer","label":"Composer","clicked":true,"title":"Composer","cards":{"title":"Composer","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/composer/targets","status":200,"len":364},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/assets/list","status":200,"len":2127}],"bad":[],"errors":[]}
+{"run":0,"id":"templates","label":"Campaign Templates","clicked":true,"title":"Campaign Templates","cards":{"title":"Campaign Templates","total":9,"blank":0,"withText":9,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/templates/meta","status":200,"len":686},{"url":"/api/templates/list","status":200,"len":13270}],"bad":[],"errors":[]}
+{"run":0,"id":"copy","label":"Suggested Copy","clicked":true,"title":"Suggested Copy","cards":{"title":"Suggested Copy","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/copy/presets","status":200,"len":859}],"bad":[],"errors":[]}
+{"run":0,"id":"calendar","label":"Marketing Calendar","clicked":true,"title":"Marketing Calendar","cards":{"title":"Marketing Calendar","total":3,"blank":0,"withText":3,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/calendar/dates?year=2026&month=07","status":200,"len":499},{"url":"/api/calendar/schedule?year=2026&month=07","status":200,"len":15}],"bad":[],"errors":[]}
+{"run":1,"id":"calendar","label":"Marketing Calendar","clicked":true,"title":"Marketing Calendar","cards":{"title":"Marketing Calendar","total":3,"blank":0,"withText":3,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/calendar/dates?year=2026&month=07","status":200,"len":499},{"url":"/api/calendar/schedule?year=2026&month=07","status":200,"len":15}],"bad":[],"errors":[]}
+{"run":1,"id":"copy","label":"Suggested Copy","clicked":true,"title":"Suggested Copy","cards":{"title":"Suggested Copy","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/copy/presets","status":200,"len":859}],"bad":[],"errors":[]}
+{"run":1,"id":"templates","label":"Campaign Templates","clicked":true,"title":"Campaign Templates","cards":{"title":"Campaign Templates","total":9,"blank":0,"withText":9,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/templates/meta","status":200,"len":686},{"url":"/api/templates/list","status":200,"len":13270}],"bad":[],"errors":[]}
+{"run":1,"id":"composer","label":"Composer","clicked":true,"title":"Composer","cards":{"title":"Composer","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/composer/targets","status":200,"len":364},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/assets/list","status":200,"len":2127}],"bad":[],"errors":[]}
+{"run":1,"id":"engine","label":"Engine","clicked":true,"title":"Engine","cards":{"title":"Engine","total":6,"blank":0,"withText":6,"withImg":2,"brokenImg":0,"samples":[]},"api":[{"url":"/api/engine/status","status":200,"len":162},{"url":"/api/engine/today?generate=0","status":200,"len":24579},{"url":"/api/engine/queue?days=7","status":200,"len":49067}],"bad":[],"errors":[]}
+{"run":1,"id":"social","label":"Social Scheduler","clicked":true,"title":"Social Scheduler","cards":{"title":"Social Scheduler","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/social/status","status":200,"len":211},{"url":"/api/social/posts","status":200,"len":7372},{"url":"/api/social/best-times","status":200,"len":979}],"bad":[],"errors":[]}
+{"run":1,"id":"board","label":"Streams Board","clicked":true,"title":"Streams Board","cards":{"title":"Streams Board","total":69,"blank":0,"withText":69,"withImg":21,"brokenImg":2,"samples":[{"textLen":252,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"},{"textLen":250,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"}]},"api":[{"url":"/api/board/columns","status":200,"len":560},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/channels/outbox","status":200,"len":14799},{"url":"/api/linkedin/drafts","status":200,"len":1385},{"url":"/api/constant-contact/campaigns","status":200,"len":11689},{"url":"/api/constant-contact/calendar","status":200,"len":15236}],"bad":["403 https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410630935506340_n.jpg?s"],"errors":["Failed to load resource: the server responded with a status of 403 ()"]}
+{"run":2,"id":"engine","label":"Engine","clicked":true,"title":"Engine","cards":{"title":"Engine","total":6,"blank":0,"withText":6,"withImg":2,"brokenImg":0,"samples":[]},"api":[{"url":"/api/engine/status","status":200,"len":162},{"url":"/api/engine/today?generate=0","status":200,"len":24579},{"url":"/api/engine/queue?days=7","status":200,"len":49067}],"bad":[],"errors":[]}
+{"run":2,"id":"board","label":"Streams Board","clicked":true,"title":"Streams Board","cards":{"title":"Streams Board","total":69,"blank":0,"withText":69,"withImg":21,"brokenImg":2,"samples":[{"textLen":252,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"},{"textLen":250,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"}]},"api":[{"url":"/api/board/columns","status":200,"len":560},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/channels/outbox","status":200,"len":14799},{"url":"/api/linkedin/drafts","status":200,"len":1385},{"url":"/api/constant-contact/campaigns","status":200,"len":11689},{"url":"/api/constant-contact/calendar","status":200,"len":15236}],"bad":["403 https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410630935506340_n.jpg?s"],"errors":["Failed to load resource: the server responded with a status of 403 ()"]}
+{"run":2,"id":"social","label":"Social Scheduler","clicked":true,"title":"Social Scheduler","cards":{"title":"Social Scheduler","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/social/status","status":200,"len":211},{"url":"/api/social/posts","status":200,"len":7372},{"url":"/api/social/best-times","status":200,"len":979}],"bad":[],"errors":[]}
+{"run":2,"id":"composer","label":"Composer","clicked":true,"title":"Composer","cards":{"title":"Composer","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/composer/targets","status":200,"len":364},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/assets/list","status":200,"len":2127}],"bad":[],"errors":[]}
+{"run":2,"id":"templates","label":"Campaign Templates","clicked":true,"title":"Campaign Templates","cards":{"title":"Campaign Templates","total":9,"blank":0,"withText":9,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/templates/meta","status":200,"len":686},{"url":"/api/templates/list","status":200,"len":13270}],"bad":[],"errors":[]}
+{"run":2,"id":"copy","label":"Suggested Copy","clicked":true,"title":"Suggested Copy","cards":{"title":"Suggested Copy","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/copy/presets","status":200,"len":859}],"bad":[],"errors":[]}
+{"run":2,"id":"calendar","label":"Marketing Calendar","clicked":true,"title":"Marketing Calendar","cards":{"title":"Marketing Calendar","total":3,"blank":0,"withText":3,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/calendar/dates?year=2026&month=07","status":200,"len":499},{"url":"/api/calendar/schedule?year=2026&month=07","status":200,"len":15}],"bad":[],"errors":[]}
+{"run":3,"id":"social","label":"Social Scheduler","clicked":true,"title":"Social Scheduler","cards":{"title":"Social Scheduler","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/social/status","status":200,"len":211},{"url":"/api/social/posts","status":200,"len":7372},{"url":"/api/social/best-times","status":200,"len":979}],"bad":[],"errors":[]}
+{"run":3,"id":"composer","label":"Composer","clicked":true,"title":"Composer","cards":{"title":"Composer","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/composer/targets","status":200,"len":364},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/assets/list","status":200,"len":2127}],"bad":[],"errors":[]}
+{"run":3,"id":"calendar","label":"Marketing Calendar","clicked":true,"title":"Marketing Calendar","cards":{"title":"Marketing Calendar","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[],"bad":[],"errors":[]}
+{"run":3,"id":"board","label":"Streams Board","clicked":true,"title":"Streams Board","cards":{"title":"Streams Board","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[],"bad":[],"errors":[]}
+{"run":3,"id":"copy","label":"Suggested Copy","clicked":true,"title":"Suggested Copy","cards":{"title":"Suggested Copy","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[],"bad":[],"errors":[]}
+{"run":3,"id":"engine","label":"Engine","clicked":true,"title":"Engine","cards":{"title":"Engine","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[],"bad":[],"errors":[]}
+{"run":3,"id":"templates","label":"Campaign Templates","clicked":true,"title":"Campaign Templates","cards":{"title":"Campaign Templates","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[],"bad":[],"errors":[]}
+{"run":4,"id":"board","label":"Streams Board","clicked":true,"title":"Streams Board","cards":{"title":"Streams Board","total":69,"blank":0,"withText":69,"withImg":21,"brokenImg":2,"samples":[{"textLen":252,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"},{"textLen":250,"imgs":1,"loaded":0,"broken":1,"brokenSrc":"https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410","cls":"bd-card"}]},"api":[{"url":"/api/board/columns","status":200,"len":560},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/channels/outbox","status":200,"len":14799},{"url":"/api/linkedin/drafts","status":200,"len":1385},{"url":"/api/constant-contact/campaigns","status":200,"len":11689},{"url":"/api/constant-contact/calendar","status":200,"len":15236}],"bad":["403 https://scontent-lax3-1.cdninstagram.com/v/t51.71878-15/750643870_2003452686960179_1239410630935506340_n.jpg?s"],"errors":["Failed to load resource: the server responded with a status of 403 ()"]}
+{"run":4,"id":"social","label":"Social Scheduler","clicked":true,"title":"Social Scheduler","cards":{"title":"Social Scheduler","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/social/status","status":200,"len":211},{"url":"/api/social/posts","status":200,"len":7372},{"url":"/api/social/best-times","status":200,"len":979}],"bad":[],"errors":[]}
+{"run":4,"id":"engine","label":"Engine","clicked":true,"title":"Engine","cards":{"title":"Engine","total":6,"blank":0,"withText":6,"withImg":2,"brokenImg":0,"samples":[]},"api":[{"url":"/api/engine/status","status":200,"len":162},{"url":"/api/engine/today?generate=0","status":200,"len":24579},{"url":"/api/engine/queue?days=7","status":200,"len":49067}],"bad":[],"errors":[]}
+{"run":4,"id":"composer","label":"Composer","clicked":true,"title":"Composer","cards":{"title":"Composer","total":0,"blank":0,"withText":0,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/composer/targets","status":200,"len":364},{"url":"/api/channels/status","status":200,"len":5799},{"url":"/api/assets/list","status":200,"len":2127}],"bad":[],"errors":[]}
+{"run":4,"id":"templates","label":"Campaign Templates","clicked":true,"title":"Campaign Templates","cards":{"title":"Campaign Templates","total":9,"blank":0,"withText":9,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/templates/meta","status":200,"len":686},{"url":"/api/templates/list","status":200,"len":13270}],"bad":[],"errors":[]}
+{"run":4,"id":"copy","label":"Suggested Copy","clicked":true,"title":"Suggested Copy","cards":{"title":"Suggested Copy","total":2,"blank":0,"withText":2,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/copy/presets","status":200,"len":859}],"bad":[],"errors":[]}
+{"run":4,"id":"calendar","label":"Marketing Calendar","clicked":true,"title":"Marketing Calendar","cards":{"title":"Marketing Calendar","total":3,"blank":0,"withText":3,"withImg":0,"brokenImg":0,"samples":[]},"api":[{"url":"/api/calendar/dates?year=2026&month=07","status":200,"len":499},{"url":"/api/calendar/schedule?year=2026&month=07","status":200,"len":15}],"bad":[],"errors":[]}
diff --git a/screenrecord/rec/run0/page@2c01d7569b49f94de71e05a9041138ba.webm b/screenrecord/rec/run0/page@2c01d7569b49f94de71e05a9041138ba.webm
new file mode 100644
index 0000000..6cd29e9
Binary files /dev/null and b/screenrecord/rec/run0/page@2c01d7569b49f94de71e05a9041138ba.webm differ
diff --git a/screenrecord/rec/run0/page@dc4fb41967627d02042518ad55804ad4.webm b/screenrecord/rec/run0/page@dc4fb41967627d02042518ad55804ad4.webm
new file mode 100644
index 0000000..9e9d765
Binary files /dev/null and b/screenrecord/rec/run0/page@dc4fb41967627d02042518ad55804ad4.webm differ
diff --git a/screenrecord/rec/run1/page@69c3c7a42f13d89b8093ee3cbdfb4684.webm b/screenrecord/rec/run1/page@69c3c7a42f13d89b8093ee3cbdfb4684.webm
new file mode 100644
index 0000000..ee6e2bf
Binary files /dev/null and b/screenrecord/rec/run1/page@69c3c7a42f13d89b8093ee3cbdfb4684.webm differ
diff --git a/screenrecord/rec/run1/page@8c820e74949b209ee3fb93803ee5fd1b.webm b/screenrecord/rec/run1/page@8c820e74949b209ee3fb93803ee5fd1b.webm
new file mode 100644
index 0000000..b360d4f
Binary files /dev/null and b/screenrecord/rec/run1/page@8c820e74949b209ee3fb93803ee5fd1b.webm differ
diff --git a/screenrecord/rec/run2/page@1616cf421912feb6679fb38f1cd1aeaf.webm b/screenrecord/rec/run2/page@1616cf421912feb6679fb38f1cd1aeaf.webm
new file mode 100644
index 0000000..b4092c9
Binary files /dev/null and b/screenrecord/rec/run2/page@1616cf421912feb6679fb38f1cd1aeaf.webm differ
diff --git a/screenrecord/rec/run2/page@355fdee8652b5a116b4b1321da3205f3.webm b/screenrecord/rec/run2/page@355fdee8652b5a116b4b1321da3205f3.webm
new file mode 100644
index 0000000..05830ab
Binary files /dev/null and b/screenrecord/rec/run2/page@355fdee8652b5a116b4b1321da3205f3.webm differ
diff --git a/screenrecord/rec/run3/page@0e731e2683305ede1ccab89a5b156259.webm b/screenrecord/rec/run3/page@0e731e2683305ede1ccab89a5b156259.webm
new file mode 100644
index 0000000..93a7fe1
Binary files /dev/null and b/screenrecord/rec/run3/page@0e731e2683305ede1ccab89a5b156259.webm differ
diff --git a/screenrecord/rec/run3/page@c60d9473e49baf6ebfdac95e693ec000.webm b/screenrecord/rec/run3/page@c60d9473e49baf6ebfdac95e693ec000.webm
new file mode 100644
index 0000000..de90d3f
Binary files /dev/null and b/screenrecord/rec/run3/page@c60d9473e49baf6ebfdac95e693ec000.webm differ
diff --git a/screenrecord/rec/run4/page@07f63bbb75a5b265add060fc2fa13ead.webm b/screenrecord/rec/run4/page@07f63bbb75a5b265add060fc2fa13ead.webm
new file mode 100644
index 0000000..e2bb43e
Binary files /dev/null and b/screenrecord/rec/run4/page@07f63bbb75a5b265add060fc2fa13ead.webm differ
diff --git a/screenrecord/rec/run4/page@9b7d861794ba0b63f83535678a98fd0f.webm b/screenrecord/rec/run4/page@9b7d861794ba0b63f83535678a98fd0f.webm
new file mode 100644
index 0000000..63dfac3
Binary files /dev/null and b/screenrecord/rec/run4/page@9b7d861794ba0b63f83535678a98fd0f.webm differ
diff --git a/scripts/nav-map.js b/scripts/nav-map.js
new file mode 100644
index 0000000..31e4b28
--- /dev/null
+++ b/scripts/nav-map.js
@@ -0,0 +1,22 @@
+const { chromium } = require('playwright');
+const BASE = 'https://marketing.designerwallcoverings.com';
+(async () => {
+ const browser = await chromium.launch();
+ const ctx = await browser.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(2000);
+ const nav = await page.evaluate(() => {
+ // find nav-ish containers
+ const items = [];
+ const cand = document.querySelectorAll('nav a, nav button, [class*="nav"] a, [class*="nav"] button, [class*="tab"], [data-panel], aside a, aside button');
+ cand.forEach(el => {
+ const label = (el.innerText || el.getAttribute('aria-label') || '').trim().slice(0, 30);
+ if (!label) return;
+ items.push({ tag: el.tagName, label, dataPanel: el.getAttribute('data-panel'), href: el.getAttribute('href'), cls: (el.className || '').slice(0, 40) });
+ });
+ return items;
+ });
+ console.log(JSON.stringify(nav, null, 1));
+ await ctx.close(); await browser.close();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/probe-403-and-posts.js b/scripts/probe-403-and-posts.js
new file mode 100644
index 0000000..60d30ee
--- /dev/null
+++ b/scripts/probe-403-and-posts.js
@@ -0,0 +1,35 @@
+const { chromium } = require('playwright');
+const BASE = 'https://marketing.designerwallcoverings.com';
+(async () => {
+ const browser = await chromium.launch();
+ const ctx = await browser.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()); });
+ const perr = [];
+ page.on('pageerror', e => perr.push('' + e));
+ await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(2000);
+
+ for (const panel of ['engine', 'social']) {
+ for (const sel of [`[data-panel="${panel}"]`, `a[href="#${panel}"]`, `a[href*="${panel}"]`]) {
+ try { const el = page.locator(sel).first(); if (await el.count()) { await el.click({ timeout: 3000 }); break; } } catch {}
+ }
+ await page.waitForTimeout(2500);
+ const info = await page.evaluate(() => {
+ const main = document.querySelector('main, .panel, #app') || document.body;
+ // find any "Failed"/error banner
+ const banners = [...document.querySelectorAll('*')].filter(e => /^(failed|error|no posts|nothing|couldn|could not)/i.test((e.childNodes.length && e.textContent || '').trim().slice(0, 40)) && e.offsetHeight > 0 && e.children.length <= 2).slice(0, 5).map(e => e.tagName + '.' + (e.className || '') + ' :: ' + e.textContent.trim().slice(0, 120));
+ return { title: (document.querySelector('h1,h2,.panel-title')?.innerText || '').slice(0, 60), banners, first600: main.innerText.trim().slice(0, 600) };
+ });
+ console.log('\n===== PANEL: ' + panel + ' =====');
+ console.log('title:', info.title);
+ console.log('banners:', JSON.stringify(info.banners, null, 1));
+ console.log('first600:\n' + info.first600);
+ }
+ console.log('\n===== FAILED RESPONSES (>=400) =====');
+ [...new Set(bad)].forEach(b => console.log(b));
+ console.log('\n===== PAGE ERRORS =====');
+ [...new Set(perr)].forEach(e => console.log(e));
+ await ctx.close(); await browser.close();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/repro-race.js b/scripts/repro-race.js
new file mode 100644
index 0000000..5d3c4aa
--- /dev/null
+++ b/scripts/repro-race.js
@@ -0,0 +1,36 @@
+// Deterministically trip the route() race: click panels rapidly (no settle).
+const { chromium } = require('playwright');
+const BASE = 'https://marketing.designerwallcoverings.com';
+const LABELS = ['Streams Board', 'Social Scheduler', 'Engine', 'Campaign Templates', 'Marketing Calendar', 'Suggested Copy'];
+(async () => {
+ const browser = await chromium.launch();
+ const ctx = await browser.newContext({ viewport: { width: 1600, height: 900 }, httpCredentials: { username: 'admin', password: 'DW2024!' } });
+ const page = await ctx.newPage();
+ const errs = [];
+ page.on('pageerror', e => errs.push('' + e));
+ page.on('console', m => m.type() === 'error' && errs.push(m.text().slice(0, 160)));
+ await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(2500);
+
+ // RAPID: click each label with only 120ms between — well under fetch latency → interleave.
+ for (const label of LABELS) {
+ try { const el = page.locator('a', { hasText: new RegExp(label, 'i') }).first(); if (await el.count()) await el.click({ timeout: 3000 }); } catch {}
+ await page.waitForTimeout(120);
+ }
+ // Now land on Streams Board and give it plenty of time to settle.
+ try { await page.locator('a', { hasText: /Streams Board/i }).first().click(); } catch {}
+ await page.waitForTimeout(4000);
+ const res = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.bd-card, [class*="card"], article');
+ const title = (document.querySelector('#paneltitle')?.innerText || '').trim();
+ const loadingStuck = /loading/i.test((document.querySelector('#panel')?.innerText || '').slice(0, 40));
+ return { title, cards: cards.length, loadingStuck, panelText: (document.querySelector('#panel')?.innerText || '').trim().slice(0, 120) };
+ });
+ console.log('AFTER RAPID NAV → land on Streams Board:');
+ console.log(' title :', res.title);
+ console.log(' cards :', res.cards, res.cards === 0 ? ' <-- BLANK (race tripped)' : ' (rendered)');
+ console.log(' loadingStuck:', res.loadingStuck);
+ console.log(' panelText :', JSON.stringify(res.panelText));
+ console.log(' errors :', JSON.stringify([...new Set(errs)]));
+ await ctx.close(); await browser.close();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/repro-run3.js b/scripts/repro-run3.js
new file mode 100644
index 0000000..8c83af9
--- /dev/null
+++ b/scripts/repro-run3.js
@@ -0,0 +1,29 @@
+// 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 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 () => {
+ for (const delay of [2800, 600, 250]) {
+ const browser = await chromium.launch();
+ const ctx = await browser.newContext({ viewport:{width:1600,height:900}, httpCredentials:{username:'admin',password:'DW2024!'} });
+ const page = await ctx.newPage();
+ let apiCount=0; page.on('request',r=>{ if(/\/api\//.test(r.url())) apiCount++; });
+ await page.goto(BASE+'/',{waitUntil:'domcontentloaded'}); await page.waitForTimeout(2500);
+ console.log(`\n===== inter-click delay = ${delay}ms =====`);
+ let blankLands=0;
+ for (const [label,id] of SEQ) {
+ apiCount=0;
+ try{const el=page.locator('a',{hasText:new RegExp(label,'i')}).first(); if(await el.count()) await el.click({timeout:3000});}catch{}
+ await page.waitForTimeout(delay);
+ const c = await cardCount(page);
+ const composerExpectsZero = id==='composer';
+ const blank = c.n===0 && !composerExpectsZero;
+ if (blank) blankLands++;
+ console.log(` ${id.padEnd(9)} land: title="${c.title}" cards=${c.n} apiDuringSettle=${apiCount}${blank?' <-- BLANK ON LAND':''}${c.loading?' [stuck Loading]':''}`);
+ }
+ console.log(` => blank-on-land panels: ${blankLands}/${SEQ.length-1}`);
+ await ctx.close(); await browser.close();
+ }
+})().catch(e=>{console.error('FATAL',e);process.exit(1);});
diff --git a/scripts/screenrecord-posts.js b/scripts/screenrecord-posts.js
new file mode 100644
index 0000000..70b14af
--- /dev/null
+++ b/scripts/screenrecord-posts.js
@@ -0,0 +1,92 @@
+// /screenrecord harness v2 — nav by REAL label text (nav <a>s have no href/data-panel).
+const { chromium } = require('playwright');
+const fs = require('fs');
+const path = require('path');
+
+const BASE = 'https://marketing.designerwallcoverings.com';
+const CREDS = { username: 'admin', password: 'DW2024!' };
+const ROOT = path.join(__dirname, '..', 'screenrecord');
+const LOG = path.join(ROOT, 'debug-log.jsonl');
+fs.mkdirSync(path.join(ROOT, 'rec'), { recursive: true });
+const append = o => fs.appendFileSync(LOG, JSON.stringify(o) + '\n');
+
+// label text (regex-safe fragment) → short id
+const PANELS = [
+ ['Streams Board', 'board'],
+ ['Social Scheduler', 'social'],
+ ['Engine', 'engine'],
+ ['Composer', 'composer'],
+ ['Campaign Templates', 'templates'],
+ ['Suggested Copy', 'copy'],
+ ['Marketing Calendar', 'calendar'],
+];
+const prior = fs.existsSync(LOG) ? fs.readFileSync(LOG, 'utf8').trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean) : [];
+const priorBad = new Set(prior.filter(r => r.errors && r.errors.length).map(r => r.id));
+function orderFor(run, els) {
+ if (run === 0) return els;
+ if (run === 1) return [...els].reverse();
+ if (run === 2) return [...els].sort((a, b) => (b[1] === 'engine') - (a[1] === 'engine'));
+ if (run === 3) { const s = [...els]; for (let i = s.length - 1; i > 0; i--) { const j = (i * 7 + run * 13) % (i + 1); [s[i], s[j]] = [s[j], s[i]]; } return s; }
+ return [...els].sort((a, b) => priorBad.has(b[1]) - priorBad.has(a[1]));
+}
+
+async function inspectCards(page) {
+ return await page.evaluate(() => {
+ const sels = ['.bd-card', '.card', '[class*="card"]', '.post', '[class*="post"]', '.engine-card', '.sched-card', 'article'];
+ const seen = new Set(); const cards = [];
+ for (const s of sels) for (const el of document.querySelectorAll(s)) {
+ if (seen.has(el)) continue; seen.add(el);
+ const r = el.getBoundingClientRect();
+ if (r.width < 40 || r.height < 20) continue;
+ cards.push(el);
+ }
+ let blank = 0, withText = 0, withImg = 0, brokenImg = 0;
+ const samples = [];
+ for (const el of cards) {
+ const txt = (el.innerText || '').trim();
+ const imgs = [...el.querySelectorAll('img')];
+ const loaded = imgs.filter(i => i.complete && i.naturalWidth > 0);
+ const broken = imgs.filter(i => i.complete && i.naturalWidth === 0 && i.getAttribute('src'));
+ if (txt.length < 2 && loaded.length === 0) blank++;
+ if (txt.length >= 2) withText++;
+ if (loaded.length) withImg++;
+ brokenImg += broken.length;
+ if (samples.length < 3 && (txt.length < 2 || broken.length)) samples.push({ textLen: txt.length, imgs: imgs.length, loaded: loaded.length, broken: broken.length, brokenSrc: (broken[0] && broken[0].getAttribute('src') || '').slice(0, 90), cls: el.className.slice(0, 40) });
+ }
+ const title = (document.querySelector('.panel-title, h1, h2')?.innerText || '').trim().slice(0, 50);
+ return { title, total: cards.length, blank, withText, withImg, brokenImg, samples };
+ });
+}
+
+(async () => {
+ for (let run = 0; run < 5; run++) {
+ const browser = await chromium.launch();
+ const ctx = await browser.newContext({ viewport: { width: 1600, height: 900 }, httpCredentials: CREDS, recordVideo: { dir: path.join(ROOT, 'rec', `run${run}`) } });
+ const page = await ctx.newPage();
+ const errs = [], api = [], bad = [];
+ page.on('console', m => { if (m.type() === 'error') errs.push(m.text().slice(0, 200)); });
+ page.on('pageerror', e => errs.push(('PAGEERROR ' + e).slice(0, 200)));
+ page.on('response', async r => {
+ const u = r.url();
+ if (r.status() >= 400) bad.push(r.status() + ' ' + u.replace(BASE, '').slice(0, 110));
+ if (/\/api\//.test(u)) { let len = -1; try { len = (await r.body()).length; } catch {} api.push({ url: u.replace(BASE, ''), status: r.status(), len }); }
+ });
+ await page.goto(BASE + '/', { waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(2500);
+
+ for (const [label, id] of orderFor(run, PANELS)) {
+ errs.length = 0; api.length = 0; bad.length = 0;
+ let clicked = false;
+ try {
+ const el = page.locator('a', { hasText: new RegExp(label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i') }).first();
+ if (await el.count()) { await el.click({ timeout: 4000 }); clicked = true; }
+ } catch {}
+ await page.waitForTimeout(2800);
+ let cards = null; try { cards = await inspectCards(page); } catch (e) { cards = { err: '' + e }; }
+ append({ run, id, label, clicked, title: cards.title, cards, api: api.slice(0, 10), bad: [...new Set(bad)].slice(0, 8), errors: [...new Set(errs)].slice(0, 6) });
+ console.log(`run${run} ${id.padEnd(9)} title="${cards.title}" clicked=${clicked} cards=${cards.total} blank=${cards.blank} text=${cards.withText} img=${cards.withImg} broken=${cards.brokenImg} 4xx=${[...new Set(bad)].length} err=${[...new Set(errs)].length}`);
+ }
+ await ctx.close(); await browser.close();
+ }
+ console.log('DONE →', LOG);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/verify-proxy.js b/scripts/verify-proxy.js
new file mode 100644
index 0000000..bb24440
--- /dev/null
+++ b/scripts/verify-proxy.js
@@ -0,0 +1,25 @@
+// Verification harness: serve LOCAL (fixed) /app.js, proxy everything else to the
+// live MCC with Basic auth injected. Lets us test the one changed file vs real data.
+const http = require('http');
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+const UP = 'marketing.designerwallcoverings.com';
+const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
+const APPJS = fs.readFileSync(path.join(__dirname, '..', 'public', 'app.js'), 'utf8');
+
+const server = http.createServer((req, res) => {
+ if (req.url === '/app.js') {
+ res.writeHead(200, { 'Content-Type': 'application/javascript' });
+ return res.end(APPJS);
+ }
+ const opts = { host: UP, path: req.url, method: req.method, headers: { ...req.headers, host: UP, authorization: AUTH } };
+ const up = https.request(opts, r => {
+ // strip upstream Set-Cookie 'Secure' so it works over local http (not needed but tidy)
+ res.writeHead(r.statusCode, r.headers);
+ r.pipe(res);
+ });
+ up.on('error', e => { res.writeHead(502); res.end('proxy err ' + e.message); });
+ req.pipe(up);
+});
+server.listen(0, () => console.log('PROXY_PORT=' + server.address().port));
← 355b730 auto-save: 2026-07-22T08:42:49 (1 files) — package-lock.json
·
back to Marketing Command Center
·
auto-save: 2026-07-22T10:13:11 (5 files) — scripts/repro-run 5a92b90 →