← back to Imageless Rescue

scripts/tag-needs-width.js

34 lines

'use strict';
// Add the "Needs-Width" tag to active-gate width-violators that STILL lack a width
// (checks live width first; skips any already widthed). Additive Shopify tag only —
// no status change, no dw_unified write. Idempotent (tagsAdd no-ops on existing tag),
// resumable. --dry-run / --limit N.
const fs=require('fs'),os=require('os'),path=require('path'),https=require('https');
const DRY=process.argv.includes('--dry-run');
const LIMIT=process.argv.includes('--limit')?parseInt(process.argv[process.argv.indexOf('--limit')+1],10):Infinity;
function tok(){if(process.env.SHOPIFY_ADMIN_TOKEN)return process.env.SHOPIFY_ADMIN_TOKEN.replace(/['"]/g,'').trim();try{return fs.readFileSync(path.join(os.homedir(),'Projects/secrets-manager/.env'),'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)[1].replace(/['"]/g,'').trim()}catch{return null}}
const T=tok(),STORE='designer-laboratory-sandbox.myshopify.com',API='2024-10',sleep=ms=>new Promise(r=>setTimeout(r,ms));
function gql(q,v){return new Promise(res=>{const b=JSON.stringify({query:q,variables:v});const r=https.request({hostname:STORE,path:`/admin/api/${API}/graphql.json`,method:'POST',headers:{'X-Shopify-Access-Token':T,'Content-Type':'application/json','Content-Length':Buffer.byteLength(b)}},x=>{let d='';x.on('data',c=>d+=c);x.on('end',()=>{try{res(JSON.parse(d))}catch{res({})}})});r.on('error',()=>res({}));r.write(b);r.end()})}
async function G(q,v){for(let a=0;a<6;a++){const r=await gql(q,v);if(r.errors&&JSON.stringify(r.errors).toUpperCase().includes('THROTTL')){await sleep(2000*(a+1));continue}return r}return{}}
const PROD=`query($id:ID!){product(id:$id){id title tags wG:metafield(namespace:"global",key:"width"){value} wC:metafield(namespace:"custom",key:"width"){value}}}`;
const TAG=`mutation($id:ID!){tagsAdd(id:$id,tags:["Needs-Width"]){userErrors{message}}}`;
(async()=>{
  const d=require('../data/active-gate-violations-2026-06-21.json');
  const vio=(d.violations||[]).filter(x=>(x.types||[]).includes('specs'));
  const LED=path.join(__dirname,'..','data','needs-width-tag-ledger.jsonl');const done=new Set();
  if(fs.existsSync(LED))for(const l of fs.readFileSync(LED,'utf8').split('\n')){if(l.trim())try{done.add(JSON.parse(l).id)}catch{}}
  const queue=vio.filter(x=>!done.has(x.id)).slice(0,LIMIT===Infinity?undefined:LIMIT);
  console.log(`tag-needs-width ${DRY?'[DRY] ':''}violators=${vio.length} done=${done.size} thisRun=${queue.length}`);
  const st={tagged:0,skipHasWidth:0,skipAlreadyTagged:0,err:0};
  for(const x of queue){
    const pr=await G(PROD,{id:x.id});const p=pr.data&&pr.data.product;if(!p){st.err++;continue}
    const w=((p.wG&&p.wG.value)||(p.wC&&p.wC.value)||'').trim();
    if(w){st.skipHasWidth++;fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'has-width-now',w})+'\n');continue}
    if((p.tags||[]).map(t=>t.toLowerCase()).includes('needs-width')){st.skipAlreadyTagged++;fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'already-tagged'})+'\n');continue}
    if(DRY){st.tagged++;if(st.tagged<=8)console.log(`  • tag Needs-Width → ${p.title.slice(0,52)}`);continue}
    const r=await G(TAG,{id:x.id});
    if(r.data&&r.data.tagsAdd&&!r.data.tagsAdd.userErrors.length){st.tagged++;fs.appendFileSync(LED,JSON.stringify({id:x.id,result:'tagged'})+'\n');if(st.tagged%100===0)console.log(`  ✓ ${st.tagged} tagged`)}else st.err++;
  }
  console.log(`\n=== ${DRY?'DRY ':''}SUMMARY ===`,JSON.stringify(st));
})().catch(e=>{console.error('FATAL',e);process.exit(1)});