[object Object]

← back to Trending Dw

trending: fix contrarian-caught issues (onerror injection, header overflow, cron honesty)

955e9e405aeb4a39b01fc3195ba8c1d5dbb53a78 · 2026-07-06 21:18:40 -0700 · Steve Abrams

- CRIT: image-404 fallback was silently broken — the pending block was injected via an
  inline onerror="..." string, but pending's own class="..." double-quotes terminated
  the attribute early (only single quotes were escaped). Moved the handler to a JS closure
  over `pending` (addEventListener) — zero escaping, unbreakable. Verified: forcing a
  404 now renders the fallback.
- header.top: added flex-wrap + gap row + tag min-width:0 — the nowrap freshness badge
  caused horizontal scroll on <~1100px viewports (sticky header made it persist). Verified
  no h-scroll at 1000px.
- tick.sh: idempotency guard scoped to -maxdepth 1 (active queue only) so a resolved-but-
  unfixed refresh memo RE-SURFACES instead of going permanently silent; refresh memo now
  HONESTLY states refresh.sh re-bakes identical seed data and the real fix is building the
  live-Exa wiring (code-only/$0) + running the metered pull (the gated spend).

Files touched

Diff

commit 955e9e405aeb4a39b01fc3195ba8c1d5dbb53a78
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jul 6 21:18:40 2026 -0700

    trending: fix contrarian-caught issues (onerror injection, header overflow, cron honesty)
    
    - CRIT: image-404 fallback was silently broken — the pending block was injected via an
      inline onerror="..." string, but pending's own class="..." double-quotes terminated
      the attribute early (only single quotes were escaped). Moved the handler to a JS closure
      over `pending` (addEventListener) — zero escaping, unbreakable. Verified: forcing a
      404 now renders the fallback.
    - header.top: added flex-wrap + gap row + tag min-width:0 — the nowrap freshness badge
      caused horizontal scroll on <~1100px viewports (sticky header made it persist). Verified
      no h-scroll at 1000px.
    - tick.sh: idempotency guard scoped to -maxdepth 1 (active queue only) so a resolved-but-
      unfixed refresh memo RE-SURFACES instead of going permanently silent; refresh memo now
      HONESTLY states refresh.sh re-bakes identical seed data and the real fix is building the
      live-Exa wiring (code-only/$0) + running the metered pull (the gated spend).
---
 public/index.html |  9 +++++++--
 scripts/tick.sh   | 22 ++++++++++++++--------
 2 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/public/index.html b/public/index.html
index caa309e..0c9ae1e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -9,7 +9,8 @@
   :root{ --cols:4; --ink:#1c1a17; --paper:#faf8f3; --line:#e4ded2; --accent:#2f5d3a; --gap:#b6392f; --muted:#8a8272; --chip:#f0ebe0; }
   *{ box-sizing:border-box; }
   body{ margin:0; font-family:"Helvetica Neue",Arial,sans-serif; color:var(--ink); background:var(--paper); -webkit-font-smoothing:antialiased; }
-  header.top{ display:flex; align-items:baseline; gap:18px; padding:18px 26px 14px; border-bottom:1px solid var(--line); position:sticky; top:0; background:var(--paper); z-index:20; }
+  header.top{ display:flex; align-items:baseline; flex-wrap:wrap; gap:8px 18px; padding:18px 26px 14px; border-bottom:1px solid var(--line); position:sticky; top:0; background:var(--paper); z-index:20; }
+  header.top .tag{ min-width:0; }
   header.top .wm{ font-weight:800; letter-spacing:.14em; text-transform:uppercase; font-size:15px; }
   header.top .wm small{ display:block; font-weight:400; letter-spacing:.22em; font-size:9px; color:var(--muted); margin-top:3px; }
   header.top .tag{ font-size:12px; color:var(--muted); font-style:italic; }
@@ -182,7 +183,7 @@ function card(it){
   const c=document.createElement('div'); c.className='card';
   const grad='linear-gradient(135deg,'+it.dominantHex+' 0%,'+shade(it.dominantHex,-18)+' 100%)';
   const pending='<div class="pending"><div class="ico">🎨</div><div class="txt">Our-lane image pending</div><div class="sub">'+(it.ourCoverage==='gap'?'GAP — awaiting generation':'match in progress')+'</div></div>';
-  c.innerHTML='<div class="swatch" style="position:relative;overflow:hidden;background:'+grad+'">'+(it.image?'<img src="'+esc(it.image)+'" alt="" loading="lazy" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover" onerror="this.closest(\'.swatch\').insertAdjacentHTML(\'beforeend\',\''+pending.replace(/'/g,"\\'")+'\');this.remove()">':pending)
+  c.innerHTML='<div class="swatch" style="position:relative;overflow:hidden;background:'+grad+'">'+(it.image?'<img src="'+esc(it.image)+'" alt="" loading="lazy" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover">':pending)
     +(it.isNew?'<div class="ribbon">NEW</div>':'')
     +'<div class="cov '+it.ourCoverage+'">'+(it.ourCoverage==='gap'?'🎯 GAP':'✓ HAVE')+'</div>'
     +(it.signalRank?'<div class="rank">▲ '+it.signalRank+'</div>':'')
@@ -197,6 +198,10 @@ function card(it){
     +'</div>';
   c.style.cursor="pointer"; c.title="Match against our catalog"; c.addEventListener("click",function(e){ if(!e.target.closest("a")&&!e.target.closest(".shopbtn")) openMatchup(it); });
   var sb=c.querySelector(".shopbtn"); if(sb){ sb.addEventListener("click",function(e){ e.stopPropagation(); putOnShopify(sb); }); }
+  // image-404 fallback attached in JS (closure over `pending`) — no inline-onerror string
+  // escaping, so the double-quoted class= attrs inside `pending` can't truncate the handler.
+  var im=c.querySelector(".swatch img");
+  if(im){ im.addEventListener("error",function(){ var sw=this.closest(".swatch"); if(sw&&!sw.querySelector(".pending")) sw.insertAdjacentHTML("beforeend",pending); this.remove(); }); }
   return c;
 }
 async function fetchPage(){
diff --git a/scripts/tick.sh b/scripts/tick.sh
index 61d52e8..6fb3f79 100755
--- a/scripts/tick.sh
+++ b/scripts/tick.sh
@@ -49,8 +49,10 @@ DRAFTED=""
 # ── 4a. GATED: metered live-refresh request when stale beyond threshold (idempotent per data vintage) ──
 if [ "$STALE_DAYS" -ge "$STALE_THRESHOLD" ] 2>/dev/null; then
   SLUG="trending-dw-refresh-${AS_OF}.md"
-  # skip if a memo for THIS data vintage already exists anywhere in the queue (pending or resolved)
-  if [ -z "$(find "$PENDING" -name "$SLUG" 2>/dev/null | head -1)" ]; then
+  # Only suppress while the memo is in the ACTIVE queue (maxdepth 1). If it was resolved/archived
+  # but the vintage is STILL $AS_OF (the "fix" didn't move the data), re-surface it — an unfixed
+  # problem must keep nagging, never go permanently silent.
+  if [ -z "$(find "$PENDING" -maxdepth 1 -name "$SLUG" 2>/dev/null | head -1)" ]; then
     cat > "$PENDING/$SLUG" <<EOF
 # GATED APPROVAL — trending-dw metered trend refresh
 
@@ -58,16 +60,20 @@ if [ "$STALE_DAYS" -ge "$STALE_THRESHOLD" ] 2>/dev/null; then
 **Officer of record:** vp-dw-commerce
 **Why:** the trending board's data is **$STALE_DAYS days old** (as of $AS_OF), past the ${STALE_THRESHOLD}-day cadence. A *trending* board is only as credible as its freshness.
 
+## HONEST NOTE (read before approving)
+\`scripts/refresh.sh\` today just re-seeds the **committed** \`seed.js\` (spottedAt hardcoded to $AS_OF) — running it as-is will NOT move the data or clear this memo. The real fix is a two-parter:
+1. **Build** the live wiring: \`refresh.sh\` → call the metered \`design-trend-scout\` (Exa) agent → write fresh items with today's spottedAt. (code-only, reversible, \$0 to build)
+2. **Run** it (metered — the gated spend).
+
 ## The ask
-Approve a metered **design-trend-scout (Exa)** refresh to re-pull current bestsellers, then re-attach our-own images and stage a deploy.
+Approve step 2 (the metered Exa pull) — and, if not yet built, approve step 1 first.
 
-- **Cost:** metered Exa research pull — estimate a few \$ per run (show + log actual to cost-tracker before running). This is the ONLY spend; the tick itself is \$0.
-- **Command (run after approval):** \`bash ~/Projects/trending-dw/scripts/refresh.sh\` once wired to live Exa (currently re-seeds committed data — the live wiring is the gated step).
+- **Cost:** metered Exa research pull — estimate a few \$ per run (show + log actual to cost-tracker before running). The tick + the wiring build are \$0; only the pull spends.
 
 ## APPROVE / REVISE / BLOCK
-- [ ] APPROVE — run the metered refresh + draft the deploy
+- [ ] APPROVE — build the live-Exa wiring (if needed) + run the metered pull + draft the deploy
 - [ ] REVISE — change cadence/threshold (currently ${STALE_THRESHOLD}d)
-- [ ] BLOCK — leave the board at its last approved vintage
+- [ ] BLOCK — leave the board at its last approved vintage (badge stays honest-stale)
 EOF
     DRAFTED="${DRAFTED}refresh:$SLUG "
   fi
@@ -77,7 +83,7 @@ fi
 LAST_COMFY="$(cat "$STATE/comfy" 2>/dev/null || echo 0)"
 if [ "$COMFY" = "1" ] && [ "$LAST_COMFY" != "1" ] && [ "${IMG_GAP:-0}" -gt 0 ] 2>/dev/null; then
   SLUG="trending-dw-imagegap-${DAY}.md"
-  if [ -z "$(find "$PENDING" -name "$SLUG" 2>/dev/null | head -1)" ]; then
+  if [ -z "$(find "$PENDING" -maxdepth 1 -name "$SLUG" 2>/dev/null | head -1)" ]; then
     cat > "$PENDING/$SLUG" <<EOF
 # OPPORTUNITY — trending-dw image-gap fill (ComfyUI is back)
 

← 4fe938f trending: 10-min health+opportunity tick + launchd cron (DTD  ·  back to Trending Dw  ·  trending: build live-refresh wiring — scout→bestsellers mapp 6ab84ef →