← back to Hollywood Import

momentum-recon2.mjs

75 lines

// Recon #2: reuse auth, (1) save storageState, (2) capture the /products grid Livewire
// payload shape (does each card carry item_number + price?), (3) dump LABELED price rows
// from a detail page so we know which number is list_price per unit. Read-only.
import { chromium } from 'playwright';
import fs from 'node:fs';

const E = Object.fromEntries(fs.readFileSync(new URL('.env', import.meta.url), 'utf8')
  .split('\n').filter(l => l && !l.startsWith('#') && l.includes('='))
  .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()]; }));
const BASE = E.MOMENTUM_BASE || 'https://momentumco.com';
const STATE = new URL('momentum-auth.json', import.meta.url).pathname;
const out = { gridPayloads: [], detail: null, notes: [] };

const browser = await chromium.launch({ channel: 'chrome', headless: false });
const haveState = fs.existsSync(STATE);
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 },
  ...(haveState ? { storageState: STATE } : {}) });
const page = await ctx.newPage();

if (!haveState) {
  await page.goto(BASE + '/login', { waitUntil: 'domcontentloaded', timeout: 60000 });
  await page.waitForTimeout(2500);
  const fv = async (sel, val) => { const l = page.locator(sel); const n = await l.count();
    for (let i=0;i<n;i++){ const el=l.nth(i); if(await el.isVisible().catch(()=>0)){ await el.fill(val,{timeout:8000}); return 1; } } return 0; };
  await fv('#email', E.MOMENTUM_USER); await fv('#password', E.MOMENTUM_PASS);
  await page.locator('button[type="submit"]:visible').first().click({ timeout: 8000 }).catch(()=>page.keyboard.press('Enter'));
  await page.waitForTimeout(8000);
  await ctx.storageState({ path: STATE });
  out.notes.push('logged in fresh, saved state');
} else out.notes.push('reused saved auth state');

// Capture Livewire grid payloads on /products
page.on('response', async (res) => {
  try {
    if (!res.url().includes('/livewire-') || !res.url().includes('/update')) return;
    const body = await res.text();
    if (!/products|item_number|itemNumber|color_number|price/i.test(body)) return;
    // find a product-card-ish fragment with a price
    const idx = body.search(/item[_]?number|preferred_color_number|"price"/i);
    out.gridPayloads.push({ bytes: body.length, sample: body.slice(Math.max(0, idx-200), idx+900) });
  } catch {}
});

await page.goto(BASE + '/products?os=1&s=priceAsc&forceSync=1', { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(()=>{});
await page.waitForTimeout(5000);
// scroll to trigger lazy grid load
for (let i=0;i<3;i++){ await page.mouse.wheel(0, 4000); await page.waitForTimeout(2500); }

// Detail page: labeled price rows
const href = await page.evaluate(() => document.querySelector('a[href*="/products/"]')?.getAttribute('href'));
out.notes.push('detail href: ' + href);
if (href) {
  await page.goto(href.startsWith('http') ? href : BASE + href, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(()=>{});
  await page.waitForTimeout(3500);
  out.detail = await page.evaluate(() => {
    const rows = [];
    // walk elements, capture any element whose text has a $price together with its nearby label text
    document.querySelectorAll('*').forEach(el => {
      if (el.children.length > 3) return;
      const t = (el.innerText || '').trim();
      if (/\$\s?\d[\d,]*\.\d{2}/.test(t) && t.length < 120) rows.push(t.replace(/\s+/g,' '));
    });
    // item number on page
    const body = document.body.innerText;
    const itemNo = (body.match(/\b[A-Z]{2,4}-?\d{3,6}\b/g) || []).slice(0, 8);
    return { url: location.href, title: document.title,
      priceRows: [...new Set(rows)].slice(0, 30), itemNoCandidates: itemNo };
  });
}

fs.writeFileSync(new URL('recon2-out.json', import.meta.url), JSON.stringify(out, null, 2));
console.log('grid payloads captured:', out.gridPayloads.length);
console.log('detail priceRows:', out.detail?.priceRows?.length);
await browser.close();