diff --git a/README.md b/README.md index bed3ce8..7e40f23 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,38 @@ Open the app in your browser and add your firms through the UI: --- +## The master dashboard + +Set **Instance Name** and **Dashboard URL** on the Settings page and this +instance does two things with them: it reports its firm and account counts every +30 seconds, and it holds a WebSocket open to `/ws/instance` so the master +can drive it. + +What the master can trigger here: + +| | | +|---|---| +| Trade | Buy / Sell / Auto on a symbol, with the same "stop after all" option as the button on this dashboard | +| Stop | Halts the auto-trade scheduler | +| Run an automation | Queues a run for `clicker/runner.py`, exactly as the AutoBuyer page does | + +Commands land in [`lib/master-link.ts`](lib/master-link.ts), which calls the same +functions the pages call — `runTrade`, `startScheduler`, `queueRun`. Nothing +reaches the broker by a path the local UI does not also use, and every command +is acked with its result or its error so the master shows what actually happened +rather than assuming. + +The link is entirely optional. With no Dashboard URL configured it never starts, +and if the master is unreachable the instance keeps trading on its own and falls +back to the plain HTTP report until the socket comes back (retrying with backoff, +capped at a minute). + +**Nothing authenticates this link in either direction.** Whatever answers at the +Dashboard URL can place orders on every account this instance holds, so point it +only at a master you control, and prefer `https` so the address cannot be +redirected in transit. If the socket will not stay up, the `connected` light on +the master is the only symptom — check this instance's logs for `[master-link]`. + ## AutoBuyer (browser automation) The AutoBuyer page drives a real browser to buy and reset prop-firm accounts. It @@ -136,8 +168,17 @@ dashboard warns when its version is behind. ### Running it -Turn **Page capture** on from the AutoBuyer page, then press an automation's -button. Progress appears per step, and **Stop** halts a run between steps. +Press an automation's button. Progress appears per step, and **Stop** halts a run +between steps. + +There is no switch to throw first. Page capture follows whichever firm tab you +are looking at and stops when you switch away, and a run starts because someone +asked for one — from this page or from the master dashboard. The only thing that +gates a run is whether `clicker/runner.py` is up to claim it, which the page +shows as **Autoclicker daemon**. + +That does mean a queued run executes as soon as the runner sees it, with no +second confirmation. Stop the runner if you want the machine idle. The browser must be visible and frontmost while a run is in flight — the clicks are real OS-level input, so the machine cannot be used for anything else, and a diff --git a/app/api/autobuyer/automations/route.ts b/app/api/autobuyer/automations/route.ts index 9f49a0e..eccb9c4 100644 --- a/app/api/autobuyer/automations/route.ts +++ b/app/api/autobuyer/automations/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from 'next/server'; -import { FIRMS, findAutomation, automationKey, describeStep, resolveSteps } from '@/lib/automations'; -import { createRun, getSetting } from '@/lib/db'; +import { FIRMS, automationKey, describeStep } from '@/lib/automations'; +import { queueRun, QueueRunError } from '@/lib/queue-run'; import { corsJson, corsPreflight } from '../cors'; /** The dashboard renders a tab per firm, and a button per automation inside it. */ @@ -30,38 +30,12 @@ export async function POST(req: NextRequest) { if (typeof body.automationId !== 'string') { return corsJson({ error: '`automationId` is required' }, { status: 400 }); } - const found = findAutomation(body.automationId); - if (!found) { - return corsJson({ error: `No automation "${body.automationId}"` }, { status: 404 }); - } - if (found.automation.steps.length === 0) { - return corsJson({ error: `"${found.automation.label}" has no steps yet` }, { status: 400 }); - } - // The dashboard switch is the master arm for anything that drives the mouse. - if (getSetting('autobuyer_enabled') !== '1') { - return corsJson({ error: 'AutoBuyer is switched off' }, { status: 409 }); - } - // Clamp every input to what the automation declared. The count decides how - // many times a purchase runs, so it is not taken on trust from the client. - const supplied = (body.inputs ?? {}) as Record; - const inputs: Record = {}; - for (const declared of found.automation.inputs ?? []) { - const raw = Number(supplied[declared.id]); - const value = Number.isFinite(raw) ? Math.floor(raw) : declared.default; - inputs[declared.id] = Math.min(declared.max, Math.max(declared.min, value)); - } - - let resolved; - try { - resolved = resolveSteps(found.firm, found.automation, inputs); - } catch (err: any) { - return corsJson({ error: err?.message ?? 'Could not expand the steps' }, { status: 400 }); - } - - const run = createRun(body.automationId, resolved.length, inputs); - return corsJson({ runId: run.id, totalSteps: run.total_steps, inputs }); + return corsJson(queueRun(body.automationId, (body.inputs ?? {}) as Record)); } catch (err: any) { + if (err instanceof QueueRunError) { + return corsJson({ error: err.message }, { status: err.status }); + } return corsJson({ error: err?.message ?? 'Failed to queue run' }, { status: 500 }); } } diff --git a/app/api/autobuyer/runs/claim/route.ts b/app/api/autobuyer/runs/claim/route.ts index dbadd8a..d6adc70 100644 --- a/app/api/autobuyer/runs/claim/route.ts +++ b/app/api/autobuyer/runs/claim/route.ts @@ -1,14 +1,10 @@ -import { claimRun, getSetting } from '@/lib/db'; +import { claimRun } from '@/lib/db'; import { findAutomation, resolveSteps, resolveAuthSteps } from '@/lib/automations'; import { corsJson, corsPreflight } from '../../cors'; /** The Python runner polls this. Returns the run plus the steps to execute, so * the runner never needs its own copy of the automation definitions. */ export async function POST() { - if (getSetting('autobuyer_enabled') !== '1') { - return corsJson({ run: null, reason: 'AutoBuyer is switched off' }); - } - const row = claimRun(); if (!row) return corsJson({ run: null }); diff --git a/app/api/autobuyer/status/route.ts b/app/api/autobuyer/status/route.ts index 83fa367..3b94e16 100644 --- a/app/api/autobuyer/status/route.ts +++ b/app/api/autobuyer/status/route.ts @@ -1,29 +1,11 @@ -import { NextRequest } from 'next/server'; -import { getSetting, setSetting, countPendingLocate } from '@/lib/db'; +import { countPendingLocate } from '@/lib/db'; import { corsJson, corsPreflight } from '../cors'; /** Polled by the Chromium extension every few seconds, and by the AutoBuyer page. - * `pendingLocate` rides along so the extension learns about queued clicks without - * a second request on every tick. */ + * The AutoBuyer has no on/off switch — it is ready whenever the runner is — so + * the only thing worth reporting here is whether clicks are waiting to be served. */ export async function GET() { - return corsJson({ - enabled: getSetting('autobuyer_enabled') === '1', - pendingLocate: countPendingLocate(), - }); -} - -/** Flipped by the ON/OFF button on the AutoBuyer page. */ -export async function PATCH(req: NextRequest) { - try { - const body = await req.json() as { enabled?: unknown }; - if (typeof body.enabled !== 'boolean') { - return corsJson({ error: '`enabled` must be a boolean' }, { status: 400 }); - } - setSetting('autobuyer_enabled', body.enabled ? '1' : '0'); - return corsJson({ enabled: body.enabled }); - } catch (err: any) { - return corsJson({ error: err?.message ?? 'Failed to set status' }, { status: 500 }); - } + return corsJson({ pendingLocate: countPendingLocate() }); } export async function OPTIONS() { diff --git a/app/autobuyer/page.tsx b/app/autobuyer/page.tsx index 8f70bad..6b6aa4b 100644 --- a/app/autobuyer/page.tsx +++ b/app/autobuyer/page.tsx @@ -115,9 +115,7 @@ function withBaseTag(html: string, url: string): string { } 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 @@ -137,10 +135,6 @@ export default function AutoBuyer() { 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'; @@ -236,24 +230,6 @@ export default function AutoBuyer() { 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); @@ -273,28 +249,11 @@ export default function AutoBuyer() {

AutoBuyer

- {/* ── Capture switch and daemon status ── */} + {/* ── Daemon status ── + There is no on/off switch. Page capture follows whichever firm + tab you are looking at, and the AutoBuyer is ready whenever the + daemon below is up — so that row is the whole of the state. */}
-
-
-

Page capture

-

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

-
- -
-

Autoclicker daemon

@@ -411,11 +370,10 @@ export default function AutoBuyer() { ))}