From 6a76993ca88bed59a19de61edac52599af2292cb Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 3 Jun 2026 21:47:11 +0800 Subject: [PATCH] fix(hub): prevent monitor board stuck on slow aggregate polling Co-authored-by: Cursor --- manual_trading_hub/.env.example | 3 +- manual_trading_hub/hub.py | 26 +++++++++- manual_trading_hub/static/app.css | 8 +++ manual_trading_hub/static/app.js | 73 +++++++++++++++++++++++----- manual_trading_hub/static/index.html | 2 +- 5 files changed, 96 insertions(+), 16 deletions(-) diff --git a/manual_trading_hub/.env.example b/manual_trading_hub/.env.example index 5096d08..8b4095f 100644 --- a/manual_trading_hub/.env.example +++ b/manual_trading_hub/.env.example @@ -56,9 +56,10 @@ HUB_TRUST_LAN=true # 四实例网页登录(直链反代/IP:端口 访问时输入;中控点「打开实例」免输) # 各 crypto_monitor_*/.env 统一:APP_USERNAME=... APP_PASSWORD=... -# 监控区 /api/monitor/board 聚合超时(秒,默认 agent 8 / flask 10) +# 监控区 /api/monitor/board 聚合超时(秒,默认 agent 8 / flask 10 / board 45) # HUB_AGENT_TIMEOUT=8 # HUB_FLASK_TIMEOUT=10 +# HUB_BOARD_TIMEOUT=45 # 为 false 时不拉各实例 /api/price_snapshot(关键位门控简化为「-」,首屏明显更快) # HUB_BOARD_KEY_PRICES=true diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 1418f6e..26559ef 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -72,6 +72,7 @@ DIR = Path(__file__).resolve().parent HUB_BUILD = "20260528-hub-market" HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8")) HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10")) +HUB_BOARD_TIMEOUT = float(os.getenv("HUB_BOARD_TIMEOUT", "45")) _board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower() HUB_BOARD_KEY_PRICES = _board_key_prices_raw in ("1", "true", "yes", "on") @@ -748,8 +749,7 @@ async def _assemble_board_row( } -@app.get("/api/monitor/board") -async def api_monitor_board(): +async def _build_monitor_board_payload() -> dict: exchanges = enabled_exchanges() async with httpx.AsyncClient() as client: agent_rows = await asyncio.gather( @@ -767,6 +767,28 @@ async def api_monitor_board(): } +@app.get("/api/monitor/board") +async def api_monitor_board(): + try: + return await asyncio.wait_for(_build_monitor_board_payload(), timeout=HUB_BOARD_TIMEOUT) + except asyncio.TimeoutError: + return JSONResponse( + { + "ok": False, + "rows": [], + "error": "board_timeout", + "msg": ( + f"监控聚合超过 {int(HUB_BOARD_TIMEOUT)} 秒。" + "请检查子代理/Flask,或设 HUB_BOARD_KEY_PRICES=false、缩短 HUB_FLASK_TIMEOUT" + ), + "updated_at": __import__("datetime").datetime.now().isoformat( + timespec="seconds" + ), + }, + status_code=504, + ) + + def _require_hub_logged_in(request: Request) -> None: if password_required() and not validate_session_token(request.cookies.get(SESSION_COOKIE)): raise HTTPException(status_code=401, detail="未登录中控") diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 18e7fc6..c438534 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -1408,6 +1408,14 @@ button.btn-sm { padding: 8px 0; } +.board-loading-sub { + margin: 12px 0 0; + font-size: 12px; + line-height: 1.5; + color: var(--muted); + max-width: 36rem; +} + .board-loading { grid-column: 1 / -1; display: flex; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 91d811a..b053e35 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -8,8 +8,10 @@ let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || ""; const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1"; const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000; - let monitorBoardFetchSeq = 0; + const HUB_MONITOR_FETCH_TIMEOUT_MS = 55000; let lastMonitorBoardUpdatedAt = ""; + let monitorBoardInFlight = false; + let monitorBoardSlowHintTimer = null; async function apiFetch(url, opts) { const r = await fetch(url, opts); @@ -344,10 +346,33 @@ } function stopMonitorPoll() { + clearTimeout(monitorTimer); clearInterval(monitorTimer); monitorTimer = null; } + function clearMonitorBoardSlowHint() { + if (monitorBoardSlowHintTimer) { + clearTimeout(monitorBoardSlowHintTimer); + monitorBoardSlowHintTimer = null; + } + } + + function scheduleMonitorBoardSlowHint(box) { + clearMonitorBoardSlowHint(); + if (!box) return; + monitorBoardSlowHintTimer = setTimeout(() => { + if (lastMonitorRows.length) return; + const el = box.querySelector(".board-loading"); + if (!el) return; + const sub = el.querySelector(".board-loading-sub"); + if (sub) { + sub.textContent = + "聚合较慢(四所子代理 + Flask)。可检查 PM2、或设 HUB_BOARD_KEY_PRICES=false 加速;下方超时后会提示错误。"; + } + }, 12000); + } + function saveMonitorBoardCache(rows, updatedAt) { try { sessionStorage.setItem( @@ -414,13 +439,22 @@ renderMonitorGrid(rows || []); } + function scheduleNextMonitorPoll() { + stopMonitorPoll(); + if (!document.getElementById("auto-monitor")?.checked) return; + if (currentPage() !== "monitor") return; + monitorTimer = setTimeout(async () => { + await loadMonitorBoard({ background: true }); + scheduleNextMonitorPoll(); + }, 5000); + } + function startMonitorPoll() { stopMonitorPoll(); const hadCache = restoreMonitorBoardFromCache(); - loadMonitorBoard({ background: hadCache }); - if (document.getElementById("auto-monitor").checked) { - monitorTimer = setInterval(() => loadMonitorBoard({ background: true }), 5000); - } + void loadMonitorBoard({ background: hadCache }).finally(() => { + scheduleNextMonitorPoll(); + }); } async function loadSettings() { @@ -576,30 +610,44 @@ async function loadMonitorBoard(opts) { const options = opts || {}; const background = !!options.background; + const force = !!options.force; + if (monitorBoardInFlight && background && !force) return; const box = document.getElementById("monitor-grid"); - const seq = ++monitorBoardFetchSeq; const showLoading = !background && !lastMonitorRows.length; if (showLoading && box) { box.innerHTML = - '
正在聚合四所数据…
'; + '
正在聚合四所数据…

'; + scheduleMonitorBoardSlowHint(box); } else if (background && lastMonitorRows.length) { applyMonitorBoardUi(lastMonitorRows, null, { stale: true }); } + monitorBoardInFlight = true; + const ctrl = new AbortController(); + const fetchTimer = setTimeout(() => ctrl.abort(), HUB_MONITOR_FETCH_TIMEOUT_MS); try { - const r = await apiFetch("/api/monitor/board"); + const r = await apiFetch("/api/monitor/board", { signal: ctrl.signal }); const data = await r.json(); - if (seq !== monitorBoardFetchSeq) return; + if (!r.ok) { + throw new Error(data.msg || data.detail || `HTTP ${r.status}`); + } lastMonitorRows = data.rows || []; saveMonitorBoardCache(lastMonitorRows, data.updated_at); applyMonitorBoardUi(lastMonitorRows, data.updated_at, { stale: false }); } catch (e) { - if (seq !== monitorBoardFetchSeq) return; + const msg = + e && e.name === "AbortError" + ? "聚合超时(约 55 秒)。请检查子代理/Flask 是否运行,或关闭 HUB_BOARD_KEY_PRICES 加速" + : String(e); if (background && lastMonitorRows.length) { showToast("监控数据刷新失败,仍显示上次缓存", true); applyMonitorBoardUi(lastMonitorRows, null, { stale: false }); return; } - if (box) box.innerHTML = `
${esc(e)}
`; + if (box) box.innerHTML = `
${esc(msg)}
`; + } finally { + clearTimeout(fetchTimer); + clearMonitorBoardSlowHint(); + monitorBoardInFlight = false; } } @@ -1870,7 +1918,8 @@ location.href = "/login"; }; - document.getElementById("btn-monitor-refresh").onclick = loadMonitorBoard; + document.getElementById("btn-monitor-refresh").onclick = () => + loadMonitorBoard({ force: true, background: !!lastMonitorRows.length }); document.getElementById("auto-monitor").onchange = startMonitorPoll; document.getElementById("btn-close-all").onclick = closeAll; document.getElementById("btn-settings-save").onclick = saveSettings; diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 533c07e..0c52974 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -245,6 +245,6 @@
- +