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