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>
78 lines
3.0 KiB
JavaScript
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');
|