A firm resting across the trade-date roll kept reporting its last snapshot, so yesterday's realized P&L and DLL lockouts counted as accounts already traded today (Nik's Topstep read 5/5 traded at 5:31 PM from 3:29 PM data). Its report is now the unsynced one until it logs back in; a rest within one trade date reports as before. /api/health also counts a firm logging back in after a rest as nothing wrong, within its 15-minute grace, rather than reading not ok for the seconds before its first sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
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 });
|
|
}
|
|
}
|