Commit Graph
100 Commits
Author SHA1 Message Date
Brandon LiandClaude Fable 5 8154f25bb9 Backups to master, replacement-VPS restore, broker proxy, multi-instance
Four features:

- Backup: lib/backup-sync.ts pushes the full settings table and firm
  credentials (the deliberate exception to "credentials never leave the
  machine") to the master's bearer-gated /api/instances/backup — every
  5 minutes when the content hash changed, within seconds of a settings
  or credential edit, and on the new backup-now command. Enabled by the
  master_backup_token setting.

- Restore: a dead VPS's replacement pulls the backup and adopts its
  uuid/name, so roster, payouts and card links carry on. Pre-boot via
  seed-settings.js --restore-from (setup-linux.sh prompts for it) or
  the Settings page's restore picker (lib/backup-restore.ts). Stop the
  old machine first; the master 409s a live-name rename.

- Proxy: the proxy_url setting routes BROKER traffic only — TopstepX
  REST + SignalR hub, Tradovate REST + websocket, Yahoo quotes — via
  lib/outbound-proxy.ts (axios agents, undici dispatcher, ws agent).
  The master link, reporter, catalog and backup push stay direct, so a
  dead proxy cannot take down the control channel. Restart to apply.

- Multi-instance: AUTOFIRMER_DB moves the SQLite file so one checkout
  runs N instances; scripts/instances.json (per-device, gitignored)
  drives start-all/update-check, and add-instance.sh / remove-instance.sh
  manage the extra PM2 processes. Absent registry = single-instance
  behavior, unchanged. Instance 1 keeps port 3000 for the clicker and
  extension.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-22 13:55:35 -05:00
Brandon LiandClaude Fable 5 97947bffde Drain stages keep the firm's consistency rule
A drain stage zeroed consistency, so the payout was planned (and the
account stopped trading) as soon as the min days were booked — even when
the cycle's biggest day still exceeded the firm's ratio (Topstep holds
its 40% through the drain). Carry stage.consistency into the effective
config and keep booking drain days until the cycle total covers
maxDay / consistency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-16 05:43:55 -05:00
Brandon LiandClaude Fable 5 1bf8ba8693 extension: detect activatable Topstep accounts
Add a popup button that scans the Topstep dashboard's GraphQL API
(crystal.topstep.com) for passed evaluations that can still be
activated into an Express Funded account.

An eval is activatable when its status is "Passed" and its subscription
id is not shared by ANY funded account it ever spawned — regardless of
that funded account's current status, since activation is one-way. The
result and its progress are mirrored to chrome.storage so the popup
renders them independent of the message port (avoids the "message port
closed" error on this ~20s scan). Activatable accounts link to their
Topstep detail page.

Adds https://*.topstep.com/* host permission; bumps to 0.15.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 10:33:56 -05:00
Brandon LiandClaude Fable 5 b1dccd7dec Revamp login guards into general per-firm page guards
Replace the single hardcoded signed-out check with a guards list on each
firm: any guard can trip mid-run via a URL substring (checked on every
step) or a selector probe (checked when a step fails). Each guard defines
recovery steps or a human-wait fallback, a per-run trigger cap, and a
resume policy — 'continue' retries the tripped step, 'restart' rewinds
the run to step 1.

Runner bumped to 0.20.0; the server still ships the legacy
signedOut/authSteps shape for stale runners, and a new runner synthesizes
a guard from a legacy payload.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-14 12:50:14 -05:00
Brandon Li 82b98b9d2d Agree to terms if it didnt work lucid 2026-09-13 16:31:04 -05:00
Brandon Li 5632d347c3 Wait for success modal to close 2026-09-13 15:27:05 -05:00
Brandon Li 00d30d1745 Click success modal 2026-09-13 15:25:34 -05:00
Brandon Li d97d3aa221 Update selector again 2026-09-13 15:22:08 -05:00
Brandon Li 2dffffd20b Updated agree to terms 2026-09-13 15:19:32 -05:00
Brandon Li 9588e3896d Push automations for lucid update 2026-09-13 15:06:12 -05:00
Brandon LiandClaude Opus 4.8 783e00755e Reconstruct MLL and reconcile funds against the real starting balance
Both the MLL floor and the books-invariant fund detector assumed a $0
starting balance (correct for Express, wrong for funded/eval sizes). A
150K account (accountSize 150000) built its balance curve from $0, so
the floor came out near -$4,500 and a $145k balance never crossed it —
the account never marked dead — while reconcileFunds saw the whole
$150k as an unexplained deposit. Seed both with accountSize so the floor
is an absolute balance (150K/$4,500 drawdown → 145,500, locked at the
funded size once the EOD high climbs a drawdown above it) and the books
invariant nets out the starting balance. Skip reconcile until the
account type is known, since the start balance is unknowable before then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 15:02:16 -05:00
Brandon LiandClaude Fable 5 fd6a2e33b6 Payout projection counts days the way the firm does (CME-holiday combining)
projectPerfectPath worked on the raw daily P&L, so for a combining firm a
holiday-session trade (e.g. the Labor Day session) counted as its own
trading day — an account one real day short of its min days looked
complete and anchored its payout to the last traded day. The projection
now folds sessions through combineCmeSessions like the live path, and
projected days skip dates that clear into the next full day.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 14:56:11 -05:00
Brandon LiandClaude Fable 5 9597db08f7 Anchor payouts to the trading day the cycle finishes, not the report day
A completed cycle's payout now lands on its last traded day (requestable
after that session's close) instead of whatever day the projection ran —
a Friday finish viewed over the weekend stayed pinned to Sunday. Also
stop inserting the platform's stale weekend realizedPnL as a Sat/Sun
P&L row, which fabricated an extra qualifying day and made accounts one
day short look already complete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 14:39:14 -05:00
Brandon LiandClaude Fable 5 1996937ef2 Mark TopstepX accounts dead only off landed fills and settled books
The MLL floor no longer recomputes while a position is open or while
the balance disagrees with the books beyond fee noise — the last
settled floor stands, so balance ≤ floor can only come true when a
booked fill crosses a settled floor (Topstep's own liquidation fill
included). The floor also survives canTrade flipping off, so a blown
account stays ☠ instead of demoting to inactive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 14:10:36 -05:00
Brandon LiandClaude Fable 5 5e14daa669 Drain days draw from a $4–$15 half-dollar grid
23 deterministic values per (account, date) instead of $5/$10/$15; the
qualifying floor moves to the grid minimum so every draw counts toward
the cycle. The per-stage min-day field no longer drives the amount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 13:57:06 -05:00
Brandon LiandClaude Fable 5 fc65b4340d Reconstruct TopstepX MLL floors from the EOD balance curve
TopstepX never reports the Maximum Loss Limit, but it's deterministic:
floor = min(0, eodBalanceHigh − trailingDrawdown), locking at the $0
starting balance. The EOD curve rebuilds from daily P&L + fund events,
with off-book fee drift spread across days proportionally to reported
fees (validated against a platform-reported MLL to ~$0.10). Stored in
autoLiqThresholds — negative, or +$0.01 once locked — so dead detection,
dashboards, and the ruin model's startThreshold all use the real room.
Guards move from '> 0' to '!== 0'; 0 still means no floor. canTrade=false
accounts are skipped. daily_pnl gains a fees column for the weighting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 13:57:06 -05:00
Brandon LiandClaude Fable 5 4edb771701 Fix TopstepX login loop on bundled builds: signalr's dynamic require
@microsoft/signalr does an unconditional require('ws')/require('eventsource')
when constructing a connection; bundled (turbopack) builds throw 'dynamic
usage of require is not supported', which the login chain mislabelled as a
login failure and retried every 60s. Externalize the package and never let
hub setup fail the login — REST polling covers everything the hub does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 13:28:09 -05:00
Brandon LiandClaude Opus 5 153b1273c3 Refuse to run the Linux setup as root
setup-linux.sh accepted being run as root and simply set SUDO="". That quietly
broke everything user-scoped: PM2 keeps its process list in $HOME/.pm2 and the
login entry goes in $HOME/.config/autostart, so under sudo both landed in /root.
The install reported success, then `pm2 list` as the real user showed nothing -
it was talking to a different daemon - and the autostart entry sat in a home
directory that never logs in.

Both scripts now refuse, and name the account to use when SUDO_USER says how
they were invoked. setup-linux.sh still uses sudo for apt, which is the only
part that needs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 13:21:59 -05:00
Brandon LiandClaude Opus 4.8 a6a79bba7e Payout projection: payoutable same-equity accounts loop without explicit stages
A payoutable account with no withdrawal stages (e.g. MFFU same-equity)
now withdraws its share and re-targets the same equity each cycle instead
of stopping after the first payout. Non-payoutable evals still stop at
one completion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-12 19:04:22 -05:00
Brandon LiandClaude Fable 5 70eab33d1b Payout projection: a completed cycle is requestable today, not next session
When target and min trading days are already satisfied, the payout fires
on the current trade date instead of being pushed to the next session —
so an account that has already hit its target with its days in shows the
payout as requestable now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 19:01:27 -05:00
Brandon LiandClaude Fable 5 bf56a76a6d Account calendar: completion cell says 'Passed' for non-payoutable types
An eval that completes its target passes to the next account rather than
paying out — the projected completion marker reads 'Passed ✓' unless the
type is marked payoutable on the master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 18:55:58 -05:00
Brandon LiandClaude Fable 5 38a9c6c1b7 Report the running build's version to the master
The version is the checkout's HEAD commit (short hash + commit time),
read once at startup — updates arrive as `git pull`, so there is no
release number to bump. It rides on the socket hello, socket reports,
and the HTTP report fallback; the field is optional in the protocol so
older masters ignore it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 18:54:19 -05:00
Brandon LiandClaude Fable 5 c493796602 Drain-day randomness in $0.50 steps (MNQ tick value), $5.00–$15.00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 18:12:12 -05:00
Brandon LiandClaude Fable 5 9b2d823210 Account calendar: reach probabilities, 2-payout horizon, outline cells, same-day payouts, real drawdown floors
- projectPerfectPath books each cycle's payout ON its final trading day
  and anchors ruin probabilities to the platform's real liquidation floor
  (Tradovate auto-liq) when reported, else the configured trailing
  drawdown with the maxLoss/$2,000 fallback
- Account calendar shows the cumulative reach % on every projected day,
  caps the projection at two payouts ahead, and renders potential days
  as dashed outlines instead of shaded cells

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 18:09:00 -05:00
Brandon LiandClaude Fable 5 52d51afebb Report each account's future payout chain with closed-form reach probabilities
projectPerfectPath prices every projected withdrawal as
min(withdrawalPct × equity profit, cap) × profit split, and tracks the
cumulative probability of reaching it under the fair-walk ruin model —
room/(room + dayTarget) per day (the per-leg product telescopes), trailing
threshold locked at breakeven after the first withdrawal, no obfuscation
branches. The reporter attaches the first 6 payouts per account so the
master's payout calendar renders real amounts and probabilities instead of
one static expectedPayout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 17:01:09 -05:00
Brandon LiandClaude Fable 5 50a5eb4713 Add TopstepX platform support, drain withdrawal stages, and perfect-path calendar projection
- TopstepXClient (ProjectX Gateway): API-key auth, SignalR user hub,
  Trade/search daily P&L, emulated OCO exits, books-invariant
  withdrawal detection for $0-start accounts
- Platform-aware client pool, credentials (api_key column), and
  credential form; $0-size account types match by prefix alone
- Drain stages: min-day ($5-$15 randomized) MNQ legs for the
  configured days, then wait for the 50% withdrawal; stage pills
  show 'Stage N – Drain'
- Account calendar projects the perfect path: future days book their
  planned targets, simulated 50% withdrawals, payout marker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-12 16:25:52 -05:00
Brandon LiandClaude Fable 5 1bca441247 Show the next trade's stop next to the daily target
Stop-enabled accounts show an amber "stop $X" beside the day target:
the day-1 net floor (live against realized P&L) or 2x a min-day's
target, mirroring exactly what the trade path will place — and nothing
when the next trade rides stopless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 20:24:44 -05:00
Brandon LiandClaude Fable 5 6685a75864 Report and badge Personal DLL days
A stop-enabled account whose day booked red via its own stop shows an
amber "Personal DLL" badge on the dashboard (the firm's lockout keeps
its red "Hit DLL") and reports as personalDll in the day outcomes
instead of loss, per account type and per firm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 18:51:03 -05:00
Brandon LiandClaude Fable 5 c58e981b1e Place OCO stop brackets for the catalog's obfuscation options
When an account type opts in, legs are placed as an OCO pair via
order/placeoco - the usual limit plus a stop: the first cycle day's stop
keeps the day's net floor at -$1,000, min-day trades stop at 2x their
target, and the day after any red day rides stopless to make it back. A
stop-out books the day: eligibility refuses the account once realized
P&L falls short of the legs fired (or is simply negative on an opted-in
account, which survives restarts). If placeoco is rejected the leg falls
back to the classic lone limit with a loud log rather than leave a
position without an exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 18:28:21 -05:00
Brandon LiandClaude Fable 5 ae0f93a44f Honor the catalog's min-legs setting and carve $500 pieces
Big days (> the $1,700 split threshold) must carve at least the account
type's minLegs from the catalog — pickLegAmount reserves the minimum
piece for each leg still owed, forcing a carve even once the residual
dips below the threshold, clamped to what the pieces allow. Days at or
below the threshold still trade as one leg. The smallest split piece
drops from $800 to $500. An older master without the field means no
minimum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-11 15:32:40 -05:00
Brandon LiandClaude Fable 5 620ab54d42 Report each account's last traded trade date
Sessions roll at 5 PM CT, so after tonight's open a day booked on
tomorrow's trade date is already in the day count — lastDay is what lets
the master tell those accounts apart from ones that have not traded the
current session yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 18:51:59 -05:00
Brandon LiandClaude Fable 5 79ee5773cb Report each live account's payout state to the master
payoutStates: one entry per live account (negative included, dead not)
with its current-cycle progress — qualifying days, full equity profit,
max qualifying day, P&L sum, and the cycle's end-of-day high-water mark —
plus the withdrawal count and the cycle's effective target/consistency/
min-days from resolveEffectiveConfig. client.dailyPnL is already the
active-cycle slice, so a withdrawal resets the reported path on its own.
Feeds the master's payout calendar; an older master ignores the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 16:43:27 -05:00
Brandon LiandClaude Fable 5 abfb703f0e Keep negative and DLL-locked accounts out of the payout histogram
The payout calendar projects accounts still on the winning path, so the
daysHist now skips accounts below their starting balance or flipped to
liquidate-only, alongside the dead ones the report already drops. They
still count in the slice totals — only the calendar stops seeing them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 16:28:37 -05:00
Brandon LiandClaude Fable 5 6c501d00ed Report per-account-type days-traded histograms to the master
Adds daysHist ({daysTraded: accountCount}) to AccountTypeStats, computed
from the same combined-day view the trade path uses and covering every
live account including ones mid-trade. Feeds the master's payout
calendar; an older master simply ignores the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 16:21:17 -05:00
Brandon LiandClaude Fable 5 0d01539338 Report DLL-locked accounts as finished at a loss
An account Tradovate flips to liquidate-only at the daily loss limit is
done for the day, but the reporter only counted a loss when the booked
realizedPnL was already negative — so a locked-out account whose P&L
hadn't caught up (or that had no matching config) showed as still to
trade. Count inactive accounts as traded at a loss.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 02:18:01 -05:00
Brandon LiandClaude Fable 5 5d2f4d607a Seed leg splits per prefix group so like accounts split targets alike
Like accounts in a batch previously carved legs with independent
Math.random, so near-identical day targets could split very differently.
Leg fractions are now drawn from groupLegFraction(prefix, tradeDate,
legIndex) — FNV-1a seeded like dailyBump — so accounts of the same
account type on the same leg of the same day split identically when
their remaining targets match, and proportionally close otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 22:03:23 -05:00
Brandon LiandClaude Fable 5 3516e6c288 Add a bulk wipe & recalculate button to the dashboard
The per-account wipe works one account at a time; when every account is
carrying stale cached rows (the pre-roll dates again), that's a lot of
clicking. POST /api/accounts/wipe-all drops the cached rows for every
account on every firm and rebuilds them all from fresh reports — firms
in parallel, accounts within a firm at fetchDaysTraded's own pacing.
The toolbar button confirms inline with the same 28-day-window warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 21:36:36 -05:00
Brandon LiandClaude Opus 4.8 ef476ccbb7 Add day-plan double-count diagnostics
The daily target sometimes inflates to base + consistency×todayPnL (e.g.
1200 + 0.4×1218.48 = 1687.39), which happens when a session's cached
daily-P&L row is dated something other than the current tradeDate: it
then lands in BOTH the prior-day list and realizedPnL, so day-start
equity reads too low while the prior-day sum stays high.

Detect it with a book-balance invariant that is coincidence-proof (it
does not care whether prior and today P&L happen to be equal): the
reconstructed day-start equity (equityProfit − realizedPnL) must equal
priorProfit + sum(prior days). The state route now logs the full account
state whenever those disagree by more than $50, and GET /api/debug
reports the same per-account breakdown on demand (?account=<substring>)
so the exact inputs can be captured the moment a number looks wrong.

Read-only; no behavior change. Remove once the trigger is fixed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 21:28:39 -05:00
Brandon LiandClaude Fable 5 5a69ea2f64 Add a wipe & recalculate button to the account detail page
Cached daily P&L rows outlive what the Cash History report still
produces (stale pre-roll dates being the recent example), and nothing
deletes them any more. POST /api/accounts/[id]/wipe drops the account's
cached P&L, fund transactions and meta, clears the in-memory view, and
rebuilds from a fresh report — fetchDaysTraded now takes an optional
account filter so one account doesn't cost a report round-trip on every
other. The button confirms inline first: rows older than the report's
28-day window cannot be refetched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 21:27:34 -05:00
Brandon LiandClaude Fable 5 bd72c4b1f6 Fold weekend-dated P&L rows into the next settling day when combining
Cash History rows can carry a weekend calendar date (Sunday-evening
activity attributed pre-roll), and those slipped through the holiday
combining as their own trading day — accounts traded only across the
Labor Day Sun/Mon/Tue stretch read 2/5 instead of 1/5. Weekends never
clear at CME any more than holidays do, so treat Saturday and Sunday as
unsettled dates and roll them forward with the rest of the stretch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 21:23:12 -05:00
Brandon Li 3443fac75c Revert "Attribute evening-session Cash History rows to the next trade date"
This reverts commit 57846d2b5f.
2026-09-08 21:21:09 -05:00
Brandon LiandClaude Fable 5 f07721019b Count CME holiday sessions as one combined trading day when the type opts in
Account types with the catalog's new combineCmeHolidays flag are read
through a combined-day view: holiday-session P&L entries fold into the
next full trading day (computed from the federal-holiday rules, so any
year works), the day count matches how the firm records it (Apex: Sun/
Mon/Tue around a Monday holiday is ONE day), and on the later sessions
of the stretch the earlier sessions' booked P&L carries into realized —
so day targets, already-traded gates, target-hit status and the panel's
day P&L all treat the stretch as a single day. Off by default; older
masters that don't send the flag behave exactly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 21:12:37 -05:00
Brandon LiandClaude Fable 5 57846d2b5f Attribute evening-session Cash History rows to the next trade date
Cash History rows are stamped with the Central calendar date, but trades
in the evening session (>= 5:00 PM CT) belong to the next trade date -
matching currentTradeDateCT and when Tradovate resets realizedPnL. The
mismatch made computeDayPlan count the same session twice (as a phantom
prior day and as today's realized PnL), inflating the day target into a
moving one: 40% consistency accounts showed dayTarget = base + 0.4 x
today's PnL instead of the anchored day-start target.

Roll those rows forward at ingestion, and make a successful Cash History
report authoritative for its 28-day window so rows cached under the old
calendar date are dropped rather than double-counted. An empty report
never deletes anything (it can't distinguish "no trades" from "no
data"), and a report still pending after polling now throws instead of
masquerading as an empty one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 20:45:19 -05:00
Brandon LiandClaude Fable 5 77ae4971f8 Batch like accounts together by prefix before any mismatches
runTrade now draws each batch from a single account-type prefix group
(largest group first) instead of slicing across prefixes; a group's
remainder fires alone on a later tick. copyTrade fills open slots with
accounts matching the positioned accounts' prefix before mismatched ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 11:37:09 -05:00
Brandon LiandClaude Fable 5 48f3c97ec5 Report day outcomes (target hit / partial / loss) to the master
The reporter already computed targetHit per account to count it as
traded; now it also classifies each account that finished the day flat
and sends targetHit/partial/loss counts per firm and account type. A
realized loss reports as loss even on a grind day where any nonzero PnL
counts as done. Fields are additive, so an older master ignores them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 11:21:21 -05:00
Brandon LiandClaude Fable 5 346c356222 Report daily session-open delay to the master dashboard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 15:35:03 -05:00
Brandon LiandClaude Fable 5 906275cc33 Add random 1-20 min daily jitter to trading session open
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 15:26:30 -05:00
Brandon LiandClaude Fable 5 a36e227e74 Add per-account-type splitEnabled flag from the catalog
When the master marks an account type splitEnabled=false, big days trade
as one unsplit leg (the legacy one-and-done behavior); min-day bumps are
unaffected. Missing field — older master or pre-toggle cached catalog —
defaults to enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 12:15:08 -05:00
Brandon Li 3ef9486977 Place order lucid button 2026-09-03 13:20:35 -05:00
Brandon Li f8bc862753 Agree to terms 2 2026-09-03 13:17:22 -05:00
Brandon Li dece6999f0 Quick resolve account types 2026-09-03 13:13:32 -05:00
Brandon Li 9d4e7a36a7 Duplicate automations 2026-09-03 13:13:03 -05:00
Brandon Li 45f71cbecf add repeating to the tradeify dashboard 2026-09-03 13:04:43 -05:00
Brandon Li 722b70df07 Fixed tradeify buying 2026-09-03 12:23:01 -05:00
Brandon LiandClaude Opus 4.8 7d677dfa89 Click the terms checkboxes directly, not the required row
.validate-required is on every required checkout field, so the old selector's
first match was usually a text input, and on the terms row the click landed in
the empty half of a full-width <p> beside the box. Target the checkbox input in
a required row and gate on :not(:checked) so the two clicks tick box one then
box two instead of toggling the same box twice; the second is skipIfNotFound in
case a checkout has only one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 21:39:15 -05:00
Brandon Li 2d0c87b900 Added frame in wait for 2026-09-02 14:06:26 -05:00
Brandon Li 4373f09cca Fix id 2026-09-02 14:02:16 -05:00
Brandon Li 68cd6760da Fix selector to be button instead of input 2026-09-02 13:59:45 -05:00
Brandon Li 2eeb97e245 Wait for coupon button on lucid 2026-09-02 13:56:41 -05:00
Brandon LiandClaude Opus 4.8 b44416e8eb Default autostart update-check interval to 1 minute
The no-change path is a single `git fetch`, so polling master every minute
costs nothing and lines up with the VPS's own 60s deploy timer instead of
lagging it by up to five.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 13:38:59 -05:00
Brandon LiandClaude Opus 4.8 8aeb9cea81 Fix frame locate tripping the signed-out guard
A frame-targeted step resolved the element inside the checkout iframe and
reported that iframe's URL as the result `url`. The signed-out guard keys off
`url` to decide whether the tab is on the login page, and Lucid's checkout
iframe sits on the apex host (lucidtrading.com) that its signedOutPattern
matches — so a signed-in checkout looked signed out, ran the login step, and
failed with no login button present.

Report the TOP tab's URL as `url` (from pageFrameOffset), keeping the child
frame's URL as `frameUrl`. Extension 0.14.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 13:34:54 -05:00
Brandon LiandClaude Opus 4.8 4b7f30de41 Bump RUNNER_EXPECTED_VERSION to 0.19.0
The frame-aware step vocabulary only the 0.19.0 runner understands, so the
server must require it too. Missed in the previous commit, which left the
dashboard flagging the restarted 0.19.0 runner as out of date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 13:25:46 -05:00
Brandon LiandClaude Opus 4.8 66c99c5499 Add frame-aware locate for cross-origin iframe steps
Selectors were only resolved against the top document, so steps targeting
the Lucid WooCommerce checkout (rendered in a lucidtrading.com iframe under
the dash.lucidtrading.com app) never matched — e.g. input#coupon_code_field.

Add an optional `frame` on click/type/waitFor naming the <iframe> element.
The extension measures the iframe in the top frame (pageFrameOffset), finds
the element across child frames (pageLocate `relative` mode), and composes
the two into desktop coordinates; the Python side is unchanged since the
result keeps the same shape. Without `frame`, every path behaves as before.

Point the Lucid checkout steps at the frame and harden the coupon selectors
(the exact field id is still unconfirmed against the live iframe).

Extension 0.13.0, runner 0.19.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 13:22:10 -05:00
Brandon Li 016f121f40 Fix 2026-09-02 13:07:23 -05:00
Brandon Li f8de384bc8 fixex selector for coupon codel ucid 2026-09-02 13:04:24 -05:00
Brandon Li 172e67f931 Fixed id 2026-09-02 13:00:30 -05:00
Brandon Li 61ba3fd268 Fixed selector again 2026-09-02 12:56:31 -05:00
Brandon Li 7f8144436b Modify coupon code selectors for lucid 2026-09-02 12:49:36 -05:00
Brandon Li b336d0edd8 Push new lucid automation 2026-09-02 12:27:58 -05:00
Brandon LiandClaude Fable 5 a088381749 Store the person uuid the master issues, so renames keep our identity
The welcome now carries a personId; it lands in the instance_id setting
and rides on every hello and HTTP report from then on. The master keys
the person by that uuid, so changing instance_name moves our history to
the new name instead of starting a stranger. Read-only in Settings on
purpose — editing it by hand would detach this machine from its person.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 12:07:25 -05:00
Brandon LiandClaude Fable 5 de9cbffd83 Randomize daily targets: split big days into legs, bump min-days
Big days (> $1700) now trade as multiple separate legs of random size
(each >= $800, $5-rounded), resumable across pauses since the remaining
target is derived from realized PnL against a day-anchored target.
Min-day targets get a deterministic random $0-25 bump seeded per
(account, trade date) so the dashboard, reporter, and orders agree.
Strict-consistency accounts randomize only their first trade ever.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 11:32:48 -05:00
Brandon LiandClaude Fable 5 8350601cf0 Skip non-Tradovate firms until their trading logic exists
The master's catalog now labels each firm with a platform (TopstepX being
the first non-Tradovate one). Sync accepts the field, defaulting anything
missing or unrecognized to tradovate so an older master still validates,
and getTradableFirms only hands Tradovate firms to the client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 11:24:24 -05:00
Brandon LiandClaude Opus 5 3ce14d4928 Sink firms with no login to the bottom, and name the account type plainly
A firm with no stored Tradovate login can do nothing until someone enters
one, so it no longer interleaves by name with the firms that are actually
running. Sorted at render rather than in /api/firms, which keeps serving the
catalog in the master's own order. Name order is preserved within each group.

The account-type picker showed "Auto — Flex 50k Funded", which put the
mechanism before the answer in the one place the answer is what you are
reading. It shows the type's name now, or Unassigned when nothing matched.
That first entry still clears a manual pin; the dot beside the select is what
says a pin is in effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 23:04:48 -05:00
Brandon LiandClaude Opus 5 8bdc85080c Take firms from the master instead of defining them per machine
This box no longer decides what firms exist. It caches the master's catalog
and stores one thing of its own: the Tradovate login for each firm. Creating
firms locally is gone — app/add, POST /api/firms, the account-config routes
and the banned-symbol route with it.

The cache is the point, not an optimisation. The scheduler needs every
account's target and size on each 60s tick, so fetching them per tick would
stop trading with positions open the first time the master rebooted. It is
one JSON blob in settings, replaced only by a complete valid response, and a
response with zero firms is refused outright: a misconfigured FIRMS_DB serves
{"firms":[]} as a healthy 200, and mirroring that would idle the whole fleet
at once.

Accounts bind to an account type by prefix AND size, the size taken from the
opening funding transaction. That does not arrive with the first sync, so an
account sits Unassigned and the resolver tries again next sync rather than
guessing. A type picked by hand is never overwritten by an auto pass. This
replaces the longest-prefix helper that had been copy-pasted into five files,
which could not tell two types with the same prefix apart at all.

firm_credentials and account_assignments deliberately have no foreign key to
the cached catalog. foreign_keys is ON, so a cascade would erase the Tradovate
passwords the moment a firm was removed on the master — routine, not
exceptional. They survive it, and are re-keyed by name if the master's database
is ever rebuilt with fresh ids.

The legacy firms/account_configs/firm_banned_symbols tables are left on disk,
unreferenced. firms is the only table that ever held plaintext credentials and
dropping it would make a rollback unrecoverable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 22:17:23 -05:00
Brandon LiandClaude Opus 5 8b2e78d766 Set Chrome as default and pin it, as far as Windows allows
Neither can be done for the running profile: UserChoice is hash-validated and
guarded by UCPD.sys since Feb 2024, and taskbar pinning lost its shell verb
years ago - both to stop installers helping themselves. What is supported is
policy, which applies when a profile is created, so this writes the
DefaultAssociationsConfiguration and taskbar layout XMLs for the next user and
does the two things that help the current one: a desktop shortcut, and opening
the Settings page where the browser choice actually has to be made.

Folded into the enrolment script's existing elevation so the install still
raises a single UAC prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:17:42 -05:00
Brandon LiandClaude Opus 5 458d667ca6 Linux needs no enrolment - it never did
The managed-device requirement is documented for Windows and macOS only, and a
Pi proved it: aarch64 Chromium fetched updates.xml with installedby=policy and
installed the CRX from a plain /etc policy file, with no enrolment, no Google
account and no Chrome. So setup-linux.sh writes the policy directly for
whichever browsers are present, and enrolment is a Windows concern again.

That also retires the ARM special case. Chromium could not be enrolled, which I
read as Chromium not being able to install this at all; the two are unrelated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:31:18 -05:00
Brandon LiandClaude Opus 5 d14bcdeed3 A Pi gets the unpacked path, not a dead end
Chrome is x86_64 only on Linux and Chromium cannot be enrolled, so on ARM the
enrolment step can never succeed. Removing load-unpacked from the installer
left those machines being told to run a script that would refuse - setup now
detects the architecture, explains why, and the summary points at the unpacked
install, which on that hardware is not a fallback but the only thing that
works. It still self-updates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:18:14 -05:00
Brandon LiandClaude Opus 5 61d6305ded Download Chrome with curl.exe, then BITS, then Invoke-WebRequest
Invoke-WebRequest pushes every buffer through the pipeline and is the slowest
of the three by a wide margin; curl.exe ships in System32 on every supported
Windows and resumes a partial file with -C -, which matters on a 166 MB
download that gets interrupted. Also checks the finished file against
Content-Length, because a truncated MSI fails in msiexec with an error that
says nothing about why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:16:45 -05:00
Brandon LiandClaude Opus 5 f52efd73c6 Correct the Chrome download sizes
The Windows enterprise MSI is 166 MB, not 120.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:11:57 -05:00
Brandon LiandClaude Opus 5 7e82a294fa Turn off the progress bar while downloading Chrome
Windows PowerShell re-renders progress on every buffer, so a 120 MB download
spends most of its time drawing a bar rather than transferring - minutes for
what should be seconds, and indistinguishable from a stall while you watch the
byte count crawl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 17:10:39 -05:00
Brandon LiandClaude Opus 5 389c714caf Bake in the enrolment token, and install Chrome when missing
Setup asked for a token that is the same on every machine - a token is reusable
and enrols any number of browsers - so it is a default in both scripts now and
step 7 is a plain yes/no. It is a modest secret: it lets someone enrol their own
browser into the org and nothing more, and rotating it is a console click and a
one-line change.

Both scripts also install Chrome if it is absent, from Google's enterprise MSI
and the official .deb, since enrolling a browser that is not there writes a
token nothing reads. Chromium is refused rather than substituted: no cloud
management, so it could never be enrolled - and on ARM, where Chrome is not
published at all, the Linux script says so instead of installing something that
cannot do the job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 16:18:20 -05:00
Brandon LiandClaude Opus 5 35dd15d584 Enrol Chrome from the installers, and drop load-unpacked
Both setup scripts ended by telling you to load the folder unpacked, which is
the one install that cannot update itself - and the policy install they moved
to next does nothing on an unmanaged machine. Enrolment is the step that
actually matters: with it, the extension arrives from the Admin console already
force-installed, and there is nothing else to configure on the box.

So step 7 now asks for an enrolment token instead. The Windows script elevates
itself for it - the token is machine-scoped, and that is the only UAC prompt in
the whole install.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 16:14:14 -05:00
Brandon LiandClaude Opus 5 95c073d4c2 Publish the CRX from a timer, and refuse an unbumped version
Mirrors the autotrader stack's auto-deploy: fetch every minute, act when master
moves, lock against overlap, rewrite .last-check every run so a silent failure
is visible. Only extension/ and deploy/ changes publish - repacking for a docs
edit would republish an identical CRX every minute.

The guard is the part that earns its place. Chrome ignores a CRX that is not
strictly newer, so shipping extension changes without bumping manifest.json is
a no-op that looks exactly like success until someone notices the fix never
arrived. That now fails loudly instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 16:10:21 -05:00
Brandon LiandClaude Opus 5 56114b727c dsregcmd cannot see Chrome Enterprise Core
The managed check asked Windows, but Enterprise Core is browser management by
Google: Windows reports a plain standalone machine however enrolled Chrome is.
The guard therefore blocked the exact path it recommends. It now asks Chrome
first - the dmtoken it writes on a successful enrolment, and the enrolment
token that requests one, in both registry views - and only then falls back to
domain and Entra join state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:55:27 -05:00
Brandon LiandClaude Opus 5 a103d7b6f1 Script the Chrome Enterprise Core enrolment
Enrolling is what lifts the off-store restriction without joining a domain, and
it is one machine-scoped token in both places Chrome looks: HKLM on Windows, a
file under /etc/opt/chrome/policies/enrollment on Linux. Both scripts take
--remove, and both say plainly that removing the token locally does not release
the seat in the Admin console.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 15:44:16 -05:00
Brandon LiandClaude Opus 5 ce6fa6c0e1 The managed-machine rule is Chromium's, not Edge's
Chrome blanks an off-store ExtensionInstallForcelist entry on an unmanaged
machine exactly as Edge does - the qualifying states are AD domain join, Entra
join, or Chrome Enterprise Core enrolment, and this box is none of them. The
guard now covers every browser and refuses up front, with the three ways
forward, instead of skipping Edge alone and letting Chrome fail the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:30:04 -05:00
Brandon LiandClaude Opus 5 e42e729189 Skip Edge on unmanaged devices instead of writing a dead policy
Unmanaged Edge installs extensions from the Microsoft Store and nowhere else:
it reads ExtensionInstallForcelist, blanks the value to [BLOCKED]<id>, and
reports an invalid extension id forever after. Writing that key buys nothing
and costs a permanent policy error plus a "Managed by your organization"
badge, so the script now checks dsregcmd and skips Edge with the two ways
round it. -Force writes it anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:25:39 -05:00
Brandon LiandClaude Opus 5 8f00d8c547 Find Edge and Chrome where they actually install
Test-BrowserInstalled probed App Paths under HKLM:\SOFTWARE only. Edge and
Chrome ship 32-bit installers, so on a 64-bit box their App Paths entries live
under WOW6432Node and a 64-bit PowerShell never saw them — the browser was
skipped, silently, and -Browser All configured nothing for it.

Checks the install directories first now, with App Paths (both registry views)
as a backstop, and prints which browsers it is configuring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:17:46 -05:00
Brandon LiandClaude Opus 5 a55ba1ccfc Let the TLS bootstrap past a stale local resolver
The droplet kept a cached answer for the new hostname long after the record
was live everywhere else, and the DNS check is a courtesy — Let's Encrypt
resolves the name itself. SKIP_DNS_CHECK=1 skips it, matching the autotrader
stack's script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:14:42 -05:00
Brandon LiandClaude Opus 5 b820168afa Configure Edge and Brave too, not just Chrome
They are Chromium: same CRX, same update manifest, same policy — only the
registry vendor key differs. The script now defaults to every one of the three
it finds installed, which is what the Linux counterpart already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:08:29 -05:00
Brandon LiandClaude Opus 5 b9da419ebc Install the extension from the update server during setup
Both installers ended by telling you to load the folder unpacked, which is the
one install that can never update itself. They now offer the policy install as
a last step instead, and only fall back to those instructions when it is
declined or fails.

Windows writes to the user's own policy hive rather than HKLM, so the step
needs no elevation and no separate admin shell; -Scope Machine is still there
for every-account installs. Linux gets the counterpart script — a managed
policy file under /etc, which is root-only because Chrome reads policy from
nowhere else on that platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:06:29 -05:00
Brandon LiandClaude Opus 5 035f93d220 Ship the extension as a signed CRX from our own update server
The dashboard-hosting stack from the previous commit is gone; it was answering
a question nobody asked. What was actually needed is Chrome's real update
channel, which an unpacked extension does not have and cannot be given.

So the extension is now signed into a CRX with an update manifest beside it,
served from the droplet, and installed on the trading machines by policy
(ExtensionInstallForcelist). From there Chrome installs it, updates it on its
own schedule, and puts it back if it goes missing. The manifest gained the
`key` that fixes the extension id, so an unpacked development load keeps the
same identity as the published build.

The self-update check now branches on how it was installed: policy installs ask
Chrome to check the update server now rather than waiting out its five-hour
cycle, and unpacked loads keep the reload-from-disk path. chrome.management
.getSelf() tells the two apart and needs no permission to do it.

The signing key lives at /opt/autofirmer-updates/key.pem on the droplet and
nowhere else. It is the extension's identity: a different key is a different
extension, and no browser would take it as an update.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:50:01 -05:00
Brandon LiandClaude Opus 5 ece6aab9cf Plain-JS Next config so the image boots without devDependencies
next start stops to install TypeScript when the config is a .ts file, which in
the container meant a yarn install into node_modules on every boot — slow
enough that the health check failed before the server answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:38:52 -05:00
Brandon LiandClaude Opus 5 d1e8668440 Let the extension update itself, and host the dashboard
Chrome has no update channel for an unpacked extension, so every pull left one
manual step: click reload on its card. The dashboard now publishes the version
its own checkout ships at /api/autobuyer/status, and the extension reloads
itself when it is behind — chrome.runtime.reload() re-reads the folder from
disk, which the update check has already pulled into. It only ever runs code
that is on disk, so a reload that changes nothing is reported and waited out
rather than repeated; a dashboard that is behind is ignored.

Also adds the deploy stack for running the dashboard on the droplet, next to
the autotrader one: it joins that nginx's network and renders a server block
into its conf.d rather than standing up a second ingress. AUTOFIRMER_DB moves
the SQLite file onto a volume, since the working directory in a container is
replaced on every deploy.

And waitFor gained `hidden`: an absent gate matched on DOM presence only, so a
modal that closes by going display:none could never satisfy it. The extension
grew a visibility probe for this — locate cannot answer it, because absent and
hidden are both errors there and a wait gate has to tell them apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 12:30:59 -05:00
Brandon LiandClaude Opus 5 1f4459985a Extension to 0.11.2
background.js changed with the widget-targeting fix, so the version has to
move with it: the AutoBuyer page now shows the version each capture was sent
by, and an unchanged number is how you tell a reload did not take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:47:00 -05:00
Brandon LiandClaude Opus 5 dfa706c7b7 Target the captcha widget the open modal is actually showing
Second reset still failed: token solved, callback reported as fired, confirm
button never enabled. The button enabling is pure client-side state — the
site's callback storing the token — so if it stays dark the LIVE callback
never ran, whatever was reported. Picking the newest client was still a guess.

There is a precise link. reCAPTCHA numbers its response fields by widget id:
the first widget gets #g-recaptcha-response, the second
#g-recaptcha-response-1. The field the open modal is showing therefore names
the client holding its callback. Use that, fall back to newest-first when
there is no id to go on.

The run log now prints which field carried the token and which widgets were on
the page, so the next failure is diagnosable from the log instead of by
hypothesis: `filled 1 response field(s) ['g-recaptcha-response-1'], fired
grecaptcha callback (widget 1)` / `page had widgets ['0','1'], live one looked
like 1`.

Also surfaces the extension version on the AutoBuyer page. The extension has
always stamped it into every capture and nothing ever read it, so the README's
claim that a version mismatch shows there was no way to check. Chrome runs the
old service worker until an unpacked extension is reloaded by hand, which is
exactly the state that makes a deployed fix look like a broken one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:46:31 -05:00
Brandon LiandClaude Opus 5 2d53639c82 Hand the token to the live captcha widget, not a dead one
First live run reset one account, then failed on the second: the confirm
button never enabled and the step reported it covered by a bare `div`.

reCAPTCHA adds a client to ___grecaptcha_cfg.clients per rendered widget and
removes none, so the second modal leaves two — and the callback walk fired the
first it found, the one belonging to the modal that already closed. The token
goes to a widget nobody is listening to, the live modal never hears it was
solved, and its button stays dark. Iteration 1 worked only because there was
one client to find. Now the newest wins: hook widgets are walked in reverse,
clients sorted by descending key, and the count of candidates is reported so
`2 widgets on the page, used the newest` says plainly when a page is stacking
stale ones.

Also from that run:

- coveredBy printed `div` and nothing else, which diagnoses nothing. It only
  ever reported tag and id, and modal backdrops and captcha overlays carry
  neither. It now names classes too.
- A covered target gets 8s to clear before the step fails. Most things over a
  button are on their way out — a backdrop mid-fade, an overlay closing. This
  does not weaken the check: still covered at the deadline still fails, and
  nothing is clicked while anything is on top of it.
- The reset list's scroll container is confirmed as a class, so the
  class-or-id hedge is trimmed. The run reported it as
  `dashboard_body MuiBox-root mui-0`.

Extension to 0.11.1 so the dashboard can confirm the reload took. Runner
VERSION deliberately unchanged — the step vocabulary is the same, and a bump
would raise the mismatch banner for a change that does not need it. The
runner does still need restarting to pick up the covered-retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:56:47 -05:00
Brandon LiandClaude Opus 5 5538215650 Load every failed account before the Tradeify reset loop
The account list loads a screenful at a time, so a reset count larger than the
first page ran out of rows to click.

Counts the reset buttons rather than the rows, so the run log reports the
number that answers the question — are there enough accounts to reset `count`
times. A stretch of non-resettable rows does not stop it early: `stable` only
advances when the scroller was ALREADY at the bottom and nothing new matched,
and every appended batch pushes scrollHeight past scrollTop + clientHeight.
Simulated at 100 rows with the resettables in the middle, at the bottom, and
at the top only — all four layouts walk the whole list.

The container is named rather than guessed. The list scrolls inside
dashboard_body, not the document, and setting scrollTop on the wrong element
scrolls nothing at all — which fails as a low match count rather than an
error. Spelled as a selector list because the live markup is unconfirmed:
querySelector takes either form, and matching neither fails loudly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:41:30 -05:00
Brandon LiandClaude Opus 5 159389be35 Solve captchas through the 2captcha API, not its extension
The two Tradeify automations clicked the 2captcha *extension's* solve button
and waited for it to go green. That needed a second extension installed and
signed in, put a real mouse click on a button whose markup is 2captcha's to
change, and gave a run no way to tell "still solving" from "not installed".

Replaced with one `solveCaptcha` step. No mouse: the extension reads the
widget's sitekey out of the page, the dashboard buys a token from 2captcha,
and the extension puts it in the response field and fires the page's own
callback.

captcha-hook.js is the part that earns its keep. A widget rendered
programmatically puts nothing in the DOM — no sitekey, and no callback. Most
pages only enable submit from that callback, so filling the hidden field alone
leaves the page looking unsolved. The hook wraps turnstile/grecaptcha/hcaptcha
`render` at document_start to hold on to what the page passed.

Solves go through 2captcha's residential proxy. Its API rejects host names in
proxyAddress, so na.proxy.2captcha.com is resolved to an IP first (cached);
the gateway was confirmed to work addressed that way.

Covers reCAPTCHA v2 (incl. invisible and Enterprise), hCaptcha and Turnstile.
NOT reCAPTCHA v3: a v3 page carries an anchor iframe with k=, so it is
detected as v2 and would be solved with the wrong task type. Everything else
unsupported fails loudly instead.

The API key and proxy credentials are hardcoded in the captcha route on
request. Unlike the sqlite database, that file is in the repo, so they travel
with it. TWOCAPTCHA_API_KEY / TWOCAPTCHA_PROXY and the settings rows still
override; neither has a field on the Settings page.

Runner VERSION and RUNNER_EXPECTED_VERSION both to 0.17.0 — they are a pair,
and a mismatch shows as a red "restart your runner" banner that blames the
wrong process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:18:00 -05:00
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