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
-19
@@ -16,14 +16,16 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync,
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ensurePythonDeps } from './ensure-python-deps.mjs';
|
||||
import { loadInstances } from './instances.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const LOG_FILE = path.join(ROOT, 'scripts', 'update.log');
|
||||
const LOCK_FILE = path.join(ROOT, 'scripts', '.update.lock');
|
||||
const LAST_CHECK_FILE = path.join(ROOT, 'scripts', '.last-check');
|
||||
const BRANCH = process.env.AUTOFIRMER_BRANCH ?? 'master';
|
||||
const PORT = process.env.AUTOFIRMER_PORT ?? '3000';
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
// One checkout, one build — but possibly several PM2 processes to restart and
|
||||
// warm afterwards (see scripts/instances.mjs).
|
||||
const INSTANCES = loadInstances();
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
const STALE_LOCK_MS = 60 * 60 * 1000;
|
||||
const LOG_MAX_BYTES = 256 * 1024;
|
||||
@@ -126,7 +128,7 @@ async function main() {
|
||||
changed.includes('package-lock.json') && 'npm install',
|
||||
changed.includes('clicker/requirements.txt') && 'pip install',
|
||||
'npm run build',
|
||||
'pm2 restart autofirmer',
|
||||
...INSTANCES.map((i) => `pm2 restart ${i.name}`),
|
||||
changed.some((f) => f.startsWith('clicker/')) && 'pm2 restart clicker',
|
||||
].filter(Boolean).join(', ')}`);
|
||||
return 0;
|
||||
@@ -153,8 +155,10 @@ async function main() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
log('restarting autofirmer');
|
||||
if (run('pm2', ['restart', 'autofirmer']).status !== 0) { log('pm2 restart autofirmer failed'); outcome = 'pm2 restart failed'; return 1; }
|
||||
for (const instance of INSTANCES) {
|
||||
log(`restarting ${instance.name}`);
|
||||
if (run('pm2', ['restart', instance.name]).status !== 0) { log(`pm2 restart ${instance.name} failed`); outcome = 'pm2 restart failed'; return 1; }
|
||||
}
|
||||
|
||||
if (changed.some((f) => f.startsWith('clicker/'))) {
|
||||
log('clicker changed — restarting it too');
|
||||
@@ -178,21 +182,29 @@ async function main() {
|
||||
}
|
||||
|
||||
async function warmUp() {
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
for (const instance of INSTANCES) {
|
||||
const base = `http://127.0.0.1:${instance.port}`;
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
if ((await fetch(`${base}/api/auto-trade`, { signal: AbortSignal.timeout(5_000) })).ok) break;
|
||||
} catch { /* still restarting */ }
|
||||
await new Promise((r) => setTimeout(r, 2_000));
|
||||
}
|
||||
try {
|
||||
if ((await fetch(`${BASE}/api/auto-trade`, { signal: AbortSignal.timeout(5_000) })).ok) break;
|
||||
} catch { /* still restarting */ }
|
||||
await new Promise((r) => setTimeout(r, 2_000));
|
||||
}
|
||||
try {
|
||||
await fetch(`${BASE}/api/state`, { signal: AbortSignal.timeout(60_000) });
|
||||
const status = await (await fetch(`${BASE}/api/auto-trade`)).json();
|
||||
log(status.running ? `scheduler resumed — ${status.action} ${status.symbol}` : 'scheduler is stopped');
|
||||
const runner = await (await fetch(`${BASE}/api/autobuyer/runner`)).json();
|
||||
log(`runner ${runner.online ? 'online' : 'OFFLINE'}${runner.stale ? ' (version stale — reload the extension)' : ''}`);
|
||||
} catch (err) {
|
||||
log(`post-restart check failed: ${err.message}`);
|
||||
await fetch(`${base}/api/state`, { signal: AbortSignal.timeout(60_000) });
|
||||
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`);
|
||||
// The clicker/extension runner only ever serves instance 1.
|
||||
if (instance.port === 3000) {
|
||||
const runner = await (await fetch(`${base}/api/autobuyer/runner`)).json();
|
||||
log(`runner ${runner.online ? 'online' : 'OFFLINE'}${runner.stale ? ' (version stale — reload the extension)' : ''}`);
|
||||
}
|
||||
} catch (err) {
|
||||
log(`${instance.name}: post-restart check failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user