← back to Hollywood Import

search-pass.mjs

56 lines

// Exhaustive live-site search pass: for each Group-2 pattern not in the grid crawl, drive the
// site's own "Search the site..." box (page fires Meilisearch) and READ THE RESPONSE the page
// receives — page-initiated, in-bounds. Record whether the pattern is live + its price.
import { chromium } from 'playwright';
import fs from 'node:fs';
const slug = s => (s || '').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
const terms = JSON.parse(fs.readFileSync(new URL('search-candidates.json', import.meta.url)));

const b = await chromium.launch({ channel: 'chrome', headless: true });
const ctx = await b.newContext({ storageState: new URL('momentum-auth.json', import.meta.url).pathname, viewport: { width: 1500, height: 1200 } });
const p = await ctx.newPage();

let lastHits = null;
p.on('response', async (res) => {
  if (!/meilisearch.*\/search/i.test(res.url())) return;
  try { const j = await res.json(); if (Array.isArray(j.hits)) lastHits = j.hits; } catch {}
});

await p.goto('https://momentumco.com/products?forceSync=1', { waitUntil: 'domcontentloaded', timeout: 40000 }).catch(()=>{});
await p.waitForTimeout(4000);
const box = p.locator('input[placeholder="Search the site..."]').first();

const out = [];
let dumpedKeys = false;
for (const { term, rows } of terms) {
  lastHits = null;
  await box.click().catch(()=>{});
  await box.fill('').catch(()=>{});
  await box.type(term, { delay: 30 }).catch(()=>{});
  // wait for a meilisearch response
  for (let i = 0; i < 20 && !lastHits; i++) await p.waitForTimeout(250);
  await p.waitForTimeout(400);
  let found = null;
  if (lastHits && lastHits.length) {
    if (!dumpedKeys) { console.log('HIT FIELDS:', Object.keys(lastHits[0]).join(',')); dumpedKeys = true; }
    const tk = slug(term);
    // a hit whose pattern/name slug equals the term = a real live match
    found = lastHits.find(h => {
      const nm = h.pattern_name || h.pattern || h.name || h.title || '';
      return slug(nm).startsWith(tk) || tk.startsWith(slug(nm));
    });
  }
  const rec = { term, rows, live: !!found };
  if (found) {
    rec.matchedPattern = found.pattern_name || found.pattern || found.name || found.title;
    rec.price = found.price ?? found.list_price ?? found.unit_price ?? found.amount ?? null;
    rec.priceFields = Object.fromEntries(Object.entries(found).filter(([k]) => /price|amount|net|cost/i.test(k)));
  }
  out.push(rec);
  console.log(`${rec.live ? '✓ LIVE ' : '· absent'} "${term}" (${rows} rows)` + (found ? ` → ${rec.matchedPattern} ${JSON.stringify(rec.priceFields)}` : ''));
}
fs.writeFileSync(new URL('search-pass-result.json', import.meta.url), JSON.stringify(out, null, 2));
const live = out.filter(o => o.live);
console.log(`\n=== ${live.length}/${out.length} terms LIVE (rows recoverable: ${live.reduce((a,o)=>a+o.rows,0)}) ===`);
await b.close();