← back to Dw Vendor Scrapers Local
Add flag-discontinued.js: HEAD-probe net-new image-less rows, mark hard-404/410 as sync_status=discontinued (reversible via discontinued_prev_status)
ba1e429943f9668a8b40e12336febf018f29eae3 · 2026-06-10 20:56:43 -0700 · SteveStudio2
Files touched
Diff
commit ba1e429943f9668a8b40e12336febf018f29eae3
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed Jun 10 20:56:43 2026 -0700
Add flag-discontinued.js: HEAD-probe net-new image-less rows, mark hard-404/410 as sync_status=discontinued (reversible via discontinued_prev_status)
---
flag-discontinued.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 61 insertions(+)
diff --git a/flag-discontinued.js b/flag-discontinued.js
new file mode 100644
index 0000000..1fe67b1
--- /dev/null
+++ b/flag-discontinued.js
@@ -0,0 +1,61 @@
+#!/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();
+})();
← 127175f recrawl-schumacher: dry-run skips DB write + self-heal pinte
·
back to Dw Vendor Scrapers Local
·
(newest)