← back to Dw Image Dup Audit
fix-run.js
160 lines
#!/usr/bin/env node
// DW PR-line dup-image remediation 2026-07-21. Writes are live Shopify (Steve-approved "run all").
// Ledger: every write appended to fix-ledger.jsonl for reversibility.
const https = require("https");
const fs = require("fs");
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const HOST = "designer-laboratory-sandbox.myshopify.com";
const API = "/admin/api/2024-10";
const LEDGER = __dirname + "/fix-ledger.jsonl";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
function req(method, path, body) {
return new Promise((res, rej) => {
const data = body ? JSON.stringify(body) : null;
const r = https.request(
{ host: HOST, path, method, headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json", ...(data ? { "Content-Length": Buffer.byteLength(data) } : {}) } },
(resp) => { let d = ""; resp.on("data", (c) => (d += c)); resp.on("end", () => res({ status: resp.statusCode, body: d, link: resp.headers.link || "" })); }
);
r.on("error", rej);
if (data) r.write(data);
r.end();
});
}
async function reqRetry(method, path, body) {
for (let i = 0; i < 5; i++) {
const r = await req(method, path, body);
if (r.status === 429) { await sleep(2000); continue; }
return r;
}
throw new Error("429 persisted " + path);
}
const ledger = (e) => fs.appendFileSync(LEDGER, JSON.stringify({ ts: new Date().toISOString(), ...e }) + "\n");
// ---- plan ----
const IMAGE_FIXES = { // handle -> correct CDN file base (already uploaded to DW Shopify Files)
"palais-gold-glam-pr-801196": "0065364_palais-gold-glam",
"palais-juniper-pr-801206": "0065363_palais-juniper",
"palais-new-jade-pr-801216": "0065362_palais-new-jade",
"palais-peacock-pr-801226": "0065360_palais-peacock",
"palais-stone-pr-801236": "0065367_palais-stone",
"bergamo-azure-pr-801136": "0065369_bergamo-azure",
"bergamo-tan-pr-801146": "0065372_bergamo-tan",
"chess-cappuccino-pr-801156": "0065377_chess-cappuccino",
"chess-oat-pr-801166": "0065375_chess-oat",
"manila-gold-glam-pr-801176": "0065380_manila-gold-glam",
"manila-oat-pr-801186": "0065378_manila-oat",
"shantung-aged-stone-pr-801246": "0065389_shantung-aged-stone",
"shantung-celadon-pr-801256": "0065388_shantung-celadon",
"shantung-deep-aqua-pr-801266": "0065385_shantung-deep-aqua",
"shantung-new-jade-pr-801276": "0065384_shantung-new-jade",
};
const IMG_VERSIONS = { // v= params captured from live CDN listing
"0065360_palais-peacock": "1783366540", "0065362_palais-new-jade": "1783366545", "0065363_palais-juniper": "1783366550",
"0065364_palais-gold-glam": "1783366555", "0065367_palais-stone": "1783366571", "0065369_bergamo-azure": "1783366582",
"0065372_bergamo-tan": "1783366597", "0065375_chess-oat": "1783366613", "0065377_chess-cappuccino": "1783366620",
"0065378_manila-oat": "1783366625", "0065380_manila-gold-glam": "1783366636", "0065384_shantung-new-jade": "1783366673",
"0065385_shantung-deep-aqua": "1783366679", "0065388_shantung-celadon": "1783366695", "0065389_shantung-aged-stone": "1783366701",
};
const DRAFT_GARBAGE = [ // placeholder gif / image.jpg / broken title
"ala-forliata-wallpaper-ele-42864", "luega-glam-weave-prt-16113",
"montenegro-metallic-natural-stripe-prp-56504", "montenegro-metallic-natural-stripe-prp-56505",
"montenegro-metallic-natural-stripe-prp-56506", "montenegro-metallic-natural-stripe-prp-56507",
"montenegro-metallic-natural-stripe-prp-56508",
"phillipe-romano-natural-with-metal-prt-16716", "phillipe-romano-natural-with-metal-prt-16174",
"pisaro-peacock-damask-walls-prp-56548", "st-barton-paper-weave-prp-56513",
"tompkins-stain-repellent-real-dupione-silk-xws-52882", "tompkins-stain-repellent-real-dupione-silk-xws-52883",
"dwjj-40600-davide", "dwjj-40560-giacomo", "dwjj-40570-giacomo", "dwjj-40540-marco",
"mr-miyago-cork-wallpaper-2", "mr-miyago-cork-wallpaper-7",
"xxp-969203-designerwallcoverings-los-angeles", // "Kingston-Color Name" template-bug title
];
(async () => {
// 1) full fetch: handle -> {id, images, title, status}
console.log("fetching PR line…");
let path = `${API}/products.json?vendor=${encodeURIComponent("Phillipe Romano")}&fields=id,title,handle,status,images&limit=250`;
const byHandle = {};
const noImageActive = [];
while (path) {
const { status, body, link } = await reqRetry("GET", path);
if (status !== 200) throw new Error("fetch " + status);
for (const p of JSON.parse(body).products) {
byHandle[p.handle] = p;
if (p.status === "active" && (!p.images || p.images.length === 0)) noImageActive.push(p.handle);
}
const m = link.match(/<https:\/\/[^>]+(\/admin[^>]+)>; rel="next"/);
path = m ? m[1] : null;
await sleep(550);
}
console.log("products:", Object.keys(byHandle).length, "| no-image actives:", noImageActive.length, noImageActive.join(","));
// 2) image re-points
let fixed = 0;
for (const [handle, base] of Object.entries(IMAGE_FIXES)) {
const p = byHandle[handle];
if (!p) { ledger({ act: "imgfix", handle, ok: false, err: "not found" }); continue; }
const src = `https://www.designerwallcoverings.com/cdn/shop/files/${base}_650_1024x1024.jpg?v=${IMG_VERSIONS[base]}`;
const cur = p.images[0];
if (cur && cur.src.includes(base.split("_")[0])) { ledger({ act: "imgfix", handle, ok: true, skip: "already correct" }); continue; }
const add = await reqRetry("POST", `${API}/products/${p.id}/images.json`, { image: { src, alt: p.title } });
await sleep(550);
if (add.status !== 200 && add.status !== 201) { ledger({ act: "imgfix", handle, ok: false, err: add.status + add.body.slice(0, 150) }); console.log("FAIL add", handle, add.status); continue; }
const newImg = JSON.parse(add.body).image;
for (const old of p.images) {
const del = await reqRetry("DELETE", `${API}/products/${p.id}/images/${old.id}.json`);
await sleep(550);
ledger({ act: "imgdel", handle, imageId: old.id, oldSrc: old.src, ok: del.status === 200 });
}
ledger({ act: "imgfix", handle, ok: true, newImageId: newImg.id, newSrc: newImg.src });
fixed++; console.log("fixed", handle);
}
// 3) drafts: garbage + no-image actives
const toDraft = [...new Set([...DRAFT_GARBAGE, ...noImageActive])];
let drafted = 0;
for (const handle of toDraft) {
const p = byHandle[handle];
if (!p) { ledger({ act: "draft", handle, ok: false, err: "not found" }); continue; }
if (p.status !== "active") { ledger({ act: "draft", handle, ok: true, skip: "not active" }); continue; }
const r = await reqRetry("PUT", `${API}/products/${p.id}.json`, { product: { id: p.id, status: "draft" } });
await sleep(550);
const ok = r.status === 200;
ledger({ act: "draft", handle, id: p.id, title: p.title, reason: DRAFT_GARBAGE.includes(handle) ? "garbage-image" : "no-image", ok });
if (ok) { drafted++; console.log("drafted", handle); } else console.log("FAIL draft", handle, r.status);
}
// 4) exact clones + Luxuria m↔lxpr + Decorator book dups, derived from audit JSON
const audit = JSON.parse(fs.readFileSync(__dirname + "/pr-dup-images-2026-07-21.json"));
const cloneDrafts = new Set();
const strip = (t) => t.replace(/\s*\|\s*Phillipe Romano\s*$/i, "").trim().toLowerCase();
for (const g of audit.colorway) {
const titles = g.items.map(([t]) => strip(t));
if (g.items.length === 2 && titles[0] === titles[1] && !/memo sample/.test(titles[0])) {
// identical-title clone pair: keep the SKU-suffixed/longer handle, draft the other
const [a, b] = g.items.map(([, h]) => h);
const score = (h) => (/-(dwpr|lxpr|grs|grt|prt|prp|prw|pr)-?\d/.test(h) ? 2 : 0) + h.length / 100;
cloneDrafts.add(score(a) >= score(b) ? b : a);
}
// Luxuria same-color m#### vs lxpr-#### -> draft the m#### handle
if (titles.every((t) => t.startsWith("luxuria")) && titles[0] === titles[1]) {
for (const [, h] of g.items) if (/-m\d+$/.test(h)) cloneDrafts.add(h);
}
}
for (const g of audit.crossline) {
for (const [t, h] of g.items) if (strip(t) === "decorator grasscloth vol. 2") cloneDrafts.add(h);
}
let clones = 0;
for (const handle of cloneDrafts) {
const p = byHandle[handle];
if (!p || p.status !== "active") { ledger({ act: "clone-draft", handle, ok: true, skip: "missing/not-active" }); continue; }
const r = await reqRetry("PUT", `${API}/products/${p.id}.json`, { product: { id: p.id, status: "draft" } });
await sleep(550);
const ok = r.status === 200;
ledger({ act: "clone-draft", handle, id: p.id, title: p.title, ok });
if (ok) { clones++; console.log("clone-drafted", handle); } else console.log("FAIL clone-draft", handle, r.status);
}
console.log(`\nDONE. image-fixed=${fixed} garbage/no-img drafted=${drafted} clone-drafted=${clones}`);
fs.writeFileSync(__dirname + "/run-summary.json", JSON.stringify({ fixed, drafted, clones, noImageActive, cloneDrafts: [...cloneDrafts] }, null, 1));
})().catch((e) => { console.error("FATAL", e); process.exit(1); });