← back to Marketing Command Center

screenrecord/DEBUG-REPORT.md

73 lines

# 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