#!/usr/bin/env bash set -euo pipefail BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}" OUTPUT_ROOT="${2:-${SCREENSHOT_OUTPUT_DIR:-artifacts/screenshots}}" TIMESTAMP="$(date -u +"%Y%m%dT%H%M%SZ")" OUTPUT_DIR="${OUTPUT_ROOT%/}/workbench-${TIMESTAMP}" CAPTURE_MOBILE="${CAPTURE_MOBILE:-1}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${ROOT}" NODE_BIN="" if command -v node >/dev/null 2>&1; then NODE_BIN="$(command -v node)" elif command -v node.exe >/dev/null 2>&1; then NODE_BIN="$(command -v node.exe)" fi if [ -z "${NODE_BIN}" ]; then echo "Node.js is required for screenshot capture." >&2 exit 1 fi if ! "${NODE_BIN}" --input-type=module -e "await import('playwright')" >/dev/null 2>&1; then cat >&2 <<'EOF' Playwright is required for screenshot capture but is not available to Node. Install or expose Playwright in the runner environment, then retry: npm install --no-save playwright npx playwright install chromium GeoIntel does not add Playwright as a frontend dependency by default; this script is an optional visual regression handoff tool. EOF exit 2 fi mkdir -p "${OUTPUT_DIR}" tmp_js="$(mktemp "${ROOT}/.capture_workbench_screenshots.XXXXXX.mjs")" trap 'rm -f "${tmp_js}"' EXIT cat >"${tmp_js}" <<'JS' import { chromium, request } from 'playwright' import { mkdir, writeFile } from 'node:fs/promises' import path from 'node:path' const [baseUrl, outputDir, captureMobileRaw] = process.argv.slice(2) const captureMobile = captureMobileRaw === '1' const normalizedBaseUrl = baseUrl.replace(/\/$/, '') const workspaces = [ ['overview', 'Overview'], ['data', 'Data'], ['map', 'Map'], ['analysis', 'QA/QC'], ['ai', 'AI Labs'], ['exports', 'Exports'], ['system', 'System'], ] const viewports = [ { name: 'desktop', width: 1366, height: 900 }, ] if (captureMobile) { viewports.push({ name: 'mobile', width: 390, height: 844 }) } function sanitize(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') } async function ensureDemoWorkflow() { const api = await request.newContext({ baseURL: normalizedBaseUrl }) const response = await api.post('/api/v1/demo/workflow') if (!response.ok()) { throw new Error(`Demo workflow seed failed with HTTP ${response.status()}`) } const payload = await response.json() if (!payload?.data?.project_id) { throw new Error('Demo workflow response did not include data.project_id') } await api.dispose() return payload.data } async function capture() { await mkdir(outputDir, { recursive: true }) const demo = await ensureDemoWorkflow() const browser = await chromium.launch() const screenshots = [] const consoleMessages = [] try { for (const viewport of viewports) { const page = await browser.newPage({ viewport }) page.on('console', (message) => { if (['error', 'warning'].includes(message.type())) { consoleMessages.push({ viewport: viewport.name, type: message.type(), text: message.text(), }) } }) page.on('pageerror', (error) => { consoleMessages.push({ viewport: viewport.name, type: 'pageerror', text: error.message, }) }) await page.goto(normalizedBaseUrl, { waitUntil: 'networkidle' }) await page.locator('#root').waitFor({ state: 'visible', timeout: 15000 }) await page.getByText('GeoIntel', { exact: false }).first().waitFor({ timeout: 15000 }) const bodyText = await page.locator('body').innerText({ timeout: 15000 }) if (/vite|webpack|runtime error|failed to compile/i.test(bodyText)) { throw new Error(`Framework error overlay detected in ${viewport.name} viewport`) } for (const [workspaceKey, label] of workspaces) { await page.locator(`[data-testid="workspace-nav-${workspaceKey}"]`).click() await page.waitForTimeout(300) await page.locator('.workspace-heading').waitFor({ state: 'visible', timeout: 10000 }) const screenshotName = `${viewport.name}-${sanitize(workspaceKey)}.png` const screenshotPath = path.join(outputDir, screenshotName) await page.screenshot({ path: screenshotPath, fullPage: false }) screenshots.push({ viewport: viewport.name, workspace: workspaceKey, label, path: screenshotPath, }) } await page.close() } } finally { await browser.close() } const relevantConsoleMessages = consoleMessages.filter((message) => message.type() !== 'warning') const manifest = { captured_at: new Date().toISOString(), base_url: normalizedBaseUrl, demo_project_id: demo.project_id, output_dir: outputDir, capture_mobile: captureMobile, screenshots, console_messages: consoleMessages, } await writeFile(path.join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8') if (relevantConsoleMessages.length > 0) { throw new Error(`Console/page errors detected. See ${path.join(outputDir, 'manifest.json')}`) } console.log(`Workbench screenshots captured: ${screenshots.length}`) console.log(`Output: ${outputDir}`) } capture().catch((error) => { console.error(error.message) process.exit(1) }) JS "${NODE_BIN}" "${tmp_js}" "${BASE_URL}" "${OUTPUT_DIR}" "${CAPTURE_MOBILE}"