← back to Designerwallcoverings
scripts/broken-image-scan/broken-image-scan.mjs
43 lines
// broken-image-scan (c44) — READ-ONLY. Prior cycles checked image PRESENCE; none
// checked whether the image actually LOADS. A product whose CDN image 404s shows a
// blank/broken PDP to a shopper. Samples N random ACTIVE handles, reads live
// /products/<handle>.json, HEADs the primary image_url; retry/backoff so throttling
// (429/timeout) isn't miscounted as broken. Only a definitive non-2xx after retries
// = BROKEN. $0.
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
const N=parseInt(process.argv[2]||'120',10);
const PSQL='/opt/homebrew/opt/postgresql@14/bin/psql', DB='postgresql:///dw_unified?host=/tmp';
const OUT=`${process.env.HOME}/.claude/yolo-queue/broken-image-scan-2026-06-16.json`;
const MD =`${process.env.HOME}/.claude/yolo-queue/broken-image-scan-2026-06-16.md`;
const rows=execFileSync(PSQL,[DB,'-At','-F','\t','-c',
`select handle, vendor from shopify_products where status='ACTIVE' and handle is not null and handle<>'' order by random() limit ${N}`],{encoding:'utf8'}).trim().split('\n').filter(Boolean).map(l=>l.split('\t'));
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function head(url){ for(let a=0;a<4;a++){ try{ const r=await fetch(url,{method:'HEAD',signal:AbortSignal.timeout(12000)}); if(r.status===429){await sleep(1500*(a+1));continue;} return r.status;}catch(e){ if(a===3) return 0; await sleep(1200*(a+1)); } } return 0; }
async function getj(h){ for(let a=0;a<3;a++){ try{ const r=await fetch(`https://www.designerwallcoverings.com/products/${encodeURIComponent(h)}.json`,{signal:AbortSignal.timeout(12000)}); if(r.status===200) return await r.json(); if(r.status===404) return {__404:true}; if(r.status===429){await sleep(1500*(a+1));continue;} }catch(e){ await sleep(1000*(a+1)); } } return null; }
const res={OK:0, BROKEN:0, NO_IMAGE:0, PDP_404:0, UNKNOWN:0}; const broken=[]; let decided=0;
for(const [h,v] of rows){
const j=await getj(h);
if(j===null){res.UNKNOWN++; await sleep(120); continue;}
if(j.__404){res.PDP_404++; await sleep(120); continue;}
const img=j.product && (j.product.image && j.product.image.src) || (j.product && (j.product.images||[])[0] && j.product.images[0].src);
if(!img){res.NO_IMAGE++; decided++; await sleep(120); continue;}
const code=await head(img);
decided++;
if(code>=200&&code<400) res.OK++;
else if(code===0) res.UNKNOWN++; // network indeterminate, don't call broken
else { res.BROKEN++; broken.push({handle:h, vendor:v, img, http:code}); }
await sleep(120);
}
const report={generated_at:new Date().toISOString(), sampled:rows.length, decided, ...res, broken_pct: decided?Math.round(1000*res.BROKEN/decided)/10:0, broken};
fs.writeFileSync(OUT,JSON.stringify(report,null,2));
let md=`# Broken product-image scan (live) — ${new Date().toISOString().slice(0,16)}\n\n`;
md+=`**READ-ONLY (live products.json + CDN HEAD, retry/backoff), \$0.** Sampled ${rows.length} ACTIVE handles. Checks whether the PRIMARY image actually LOADS (presence != loadability).\n\n`;
md+=`| outcome | count |\n|---|--:|\n| image loads OK | ${res.OK} |\n| **BROKEN (img non-2xx)** | **${res.BROKEN}** |\n| product has NO image | ${res.NO_IMAGE} |\n| PDP 404 (mirror-ACTIVE, live-gone) | ${res.PDP_404} |\n| unknown (network) | ${res.UNKNOWN} |\n\n`;
md+=`Broken rate (of decided ${report.decided}): **${report.broken_pct}%**\n\n`;
if(broken.length){ md+=`## BROKEN images\n| handle | vendor | http | img |\n|---|---|--:|---|\n`; for(const b of broken) md+=`| ${b.handle} | ${b.vendor} | ${b.http} | ${b.img.slice(0,60)} |\n`; }
fs.writeFileSync(MD,md);
console.log(`[broken-image] sampled=${rows.length} decided=${report.decided} | OK=${res.OK} BROKEN=${res.BROKEN} NO_IMAGE=${res.NO_IMAGE} PDP404=${res.PDP_404} UNKNOWN=${res.UNKNOWN} | broken=${report.broken_pct}%`);
for(const b of broken.slice(0,8)) console.log(` ⚠️ ${b.handle} [${b.vendor}] http=${b.http}`);
console.log(`Report: ${MD}`);