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:
Brandon Li
2026-09-22 13:55:35 -05:00
co-authored by Claude Fable 5
parent 97947bffde
commit 8154f25bb9
25 changed files with 1396 additions and 76 deletions
+38 -1
View File
@@ -1,9 +1,22 @@
import Database from 'better-sqlite3';
import { mkdirSync } from 'fs';
import path from 'path';
const db = new Database(path.join(process.cwd(), 'autotrader.sqlite'));
// AUTOFIRMER_DB lets several instances share one checkout, each with its own
// database. It is read once, at module load — schema creation and seeding are
// import side effects — so it must be in the environment at process start.
const DB_PATH = process.env.AUTOFIRMER_DB
? path.resolve(process.env.AUTOFIRMER_DB)
: path.join(process.cwd(), 'autotrader.sqlite');
mkdirSync(path.dirname(DB_PATH), { recursive: true });
const db = new Database(DB_PATH);
db.pragma('foreign_keys = ON');
// If two processes are ever misconfigured onto the same file, WAL + a busy
// timeout turns SQLITE_BUSY crashes into brief contention.
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
db.exec(`
CREATE TABLE IF NOT EXISTS instruments (
@@ -123,6 +136,11 @@ export function listAssignments(firmId: string): AssignmentRow[] {
return db.prepare('SELECT * FROM account_assignments WHERE firm_id = ?').all(firmId) as AssignmentRow[];
}
/** Every assignment, for the backup pushed to the master. */
export function listAllAssignments(): AssignmentRow[] {
return db.prepare('SELECT * FROM account_assignments').all() as AssignmentRow[];
}
export function setAssignment(
accountId: number,
firmId: string,
@@ -179,12 +197,31 @@ seedSetting.run('twocaptcha_api_key', '');
// Blank falls back to the proxy hardcoded in app/api/autobuyer/captcha; `none`
// solves without one.
seedSetting.run('twocaptcha_proxy', '');
// Bearer for pushing/pulling the encrypted settings+credentials backup on the
// master (its INSTANCE_BACKUP_TOKEN). Blank = backups off.
seedSetting.run('master_backup_token', '');
// Routes broker traffic (TopstepX, Tradovate, Yahoo quotes) through a proxy:
// http(s):// or socks5(h)://, user:pass ok. The master link stays direct on
// purpose. Blank = direct. Takes effect on restart.
seedSetting.run('proxy_url', '');
// Mirrors the in-memory scheduler state so a restart can resume it.
seedSetting.run('scheduler_running', '0');
seedSetting.run('scheduler_action', 'Buy');
seedSetting.run('scheduler_symbol', 'NQ');
seedSetting.run('scheduler_stop_after_all', '0');
/** Wraps several writes in one transaction (used by the backup restore, so a
* half-applied backup cannot survive a crash). */
export function runInTransaction<T>(fn: () => T): T {
return db.transaction(fn)();
}
/** The whole table, for the backup pushed to the master. */
export function listSettings(): Record<string, string> {
const rows = db.prepare('SELECT key, value FROM settings').all() as { key: string; value: string }[];
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
}
export function getSetting(key: string): string | null {
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined;
return row?.value ?? null;