← back to Hollywood Import
momentum-recon.mjs
100 lines
// Momentum trade-portal LOGIN + RECON (read-only). Authorized by Steve (trade creds in .env).
// Goal: log in, then discover (a) the product-listing JSON endpoint and (b) where per-SKU
// price + unit-of-measure live once authenticated. Writes findings to recon-out.json.
// reCAPTCHA v3 (invisible) — a real Chrome session mints the token; no 2captcha needed.
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 out = { loggedIn: false, jsonEndpoints: [], productSample: null, notes: [] };
const browser = await chromium.launch({ channel: 'chrome', headless: false });
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 } });
const page = await ctx.newPage();
// Capture JSON-bearing responses to find the listing/price API
const seen = new Set();
page.on('response', async (res) => {
try {
const url = res.url();
const ct = (res.headers()['content-type'] || '');
if (!ct.includes('json')) return;
if (url.includes('recaptcha') || url.includes('cloudinary') || url.includes('meilisearch')) return;
if (seen.has(url.split('?')[0])) return;
seen.add(url.split('?')[0]);
const body = await res.text();
const hasPrice = /price|"amount"|per_yard|per_roll|unit/i.test(body);
out.jsonEndpoints.push({ url, status: res.status(), bytes: body.length, hasPrice,
preview: hasPrice ? body.slice(0, 1200) : body.slice(0, 200) });
} catch {}
});
console.log('→ login');
await page.goto(BASE + '/login', { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForTimeout(2500);
// There may be a hidden duplicate + a visible form. Pick the VISIBLE email input.
async function fillVisible(sel, val) {
const loc = page.locator(sel);
const n = await loc.count();
for (let i = 0; i < n; i++) {
const el = loc.nth(i);
if (await el.isVisible().catch(() => false)) {
await el.scrollIntoViewIfNeeded().catch(() => {});
await el.click({ delay: 50 }).catch(() => {});
await el.fill(val, { timeout: 8000 });
return true;
}
}
return false;
}
let okE = await fillVisible('#email', E.MOMENTUM_USER) || await fillVisible('input[type="email"]', E.MOMENTUM_USER);
if (!okE) {
// maybe a "Sign in" trigger opens the form
await page.locator('text=/sign in|log ?in/i').first().click({ timeout: 5000 }).catch(() => {});
await page.waitForTimeout(1500);
okE = await fillVisible('#email', E.MOMENTUM_USER) || await fillVisible('input[type="email"]', E.MOMENTUM_USER);
}
const okP = await fillVisible('#password', E.MOMENTUM_PASS) || await fillVisible('input[type="password"]', E.MOMENTUM_PASS);
out.notes.push('filled email=' + okE + ' password=' + okP);
await page.screenshot({ path: new URL('recon-login.png', import.meta.url).pathname, fullPage: false }).catch(() => {});
await page.locator('button[type="submit"]:visible, input[type="submit"]:visible').first().click({ timeout: 8000 })
.catch(() => page.keyboard.press('Enter'));
await page.waitForTimeout(8000);
const authed = await page.evaluate(() => document.querySelector('meta[name="user-authenticated"]')?.content);
out.loggedIn = authed === 'true';
out.notes.push('user-authenticated meta = ' + authed + ' | url=' + page.url());
console.log('→ authed:', authed, page.url());
// Browse the catalog (price-sort) so the listing endpoint fires
console.log('→ /products');
await page.goto(BASE + '/products?os=1&s=priceAsc&forceSync=1', { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(() => {});
await page.waitForTimeout(4000);
// Grab one product detail to see authed price markup
const firstHref = await page.evaluate(() => {
const a = [...document.querySelectorAll('a[href*="/products/"]')].map(x => x.getAttribute('href')).filter(Boolean);
return a[0] || null;
});
out.notes.push('first product href: ' + firstHref);
if (firstHref) {
const u = firstHref.startsWith('http') ? firstHref : BASE + firstHref;
await page.goto(u, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(() => {});
await page.waitForTimeout(3000);
out.productSample = await page.evaluate(() => {
const txt = document.body.innerText;
const prices = [...txt.matchAll(/\$\s?\d[\d,]*\.\d{2}|\bper\s+(yard|roll|sq\.?\s?ft|sf|panel)\b/gi)].map(m => m[0]).slice(0, 20);
return { url: location.href, title: document.title, priceMentions: prices,
bodyHasPrice: /\$\d/.test(txt) };
});
}
fs.writeFileSync(new URL('recon-out.json', import.meta.url), JSON.stringify(out, null, 2));
console.log('→ endpoints w/ price:', out.jsonEndpoints.filter(e => e.hasPrice).length, '/', out.jsonEndpoints.length);
console.log('→ wrote recon-out.json');
await browser.close();