Commit Graph
16 Commits
Author SHA1 Message Date
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 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 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 0ba27161ce Record a heartbeat so a silent update checker is distinguishable from a dead one
There was no way to tell "running, nothing to pull" from "not running". The
no-change path logs nothing by design, and PM2 reports a cron-restart process
as `stopped` with ↺ 0 even while firing on schedule — I verified that against
PM2 7.0.4: a one-minute cron fired four times without the counter moving once.

scripts/.last-check is now rewritten on every run with the outcome. It is a
single overwritten line, so it needs no trimming.

The outcome is set by each exit path and written once in `finally`, rather than
calling record at each return — the first version of this did the latter and
already missed the build-failure path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 19:43:43 -05:00
Brandon LiandClaude Opus 5 c5c946d3cc Add Linux/Raspberry Pi setup and port the clicker to X11
The clicker had no Linux support at all: pygetwindow has no X11 backend, so
_other_activate returned "window management is unsupported" and every step
failed before it could click. focus.py now has a third backend that raises the
browser with xdotool and reads the focused window's WM_CLASS to verify it came
forward - the same activate-then-confirm shape as the macOS and Windows paths.

setup-linux.sh mirrors setup-windows.bat with apt instead of winget. Three
things are specific to this platform rather than incidental:

- Node comes from NodeSource. Pi OS ships one too old for Next 16, and the LTS
  line is also what better-sqlite3 publishes prebuilt arm64 binaries for.
- Python dependencies go in a virtualenv. Pi OS Bookworm enforces PEP 668, so
  pip into the system interpreter fails with externally-managed-environment.
- Autostart uses an XDG ~/.config/autostart entry, the direct analogue of the
  Windows Startup folder: no sudo, and it runs inside the graphical session,
  which the clicker needs for DISPLAY.

Wayland is called out in four places - the session guard, diagnose.py, the
installer and both READMEs - because Pi OS on a Pi 5 defaults to it and the
clicker simply cannot work there. Wayland does not let one client synthesise
input into another, so this is a switch-to-X11 situation, not a bug to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:53:25 -05:00
Brandon LiandClaude Opus 5 be8c628778 Self-trim update.log
Capped at 256 KB, trimmed back to the newest 500 lines. Done once per run
before anything is written: the no-change path logs nothing, so that is the
only point at which the file can have grown since the last run. Failures are
swallowed - housekeeping must never be the reason an update does not land.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:40:19 -05:00
Brandon LiandClaude Opus 5 724ba5581c Drop Task Scheduler; autostart without administrator rights
Register-ScheduledTask failed with Access denied (0x80070005). Writing to the
root task folder needs elevation, so the claim that this install needed no
admin was simply wrong. Rather than demand UAC, use two mechanisms that need
no privileges at all:

- a Startup-folder entry (AutoFirmer.cmd) runs start-all.bat at logon
- PM2's own --cron-restart with --no-autorestart drives the 5-minute update
  check, so PM2 owns the schedule it was already going to resurrect anyway

Both still run in the logged-in interactive session, which is the requirement
that ruled out a Windows service in the first place: the clicker sends real
input and needs a desktop.

pm2 save now runs after the updater is registered, so `pm2 resurrect` brings
back all three processes rather than two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:24:47 -05:00
Brandon LiandClaude Opus 5 7e4714c33d Stop PM2's stderr from aborting the autostart installer
`pm2 delete autofirmer` on a machine that has never registered it writes
"[PM2][ERROR] Process or Namespace autofirmer not found" to stderr. PM2 ships a
PowerShell shim, and under $ErrorActionPreference = 'Stop' any native stderr
becomes a terminating NativeCommandError - so the routine "remove it if it is
there" line killed the install before a single task was registered. The 2>$null
at the call site was useless: the error is raised inside pm2.ps1.

Route every pm2 and npm call through helpers that drop to 'Continue' and judge
by $LASTEXITCODE. npm had the same latent problem, since its warnings also go
to stderr.

Also start Next directly instead of via `npm start`. On Windows npm is a .cmd
shim, so PM2 was supervising the shim while the real server ran as its child -
restarts and stops would have missed the process that actually matters.

The file is now pure ASCII. PowerShell 5.1 reads a non-BOM UTF-8 script as
ANSI, so the box-drawing characters in the comments were a latent hazard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 18:21:59 -05:00
Brandon LiandClaude Opus 5 4288d8c298 Auto-start both processes at logon and self-update from master
Instances were started by hand and updated by hand, so they drifted behind
master silently. Now: PM2 supervises the dashboard and the clicker, a logon
task brings them up, and a 5-minute task pulls, rebuilds and restarts when
master moves.

Restarting on every push is only safe because the scheduler now survives it.
It was pure in-memory state (_global.__autoTrader), so any restart silently
stopped automated trading with the dashboard simply showing it as off. It now
mirrors running/action/symbol/stopAfterAll to the settings table, and
resumeSchedulerIfPersisted() picks it back up from the getClients() bootstrap.
No sync-wait was needed there: tick() already skips while a client reports
!syncComplete and while any account holds a position.

A failed build is never deployed — the build runs before anything restarts, so
a broken push leaves the previous build serving.

start-all and update-check both warm the app with a request afterwards. That is
load-bearing: getClients() is lazily bootstrapped, so until something makes an
HTTP request the Tradovate clients, the reporter and the resumed schedule never
start. That was already true of manual restarts.

Logic lives in Node so a macOS or Linux port only needs an equivalent of
install-autostart.ps1. Python deps are hash-guarded, so the common path is one
hash and one import with no network, and failure is non-fatal since only the
clicker needs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 17:36:25 -05:00
Brandon LiandClaude Opus 5 9bdd20f83a Add Windows setup script for new instances
Standalone batch file: clones (or pulls), installs, builds, seeds the
reporter settings, and writes a start-autofirmer.bat. Prompts for the master
dashboard URL and instance name, defaulting to %COMPUTERNAME%.

Two install hazards it handles:
- better-sqlite3 has no prebuilt binary above Node 22, so install silently
  falls back to node-gyp and dies without Visual Studio Build Tools. The
  script warns first and names the fix if install fails anyway
- playwright is declared but referenced nowhere in the source, so its
  postinstall browser download is skipped

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 16:16:06 -05:00