← back to Sanderson Onboard

scripts/harvest_trade.js

56 lines

// Daily trade-portal harvest (HEADLESS, reuse persistent auth profile). Best-effort: on auth loss -> NEEDS_REAUTH flag.
const puppeteer = require('/Users/macstudio3/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/node_modules/puppeteer');
const fs = require('fs'), path = require('path');
const ROOT = path.resolve(__dirname, '..');
const PROFILE = ROOT + '/chrome-profile';
const DATA = ROOT + '/data';
const BASE = 'https://trade.sandersondesigngroup.com/us';
const sleep = ms => new Promise(r=>setTimeout(r,ms));
const log = s => { console.log(s); fs.appendFileSync(DATA+'/trade_harvest.log', new Date().toISOString()+' '+s+'\n'); };

async function loggedIn(p){
  return await p.evaluate(()=>{
    const links=Array.from(document.querySelectorAll('a')).map(a=>(a.getAttribute('href')||'')+' '+(a.textContent||''));
    const t=document.body.innerText.toLowerCase();
    return links.some(s=>/logout|sign ?out/i.test(s)) || t.includes('my account') || t.includes('quick order');
  });
}
async function loadAll(p, cap){
  let last=-1, stable=0;
  for(let i=0;i<cap;i++){
    const info=await p.evaluate(()=>{const m=document.body.innerText.match(/Viewing\s+(\d+)\s+of\s+(\d+)/i);return m?{shown:+m[1],total:+m[2]}:{shown:0,total:0};});
    if(info.shown && info.shown>=info.total) return info;
    if(info.shown===last){ if(++stable>=3) return info; } else { stable=0; last=info.shown; }
    const clicked=await p.evaluate(()=>{const b=Array.from(document.querySelectorAll('button,a')).find(x=>/^load more/i.test((x.textContent||'').trim())&&x.offsetParent!==null); if(b){b.click();return true;} return false;});
    if(!clicked) await p.evaluate(()=>window.scrollTo(0,document.body.scrollHeight));
    await sleep(3200);
  }
  return await p.evaluate(()=>{const m=document.body.innerText.match(/Viewing\s+(\d+)\s+of\s+(\d+)/i);return m?{shown:+m[1],total:+m[2]}:{shown:0,total:0};});
}
(async()=>{
  try{ fs.existsSync(DATA+'/NEEDS_REAUTH') && fs.unlinkSync(DATA+'/NEEDS_REAUTH'); }catch(e){}
  const b=await puppeteer.launch({headless:true, userDataDir:PROFILE,
    args:['--no-sandbox','--disable-dev-shm-usage','--window-size=1600,1000','--disable-blink-features=AutomationControlled']});
  const p=(await b.pages())[0]||await b.newPage(); await p.setViewport({width:1600,height:1000}); p.setDefaultNavigationTimeout(60000);
  await p.goto(BASE+'/',{waitUntil:'networkidle2'}); await sleep(3000);
  try{ await p.evaluate(()=>{const t=Array.from(document.querySelectorAll('button,a')).find(x=>/^(accept|agree)/i.test((x.textContent||'').trim()));if(t)t.click();}); }catch(e){}
  await sleep(1500);
  if(!(await loggedIn(p))){
    fs.writeFileSync(DATA+'/NEEDS_REAUTH', new Date().toISOString()+' trade portal session expired -> run scripts/reauth.sh (headed login)\n');
    log('AUTH LOST -> NEEDS_REAUTH written; skipping trade harvest');
    await b.close(); process.exit(3);
  }
  const summary={};
  for(const cat of ['wallpaper','fabric']){
    log('loading '+cat+'...');
    await p.goto(BASE+'/products/'+cat,{waitUntil:'networkidle2'}); await sleep(4000);
    const info=await loadAll(p, cat==='fabric'?260:120);
    const txt=await p.evaluate(()=>document.body.innerText);
    fs.writeFileSync(DATA+'/sanderson_'+cat+'_raw.txt', txt);
    summary[cat]=info; log(cat+': loaded '+info.shown+'/'+info.total);
  }
  fs.writeFileSync(DATA+'/trade_summary.json', JSON.stringify(summary,null,2));
  log('TRADE HARVEST OK '+JSON.stringify(summary));
  await b.close();
})().catch(e=>{ log('ERROR '+e.message); process.exit(1); });