3 Commits
Author SHA1 Message Date
Brandon LiandClaude Opus 5 25289cb9b3 Let the master dashboard drive this instance; drop the AutoBuyer switch
Three changes that ride together because they touch the same surface.

A control link to the master. lib/master-link.ts dials out to
<master>/ws/instance and holds it open, so the dashboard can trigger a trade or
queue an AutoBuyer run here. Commands call the same functions the local pages
call — runTrade, startScheduler, queueRun — so nothing reaches the broker by a
path the UI does not also use. Reports prefer the socket and fall back to the
existing POST, which keeps older masters working unchanged.

startMasterLink is idempotent, and that matters more than it looks: getClients()
is lazy and starts the link, so the first command re-enters it from inside a
command the live socket is waiting to ack. Tearing the socket down there loses
the ack and reports a trade that actually executed as a failure — which invites
pressing the button twice.

queueRun is lifted out of the automations route so the socket and HTTP share one
copy of the input clamping that decides how many times a purchase fires.

Page capture loses its switch and gains a scope. It now runs every tick, but
only against the active tab of the last-focused window that matches
host_permissions — so it follows the page in front of you and stops when you
switch away, where before it would fall back to any firm tab left open in the
background.

autobuyer_enabled is gone entirely. The AutoBuyer is ready whenever the runner
is; a run happens because someone asked for one. That does mean a queued run
executes as soon as clicker/runner.py claims it, with no second gate — stopping
the runner is now the way to make a machine idle.

Extension to 0.10.0; it needs a manual reload at chrome://extensions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:31:31 -05:00
Brandon LiandClaude Opus 5 22db6eae8d Add repeat blocks, per-run inputs, skipIfNotFound, and orphaned-run recovery
Repeat. A `repeat` block runs its steps several times, with the count either
fixed in the config or taken from an input the user sets on the dashboard. The
block is unrolled in resolveSteps before the runner sees it, so the runner needs
no loop, the run's total step count stays honest, and every iteration appears in
the log as its own line — a failure on the third purchase reads as "(3/5)"
rather than as an indistinguishable repeat of the first.

Counts are clamped server-side against the automation's declared min/max, and
expansion is capped at 400 steps and three levels of nesting. Each iteration can
be a purchase, so the number is not taken on trust from the client, and the
confirmation dialog names it before anything runs.

skipIfNotFound on a click or type step tolerates an element that is not on the
page — a cookie banner, a modal that only sometimes appears. Only absence is
tolerated. That distinction needed a new NotFoundError: previously a missing
element, an unreachable dashboard, a missing tab and a covered button all
surfaced as the same DashboardError, and skipping that whole class would mean a
step quietly passing while the extension was down.

Orphaned runs are now reaped. Only one run executes at a time, so a run left in
'running' when its runner went away blocked every future run — restarting the
daemon mid-run deadlocked the queue, which is exactly what happened. The
heartbeat decides: a runner that is gone, or up and reporting idle, is not
driving that run whatever the status column says. Gated on the busy flag rather
than elapsed time alone, since a run sitting in a waitFor gate or a sign-in wait
can legitimately go minutes without progress.

Lucid Trading is scaffolded with no automations yet. One match pattern covers
both its hosts — `*.` matches the apex as well as subdomains, confirmed against
a live tab. Its signed-out pattern is `//lucidtrading.com/` rather than
`lucidtrading.com/dashboard`: the leading slashes anchor it to the start of the
host, and without them the substring also matches dash.lucidtrading.com, which
would abort every step while properly signed in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 13:22:45 -05:00
Brandon LiandClaude Opus 5 b748f95372 Add automation framework, typing, and runner for the autobuyer
Turns the autobuyer from a page scraper into something that acts. A dashboard
button queues a run; a desktop process executes it against the real browser.

lib/automations.ts — automations are declarative step lists nested inside the
firm whose site they drive. Steps are click / type / wait / navigate, and they
inherit the firm's tab pattern and URL, so one firm's automation can't act on
another's tab. Adding a button means adding an entry here; the page renders
buttons from the API and the runner receives steps from the server, so neither
needs editing. Runs key on firm:automation — every firm will plausibly have its
own "buy-accounts", and a bare id would resolve to the wrong one.

clicker/runner.py — the daemon behind the buttons. Claims a queued run, works
through the steps, reports each one back for the page's live log. Only one run
executes at a time: two processes driving one physical mouse would interleave
clicks. Heartbeats on its own thread, because a step can block for tens of
seconds and folding the beat into the main loop would show the runner as offline
in the middle of the run it was executing.

clicker/actions.py — one implementation of the safety checks, shared by the CLI
and the runner. Refuses to act when the element is covered by an overlay, when
coordinates fall off-screen, when the browser can't be confirmed frontmost, or
(for type) when the target isn't an editable field.

Typing: uneven human cadence, and the field is read back afterwards and compared
against what was typed — a field that never took focus fails silently and looks
identical to success otherwise. Non-ASCII is rejected because pyautogui skips
those characters without complaint, and newlines because Enter may submit the
form. Typos are deliberately not simulated: a mistyped digit in a trading form
is a real loss, and the correction is the part that can go wrong.

Extension: opens the firm's page when no tab matches, navigates to a specific
page for a navigate step (skipped when already there, so page state survives),
and retries the locate while a freshly loaded React app mounts — `complete` only
means the document loaded.

Staleness reporting, after it cost three debugging rounds: Chrome doesn't reload
an unpacked extension and Python doesn't reload a running process, so both now
report their version. A stale runner gets a red banner naming both versions and
the automation buttons are disabled, rather than failing mid-run on a step type
it predates.

Scale detection is now conservative: a raw OS/browser width ratio is only
trusted when it lands on a real scaling factor. On this multi-monitor desktop
the previous logic would have silently halved every coordinate.

Verified end to end against the live browser: navigate, locate, and a real
click (run #12, all three steps). API round-trips, claim-once semantics, run
cancellation, the heartbeat online/offline lifecycle, motion geometry and
timing, focus activation, and typing verification all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:00:33 -05:00