import { readFileSync } from 'node:fs'; import path from 'node:path'; import { NextResponse } from 'next/server'; import { getClients, isWaking, sessionRestEnabled, sessionRestInfo } from '@/lib/clients'; import { getTradableFirms } from '@/lib/firm-catalog'; import { getSchedulerHealth, getSchedulerStatus } from '@/lib/auto-trade'; import { getBuildVersion } from '@/lib/version'; import { errorMessage, isClientReady } from '@/lib/platform-client'; // Liveness has to be computed now, never served from a cache. export const dynamic = 'force-dynamic'; /** A one-line marker file, or null when it isn't there. */ function readMarker(...segments: string[]): string | null { try { return readFileSync(path.join(process.cwd(), ...segments), 'utf8').trim() || null; } catch { return null; } } /** * GET /api/health — is this instance able to trade, and if not, why. * * The numbers to read before restarting a broken box, and what start-all * prints after boot. Always 200 when the handler runs: `ok` in the body is the * verdict (every tradable firm that has a client is synced, and synced * recently), so a caller can tell "unhealthy" from "not answering". * * Calls getClients(), so hitting it also boots an instance that somehow * hasn't been. * * Built field by field on purpose — firm rows carry passwords and API keys, * and nothing here may echo a credential or a token. */ export async function GET() { try { const now = Date.now(); const clients = getClients(); const entries = getTradableFirms().map((firm) => { const base = { firmId: firm.id, firm: firm.name, platform: firm.platform }; const client = clients.get(firm.id); if (!client) return { ...base, hasClient: false, ready: false, resting: null, waking: false, accounts: 0 }; const ready = isClientReady(client, now); const accounts = client.accountList.length; // Logged out on purpose while nothing needs it (lib/clients.ts): // not ready, and nothing wrong. Nor is logging back in after it, // within its grace — a login that fails stays not ready past that. const resting = sessionRestInfo(firm.id); const waking = isWaking(firm.id, now); try { return { ...base, hasClient: true, ready, resting, waking, accounts, ...client.health() }; } catch (err) { // One client's broken health() must not hide the rest. return { ...base, hasClient: true, ready, resting, waking, accounts, healthError: errorMessage(err) }; } }); return NextResponse.json({ ok: entries.every((e) => !e.hasClient || e.ready || !!e.resting || e.waking), sessionRest: sessionRestEnabled(), now, uptimeSec: Math.round(process.uptime()), version: getBuildVersion(), // Read from disk on every call: the build .next holds now (the // running one, unless a build landed since this process started), // and the commit the updater last finished building. buildId: readMarker('.next', 'BUILD_ID'), builtCommit: readMarker('scripts', '.built-commit'), scheduler: { ...getSchedulerStatus(), ...getSchedulerHealth() }, clients: entries, }); } catch (err) { return NextResponse.json({ ok: false, error: errorMessage(err) }, { status: 500 }); } }