Files
autofirmer-expanded/scripts/start-all.mjs
T
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

78 lines
3.0 KiB
JavaScript

/**
* Bring the device up: clicker dependencies, PM2 processes, then a warm-up of
* every registered instance.
*
* Run by the "at log on" scheduled task. Safe to run by hand at any time.
*
* The warm-up is not cosmetic. getClients() in lib/clients.ts is lazily
* bootstrapped — Tradovate clients, the contract resolver, the reporter and the
* persisted-schedule resume all start inside it — so until something makes an
* HTTP request the process sits idle and none of that happens.
*/
import { spawnSync } from 'node:child_process';
import { ensurePythonDeps } from './ensure-python-deps.mjs';
import { ROOT, loadInstances } from './instances.mjs';
const log = (msg) => console.log(`[start-all] ${msg}`);
function run(cmd, args) {
return spawnSync(cmd, args, {
cwd: ROOT,
encoding: 'utf8',
stdio: 'inherit',
shell: process.platform === 'win32',
});
}
async function waitForApp(base, timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const res = await fetch(`${base}/api/auto-trade`, { signal: AbortSignal.timeout(5_000) });
if (res.ok) return true;
} catch { /* not up yet */ }
await new Promise((r) => setTimeout(r, 2_000));
}
return false;
}
// ── clicker dependencies (non-fatal) ────────────────────────────────────────
ensurePythonDeps({ log });
// ── PM2 ─────────────────────────────────────────────────────────────────────
log('restoring PM2 processes');
if (run('pm2', ['resurrect']).status !== 0) {
log('pm2 resurrect failed — is PM2 installed and has `pm2 save` been run?');
process.exit(1);
}
// ── warm-up, one instance at a time ─────────────────────────────────────────
let failed = false;
for (const instance of loadInstances()) {
const base = `http://127.0.0.1:${instance.port}`;
log(`waiting for ${instance.name} to answer on ${base}`);
if (!(await waitForApp(base))) {
log(`${instance.name} did not come up on ${base} within 120s — check \`pm2 logs ${instance.name}\``);
failed = true;
continue;
}
log(`warming up ${instance.name} (this is what triggers the client bootstrap)`);
try {
await fetch(`${base}/api/state`, { signal: AbortSignal.timeout(60_000) });
} catch (err) {
log(`warm-up request failed: ${err.message}`);
}
try {
const status = await (await fetch(`${base}/api/auto-trade`)).json();
log(status.running
? `${instance.name}: scheduler resumed — ${status.action} ${status.symbol}`
: `${instance.name}: scheduler is stopped`);
} catch { /* non-critical */ }
}
if (failed) process.exit(1);
log('up');