'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; interface ElementPosition { index: number; tag: string; id: string | null; text: string; visible: boolean; inViewport: boolean; viewport: { x: number; y: number; width: number; height: number }; page: { x: number; y: number }; screen: { x: number; y: number }; } interface ViewportMetrics { scrollX: number; scrollY: number; innerWidth: number; innerHeight: number; screenX: number; screenY: number; chromeHeight: number; devicePixelRatio: number; } interface Capture { url: string; title: string; html: string; viewport: Partial; elements: ElementPosition[]; capturedAt: number; } interface AutomationInput { id: string; label: string; default: number; min: number; max: number; } interface AutomationSummary { /** firm:automation — what runs are recorded against. */ key: string; id: string; label: string; description: string; confirm: string | null; inputs: AutomationInput[]; steps: string[]; } interface FirmSummary { id: string; label: string; urlPattern: string; automations: AutomationSummary[]; } interface RunLogEntry { at: number; step: string; ok: boolean; detail?: string; } interface Run { id: number; automationId: string; automationLabel: string; status: 'queued' | 'running' | 'done' | 'error' | 'cancelled'; stepIndex: number; totalSteps: number; log: RunLogEntry[]; error: string | null; createdAt: number; updatedAt: number; } interface RunnerStatus { online: boolean; lastSeen: number | null; ageMs: number | null; host?: string | null; pid?: number | null; dryRun?: boolean; busy?: boolean; version?: string | null; expectedVersion?: string; stale?: boolean; } const RUN_BADGE: Record = { queued: 'bg-slate-100 text-slate-600', running: 'bg-blue-100 text-blue-700', done: 'bg-green-100 text-green-700', error: 'bg-red-100 text-red-700', cancelled: 'bg-amber-100 text-amber-700', }; const POLL_MS = 2000; /** Point relative URLs (stylesheets, images, fonts) at the origin the snapshot came * from, otherwise the render comes out unstyled. The HTML parser hoists a leading * into , so prepending works even if the markup has no explicit head. */ function withBaseTag(html: string, url: string): string { const base = ``; const head = html.match(/]*>/i); if (!head || head.index === undefined) return base + html; const at = head.index + head[0].length; return html.slice(0, at) + base + html.slice(at); } export default function AutoBuyer() { const [enabled, setEnabled] = useState(false); const [capture, setCapture] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [view, setView] = useState<'rendered' | 'source'>('source'); // The extension posts a new capture every few seconds, and re-feeding srcDoc would // reload the frame that often. So the rendered view shows a snapshot pinned when you // opened it — clicking Rendered again re-pins to the latest capture. const [pinned, setPinned] = useState(null); const [firms, setFirms] = useState([]); const [activeFirm, setActiveFirm] = useState(null); const [runs, setRuns] = useState([]); const [busyRun, setBusyRun] = useState(false); const [runner, setRunner] = useState(null); // Per-automation input values, keyed ".". const [inputValues, setInputValues] = useState>({}); // Tracks the newest capture we already hold, so the poll can skip re-downloading it. const lastAtRef = useRef(0); const poll = useCallback(async () => { try { const s = await fetch('/api/autobuyer/status', { cache: 'no-store' }).then((r) => r.json()); setEnabled(!!s.enabled); if (!s.enabled) return; const url = lastAtRef.current ? `/api/autobuyer/capture?since=${lastAtRef.current}` : '/api/autobuyer/capture'; const data = await fetch(url, { cache: 'no-store' }).then((r) => r.json()); if (data.unchanged || !data.capture) return; lastAtRef.current = data.capture.capturedAt; setCapture(data.capture); setError(null); } catch (err: any) { setError(err?.message ?? 'Poll failed'); } }, []); useEffect(() => { poll(); const id = setInterval(poll, POLL_MS); return () => clearInterval(id); }, [poll]); useEffect(() => { fetch('/api/autobuyer/automations') .then((r) => r.json()) .then((d) => { const list: FirmSummary[] = d.firms ?? []; setFirms(list); setActiveFirm((current) => current ?? list[0]?.id ?? null); }) .catch(() => {}); }, []); // Runs tick faster than the capture poll — a run is something you watch. const pollRuns = useCallback(async () => { try { const [d, r] = await Promise.all([ fetch('/api/autobuyer/runs', { cache: 'no-store' }).then((x) => x.json()), fetch('/api/autobuyer/runner', { cache: 'no-store' }).then((x) => x.json()), ]); setRuns(d.runs ?? []); setRunner(r); } catch { /* transient; the next tick retries */ } }, []); useEffect(() => { pollRuns(); const id = setInterval(pollRuns, 1000); return () => clearInterval(id); }, [pollRuns]); const activeRun = runs.find((r) => r.status === 'queued' || r.status === 'running') ?? null; const firmAutomations = firms.find((f) => f.id === activeFirm)?.automations ?? []; function inputValue(automation: AutomationSummary, input: AutomationInput): number { return inputValues[`${automation.key}.${input.id}`] ?? input.default; } async function startRun(automation: AutomationSummary) { const inputs: Record = {}; for (const input of automation.inputs) inputs[input.id] = inputValue(automation, input); // Anything that drives the real mouse against a broker gets a confirmation, // and it names the counts — the difference between buying one and buying ten // is a number in a box that is easy to misread. const summary = automation.inputs .map((i) => `${i.label}: ${inputs[i.id]}`) .join('\n'); if (automation.confirm && !window.confirm( `${automation.confirm}` + (summary ? `\n\n${summary}` : '') + `\n\nSteps:\n${automation.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}` )) return; setBusyRun(true); try { const res = await fetch('/api/autobuyer/automations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ automationId: automation.key, inputs }), }); const data = await res.json(); if (!res.ok) setError(data.error ?? 'Failed to start'); else setError(null); await pollRuns(); } catch (err: any) { setError(err?.message ?? 'Failed to start'); } finally { setBusyRun(false); } } async function cancelRun(id: number) { await fetch(`/api/autobuyer/runs?id=${id}`, { method: 'DELETE' }); await pollRuns(); } async function toggle() { setBusy(true); try { const res = await fetch('/api/autobuyer/status', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !enabled }), }); const data = await res.json(); setEnabled(!!data.enabled); setError(null); } catch (err: any) { setError(err?.message ?? 'Failed to toggle'); } finally { setBusy(false); } } function showRendered() { setView('rendered'); if (capture) setPinned(capture); } async function clearCapture() { await fetch('/api/autobuyer/capture', { method: 'DELETE' }); lastAtRef.current = 0; setCapture(null); setPinned(null); } const stale = capture ? Date.now() - capture.capturedAt > 15000 : false; return (

AutoBuyer

{/* ── Capture switch and daemon status ── */}

Page capture

While on, the AutoFirmer Capture extension posts the target tab's HTML here every few seconds

Autoclicker daemon

{runner?.online ? <>Executes queued runs · {runner.host ?? 'unknown host'} pid {runner.pid ?? '?'} · v{runner.version ?? '?'} : <>Not running — start it with python clicker/runner.py}

{runner?.online && runner.dryRun && ( dry run )} {runner?.online && runner.stale && ( out of date )} {!runner?.online ? 'Offline' : runner.busy ? 'Busy' : 'Online'}
{error && (
{error}
)} {/* ── One tab per firm ── */}
{firms.map((firm) => ( ))}
{!runner?.online && (
Nothing will run until the runner is started — it's the process that moves the mouse. In the project directory:{' '} python clicker/runner.py
)} {runner?.online && runner.stale && (
The runner is out of date (reporting v{runner.version ?? 'unknown'}, this dashboard needs v{runner.expectedVersion}). It will fail on step types it predates. Restart it: stop with Ctrl-C and run{' '} python clicker/runner.py again.
)} {runner?.online && runner.dryRun && (
The runner is in dry-run mode: it will walk through the steps and move the cursor, but never press or type. Restart it without --dry-run to act for real.
)}
{firmAutomations.length === 0 ? (
{firms.length === 0 ? 'No firms configured' : 'No automations for this firm yet'}
) : firmAutomations.map((a, i) => (

{a.label}

{a.description} · {a.steps.length} step{a.steps.length === 1 ? '' : 's'}

{a.inputs.map((input) => ( ))}
))}
{/* ── Run status ── */} {runs.length > 0 && ( <>

Runs

{runs.map((run) => (
{run.status} {run.automationLabel} step {Math.min(run.stepIndex + (run.status === 'done' ? 0 : 1), run.totalSteps)}/{run.totalSteps} {new Date(run.createdAt).toLocaleTimeString()} {(run.status === 'queued' || run.status === 'running') && ( )}
{run.log.length > 0 && (
    {run.log.map((entry, i) => (
  1. {entry.ok ? '✓' : '✗'} {' '} {entry.step} {entry.detail && — {entry.detail}}
  2. ))}
)} {run.error && (

{run.error}

)}
))}
)} {/* ── Latest capture ── */}

Latest capture

{!enabled ? (
Capture is off
) : !capture ? (
Waiting for the extension… open a page in Chrome with the extension installed
) : (
{capture.title || '(untitled)'} {capture.url} {(capture.html.length / 1024).toFixed(1)} KB {new Date(capture.capturedAt).toLocaleTimeString()} {stale && ' (stale)'} {(['rendered', 'source'] as const).map((v) => ( ))}
{capture.viewport?.innerWidth != null && (
viewport {capture.viewport.innerWidth}×{capture.viewport.innerHeight} scroll {Math.round(capture.viewport.scrollX ?? 0)},{Math.round(capture.viewport.scrollY ?? 0)} window@screen {capture.viewport.screenX},{capture.viewport.screenY} chrome {capture.viewport.chromeHeight}px dpr {capture.viewport.devicePixelRatio}
)} {capture.elements?.length > 0 && (

Matched elements ({capture.elements.length})

{capture.elements.map((el) => ( ))}
tag text window x,y size page x,y screen x,y vis
{el.tag}{el.id ? `#${el.id}` : ''} {el.text} {Math.round(el.viewport.x)},{Math.round(el.viewport.y)} {Math.round(el.viewport.width)}×{Math.round(el.viewport.height)} {Math.round(el.page.x)},{Math.round(el.page.y)} {Math.round(el.screen.x)},{Math.round(el.screen.y)} {el.inViewport ? '✓' : el.visible ? 'off-screen' : 'hidden'}
)} {view === 'rendered' ? ( <> {pinned && pinned.capturedAt !== capture.capturedAt && (
Frozen snapshot from {new Date(pinned.capturedAt).toLocaleTimeString()} — click Rendered again to refresh
)} {/* allow-scripts WITHOUT allow-same-origin: the frame runs the page's own JS inside an opaque origin, so it cannot reach this dashboard's DOM, storage, or same-origin API routes — /api/firms serves firm credentials. Granting both flags together is what would let a frame drop its own sandbox. Scripts are needed because most sites ship content at opacity:0 and fade it in with JS — blocked, they render blank. */}