← 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();