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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
97947bffde
commit
8154f25bb9
+31
-28
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Bring an instance up: clicker dependencies, PM2 processes, then a warm-up.
|
||||
* 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.
|
||||
*
|
||||
@@ -9,13 +10,8 @@
|
||||
* HTTP request the process sits idle and none of that happens.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ensurePythonDeps } from './ensure-python-deps.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const PORT = process.env.AUTOFIRMER_PORT ?? '3000';
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
import { ROOT, loadInstances } from './instances.mjs';
|
||||
|
||||
const log = (msg) => console.log(`[start-all] ${msg}`);
|
||||
|
||||
@@ -28,11 +24,11 @@ function run(cmd, args) {
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForApp(timeoutMs = 120_000) {
|
||||
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) });
|
||||
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));
|
||||
@@ -50,25 +46,32 @@ if (run('pm2', ['resurrect']).status !== 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── warm-up ─────────────────────────────────────────────────────────────────
|
||||
log('waiting for the dashboard to answer');
|
||||
if (!(await waitForApp())) {
|
||||
log(`dashboard did not come up on ${BASE} within 120s — check \`pm2 logs autofirmer\``);
|
||||
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 */ }
|
||||
}
|
||||
|
||||
log('warming up (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
|
||||
? `scheduler resumed — ${status.action} ${status.symbol}`
|
||||
: 'scheduler is stopped');
|
||||
} catch { /* non-critical */ }
|
||||
|
||||
if (failed) process.exit(1);
|
||||
log('up');
|
||||
|
||||
Reference in New Issue
Block a user