[object Object]

← back to All Designerwallcoverings

Auto-discover all co-located pm2 vendor viewers + read /api/skus feed → 49→156 vendors live in all.dw (fulfills 'every vendor viewer must be live'); private-label sources stay BANNED

80c128c7299f6137431a4b3629488ad5213b4b01 · 2026-07-14 13:11:20 -0700 · Steve Abrams

Files touched

Diff

commit 80c128c7299f6137431a4b3629488ad5213b4b01
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jul 14 13:11:20 2026 -0700

    Auto-discover all co-located pm2 vendor viewers + read /api/skus feed → 49→156 vendors live in all.dw (fulfills 'every vendor viewer must be live'); private-label sources stay BANNED
---
 scripts/crawl-microsites.js | 69 +++++++++++++++++++++++++++++++++++++++------
 1 file changed, 61 insertions(+), 8 deletions(-)

diff --git a/scripts/crawl-microsites.js b/scripts/crawl-microsites.js
index 88f62dd..45cd86d 100644
--- a/scripts/crawl-microsites.js
+++ b/scripts/crawl-microsites.js
@@ -48,10 +48,42 @@ function seedFromRegistry() {
   const reg = JSON.parse(fs.readFileSync(SEED, 'utf8'));
   const out = [];
   for (const b of reg.brands || []) out.push({ slug: b.slug, vendor: b.vendor || null, type: 'brand', note: b.note || null, internalViewer: b.internalViewer || null });
-  for (const i of reg.internal || []) out.push({ slug: i.slug, vendor: null, type: 'internal', note: i.label || null });
+  for (const i of reg.internal || []) out.push({ slug: i.slug, vendor: null, type: 'internal', note: i.label || null, internalViewer: i.internalViewer || null });
   return out;
 }
 
+// Auto-discover EVERY co-located vendor viewer running on this host and seed its
+// localhost feed — so any *.DW / internal.*.DW viewer that is up is crawled with
+// ZERO registry upkeep (Steve's standing rule: all vendor viewers must be live in all.dw).
+// The norm-fold in mergeSeeds collapses these onto their public CF twin (no duplicates).
+function seedFromPm2() {
+  try {
+    const cp = require('child_process');
+    const procs = JSON.parse(cp.execSync('pm2 jlist', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }));
+    const lsof = cp.execSync('lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null || true', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
+    const pidPort = new Map();
+    for (const line of lsof.split('\n')) {
+      const m = line.match(/^\S+\s+(\d+)\s.*?:(\d+)\s+\(LISTEN\)/);
+      if (m && !pidPort.has(m[1])) pidPort.set(m[1], m[2]);
+    }
+    const SUFFIX = /-(viewer|internal|landing|showroom|house|site)$/;
+    const out = [];
+    for (const p of procs) {
+      const name = p.name || '';
+      const cwd = (p.pm2_env && p.pm2_env.pm_cwd) || '';
+      const isViewer = SUFFIX.test(name) || /dw-vendor-microsites\/vendors\//.test(cwd);
+      if (!isViewer) continue;
+      const port = pidPort.get(String(p.pid));
+      if (!port) continue;
+      const slug = name.replace(SUFFIX, '').replace(/_/g, '-').toLowerCase();
+      if (!slug) continue;
+      out.push({ slug, vendor: null, type: 'pm2', internalViewer: `http://127.0.0.1:${port} (pm2 ${name})` });
+    }
+    console.log(`seedFromPm2: ${out.length} co-located vendor viewers discovered`);
+    return out;
+  } catch (e) { console.warn('seedFromPm2 skipped:', e.message); return []; }
+}
+
 async function seedFromDb() {
   // Best-effort: candidate <slug>.designerwallcoverings.com for every vendor with
   // active products. Unreachable candidates simply drop out of the crawl.
@@ -145,21 +177,32 @@ async function seedFromCfZone() {
   return out;
 }
 
-function mergeSeeds(reg, db, cf) {
+function mergeSeeds(reg, db, cf, pm2) {
   const bySlug = new Map();
   // A registry brand can pin a canonical slug for a vendor whose auto-slug differs
   // (brand slug "pierre-frey" vs slugify("Pierre Frey")="pierrefrey"). Fold the
   // DB-derived twin into the registry entry by vendor so we don't emit a dark duplicate.
   const vendorToSlug = new Map();
   for (const s of reg) if (s.vendor) vendorToSlug.set(String(s.vendor).toLowerCase(), s.slug);
+  // Also fold hyphen/underscore twins (pm2 "andrew-martin" vs CF "andrewmartin") onto one
+  // canonical key so a co-located pm2 feed ENRICHES the public twin instead of duplicating it.
+  const norm = (v) => String(v || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+  const normToKey = new Map();
   // Order matters: registry first (richest metadata + canonical slugs), then DB
-  // (vendor twins), then CF-zone (authoritative fill for everything else). Earlier
-  // sources win on key collisions via the `...s, ...prev` merge below.
-  for (const s of [...reg, ...db, ...(cf || [])]) {
+  // (vendor twins), then CF-zone (authoritative fill), then pm2 (localhost feeds).
+  // Earlier sources win on identity fields; internalViewer is FILLED from whoever has it.
+  for (const s of [...reg, ...db, ...(cf || []), ...(pm2 || [])]) {
     if (!s.slug || BANNED.test(s.slug) || BANNED.test(s.vendor || '')) continue;
-    const key = (s.vendor && vendorToSlug.get(String(s.vendor).toLowerCase())) || s.slug;
+    const n = norm(s.slug);
+    const key = (s.vendor && vendorToSlug.get(String(s.vendor).toLowerCase())) || normToKey.get(n) || s.slug;
+    if (!normToKey.has(n)) normToKey.set(n, key);
     const prev = bySlug.get(key);
-    bySlug.set(key, prev ? { ...s, ...prev, activeProductsDB: prev.activeProductsDB ?? s.activeProductsDB, dbSampleImage: prev.dbSampleImage ?? s.dbSampleImage } : s);
+    bySlug.set(key, prev
+      ? { ...s, ...prev,
+          internalViewer: prev.internalViewer || s.internalViewer || null,
+          activeProductsDB: prev.activeProductsDB ?? s.activeProductsDB,
+          dbSampleImage: prev.dbSampleImage ?? s.dbSampleImage }
+      : s);
   }
   return [...bySlug.values()];
 }
@@ -263,6 +306,16 @@ async function feedProducts(host, feedBase) {
       const arr = Array.isArray(j.rows) ? j.rows : Array.isArray(j.products) ? j.products : null;
       if (arr && arr.length) return buildFeedResult(arr, host, j.total ?? j.count ?? null, false);
     }
+  } catch { /* fall through */ }
+  // 1b. dw-vendor-microsites line-viewer family serves its catalog at /api/skus
+  //     → shape { total, rows:[{sku, mfr_sku, pattern_name, …}] }.
+  try {
+    const { status, body } = await getText(`${dwBase}/api/skus?limit=100000&all=1`, FULL_FEED_TIMEOUT_MS);
+    if (status === 200) {
+      const j = JSON.parse(body);
+      const arr = Array.isArray(j.rows) ? j.rows : Array.isArray(j.skus) ? j.skus : null;
+      if (arr && arr.length) return buildFeedResult(arr, host, j.total ?? j.all ?? j.count ?? null, false);
+    }
   } catch { /* fall through to Shopify */ }
   // 2. Shopify storefront — follow /products.json to completion.
   try {
@@ -345,7 +398,7 @@ async function pool(items, worker, n) {
 async function crawlMicrosites() {
   const t0 = Date.now();
   const [db, cf] = await Promise.all([seedFromDb(), seedFromCfZone()]);
-  const seeds = mergeSeeds(seedFromRegistry(), db, cf);
+  const seeds = mergeSeeds(seedFromRegistry(), db, cf, seedFromPm2());
   const sites = await pool(seeds, crawlOne, CONCURRENCY);
 
   const up = sites.filter((s) => s.up);

← 2a6cffc chore: v1.4.1 (session close) — vendor facet alpha-sort  ·  back to All Designerwallcoverings  ·  chore: v1.5.0 (session close — pm2 viewer auto-discovery + / 9317a40 →