import { writeFileSync } from 'fs'; import { getPage } from './browse.js'; const baseUrl = 'http://localhost:3000'; const username = 'analyst@fhb.local'; const password = process.env.CAMOUFOX_PASSWORD ?? ''; const reportPath = '/Users/stefano.fiorini/Documents/projects/fhb-loan-spreading-pilot-a/docs/plans/2026-01-24-financials-analysis-redesign/web-automation-scan.md'; type NavResult = { requestedUrl: string; url: string; status: number | null; title: string; error?: string; }; async function gotoWithStatus(page: any, url: string): Promise { const resp = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e: unknown) => ({ error: e })); if (resp?.error) { return { requestedUrl: url, url: page.url(), status: null, title: await page.title().catch(() => ''), error: String(resp.error), }; } return { requestedUrl: url, url: page.url(), status: resp ? resp.status() : null, title: await page.title().catch(() => ''), }; } async function textOrNull(page: any, selector: string): Promise { const loc = page.locator(selector).first(); try { if ((await loc.count()) === 0) return null; const txt = await loc.textContent(); return txt ? txt.trim().replace(/\s+/g, ' ') : null; } catch { return null; } } async function main() { const { page, browser } = await getPage({ headless: true }); const lines: string[] = []; lines.push('# Web Automation Scan (local)'); lines.push(''); lines.push(`- Base URL: ${baseUrl}`); lines.push(`- Timestamp: ${new Date().toISOString()}`); lines.push(''); try { lines.push('## Login'); await gotoWithStatus(page, `${baseUrl}/login`); await page.locator('input[name="email"]').fill(username); await page.locator('input[name="password"]').fill(password); await page.locator('button[type="submit"]').click(); await page.waitForTimeout(2500); const cookies = await page.context().cookies(); const sessionCookie = cookies.find((c: any) => c.name === 'fhb_session'); lines.push(`- After submit URL: ${page.url()}`); lines.push(`- Has session cookie (fhb_session): ${Boolean(sessionCookie)}`); lines.push(''); lines.push('## Demo Case'); const casesNav = await gotoWithStatus(page, `${baseUrl}/cases`); lines.push(`- GET /cases → status ${casesNav.status ?? 'ERR'}, final ${casesNav.url}`); const envCaseId = process.env.SCAN_CASE_ID?.trim() || null; let selectedCaseId: string | null = envCaseId; if (!selectedCaseId) { const caseLinks = await page.$$eval('a[href^="/cases/"]', (as) => as .map((a) => ({ href: (a as HTMLAnchorElement).getAttribute('href') || '', text: (a.textContent || '').trim(), })) .filter((x) => x.href.includes('/cases/')) ); const preferredTitles = ['Demo - Strong Borrower', 'Demo - Weak Borrower', 'Demo - Incomplete']; for (const title of preferredTitles) { const match = caseLinks.find((l) => l.text.includes(title) && l.href.includes('/cases/')); const href = match?.href ?? ''; const m = href.match(/\/cases\/([0-9a-f-]{36})/i); if (m) { selectedCaseId = m[1]; break; } } if (!selectedCaseId) { const firstHref = caseLinks.map((l) => l.href).find((h) => /\/cases\/[0-9a-f-]{36}/i.test(h)) ?? null; const m = firstHref?.match(/\/cases\/([0-9a-f-]{36})/i) ?? null; selectedCaseId = m?.[1] ?? null; } } lines.push(`- Selected caseId: ${selectedCaseId ?? '(none found)'}`); if (!selectedCaseId) { lines.push(''); lines.push('⚠️ Could not find a demo case link on /cases.'); writeFileSync(reportPath, lines.join('\n') + '\n', 'utf-8'); return; } const caseBase = `${baseUrl}/cases/${selectedCaseId}/journey`; lines.push(''); lines.push('## Route Checks'); const routesToCheck = [ `${caseBase}`, `${caseBase}/financials`, `${caseBase}/financials/income`, `${caseBase}/analysis`, `${caseBase}/analysis/configure`, `${caseBase}/analysis/ai`, `${caseBase}/analysis/ai/detail`, `${caseBase}/spreads`, ]; for (const url of routesToCheck) { const r = await gotoWithStatus(page, url); const h1 = await textOrNull(page, 'h1'); const finalPath = r.url.startsWith(baseUrl) ? r.url.slice(baseUrl.length) : r.url; lines.push(`- ${url.slice(baseUrl.length)} → status ${r.status ?? 'ERR'} (final ${finalPath})${h1 ? `, h1="${h1}"` : ''}`); } lines.push(''); lines.push('## Spreadsheet Analysis (UI)'); await gotoWithStatus(page, `${caseBase}/analysis/configure`); const runButton = page.locator('button:has-text("Run Analysis")').first(); const disabled = await runButton.isDisabled().catch(() => true); lines.push(`- Run button disabled: ${disabled}`); if (!disabled) { await runButton.click(); const resultsWait = page .waitForURL('**/journey/analysis/results**', { timeout: 180000 }) .then(() => 'results' as const); const errorWait = page .locator('[role="alert"]') .filter({ hasText: 'Error' }) .first() .waitFor({ timeout: 180000 }) .then(() => 'error' as const); const outcome = await Promise.race([resultsWait, errorWait]).catch(() => 'timeout' as const); if (outcome === 'results') { await page.waitForTimeout(1500); lines.push(`- Results URL: ${page.url().replace(baseUrl, '')}`); const downloadHref = await page .locator('a[href*="/journey/analysis/download"]') .first() .getAttribute('href') .catch(() => null); if (downloadHref) { const dlUrl = downloadHref.startsWith('http') ? downloadHref : `${baseUrl}${downloadHref}`; const dlResp = await page.goto(dlUrl, { waitUntil: 'commit', timeout: 60000 }).catch(() => null); lines.push( `- Download route status: ${dlResp?.status() ?? 'ERR'} (Content-Type: ${dlResp?.headers()?.['content-type'] ?? 'n/a'})` ); } else { lines.push('- Download link not found on results page'); } } else if (outcome === 'error') { const errorText = await page .locator('[role="alert"]') .first() .textContent() .then((t: string | null) => (t ? t.trim().replace(/\\s+/g, ' ') : null)) .catch(() => null); lines.push(`- Stayed on configure page; saw error callout: ${errorText ?? '(unable to read)'}`); lines.push('- Skipping download check because analysis did not complete.'); } else { lines.push('- Timed out waiting for results or error after clicking Run Analysis.'); } } else { lines.push('- Skipped running analysis because Run button was disabled.'); } lines.push(''); lines.push('## Notes'); lines.push('- This scan avoids scraping financial values; it records route availability and basic headings.'); writeFileSync(reportPath, lines.join('\n') + '\n', 'utf-8'); } finally { await browser.close(); } } main().catch((err) => { console.error(err); process.exitCode = 1; });