← 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
M DW-Programming/ImportNewSkufromURL/Skills/morris-and-co/catalog.skill.tsM DW-Programming/ImportNewSkufromURL/Skills/ralph-lauren/catalog.skill.tsA DW-Programming/ImportNewSkufromURL/lib/puppeteer-preflight.tsM DW-Programming/ImportNewSkufromURL/lib/scrapers/arbor-wood-new-products-scraper.tsM DW-Programming/ImportNewSkufromURL/lib/scrapers/arc-com-digital-solutions-new-products-scraper.tsM DW-Programming/ImportNewSkufromURL/scripts/scraper-healthcheck.ts
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 →