9 Commits
Author SHA1 Message Date
Brandon LiandClaude Fable 5 ba8bd50da0 Backups push automatically; the token is only for restore
Matches the master's tokenless-push change: pushBackup no longer waits
for master_backup_token — every instance with a uuid backs up on its
own, and the 5-minute tick force-pushes so a poisoned master copy
self-heals. The token now only gates pulling a backup back (restore),
so setup-linux.sh asks for it only when restoring and the Settings
help text says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-22 14:08:03 -05:00
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 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 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 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 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 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