← back to Designer Wallcoverings
Add Tres Tintas live-PDP click-through QA + reverify; draft Rebel Walls re-vendor task
7227f97758b0a8ed0677d4c45fd37e0df10ab642 · 2026-06-23 15:49:57 -0700 · Steve
Click-through QA (tres-tintas-clickthrough.js) loads every live PDP and verifies
description/specs/image actually RENDER (not just exist in the API). Bulk runs
hit Cloudflare throttling (~⅓ of 592 requests), so tres-tintas-reverify.js
re-checks flagged SKUs one-at-a-time with retries+pacing to separate throttle
noise from real failures. True result: catalog renders correctly; 197 throttle
false-positives, 229 murals correctly without a repeat, ~15 genuinely broken =
all Rebel Walls products mis-vendored as Tres Tintas. Drafted the re-vendoring
fix to pending-approval (gated: bulk vendor change on live products).
Files touched
A pending-approval/tres-tintas-rebel-walls-misvendored-fix.mdA shopify/scripts/tres-tintas-clickthrough.jsA shopify/scripts/tres-tintas-reverify.js
Diff
commit 7227f97758b0a8ed0677d4c45fd37e0df10ab642
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 23 15:49:57 2026 -0700
Add Tres Tintas live-PDP click-through QA + reverify; draft Rebel Walls re-vendor task
Click-through QA (tres-tintas-clickthrough.js) loads every live PDP and verifies
description/specs/image actually RENDER (not just exist in the API). Bulk runs
hit Cloudflare throttling (~⅓ of 592 requests), so tres-tintas-reverify.js
re-checks flagged SKUs one-at-a-time with retries+pacing to separate throttle
noise from real failures. True result: catalog renders correctly; 197 throttle
false-positives, 229 murals correctly without a repeat, ~15 genuinely broken =
all Rebel Walls products mis-vendored as Tres Tintas. Drafted the re-vendoring
fix to pending-approval (gated: bulk vendor change on live products).
---
.../tres-tintas-rebel-walls-misvendored-fix.md | 46 +++++++++
shopify/scripts/tres-tintas-clickthrough.js | 103 +++++++++++++++++++++
shopify/scripts/tres-tintas-reverify.js | 73 +++++++++++++++
3 files changed, 222 insertions(+)
diff --git a/pending-approval/tres-tintas-rebel-walls-misvendored-fix.md b/pending-approval/tres-tintas-rebel-walls-misvendored-fix.md
new file mode 100644
index 00000000..f5c3c912
--- /dev/null
+++ b/pending-approval/tres-tintas-rebel-walls-misvendored-fix.md
@@ -0,0 +1,46 @@
+# Task: Re-vendor 15 mis-vendored "Rebel Walls" products (currently tagged vendor "Tres Tintas")
+
+**Surfaced by:** the Tres Tintas catalog completion + click-through QA (2026-06-23).
+**Owner:** vp-dw-commerce (DW catalog). **Status:** DRAFT — needs Steve's approval (bulk vendor change on live products = gated).
+
+## What's wrong
+
+15 products have their Shopify **vendor field set to `Tres Tintas`** but are actually **Rebel Walls** products — their handles are `dwrw-722xx` (DW-Rebel-Walls prefix) and their titles read `… | Rebel Walls`. They surfaced in every `vendor:'Tres Tintas'` query and so couldn't be completed from the Tres Tintas vendor (they don't exist on trestintas.com — verified). They have no `pattern-ref` tag, missing width/description rendering on the live PDP, and 11 return PAGE_LOAD_FAIL (likely unpublished/404 or broken handles).
+
+## The 15 products
+
+| Shopify ID | Handle | Title | Click-through fail |
+|---|---|---|---|
+| 7433014607923 | dwrw-72318 | Tres Tintas Pond \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015099443 | dwrw-72303 | Tres Tintas Mural Damasco \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015164979 | dwrw-72301 | Tres Tintas Mural Cochin \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015132211 | dwrw-72302 | Tres Tintas Mural Kathmandu \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015230515 | dwrw-72300 | Tres Tintas Mural Taraz \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015263283 | dwrw-72299 | Tres Tintas Honeymoon \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015296051 | dwrw-72298 | Tres Tintas Pond \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015328819 | dwrw-72294 | Tres Tintas Oceans \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015361587 | dwrw-72293 | Tres Tintas Oceans \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015427123 | dwrw-72292 | Tres Tintas Mayolica \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015492659 | dwrw-72289 | Tres Tintas Stamps \| Rebel Walls | PAGE_LOAD_FAIL |
+| 7433015525427 | dwrw-72288 | Tres Tintas Paris \| Rebel Walls | DESC+WIDTH not rendered |
+| 7433015623731 | dwrw-72286 | Tres Tintas Florence \| Rebel Walls | DESC+WIDTH not rendered |
+| 7433015656499 | dwrw-72284 | Tres Tintas Oceans \| Rebel Walls | DESC+WIDTH+REPEAT not rendered |
+| 7433015689267 | dwrw-72281 | Tres Tintas City \| Rebel Walls | DESC+WIDTH not rendered |
+
+Full data: `/tmp/rebel-walls-misvendored.json`.
+
+## Proposed fix (each step gated on Steve)
+
+1. **Confirm true vendor.** These look like the OLDER Rebel Walls cohort (DWRW-722xx, the 2022 Patina/Bellewood-era SKUs per memory `reference_rebel_walls_two_cohort_dedup_push`). Confirm they belong to Rebel Walls, not Tres Tintas.
+2. **Set `vendor = "Rebel Walls"`** on all 15 (Shopify product update). Removes them from the Tres Tintas catalog and folds them into Rebel Walls reporting/onboarding.
+3. **Clean the titles** — drop the stray `Tres Tintas` prefix: `Pond | Rebel Walls`, `Honeymoon | Rebel Walls`, etc.
+4. **Investigate the 11 PAGE_LOAD_FAIL** — determine if they're unpublished/archived (then leave as-is) or live-but-broken handles (then fix). Do NOT auto-republish.
+5. **Complete specs/description/room** via the Rebel Walls pipeline (rebelwalls.com), NOT Tres Tintas.
+
+## Why gated
+Bulk vendor reassignment + title edits on 15 live, active, customer-facing products. Reversible but customer-facing → Steve approves before execution.
+
+## Sign-off
+- [ ] **APPROVE** — execute steps 1-3 (re-vendor + title clean), draft 4-5 separately
+- [ ] **REVISE** — …
+- [ ] **BLOCK** — …
diff --git a/shopify/scripts/tres-tintas-clickthrough.js b/shopify/scripts/tres-tintas-clickthrough.js
new file mode 100644
index 00000000..87b17437
--- /dev/null
+++ b/shopify/scripts/tres-tintas-clickthrough.js
@@ -0,0 +1,103 @@
+#!/usr/bin/env node
+/**
+ * tres-tintas-clickthrough.js — live-PDP "click-through" QA on every SKU.
+ *
+ * For each Tres Tintas product it loads the ACTUAL live storefront product page
+ * (designerwallcoverings.com/products/<handle>) and verifies what a customer
+ * would see actually RENDERS — not just that the metafield exists in the API:
+ * - DESC: the product's description prose appears on the page (no old spec table)
+ * - SPEC: width + repeat values render (ROLL) / material + weight (MURAL)
+ * - IMG : >=1 product image element on the page
+ *
+ * Cross-references each page against the product's own API data so the checks
+ * are exact (looks for THIS product's width string in the page, etc.).
+ * Concurrency-limited. Prints a per-failure report.
+ */
+const fs = require('fs');
+const path = require('path');
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const SITE = 'https://www.designerwallcoverings.com';
+const API = '2024-01';
+const CONC = parseInt(process.env.CONC || '6', 10);
+const LIMIT = process.env.LIMIT ? parseInt(process.env.LIMIT, 10) : Infinity;
+function envval(k){const e=fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8');const m=e.match(new RegExp(`^${k}=["']?([^"'\\n]+)`,'m'));return m?m[1].trim():null;}
+const H = { 'X-Shopify-Access-Token': envval('SHOPIFY_DRAFT_TOKEN'), 'Content-Type': 'application/json' };
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120 Safari/537.36';
+const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
+async function fr(u,o,t=3){let e;for(let i=0;i<t;i++){try{return await fetch(u,{...o,signal:AbortSignal.timeout(45000)});}catch(x){e=x;await sleep(600*(i+1));}}throw e;}
+async function gql(q,v){const r=await fr(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:H,body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors)throw new Error(JSON.stringify(j.errors));return j.data;}
+
+function firstWords(html, n=6){ const t=(html||'').replace(/<[^>]+>/g,' ').replace(/&[a-z]+;/gi,' ').replace(/\s+/g,' ').trim(); return t.split(' ').slice(0,n).join(' '); }
+
+async function checkOne(p){
+ const id=p.id.split('/').pop();
+ const handle=p.handle;
+ const mf={}; p.metafields.edges.forEach(x=>mf[`${x.node.namespace}.${x.node.key}`]=x.node.value);
+ const imgCount=p.images.edges.length;
+ const isMural = !mf['custom.width'] && !!mf['custom.product_weight'];
+ const res={id,handle,title:p.title,fail:[]};
+
+ // load the live PDP
+ let html='';
+ try { html = await (await fr(`${SITE}/products/${handle}`,{headers:{'User-Agent':UA}})).text(); }
+ catch(e){ res.fail.push('PAGE_LOAD_FAIL'); return res; }
+ if(/page not found|404 not found/i.test(html) && html.length<50000){ res.fail.push('PAGE_404'); return res; }
+ const lc = html;
+
+ // DESC: a recognizable chunk of the product's own description prose appears
+ const descSig = firstWords(p.descriptionHtml, 6);
+ if(!descSig || descSig.length<8){ res.fail.push('NO_DESC_DATA'); }
+ else if(!lc.includes(descSig)){
+ // fall back: any 4-word run from the description
+ const words=(p.descriptionHtml||'').replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim().split(' ');
+ const run=words.slice(0,4).join(' ');
+ if(!run||!lc.includes(run)) res.fail.push('DESC_NOT_RENDERED');
+ }
+
+ // SPEC: roll -> width + repeat values present; mural -> material + weight present
+ if(isMural){
+ if(mf['custom.material'] && !lc.includes(mf['custom.material'])) res.fail.push('MATERIAL_NOT_RENDERED');
+ if(mf['custom.product_weight'] && !lc.includes(mf['custom.product_weight'].replace('g/m²','g'))) res.fail.push('WEIGHT_NOT_RENDERED');
+ } else {
+ if(!mf['custom.width']) res.fail.push('NO_WIDTH_DATA');
+ else if(!lc.includes(mf['custom.width'].split(' / ')[0])) res.fail.push('WIDTH_NOT_RENDERED');
+ if(!mf['custom.pattern_repeat']) res.fail.push('NO_REPEAT_DATA');
+ else if(!/^no/i.test(mf['custom.pattern_repeat']) && !lc.includes(mf['custom.pattern_repeat'].split(' / ')[0])) res.fail.push('REPEAT_NOT_RENDERED');
+ }
+
+ // IMG: >=1 product image in API AND a product image element on the page
+ if(imgCount<1) res.fail.push('NO_IMAGE_DATA');
+ else if(!/(cdn\.shopify\.com\/s\/files|\/cdn\/shop\/(files|products))[^"'\\]*\.(jpg|jpeg|png|webp)/i.test(lc)) res.fail.push('IMG_NOT_RENDERED');
+
+ return res;
+}
+
+(async()=>{
+ const q=`query($c:String){products(first:100,query:"vendor:'Tres Tintas'",after:$c){pageInfo{hasNextPage endCursor}
+ edges{node{id handle title descriptionHtml
+ images(first:10){edges{node{url}}}
+ metafields(first:50){edges{node{namespace key value}}}}}}}`;
+ let cursor=null, all=[];
+ while(true){ const conn=(await gql(q,{c:cursor})).products; all.push(...conn.edges.map(e=>e.node));
+ if(conn.pageInfo.hasNextPage && all.length<LIMIT) cursor=conn.pageInfo.endCursor; else break; }
+ all = all.slice(0, LIMIT===Infinity?all.length:LIMIT);
+ console.log(`click-through QA on ${all.length} live PDPs (concurrency ${CONC})…`);
+
+ const DELAY = parseInt(process.env.DELAY || '0', 10); // per-request pace (ms) to dodge Cloudflare throttling
+ const results=[]; let i=0, done=0;
+ async function worker(){ while(i<all.length){ const p=all[i++]; const r=await checkOne(p); results.push(r); done++; if(done%50===0)console.log(` …${done}/${all.length}`); if(DELAY)await sleep(DELAY);} }
+ await Promise.all(Array.from({length:CONC}, worker));
+
+ const failed = results.filter(r=>r.fail.length);
+ const pass = results.length - failed.length;
+ console.log(`\n=== CLICK-THROUGH QA RESULTS ===`);
+ console.log(`PASS (desc+spec+img all render): ${pass}/${results.length}`);
+ console.log(`FAILED: ${failed.length}`);
+ // tally failure reasons
+ const tally={}; failed.forEach(r=>r.fail.forEach(f=>tally[f]=(tally[f]||0)+1));
+ console.log('failure breakdown:', JSON.stringify(tally));
+ console.log('\n--- failing products ---');
+ failed.slice(0,60).forEach(r=>console.log(` ${r.id} [${r.fail.join(',')}] ${r.title.slice(0,45)} /products/${r.handle}`));
+ fs.writeFileSync('/tmp/tres-tintas-clickthrough-failures.json', JSON.stringify(failed,null,1));
+ console.log(`\nfull failure list -> /tmp/tres-tintas-clickthrough-failures.json`);
+})().catch(e=>{console.error(e);process.exit(1);});
diff --git a/shopify/scripts/tres-tintas-reverify.js b/shopify/scripts/tres-tintas-reverify.js
new file mode 100644
index 00000000..2660d527
--- /dev/null
+++ b/shopify/scripts/tres-tintas-reverify.js
@@ -0,0 +1,73 @@
+#!/usr/bin/env node
+/**
+ * tres-tintas-reverify.js — turn the Cloudflare-noisy click-through failures into
+ * a TRUE failure list. Re-checks ONLY the previously-flagged SKUs, ONE AT A TIME
+ * with a generous delay (so Cloudflare doesn't throttle), retrying the page load.
+ * A SKU that passes on a calm individual retry was throttle-noise; one that still
+ * fails is a real render/data gap.
+ *
+ * Reads /tmp/tres-tintas-clickthrough-failures.json (the flagged set), re-runs the
+ * same DESC/SPEC/IMG checks per product against a freshly-loaded live PDP.
+ */
+const fs = require('fs');
+const path = require('path');
+const STORE='designer-laboratory-sandbox.myshopify.com', SITE='https://www.designerwallcoverings.com', API='2024-01';
+function envval(k){const e=fs.readFileSync(path.join(process.env.HOME,'Projects/secrets-manager/.env'),'utf8');const m=e.match(new RegExp(`^${k}=["']?([^"'\\n]+)`,'m'));return m?m[1].trim():null;}
+const H={'X-Shopify-Access-Token':envval('SHOPIFY_DRAFT_TOKEN'),'Content-Type':'application/json'};
+const UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120 Safari/537.36';
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+async function fr(u,o,t=4){let e;for(let i=0;i<t;i++){try{const r=await fetch(u,{...o,signal:AbortSignal.timeout(45000)});if(r.ok)return r;e=new Error('HTTP '+r.status);}catch(x){e=x;}await sleep(1500*(i+1));}throw e;}
+async function gql(q,v){const r=await fr(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:H,body:JSON.stringify({query:q,variables:v})});const j=await r.json();if(j.errors)throw new Error(JSON.stringify(j.errors));return j.data;}
+
+function checkRender(html, p, mf, imgCount){
+ const fail=[]; const lc=html;
+ const isMural=!mf['custom.width']&&!!mf['custom.product_weight'];
+ // page actually loaded with product content?
+ if(html.length<200000 || !/product/i.test(html)){ return ['PAGE_THIN_OR_BLOCKED']; }
+ // DESC
+ const words=(p.descriptionHtml||'').replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim().split(' ');
+ const run=words.slice(0,4).join(' ');
+ if(words.length<3) fail.push('NO_DESC_DATA');
+ else if(run && !lc.includes(run)) fail.push('DESC_NOT_RENDERED');
+ // SPEC
+ if(isMural){
+ if(mf['custom.material']&&!lc.includes(mf['custom.material'])) fail.push('MATERIAL_NOT_RENDERED');
+ } else {
+ if(!mf['custom.width']) fail.push('NO_WIDTH_DATA');
+ else if(!lc.includes(mf['custom.width'].split(' / ')[0])) fail.push('WIDTH_NOT_RENDERED');
+ if(!mf['custom.pattern_repeat']) fail.push('NO_REPEAT_DATA');
+ else if(!/^no/i.test(mf['custom.pattern_repeat'])&&!lc.includes(mf['custom.pattern_repeat'].split(' / ')[0])) fail.push('REPEAT_NOT_RENDERED');
+ }
+ // IMG
+ if(imgCount<1) fail.push('NO_IMAGE_DATA');
+ else if(!/(cdn\.shopify\.com\/s\/files|\/cdn\/shop\/(files|products))[^"'\\]*\.(jpg|jpeg|png|webp)/i.test(lc)) fail.push('IMG_NOT_RENDERED');
+ return fail;
+}
+
+(async()=>{
+ const flagged = JSON.parse(fs.readFileSync('/tmp/tres-tintas-clickthrough-failures.json','utf8'));
+ console.log(`re-verifying ${flagged.length} flagged SKUs individually (calm pace)…`);
+ const q=`query($id:ID!){product(id:$id){id handle title descriptionHtml images(first:10){edges{node{url}}} metafields(first:50){edges{node{namespace key value}}}}}`;
+ const real=[], cleared=[];
+ let n=0;
+ for(const f of flagged){
+ n++;
+ const p=(await gql(q,{id:`gid://shopify/Product/${f.id}`})).product;
+ const mf={}; p.metafields.edges.forEach(x=>mf[`${x.node.namespace}.${x.node.key}`]=x.node.value);
+ const imgCount=p.images.edges.length;
+ let html=''; try{ html=await (await fr(`${SITE}/products/${p.handle}`,{headers:{'User-Agent':UA}})).text(); }catch(e){ html=''; }
+ const fail = html ? checkRender(html, p, mf, imgCount) : ['PAGE_LOAD_FAIL'];
+ if(fail.length){ real.push({id:f.id,handle:p.handle,title:p.title,fail}); }
+ else { cleared.push(f.id); }
+ if(n%40===0)console.log(` …${n}/${flagged.length} (real:${real.length} cleared:${cleared.length})`);
+ await sleep(700); // calm pace — one IP, stay under Cloudflare
+ }
+ console.log(`\n=== TRUE RESULTS (after individual re-verify) ===`);
+ console.log(`flagged: ${flagged.length} | throttle-noise CLEARED: ${cleared.length} | GENUINE issues: ${real.length}`);
+ const tally={}; real.forEach(r=>r.fail.forEach(x=>tally[x]=(tally[x]||0)+1));
+ console.log('genuine breakdown:', JSON.stringify(tally));
+ console.log('\n--- genuine issues ---');
+ real.slice(0,80).forEach(r=>console.log(` ${r.id} [${r.fail.join(',')}] ${r.title.slice(0,46)}`));
+ fs.writeFileSync('/tmp/tres-tintas-reverify-real.json', JSON.stringify(real,null,1));
+ console.log(`\ntrue failure list -> /tmp/tres-tintas-reverify-real.json`);
+})().catch(e=>{console.error(e);process.exit(1);});
← 1a27c781 Rotating new-arrivals collection hero (DTD source=A/curation
·
back to Designer Wallcoverings
·
Stage rotating hero on dev + pending-approval memo + verifie e58e4e17 →