← back to Dw Vendor Scrapers Local
flag-discontinued.js
62 lines
#!/usr/bin/env node
// flag-discontinued.js — probe net-new rows missing all_images; mark dead (404/410) URLs
// as discontinued. ADDITIVE + REVERSIBLE: backs up prior sync_status to discontinued_prev_status.
// Usage: node flag-discontinued.js <vendor_code> [--limit N] [--dry-run] [--apply]
require('dotenv').config({ path: '../dw-price-stock/.env' });
const { Pool } = require('pg');
const VENDOR = process.argv[2];
const DRY = process.argv.includes('--dry-run') || !process.argv.includes('--apply');
const LIM = (() => { const i = process.argv.indexOf('--limit'); return i>-1?parseInt(process.argv[i+1],10):0; })();
if (!VENDOR) { console.error('need vendor_code'); process.exit(1); }
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36';
const CONC = 6;
async function head(url){
try {
const ctrl = new AbortController(); const t = setTimeout(()=>ctrl.abort(), 12000);
let r = await fetch(url, { method:'HEAD', redirect:'follow', signal:ctrl.signal, headers:{'User-Agent':UA} });
// some servers 405 on HEAD -> retry GET
if (r.status===405 || r.status===403) r = await fetch(url, { method:'GET', redirect:'follow', signal:ctrl.signal, headers:{'User-Agent':UA} });
clearTimeout(t); return r.status;
} catch(e){ return 0; } // 0 = network error (don't flag — could be transient)
}
(async () => {
const { rows } = await pool.query(
`SELECT mfr_sku, product_url FROM vendor_catalog
WHERE vendor_code=$1
AND (all_images IS NULL OR btrim(all_images::text) IN ('','[]','null','{}'))
AND COALESCE(on_shopify,false)=false AND shopify_product_id IS NULL
AND product_url IS NOT NULL AND product_url <> ''
AND COALESCE(sync_status,'') NOT IN ('discontinued','never_push','archived_duplicate')
${LIM?`LIMIT ${LIM}`:''}`, [VENDOR]);
console.log(`${VENDOR}: probing ${rows.length} net-new image-less URLs (DRY=${DRY})`);
let dead=[], live=0, neterr=0, i=0;
async function worker(){
while (i < rows.length){
const row = rows[i++];
const st = await head(row.product_url);
if (st===404 || st===410) dead.push(row.mfr_sku);
else if (st===0) neterr++;
else live++;
if ((dead.length+live+neterr) % 100 === 0) console.log(` ${dead.length+live+neterr}/${rows.length} | dead ${dead.length} live ${live} neterr ${neterr}`);
}
}
await Promise.all(Array.from({length:CONC}, worker));
console.log(`RESULT ${VENDOR}: dead=${dead.length} live=${live} neterr=${neterr} of ${rows.length}`);
if (!DRY && dead.length){
// chunked update with backup of prior status
for (let j=0;j<dead.length;j+=500){
const chunk = dead.slice(j,j+500);
await pool.query(
`UPDATE vendor_catalog SET
discontinued_prev_status = COALESCE(discontinued_prev_status, sync_status),
sync_status='discontinued', updated_at=NOW()
WHERE vendor_code=$1 AND mfr_sku = ANY($2)`, [VENDOR, chunk]);
}
console.log(` APPLIED: ${dead.length} rows -> sync_status='discontinued' (prev backed up to discontinued_prev_status)`);
}
await pool.end();
})();