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>
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>
.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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>