import { useCallback, useEffect, useState } from "react"; import { apiFetch, type NodeCard } from "../api"; function pickStrategy(n: NodeCard) { const fleet = (n.fleet || {}) as Record; const health = (n.health || {}) as Record; const strat = (fleet.strategy as Record | undefined) || (health.strategy as Record | undefined) || {}; const position = (fleet.position as Record | undefined) || {}; return { mode: String(fleet.mode || health.mode || "-"), running: strat.running, phase: String(strat.phase ?? "-"), rounds: strat.rounds_done, market: fleet.market_connected ?? health.market_connected, exchange: String(fleet.exchange || health.exchange || "-"), strat, position, pair: fleet.pair as Record | null | undefined, index_px: fleet.index_px, }; } function fmt(v: unknown, digits = 4): string { if (v == null || v === "") return "—"; const n = Number(v); if (!Number.isFinite(n)) return String(v); return n.toFixed(digits); } export default function MonitorPage() { const [nodes, setNodes] = useState([]); const [err, setErr] = useState(""); const [busy, setBusy] = useState>({}); const [selected, setSelected] = useState>(new Set()); const [pollSec, setPollSec] = useState(8); const [detailId, setDetailId] = useState(null); const refresh = useCallback(async () => { try { const r = await apiFetch<{ nodes: NodeCard[] }>("/api/nodes/status/all"); setNodes(r.nodes || []); setErr(""); } catch (ex) { setErr(ex instanceof Error ? ex.message : String(ex)); } }, []); useEffect(() => { apiFetch<{ poll_interval_sec?: number }>("/api/auth/me") .then((m) => { if (m.poll_interval_sec) setPollSec(m.poll_interval_sec); }) .catch(() => undefined); void refresh(); const id = window.setInterval(() => void refresh(), pollSec * 1000); return () => window.clearInterval(id); }, [refresh, pollSec]); async function act(id: number, action: "start" | "pause" | "update" | "login") { setBusy((b) => ({ ...b, [id]: action })); setErr(""); try { if (action === "login") { const r = await apiFetch<{ url: string }>(`/api/nodes/${id}/login-url`, { method: "POST", }); window.open(r.url, "_blank", "noopener,noreferrer"); } else { await apiFetch(`/api/nodes/${id}/${action === "pause" ? "pause" : action}`, { method: "POST", }); await refresh(); } } catch (ex) { setErr(ex instanceof Error ? ex.message : String(ex)); } finally { setBusy((b) => { const n = { ...b }; delete n[id]; return n; }); } } async function batchUpdate() { const ids = [...selected]; if (!ids.length) return; setErr(""); try { await apiFetch("/api/nodes/update-batch", { method: "POST", body: JSON.stringify({ ids }), }); await refresh(); } catch (ex) { setErr(ex instanceof Error ? ex.message : String(ex)); } } function toggle(id: number) { setSelected((prev) => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); } const detailNode = detailId != null ? nodes.find((x) => x.id === detailId) : null; const detail = detailNode ? pickStrategy(detailNode) : null; const detailRunning = detail != null && (detail.running === true || detail.running === 1); const detailLegs = Array.isArray(detail?.position?.legs) ? (detail!.position.legs as Record[]) : []; return (

监控区

{err ?
{err}
: null}
{nodes.map((n) => { const s = pickStrategy(n); const running = s.running === true || s.running === 1; return (
setDetailId(n.id)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setDetailId(n.id); } }} >
{n.online ? "在线" : "离线"}
{n.base_url}
模式
{s.mode}
交易所
{s.exchange}
策略
{running ? "运行中" : "已停"} · {s.phase}
轮次
{s.rounds ?? "-"}
行情
{s.market ? "已连接" : "断开"}
Token
{n.token_configured ? n.fleet_ok === false ? "配对失败" : "已配对" : "未配对"}
{n.fleet_error ?
{n.fleet_error}
: null} {n.error ?
{n.error}
: null}
e.stopPropagation()}>
); })}
{!nodes.length ? (

暂无策略机。请到「系统设置」添加并生成 Token。

) : null} {detailNode && detail ? (
setDetailId(null)} role="presentation" >
e.stopPropagation()} role="dialog" aria-modal="true" aria-label={`${detailNode.name} 详情`} >

{detailNode.name}

{detailNode.base_url}

策略详情

状态
{detailRunning ? "运行中" : "已停"} · {detail.phase}
模式 / 交易所
{detail.mode} / {detail.exchange}
轮次
{detail.rounds ?? "—"}
组 ID
{String(detail.strat.group_id || detail.position.group_id || "—")}
杠杆 / 保证金
{fmt(detail.strat.leverage, 1)}x /{" "} {String(detail.strat.perp_margin_mode || "—")}
名义 永续/期权
{fmt(detail.strat.perp_qty_eth, 4)} /{" "} {fmt(detail.strat.option_qty_eth, 4)} ETH
出场目标
{fmt(detail.strat.exit_target_usdt, 2)} USDT
定仓
{String(detail.strat.sizing_mode || "—")} {detail.strat.risk_last_k != null ? ` · k=${fmt(detail.strat.risk_last_k, 2)}` : ""}
指数
{fmt(detail.index_px, 2)}
合约对
{detail.pair ? `${detail.pair.expiry_ymd || "?"} @ ${detail.pair.strike ?? "?"}` : "—"}
{detail.strat.last_error ? (
最近错误
{String(detail.strat.last_error)}
) : null}

持仓

状态:{String(detail.position.status || "flat")} {detail.position.net_pnl != null ? ` · 净浮盈 ${fmt(detail.position.net_pnl, 2)} USDT` : ""} {detail.position.expiry_ymd ? ` · 到期 ${String(detail.position.expiry_ymd)} @ ${fmt(detail.position.strike, 0)}` : ""}

{detailLegs.length ? (
{detailLegs.map((lg, i) => ( ))}
方向 合约 数量 均价 标记 浮盈
{String(lg.kind || "—")} {String(lg.side || "—")} {String(lg.inst_id || "—")} {fmt(lg.qty, 4)} {fmt(lg.avg_px, 4)} {fmt(lg.mark_px, 4)} {fmt(lg.upl, 2)}
) : (

当前无持仓腿

)}
) : null}
); }