diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 5fb7082..4330f03 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -650,22 +650,75 @@ button:disabled { color: var(--muted); } -.host-status-bar { - display: flex; - flex-direction: column; - gap: 12px; +.host-status-panel { margin: 0 0 12px; - padding: 12px 14px; border-radius: var(--radius); border: 1px solid var(--border-soft); background: var(--panel); font-size: 12px; } -.host-status-bar.hidden { +.host-status-panel.hidden { display: none !important; } +.host-status-summary { + display: flex; + align-items: center; + gap: 8px 12px; + padding: 10px 12px; + cursor: pointer; + list-style: none; + user-select: none; +} + +.host-status-summary::-webkit-details-marker { + display: none; +} + +.host-status-summary::before { + content: "▸"; + color: var(--muted); + font-size: 11px; + transition: transform 0.15s ease; + flex-shrink: 0; +} + +.host-status-panel[open] > .host-status-summary::before { + transform: rotate(90deg); +} + +.host-status-summary-title { + font-weight: 600; + color: var(--text); + white-space: nowrap; +} + +.host-status-summary-text { + color: var(--muted); + font-size: 11px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 auto; +} + +.host-status-bar { + display: flex; + flex-direction: column; + gap: 12px; + padding: 0 12px 12px; + border-top: 1px solid var(--border-soft); + margin-top: 0; + padding-top: 12px; + border-radius: 0; + border-left: none; + border-right: none; + border-bottom: none; + background: transparent; +} + .host-status-top { display: flex; flex-wrap: wrap; @@ -2791,9 +2844,17 @@ body.login-page { margin-bottom: 8px; } + .host-status-panel { + margin-bottom: 10px; + } + + .host-status-summary { + flex-wrap: wrap; + padding: 8px 10px; + } + .host-status-bar { - gap: 10px; - padding: 10px 12px; + padding: 10px; } .host-status-top { diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 7f07c37..8573f73 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -69,6 +69,9 @@ let sseReconnectTimer = null; let hostStatusTimer = null; const HOST_STATUS_POLL_MS = 5000; + const HOST_STATUS_OPEN_KEY = "hub-host-status-open"; + const HOST_RESOURCE_ALERT_THRESHOLD = 85; + const hostResourceAlertLatch = { cpu: false, mem: false }; function fmtHostBytes(n) { const v = Number(n); @@ -109,9 +112,61 @@ if (level === "bad") fillEl.classList.add("bad"); } + function checkHostResourceAlert(cpu, mem) { + const msgs = []; + const cpuP = Number(cpu && cpu.percent); + if (Number.isFinite(cpuP) && cpuP >= HOST_RESOURCE_ALERT_THRESHOLD) { + if (!hostResourceAlertLatch.cpu) { + msgs.push("CPU 使用率 " + cpuP + "%"); + hostResourceAlertLatch.cpu = true; + } + } else { + hostResourceAlertLatch.cpu = false; + } + const memP = Number(mem && mem.percent); + if (Number.isFinite(memP) && memP >= HOST_RESOURCE_ALERT_THRESHOLD) { + if (!hostResourceAlertLatch.mem) { + msgs.push("内存使用率 " + memP + "%"); + hostResourceAlertLatch.mem = true; + } + } else { + hostResourceAlertLatch.mem = false; + } + if (msgs.length) { + window.alert( + "服务器资源告警\n\n" + msgs.join("\n") + "\n\n请及时关注中控服务器负载。" + ); + } + } + + function hostStatusSummaryText(data) { + if (!data || !data.ok) return (data && data.msg) || "状态不可用"; + const cpu = data.cpu || {}; + const mem = data.memory || {}; + const disk = data.disk || {}; + const parts = []; + const host = String(data.hostname || "").trim(); + if (host) parts.push(host); + if (cpu.percent != null) parts.push("CPU " + cpu.percent + "%"); + if (mem.percent != null) parts.push("内存 " + mem.percent + "%"); + if (disk.percent != null) parts.push("硬盘 " + disk.percent + "%"); + return parts.join(" · ") || "—"; + } + + function initHostStatusPanel() { + const panel = document.getElementById("host-status-panel"); + if (!panel) return; + panel.open = loadBoolPref(HOST_STATUS_OPEN_KEY, false); + panel.addEventListener("toggle", function () { + saveBoolPref(HOST_STATUS_OPEN_KEY, !!panel.open); + }); + } + function renderHostStatusBar(data) { + const panel = document.getElementById("host-status-panel"); + const summaryText = document.getElementById("host-status-summary-text"); const bar = document.getElementById("host-status-bar"); - if (!bar) return; + if (!panel || !bar) return; const dot = document.getElementById("host-status-dot"); const name = document.getElementById("host-status-name"); const uptime = document.getElementById("host-status-uptime"); @@ -124,8 +179,9 @@ const diskSub = document.getElementById("host-disk-sub"); const netUp = document.getElementById("host-net-up"); const netDown = document.getElementById("host-net-down"); + panel.classList.remove("hidden"); + if (summaryText) summaryText.textContent = hostStatusSummaryText(data); if (!data || !data.ok) { - bar.classList.remove("hidden"); if (dot) dot.className = "host-status-dot bad"; if (name) { name.textContent = "服务器"; @@ -143,11 +199,11 @@ if (netDown) netDown.textContent = "↓ —"; return; } - bar.classList.remove("hidden"); const cpu = data.cpu || {}; const mem = data.memory || {}; const disk = data.disk || {}; const net = data.network || {}; + checkHostResourceAlert(cpu, mem); const levels = [ hostMetricLevel(cpu.percent), hostMetricLevel(mem.percent), @@ -203,6 +259,7 @@ function startHostStatusPoll() { stopHostStatusPoll(); + initHostStatusPanel(); void fetchHostStatus(); hostStatusTimer = setInterval(fetchHostStatus, HOST_STATUS_POLL_MS); } @@ -2585,7 +2642,6 @@ opts ) { const options = opts || {}; - const compact = !!options.compact; const symAttr = esc(x.symbol || "").replace(/"/g, """); const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """); const side = sideAttr || "long"; @@ -2603,9 +2659,7 @@ const mo = monitorOrder || {}; const tcBadge = !isTrendContext(mo, trendPlan) && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : ""; - const actionCell = compact - ? `` - : `