diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index cb243f5..18e7fc6 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -160,6 +160,17 @@ a:hover { background: rgba(255, 77, 109, 0.1); } +.sys-pill.syncing { + opacity: 0.85; + animation: sys-pill-pulse 1.2s ease-in-out infinite; +} + +@keyframes sys-pill-pulse { + 50% { + opacity: 0.55; + } +} + .top-nav { display: flex; gap: 4px; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index e75b212..91d811a 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -6,6 +6,10 @@ let tpslPending = null; let lastMonitorRows = []; 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; + let lastMonitorBoardUpdatedAt = ""; async function apiFetch(url, opts) { const r = await fetch(url, opts); @@ -344,11 +348,78 @@ monitorTimer = null; } + function saveMonitorBoardCache(rows, updatedAt) { + try { + sessionStorage.setItem( + HUB_MONITOR_BOARD_CACHE_KEY, + JSON.stringify({ + version: 1, + updated_at: updatedAt || "", + rows: rows || [], + saved_at: Date.now(), + }) + ); + } catch (_) {} + } + + function loadMonitorBoardFromCache() { + try { + const raw = sessionStorage.getItem(HUB_MONITOR_BOARD_CACHE_KEY); + if (!raw) return null; + const data = JSON.parse(raw); + if (!data || !Array.isArray(data.rows) || !data.rows.length) return null; + const age = Date.now() - Number(data.saved_at || 0); + if (!Number.isFinite(age) || age > HUB_MONITOR_CACHE_MAX_AGE_MS) { + sessionStorage.removeItem(HUB_MONITOR_BOARD_CACHE_KEY); + return null; + } + return data; + } catch (_) { + return null; + } + } + + function restoreMonitorBoardFromCache() { + const cached = loadMonitorBoardFromCache(); + if (!cached) return false; + lastMonitorRows = cached.rows; + lastMonitorBoardUpdatedAt = cached.updated_at || ""; + applyMonitorBoardUi(cached.rows, lastMonitorBoardUpdatedAt, { stale: true }); + return true; + } + + function applyMonitorBoardUi(rows, updatedAt, opts) { + const options = opts || {}; + const tsRaw = updatedAt || lastMonitorBoardUpdatedAt || ""; + if (updatedAt) lastMonitorBoardUpdatedAt = updatedAt; + const online = (rows || []).filter((x) => x.http_ok && (x.agent || {}).ok !== false).length; + const pill = document.getElementById("sys-status"); + if (pill) { + pill.textContent = rows.length ? `LINK ${online}/${rows.length}` : "NO DATA"; + pill.classList.toggle("warn", rows.length && online < rows.length); + if (options.stale) pill.classList.add("syncing"); + else pill.classList.remove("syncing"); + } + const upd = document.getElementById("monitor-updated"); + if (upd) { + const ts = tsRaw.replace("T", " "); + upd.textContent = options.stale + ? ts + ? `缓存 ${ts} · 刷新中…` + : "刷新中…" + : ts + ? `UPD ${ts}` + : ""; + } + renderMonitorGrid(rows || []); + } + function startMonitorPoll() { stopMonitorPoll(); - loadMonitorBoard(); + const hadCache = restoreMonitorBoardFromCache(); + loadMonitorBoard({ background: hadCache }); if (document.getElementById("auto-monitor").checked) { - monitorTimer = setInterval(loadMonitorBoard, 5000); + monitorTimer = setInterval(() => loadMonitorBoard({ background: true }), 5000); } } @@ -502,32 +573,33 @@ return `已保本`; } - async function loadMonitorBoard() { + async function loadMonitorBoard(opts) { + const options = opts || {}; + const background = !!options.background; const box = document.getElementById("monitor-grid"); - const showLoading = !lastMonitorRows.length; + const seq = ++monitorBoardFetchSeq; + const showLoading = !background && !lastMonitorRows.length; if (showLoading && box) { box.innerHTML = '