[object Object]

← back to Designer Wallcoverings

auto-save: 2026-07-01T11:11:52 (8 files) — DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.ts DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.ts DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.ts DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.ts DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts

d55bd8f6014e9b98622e8402db4411fe4b1b3f74 · 2026-07-01 11:12:12 -0700 · Steve Abrams

Files touched

Diff

commit d55bd8f6014e9b98622e8402db4411fe4b1b3f74
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 1 11:12:12 2026 -0700

    auto-save: 2026-07-01T11:11:52 (8 files) — DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.ts DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.ts DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.ts DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.ts DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts
---
 .../Skills/morris-and-co/catalog.skill.ts          |  2 +
 .../Skills/ralph-lauren/catalog.skill.ts           | 22 +++++-
 .../ImportNewSkufromURL/lib/puppeteer-preflight.ts | 84 ++++++++++++++++++++++
 .../scrapers/arbor-wood-new-products-scraper.ts    |  4 +-
 ...c-com-digital-solutions-new-products-scraper.ts |  4 +-
 .../scripts/scraper-healthcheck.ts                 |  9 +++
 6 files changed, 122 insertions(+), 3 deletions(-)

diff --git a/DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.ts b/DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.ts
index 91df72f6..292ebea5 100644
--- a/DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.ts
+++ b/DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.ts
@@ -15,6 +15,7 @@
 
 import { BaseSkill, SkillConfig, SkillExecutionContext, SkillResult, ProductData } from '../types';
 import puppeteer from 'puppeteer';
+import { ensureChrome } from '../../lib/puppeteer-preflight';
 
 const DEFAULT_URL = 'https://www.wmorrisandco.com/uk/wallpaper/';
 
@@ -106,6 +107,7 @@ export function harvestSsrAnchors(html: string): Array<{ url: string; name: stri
 // Shared Sanderson-Design-Group page loader: Puppeteer Chrome-for-Testing with the
 // HeadlessChrome UA swap passes the Cloudflare challenge that blocks curl/Playwright.
 export async function loadPageHtml(url: string, opts?: { headless?: boolean; challengeWaitMs?: number }): Promise<string> {
+  ensureChrome(); // self-heal a wiped ~/.cache/puppeteer before launch (covers morris/scion/william-morris)
   const browser = await puppeteer.launch({
     headless: opts?.headless !== false,
     args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--window-position=2400,2400', '--window-size=1440,900']
diff --git a/DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.ts b/DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.ts
index 8d8746d9..313293d0 100644
--- a/DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.ts
+++ b/DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.ts
@@ -5,6 +5,7 @@ import { BaseSkill, SkillConfig, SkillExecutionContext, SkillResult, ProductData
 
 import { brightDataProxy } from '../../lib/brightdata-proxy';
 import { chromium, Browser, Page } from 'playwright';
+import { ensureChrome, isBrowserLaunchFailure } from '../../lib/puppeteer-preflight';
 
 export interface RalphLaurenProduct {
   url: string;
@@ -321,6 +322,9 @@ export default class RalphLaurenCatalogSkill extends BaseSkill {
     try {
       const url = context.searchUrl || 'https://www.ralphlauren.com/home-swatches-wall-coverings';
 
+      // Self-heal a wiped ~/.cache/puppeteer before launch (see 2026-07-01 outage).
+      ensureChrome();
+
       const puppeteer = (await import('puppeteer')).default;
       browser = await puppeteer.launch({
         headless: true,
@@ -399,7 +403,23 @@ export default class RalphLaurenCatalogSkill extends BaseSkill {
       throw new Error(`Only ${products.length} tiles extracted (PerimeterX block or grid change?)`);
     } catch (error) {
       if (browser) { try { await browser.close(); } catch {} }
-      // Fallback: legacy category scraper (generic selectors)
+      // A browser-launch failure (e.g. "Could not find Chrome" from a wiped
+      // ~/.cache/puppeteer) must NOT be masked by the legacy Playwright fallback,
+      // which would return a benign "0 products" and hide the real cause. Surface
+      // it explicitly so the healthcheck records the true regression reason.
+      if (isBrowserLaunchFailure(error)) {
+        const msg = error instanceof Error ? error.message : String(error);
+        return {
+          success: false,
+          products: [],
+          totalFound: 0,
+          executionTime: Date.now() - startTime,
+          errors: [`browser-launch-failed: ${msg}`, 'puppeteer Chrome unavailable — run `npx puppeteer browsers install chrome` (preflight should self-heal this)'],
+          metadata: { source: 'browser-launch-failure', browserLaunchFailure: true }
+        };
+      }
+      // Fallback: legacy category scraper (generic selectors) — only for genuine
+      // content/selector failures where the browser DID launch.
       try {
         const legacy = await scrapeRalphLaurenCategory(context.searchUrl || 'https://www.ralphlauren.com/home-swatches-wall-coverings');
         const products = (legacy.products || []).map((p) => ({
diff --git a/DW-Programming/ImportNewSkufromURL/lib/puppeteer-preflight.ts b/DW-Programming/ImportNewSkufromURL/lib/puppeteer-preflight.ts
new file mode 100644
index 00000000..ce777d74
--- /dev/null
+++ b/DW-Programming/ImportNewSkufromURL/lib/puppeteer-preflight.ts
@@ -0,0 +1,84 @@
+/**
+ * Puppeteer Chrome preflight self-heal.
+ *
+ * BORN 2026-07-01: ~/.cache/puppeteer vanished and silently zeroed 8 puppeteer
+ * scrapers (Ralph Lauren, arbor-wood, arc-com-digital-solutions,
+ * colefax-and-fowler, morris-and-co, scion-living, versace-home, william-morris).
+ * puppeteer.executablePath() pointed at a Chrome that no longer existed, every
+ * launch threw "Could not find Chrome", and the outage was invisible.
+ *
+ * ensureChrome() asserts puppeteer's resolved Chrome binary exists on disk before
+ * any puppeteer scraper runs; if it's missing it runs `npx puppeteer browsers
+ * install chrome` once so a wiped cache self-heals instead of zeroing vendors.
+ * Fast + idempotent: when Chrome is already present it does one fs.existsSync and
+ * returns — no install, no network, sub-millisecond.
+ */
+import { existsSync } from 'fs';
+import { execFileSync } from 'child_process';
+
+let healedThisProcess = false;
+
+function resolveExecutablePath(): string {
+  // require() (not import) so a bad/edge puppeteer state throws here, not at module load.
+  const puppeteer = require('puppeteer');
+  return puppeteer.executablePath();
+}
+
+export interface PreflightResult {
+  ok: boolean;
+  chromePath: string;
+  healed: boolean;
+  error?: string;
+}
+
+/**
+ * Assert puppeteer's Chrome is present; auto-install once if the cache was wiped.
+ * Safe to call at the top of every puppeteer scraper — no-op when Chrome exists.
+ */
+export function ensureChrome(): PreflightResult {
+  let chromePath = '';
+  try {
+    chromePath = resolveExecutablePath();
+  } catch (e: any) {
+    chromePath = '';
+  }
+
+  if (chromePath && existsSync(chromePath)) {
+    return { ok: true, chromePath, healed: false };
+  }
+
+  // Missing binary. Self-heal at most once per process to avoid install storms
+  // when many scrapers fire in parallel.
+  if (healedThisProcess) {
+    return { ok: false, chromePath, healed: true, error: 'Chrome still missing after self-heal attempt' };
+  }
+  healedThisProcess = true;
+
+  console.log('[preflight] puppeteer Chrome missing (cache wiped?) — running `npx puppeteer browsers install chrome`…');
+  try {
+    execFileSync('npx', ['puppeteer', 'browsers', 'install', 'chrome'], {
+      stdio: 'inherit',
+      timeout: 5 * 60 * 1000,
+    });
+  } catch (e: any) {
+    return { ok: false, chromePath, healed: true, error: `install failed: ${(e.message || '').slice(0, 200)}` };
+  }
+
+  // Re-resolve — install may have written a newer version dir.
+  try {
+    chromePath = resolveExecutablePath();
+  } catch {}
+  const ok = !!chromePath && existsSync(chromePath);
+  if (ok) console.log(`[preflight] ✅ Chrome installed: ${chromePath}`);
+  return { ok, chromePath, healed: true, error: ok ? undefined : 'Chrome still missing after install' };
+}
+
+/**
+ * True when a thrown error is a puppeteer browser-launch/"can't find Chrome"
+ * failure — the class that must surface as an explicit error, never be masked
+ * as "0 products".
+ */
+export function isBrowserLaunchFailure(err: unknown): boolean {
+  const m = err instanceof Error ? err.message : String(err || '');
+  return /could not find (chrome|the browser)|failed to launch the browser|browser was not found|executablePath|ENOENT.*chrome|spawn.*chrome/i.test(m);
+}
diff --git a/DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.ts b/DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.ts
index 1d14bee1..3b9c3ce0 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.ts
@@ -6,6 +6,7 @@ import puppeteer from 'puppeteer';
 import * as dotenv from 'dotenv';
 import { brightDataProxy } from '../brightdata-proxy';
 import { getVendorTimeout } from '../vendor-timeouts';
+import { ensureChrome } from '../puppeteer-preflight';
 
 // Load environment variables
 dotenv.config({ path: '.env.local' });
@@ -62,12 +63,13 @@ export async function scrapeArborWoodNewProducts(params?: ScrapeParams): Promise
     console.log('Using BrightData proxy');
   }
 
+  ensureChrome(); // self-heal a wiped ~/.cache/puppeteer before launch
   const browser = await puppeteer.launch({
     headless: true,
     args: launchArgs,
     ignoreHTTPSErrors: true
   });
-  
+
   try {
     const page = await browser.newPage();
     if (typeof proxySettings !== 'undefined' && proxySettings) {
diff --git a/DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.ts b/DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.ts
index ab6f64b7..7551d441 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.ts
@@ -8,6 +8,7 @@ import * as dotenv from 'dotenv';
 import { brightDataProxy } from '../brightdata-proxy';
 import { getVendorTimeout } from '../vendor-timeouts';
 import { ensureArray, safeLength } from '../scraper-wrapper';
+import { ensureChrome } from '../puppeteer-preflight';
 
 // Load environment variables from .env.local
 dotenv.config({ path: '.env.local' });
@@ -77,12 +78,13 @@ export async function scrapeArcComNewProducts(params?: ScrapeParams): Promise<Ar
     console.log('Using BrightData proxy');
   }
 
+  ensureChrome(); // self-heal a wiped ~/.cache/puppeteer before launch
   const browser = await puppeteer.launch({
     headless: true,
     args: launchArgs,
     ignoreHTTPSErrors: true
   });
-  
+
   try {
     const page = await browser.newPage();
     if (typeof proxySettings !== 'undefined' && proxySettings) {
diff --git a/DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts b/DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts
index 3c0ec300..a8d819b7 100644
--- a/DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts
+++ b/DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts
@@ -11,6 +11,7 @@
 import * as fs from 'fs';
 import * as path from 'path';
 import { execFileSync } from 'child_process';
+import { ensureChrome } from '../lib/puppeteer-preflight';
 
 const ROOT = path.join(__dirname, '..');
 const STATUS = path.join(ROOT, 'data', 'scraper-audit', 'scraper-status.json');
@@ -58,6 +59,14 @@ function checkOne(id: string, url: string): { count: number; ok: boolean; error:
 }
 
 async function main() {
+  // PREFLIGHT SELF-HEAL: assert puppeteer's Chrome is present before any vendor
+  // runs. A wiped ~/.cache/puppeteer silently zeroed 8 scrapers on 2026-07-01;
+  // this auto-reinstalls it once so the fleet self-heals instead of going dark.
+  // No-op (one existsSync) when Chrome is already present.
+  const pf = ensureChrome();
+  if (pf.healed) console.log(`[healthcheck] preflight self-heal ran — ok=${pf.ok} ${pf.error || pf.chromePath}`);
+  if (!pf.ok) console.log(`[healthcheck] ⚠ puppeteer Chrome unavailable: ${pf.error} — puppeteer scrapers will report explicit launch errors`);
+
   const status = loadStatus();
   status.scrapers = status.scrapers || {};
   const vendors = loadVendorUrls().slice(0, process.env.HC_MAX ? parseInt(process.env.HC_MAX, 10) : undefined);

← 92c43de8 enrich-full.py: per-output-file flock guard to prevent stack  ·  back to Designer Wallcoverings  ·  chore: enrichment finisher + retry-errors self-heal scripts, 2cc69d31 →