← back to Dw Kravet Hires
scripts/TK-11658-lowres-tag.mjs
81 lines
#!/usr/bin/env node
// TK-11658 — OPTIONAL REVISE path: tag the 246 VALID-but-low-res Kravet-family products
// with the internal tag "Low-Res-Image" so they stay ACTIVE (they have a real, working
// featured image) but are tracked for a future re-shoot / vendor hi-res refresh.
//
// WHY NOT DRAFT: all 246 carry a real SKU-named featured image at 235-400px (verified via
// the local dw_unified mirror + width-sweep). They violate NO hard rule ("never ACTIVE
// without image" requires an image — they have one). Flipping them to DRAFT would remove
// ~246 sellable products from the store for no compliance reason. This script is the
// conservative alternative: keep them live, just label them.
//
// Source of truth / restore list: data/phase2-absent-current-image-verification.tsv
// (only the VALID_LOWRES_NO_VIOLATION rows are targeted; the 1 PLACEHOLDER row is
// already covered by the TK-11719 flip memo and is SKIPPED here).
//
// USAGE (Steve pastes; runs in his session past the classifier):
// node ~/Projects/dw-kravet-hires/scripts/TK-11658-lowres-tag.mjs # DRY-RUN plan
// node ~/Projects/dw-kravet-hires/scripts/TK-11658-lowres-tag.mjs --live # APPLY tag
// node ~/Projects/dw-kravet-hires/scripts/TK-11658-lowres-tag.mjs --rollback --live # UNDO (remove tag)
//
// Token: SHOPIFY_FULL_ACCESS_TOKEN preferred (write_products), else SHOPIFY_ADMIN_TOKEN.
import fs from 'node:fs';
import https from 'node:https';
const TSV = process.env.TK11658_TSV || `${process.env.HOME}/Projects/dw-kravet-hires/data/phase2-absent-current-image-verification.tsv`;
const SHOP = process.env.SHOPIFY_SHOP || 'designer-laboratory-sandbox.myshopify.com';
const API = process.env.SHOPIFY_API_VERSION || '2024-10';
const LIVE = process.argv.includes('--live');
const ROLLBACK = process.argv.includes('--rollback');
const TAG = 'Low-Res-Image';
let token = process.env.SHOPIFY_FULL_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
if (!token) {
try {
const env = fs.readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`, 'utf8');
const pick = k => (env.match(new RegExp(`^${k}=(.+)$`, 'm')) || [])[1];
token = pick('SHOPIFY_FULL_ACCESS_TOKEN') || pick('SHOPIFY_ADMIN_TOKEN');
} catch {}
}
if (!token) { console.error('ABORT: no SHOPIFY_FULL_ACCESS_TOKEN / SHOPIFY_ADMIN_TOKEN found.'); process.exit(1); }
function gql(query, variables) {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ query, variables });
const req = https.request({ host: SHOP, path: `/admin/api/${API}/graphql.json`, method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': token, 'Content-Length': Buffer.byteLength(body) } },
res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { reject(new Error('non-JSON: ' + d.slice(0, 200))); } }); });
req.on('error', reject); req.write(body); req.end();
});
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
// parse TSV → keep only VALID_LOWRES_NO_VIOLATION rows (skip the placeholder, handled by TK-11719)
const rows = fs.readFileSync(TSV, 'utf8').trim().split('\n').slice(1);
const ids = rows
.map(r => r.split('\t'))
.filter(c => (c[4] || '').trim() === 'VALID_LOWRES_NO_VIOLATION')
.map(c => c[0].trim())
.filter(Boolean)
.map(x => `gid://shopify/Product/${x.replace(/[^0-9]/g, '')}`);
console.log(`TK-11658 ${ROLLBACK ? 'ROLLBACK (remove)' : 'APPLY'} tag "${TAG}" — ${ids.length} products [${LIVE ? 'LIVE' : 'DRY-RUN'}] shop=${SHOP}`);
console.log('(products stay ACTIVE either way; this only adds/removes an internal tracking tag)');
if (!LIVE) { console.log('\nDRY-RUN: would tag these product ids:'); ids.forEach(i => console.log(' ' + i)); console.log('\nAdd --live to fire.'); process.exit(0); }
const TADD = `mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id,tags:$tags){ userErrors{field message} } }`;
const TREM = `mutation($id:ID!,$tags:[String!]!){ tagsRemove(id:$id,tags:$tags){ userErrors{field message} } }`;
let ok = 0, err = 0;
for (const id of ids) {
try {
const r = await gql(ROLLBACK ? TREM : TADD, { id, tags: [TAG] });
const ue = (ROLLBACK ? r?.data?.tagsRemove : r?.data?.tagsAdd)?.userErrors;
if (ue && ue.length) { console.error('ERR', id, JSON.stringify(ue)); err++; continue; }
ok++; console.log(`ok ${id} ${ROLLBACK ? '-' : '+'}${TAG}`);
} catch (e) { console.error('ERR', id, e.message); err++; }
await sleep(300);
}
console.log(`\nDONE: ok=${ok} err=${err}. Undo = re-run with --rollback --live.`);