← back to Designer Wallcoverings
Daisy contrarian fixes: strip coordinate-image contamination, GRS-10109 color, settlement->DB, repeat/match metafields; + interior-design style/color enrich for veneers+CR
87ba5cdc4981e3dafe00aad8ad0265732e47dd29 · 2026-07-06 15:20:23 -0700 · Steve
Files touched
A scripts/wallquest-refresh/interior-design-enrich.cjsA scripts/wallquest-refresh/push-repeat-match-daisy-bennett.cjsA scripts/wallquest-refresh/strip-coords-daisy-bennett.cjs
Diff
commit 87ba5cdc4981e3dafe00aad8ad0265732e47dd29
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 6 15:20:23 2026 -0700
Daisy contrarian fixes: strip coordinate-image contamination, GRS-10109 color, settlement->DB, repeat/match metafields; + interior-design style/color enrich for veneers+CR
---
.../wallquest-refresh/interior-design-enrich.cjs | 45 ++++++++++++++++++++++
.../push-repeat-match-daisy-bennett.cjs | 20 ++++++++++
.../strip-coords-daisy-bennett.cjs | 32 +++++++++++++++
3 files changed, 97 insertions(+)
diff --git a/scripts/wallquest-refresh/interior-design-enrich.cjs b/scripts/wallquest-refresh/interior-design-enrich.cjs
new file mode 100644
index 00000000..69d1b2ae
--- /dev/null
+++ b/scripts/wallquest-refresh/interior-design-enrich.cjs
@@ -0,0 +1,45 @@
+// Interior-design STYLE + COLOR enrichment for today's WallQuest non-vinyl items lacking it.
+// Gemini vision (designer lexicon) → ai_styles/ai_colors written to the source table + pushed as
+// Shopify tags. SOURCE_TABLE selects the book; only processes rows missing ai_styles.
+const { Client } = require('pg'); const https = require('https');
+const KEY = process.env.GEMINI_API_KEY; const MODEL = 'gemini-3.5-flash';
+const GURL = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${KEY}`;
+const TOK = process.env.SHOPIFY_ADMIN_TOKEN; const DOMAIN='designer-laboratory-sandbox.myshopify.com';
+const CONN='postgresql://dw_admin:DW2024SecurePass@127.0.0.1:5432/dw_unified';
+const TABLE = process.env.SOURCE_TABLE || 'phillipe_romano_catalog';
+const WHERE = process.env.WHERE || "created_at::date=CURRENT_DATE";
+const LIMIT = parseInt(process.env.LIMIT || '999', 10);
+const PROMPT = `You are an interior designer cataloguing a wallcovering. From the image return STRICT JSON only:
+{"styles":["max 4 interior-design styles from: Coastal, Organic Modern, Contemporary, Traditional, Transitional, Mid-Century Modern, Bohemian, Rustic, Minimalist, Art Deco, Hollywood Regency, Japandi, Farmhouse, Industrial, Scandinavian, Maximalist, Chinoiserie, Grandmillennial"],
+"colors":["max 4 plain-english colorway names e.g. Oatmeal, Alabaster, Greige, Celadon, Navy, Charcoal, Sand, Sage"],
+"dominant_color":"one plain color","dominant_hex":"#RRGGBB"}
+Return ONLY the JSON.`;
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+function dl(url){return new Promise((res,rej)=>{const rq=(u,n=0)=>{if(n>5)return rej(new Error('redir'));https.get(u,{timeout:15000,headers:{'User-Agent':'curl/8'}},r=>{if(r.statusCode>=300&&r.statusCode<400&&r.headers.location)return rq(r.headers.location,n+1);if(r.statusCode!==200){r.resume();return rej(new Error('HTTP '+r.statusCode));}const c=[];r.on('data',d=>c.push(d));r.on('end',()=>res({b64:Buffer.concat(c).toString('base64'),mime:(r.headers['content-type']||'image/jpeg').split(';')[0]}));}).on('error',rej).on('timeout',function(){this.destroy();rej(new Error('to'))})};rq(url)})}
+function gem(b64,mime){const p={contents:[{parts:[{text:PROMPT},{inline_data:{mime_type:mime,data:b64}}]}],generationConfig:{temperature:0.2,maxOutputTokens:1200,responseMimeType:'application/json'}};return new Promise((res,rej)=>{const u=new URL(GURL);const body=JSON.stringify(p);const r=https.request({hostname:u.hostname,path:u.pathname+u.search,method:'POST',headers:{'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)},timeout:40000},rs=>{let d='';rs.on('data',c=>d+=c);rs.on('end',()=>{try{const j=JSON.parse(d);if(j.error)return rej(new Error(j.error.message));let t=(j.candidates?.[0]?.content?.parts?.[0]?.text||'').trim();const m=t.match(/\{[\s\S]*\}/);res(JSON.parse(m?m[0]:t))}catch(e){rej(e)}})});r.on('error',rej);r.on('timeout',()=>{r.destroy();rej(new Error('gto'))});r.write(body);r.end()})}
+function shopify(method,path,body){return new Promise((res,rej)=>{const data=body?JSON.stringify(body):null;const r=https.request({hostname:DOMAIN,path:`/admin/api/2024-10/${path}`,method,headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json',...(data?{'Content-Length':Buffer.byteLength(data)}:{})}},rs=>{let d='';rs.on('data',c=>d+=c);rs.on('end',()=>{try{res(d?JSON.parse(d):{})}catch(e){res({})}})});r.on('error',rej);if(data)r.write(data);r.end()})}
+(async()=>{
+ const db=new Client({connectionString:CONN}); await db.connect();
+ const { rows } = await db.query(`SELECT id, image_url, shopify_product_id FROM ${TABLE} WHERE ${WHERE} AND (ai_styles IS NULL OR ai_styles::text IN ('[]','null')) AND coalesce(image_url,'')<>'' ORDER BY id LIMIT ${LIMIT}`);
+ console.log(`${TABLE}: ${rows.length} rows to enrich`);
+ let ok=0, err=0;
+ for (const r of rows) {
+ try {
+ const {b64,mime}=await dl(r.image_url);
+ let g; try { g=await gem(b64,mime); } catch(e1){ await sleep(800); g=await gem(b64,mime); } // retry once
+ // only the columns common to all book tables (ai_styles, ai_colors, color_hex)
+ await db.query(`UPDATE ${TABLE} SET ai_styles=$1, ai_colors=$2, color_hex=coalesce(nullif(color_hex,''),$3), updated_at=now() WHERE id=$4`,
+ [JSON.stringify(g.styles||[]), JSON.stringify(g.colors||[]), g.dominant_hex||null, r.id]);
+ if (r.shopify_product_id) {
+ const cur=await shopify('GET',`products/${r.shopify_product_id}.json?fields=tags`);
+ const tags=new Set((cur.product?.tags||'').split(',').map(s=>s.trim()).filter(Boolean));
+ (g.styles||[]).forEach(s=>tags.add(s)); (g.colors||[]).forEach(c=>tags.add(c));
+ await shopify('PUT',`products/${r.shopify_product_id}.json`,{product:{id:Number(r.shopify_product_id),tags:[...tags].join(', ')}});
+ }
+ ok++; if(ok<=3||ok%25===0)console.log(` ✅ [${ok}] id=${r.id} styles=${(g.styles||[]).join('/')} colors=${(g.colors||[]).join('/')}`);
+ } catch(e){ err++; if(err<=5)console.log(` ⚠ id=${r.id}: ${String(e).slice(0,50)}`); }
+ await sleep(500);
+ }
+ console.log(`\n${TABLE} ENRICH DONE ok=${ok} err=${err}`);
+ await db.end();
+})().catch(e=>{console.error('FATAL',e.message);process.exit(1)});
diff --git a/scripts/wallquest-refresh/push-repeat-match-daisy-bennett.cjs b/scripts/wallquest-refresh/push-repeat-match-daisy-bennett.cjs
new file mode 100644
index 00000000..3820c717
--- /dev/null
+++ b/scripts/wallquest-refresh/push-repeat-match-daisy-bennett.cjs
@@ -0,0 +1,20 @@
+// FIX 4: surface Repeat/Match on Daisy PDPs — push to the global.* keys the theme actually reads.
+const { execSync } = require('child_process'); const https = require('https');
+const TOK = process.env.SHOPIFY_ADMIN_TOKEN; const DOMAIN='designer-laboratory-sandbox.myshopify.com';
+function api(method,path,body){return new Promise((res,rej)=>{const data=body?JSON.stringify(body):null;const r=https.request({hostname:DOMAIN,path:`/admin/api/2024-10/${path}`,method,headers:{'X-Shopify-Access-Token':TOK,'Content-Type':'application/json',...(data?{'Content-Length':Buffer.byteLength(data)}:{})}},rs=>{let d='';rs.on('data',c=>d+=c);rs.on('end',()=>{try{res(d?JSON.parse(d):{})}catch(e){res({})}})});r.on('error',rej);if(data)r.write(data);r.end()})}
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+(async()=>{
+ const rows=execSync(`PGPASSWORD=DW2024SecurePass psql -h 127.0.0.1 -U dw_admin -d dw_unified -tA -F'|' -c "SELECT dw_sku,shopify_product_id,coalesce(nullif(trim(repeat_v),''),'Texture'),coalesce(nullif(trim(match_type),''),'Random Match') FROM daisy_bennett_catalog WHERE on_shopify AND shopify_product_id IS NOT NULL ORDER BY dw_sku"`).toString().trim().split('\n').map(l=>l.split('|'));
+ let ok=0;
+ for(const [sku,pid,rep,match] of rows){
+ const mf=[
+ {namespace:'global',key:'repeat',value:rep,type:'single_line_text_field'},
+ {namespace:'global',key:'match',value:match,type:'single_line_text_field'},
+ {namespace:'dwc',key:'repeat',value:rep,type:'single_line_text_field'},
+ ];
+ for(const m of mf){ await api('POST',`products/${pid}/metafields.json`,{metafield:m}); await sleep(100); }
+ ok++; if(ok<=3||ok%20===0)console.log(` ✅ ${sku}: repeat="${rep}" match="${match}"`);
+ await sleep(120);
+ }
+ console.log(`\nREPEAT/MATCH pushed to ${ok} products`);
+})().catch(e=>{console.error('FATAL',e.message);process.exit(1)});
diff --git a/scripts/wallquest-refresh/strip-coords-daisy-bennett.cjs b/scripts/wallquest-refresh/strip-coords-daisy-bennett.cjs
new file mode 100644
index 00000000..14ec6ca0
--- /dev/null
+++ b/scripts/wallquest-refresh/strip-coords-daisy-bennett.cjs
@@ -0,0 +1,32 @@
+// Strip WallQuest coordinate-contamination from Daisy Bennett galleries. KEEP: images whose
+// filename contains the product's pattern first-word (its own swatch) OR alt='room setting'.
+// DELETE the rest (foreign coordinate swatches). DRY=1 to preview.
+const { execSync } = require('child_process'); const https = require('https');
+const TOK = process.env.SHOPIFY_ADMIN_TOKEN; const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const DRY = process.env.DRY === '1'; const LIMIT = parseInt(process.env.LIMIT || '999', 10);
+function api(method, path){return new Promise((res,rej)=>{const r=https.request({hostname:DOMAIN,path:`/admin/api/2024-10/${path}`,method,headers:{'X-Shopify-Access-Token':TOK}},rs=>{let d='';rs.on('data',c=>d+=c);rs.on('end',()=>{try{res(d?JSON.parse(d):{})}catch(e){res({})}})});r.on('error',rej);r.end()})}
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+(async()=>{
+ const rows = execSync(`PGPASSWORD=DW2024SecurePass psql -h 127.0.0.1 -U dw_admin -d dw_unified -tA -F'|' -c "SELECT dw_sku,shopify_product_id,lower(split_part(pattern_name,' ',1)) FROM daisy_bennett_catalog WHERE on_shopify AND shopify_product_id IS NOT NULL ORDER BY dw_sku"`).toString().trim().split('\n').map(l=>l.split('|'));
+ let totalDel=0, prod=0, i=0;
+ for (const [sku,pid,pat] of rows) {
+ if (i++>=LIMIT) break;
+ const p = (await api('GET',`products/${pid}/images.json`)).images||[];
+ const keep=[], del=[];
+ for (const im of p) {
+ const fn = (im.src||'').split('/').pop().split('?')[0].toLowerCase();
+ const isRoom = /room setting/i.test(im.alt||'');
+ const isPlaceholder = /default-image/.test(fn);
+ // real product swatch = first 2 positions (main + _650), unless a placeholder; room always kept
+ const isOwn = im.position<=2 && !isPlaceholder;
+ (isRoom||isOwn ? keep : del).push(im);
+ }
+ if (del.length) {
+ prod++; totalDel+=del.length;
+ if (DRY) console.log(` ${sku}: keep ${keep.length} [${keep.map(k=>(k.src||'').split('/').pop().split('?')[0].slice(0,22)).join(', ')}] | DEL ${del.length} [${del.map(k=>(k.src||'').split('/').pop().split('?')[0].slice(0,22)).join(', ')}]`);
+ else { for (const im of del) { await api('DELETE',`products/${pid}/images/${im.id}.json`); await sleep(120); } if(prod<=3||prod%15===0)console.log(` ✅ ${sku}: deleted ${del.length} coord imgs, kept ${keep.length}`); }
+ }
+ await sleep(180);
+ }
+ console.log(`\n${DRY?'DRY-RUN':'STRIP DONE'} — products touched=${prod} images ${DRY?'to delete':'deleted'}=${totalDel}`);
+})().catch(e=>{console.error('FATAL',e.message);process.exit(1)});
← 91e639ed WallQuest title material-append + renumber planners
·
back to Designer Wallcoverings
·
wallquest import rule: no price on a loaded page = discontin 9c4de69d →