/** * 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 used to be what started trading: getClients() in lib/clients.ts * was only bootstrapped by the first HTTP request. Builds with * instrumentation.ts boot it when the server starts, so the warm-up is now a * readiness check — it waits for each instance to answer, loads the dashboard * state once, and logs the scheduler and /api/health status. (On an older * build it still does the bootstrapping, so it stays.) */ 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}`); 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 */ } // One line of client health. Right after boot a client or two may still // be logging in, so this reports; it doesn't fail the run. Builds from // before /api/health answer 404 — nothing to say then. try { const res = await fetch(`${base}/api/health`, { signal: AbortSignal.timeout(10_000) }); if (res.status !== 404) { const health = await res.json(); if (!res.ok) { log(`${instance.name}: /api/health failed — ${health.error ?? `HTTP ${res.status}`}`); } else { // Firms without a client yet aren't failing to sync — same // rule as the route's own `ok` (and the updater's line). const clients = (Array.isArray(health.clients) ? health.clients : []) .filter((c) => c?.hasClient !== false); const waiting = clients.filter((c) => !c.ready).map((c) => c.firm); log(`${instance.name}: ${clients.length - waiting.length}/${clients.length} firm(s) ready` + (waiting.length > 0 ? ` — not ready yet: ${waiting.join(', ')}` : '')); } } } catch { /* non-critical */ } } if (failed) process.exit(1); log('up');