← 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
M public/index.htmlM scripts/tick.sh
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 →