← back to Eur Recrawl
probe.mjs
129 lines
// Osborne trade-portal price probe — GATED (Steve authorized 2026-07-30).
// Self-sources OSBORNE_USERNAME/PASSWORD from the secrets .env so credentials
// never appear in a tool call or transcript. Logs in with a real browser,
// looks up ONE known product (W7900-01 Lodhi), captures the logged-in price,
// and screenshots each step. Read-only against the vendor's own account.
import { chromium } from 'playwright';
import fs from 'fs';
import os from 'os';
import path from 'path';
function envVal(key) {
const p = path.join(os.homedir(), 'Projects/secrets-manager/.env');
for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
if (line.startsWith(key + '=')) return line.slice(key.length + 1).trim().replace(/^"|"$/g, '');
}
return null;
}
const USER = envVal('OSBORNE_USERNAME');
const PASS = envVal('OSBORNE_PASSWORD');
const OUT = path.dirname(new URL(import.meta.url).pathname);
const shot = (n) => path.join(OUT, `probe-${n}.png`);
const STAGE = process.env.STAGE || 'recon';
const log = (...a) => console.log(...a);
const AUTH = path.join(OUT, '.auth/osborne.json');
const useAuth = STAGE !== 'recon' && STAGE !== 'login' && fs.existsSync(AUTH);
const browser = await chromium.launch({ headless: true });
const ctx = await browser.newContext({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36',
viewport: { width: 1440, height: 900 },
...(useAuth ? { storageState: AUTH } : {}),
});
const page = await ctx.newPage();
try {
log('creds present:', !!USER, !!PASS);
await page.goto('https://tradenew.osborneandlittle.com/', { waitUntil: 'networkidle', timeout: 60000 });
await page.waitForTimeout(2500);
await page.screenshot({ path: shot('01-landing'), fullPage: true });
log('title:', await page.title(), '| url:', page.url());
// dump interactive elements so we can see the login form shape
const els = await page.evaluate(() => {
const out = [];
for (const e of document.querySelectorAll('input,button,a[href],select')) {
out.push({
tag: e.tagName, type: e.type || '', name: e.name || '', id: e.id || '',
ph: e.placeholder || '', text: (e.innerText || e.value || '').slice(0, 40),
});
}
return out;
});
log('ELEMENTS:', JSON.stringify(els.slice(0, 40), null, 1));
if (STAGE === 'recon') {
log('recon only — stopping before login');
}
if (STAGE === 'login' || STAGE === 'probe') {
await page.fill('#username', USER);
await page.fill('#password', PASS);
await page.click('button:has-text("Login"), input[type=submit], #btnLogin');
await page.waitForLoadState('networkidle', { timeout: 60000 }).catch(() => {});
await page.waitForTimeout(3000);
await page.screenshot({ path: shot('02-postlogin'), fullPage: true });
log('post-login url:', page.url(), '| title:', await page.title());
// find a search box + dump nav so we can locate the product-lookup path
const nav = await page.evaluate(() => {
const inputs = [...document.querySelectorAll('input[type=text],input[type=search]')]
.map(e => ({ name: e.name, id: e.id, ph: e.placeholder }));
const links = [...document.querySelectorAll('a[href]')]
.map(a => ({ t: (a.innerText || '').trim().slice(0, 24), href: a.getAttribute('href') }))
.filter(l => l.t).slice(0, 30);
return { inputs, links, bodyText: document.body.innerText.slice(0, 300) };
});
log('SEARCH INPUTS:', JSON.stringify(nav.inputs));
log('NAV LINKS:', JSON.stringify(nav.links, null, 1));
log('BODY:', nav.bodyText);
// persist the authed session for reuse + handoff to claude-eur
fs.mkdirSync(path.join(OUT, '.auth'), { recursive: true });
await ctx.storageState({ path: path.join(OUT, '.auth/osborne.json') });
log('SAVED session -> .auth/osborne.json');
// map the full nav (incl dropdown items) — looking for PRICE LISTS + STOCK ENQUIRY paths
const allnav = await page.evaluate(() => [...document.querySelectorAll('a[href]')]
.map(a => ({ t: (a.innerText || '').trim().slice(0, 30), href: a.getAttribute('href') }))
.filter(l => l.href && (l.href.length > 1)));
log('ALL NAV HREFS:', JSON.stringify(allnav, null, 1));
}
if (STAGE === 'download') {
// download every price-list PDF (except credit-card) via the authed session
const links = JSON.parse(fs.readFileSync(path.join(OUT, 'pricelist_links.json'), 'utf8'));
fs.mkdirSync(path.join(OUT, 'pdfs'), { recursive: true });
for (const { name, url } of links) {
if (/credit/i.test(name)) continue;
const fn = url.split('?')[0].split('/').pop();
try {
const resp = await ctx.request.get(url, { timeout: 60000 });
const buf = await resp.body();
fs.writeFileSync(path.join(OUT, 'pdfs', fn), buf);
log(`downloaded ${fn}: ${resp.status()} ${buf.length}B [${name}]`);
} catch (e) { log(`FAILED ${fn}: ${e.message}`); }
}
}
if (STAGE === 'pricelists') {
// reuse saved session; go to the price-lists page and dump what's downloadable
await page.goto('https://tradenew.osborneandlittle.com/user/pricelists', { waitUntil: 'networkidle', timeout: 60000 }).catch(()=>{});
await page.waitForTimeout(2500);
await page.screenshot({ path: shot('03-pricelists'), fullPage: true });
const pl = await page.evaluate(() => ({
url: location.href, title: document.title,
downloads: [...document.querySelectorAll('a[href]')].map(a=>({t:(a.innerText||'').trim().slice(0,40),href:a.href})).filter(l=>/\.(csv|xls|xlsx|pdf)|download|pricelist/i.test(l.href+l.t)).slice(0,30),
body: document.body.innerText.slice(0,500),
}));
log('PRICELIST PAGE:', JSON.stringify(pl, null, 1));
}
} catch (e) {
log('ERROR:', e.message);
try { await page.screenshot({ path: shot('error'), fullPage: true }); } catch {}
} finally {
await browser.close();
}