feat(hub): fold host status by default, add entrust on grid positions, alert on high CPU/memory

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-13 14:14:48 +08:00
parent a89b446d74
commit 9e395b6732
4 changed files with 149 additions and 29 deletions
+69 -8
View File
@@ -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 {
+61 -7
View File
@@ -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, "&quot;");
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, "&quot;");
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
? `<button type="button" class="btn-close-pos btn-sm danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>`
: `<div class="pos-action-group">
const actionCell = `<div class="pos-action-group">
<button type="button" class="btn-place-tpsl btn-sm ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}" data-contracts="${contractsAttr}" data-sl="${slAttr}" data-tp="${tpAttr}">委托</button>
<button type="button" class="btn-close-pos btn-sm danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
</div>`;
+18 -13
View File
@@ -61,18 +61,23 @@
<div class="page-head">
<h1><span class="head-tag">MON</span> 监控区</h1>
</div>
<div id="host-status-bar" class="host-status-bar hidden" aria-label="服务器运行状态" aria-live="polite">
<div class="host-status-top">
<div class="host-status-head">
<span class="host-status-dot ok" id="host-status-dot" aria-hidden="true"></span>
<span class="host-status-name" id="host-status-name" title="">服务器</span>
<details id="host-status-panel" class="host-status-panel hidden" aria-label="服务器运行状态">
<summary class="host-status-summary">
<span class="host-status-dot ok" id="host-status-dot" aria-hidden="true"></span>
<span class="host-status-summary-title">服务器状态</span>
<span class="host-status-summary-text" id="host-status-summary-text">加载中…</span>
</summary>
<div id="host-status-bar" class="host-status-bar" aria-live="polite">
<div class="host-status-top">
<div class="host-status-head">
<span class="host-status-name" id="host-status-name" title="">服务器</span>
</div>
<div class="host-status-meta">
<span class="host-status-uptime" id="host-status-uptime"></span>
<span class="host-status-updated" id="host-status-updated"></span>
</div>
</div>
<div class="host-status-meta">
<span class="host-status-uptime" id="host-status-uptime"></span>
<span class="host-status-updated" id="host-status-updated"></span>
</div>
</div>
<div class="host-status-metrics">
<div class="host-status-metrics">
<div class="host-metric-card" id="host-metric-cpu">
<div class="host-metric-head">
<span class="host-metric-label">CPU</span>
@@ -108,7 +113,7 @@
</div>
</div>
</div>
</div>
</details>
<div id="monitor-alert-summary" class="monitor-alert-summary hidden" aria-live="polite"></div>
<div class="toolbar">
<button type="button" id="btn-monitor-refresh" class="primary">立即刷新</button>
@@ -648,6 +653,6 @@
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
<script src="/assets/ai_review_render.js?v=3"></script>
<script src="/assets/time_close_ui.js?v=2"></script>
<script src="/assets/app.js?v=20260613-host-status-layout"></script>
<script src="/assets/app.js?v=20260613-host-status-fold-alert"></script>
</body>
</html>
+1 -1
View File
@@ -151,7 +151,7 @@ Chrome **桌面快捷方式**图标来自站点 `favicon` / `manifest`(已配
| 功能 | 说明 |
|------|------|
| **服务器状态** | 标题下方展示本机 **CPU / 内存 / 硬盘 / 网络**`GET /api/host/status`,每 5 秒刷新);四指标分卡片两行排版,主机名过长自动省略;≥75% 黄、≥90% 红。依赖 `manual_trading_hub/.venv`**psutil**(勿用系统 `pip`,见 [部署文档.md](./部署文档.md))。可选 `HUB_HOST_DISK_PATH` 指定监控磁盘(默认 Linux `/`、Windows 系统盘) |
| **服务器状态** | 标题下方可折叠条(**默认收起**),摘要行显示 CPU/内存/硬盘;展开见四指标卡片(`GET /api/host/status`,每 5 秒刷新)。**CPU 或内存 ≥85%** 时浏览器弹窗告警(降至 85% 以下后再次超标会再提示)。依赖 `manual_trading_hub/.venv`**psutil**(勿用系统 `pip`,见 [部署文档.md](./部署文档.md))。可选 `HUB_HOST_DISK_PATH` 指定监控磁盘 |
| **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**、**风险%**、**程序监控·止盈价**、**盈亏比**,与实例策略页一致);独立卡片:**关键位**、**下单监控**、**趋势回调**(单计划 **两列**:左=币种基本信息与 3×2 指标,右=**补仓计划明细**,底=**保本偏移%** 可编辑 + **保本移交** / **结束计划**(中控直接调实例,与 `/strategy` 一致)、快照可用/计划保证金/杠杆)、**顺势加仓** |
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |