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:
@@ -650,22 +650,75 @@ button:disabled {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-status-bar {
|
.host-status-panel {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-status-bar.hidden {
|
.host-status-panel.hidden {
|
||||||
display: none !important;
|
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 {
|
.host-status-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -2791,9 +2844,17 @@ body.login-page {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.host-status-panel {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-status-summary {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.host-status-bar {
|
.host-status-bar {
|
||||||
gap: 10px;
|
padding: 10px;
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-status-top {
|
.host-status-top {
|
||||||
|
|||||||
@@ -69,6 +69,9 @@
|
|||||||
let sseReconnectTimer = null;
|
let sseReconnectTimer = null;
|
||||||
let hostStatusTimer = null;
|
let hostStatusTimer = null;
|
||||||
const HOST_STATUS_POLL_MS = 5000;
|
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) {
|
function fmtHostBytes(n) {
|
||||||
const v = Number(n);
|
const v = Number(n);
|
||||||
@@ -109,9 +112,61 @@
|
|||||||
if (level === "bad") fillEl.classList.add("bad");
|
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) {
|
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");
|
const bar = document.getElementById("host-status-bar");
|
||||||
if (!bar) return;
|
if (!panel || !bar) return;
|
||||||
const dot = document.getElementById("host-status-dot");
|
const dot = document.getElementById("host-status-dot");
|
||||||
const name = document.getElementById("host-status-name");
|
const name = document.getElementById("host-status-name");
|
||||||
const uptime = document.getElementById("host-status-uptime");
|
const uptime = document.getElementById("host-status-uptime");
|
||||||
@@ -124,8 +179,9 @@
|
|||||||
const diskSub = document.getElementById("host-disk-sub");
|
const diskSub = document.getElementById("host-disk-sub");
|
||||||
const netUp = document.getElementById("host-net-up");
|
const netUp = document.getElementById("host-net-up");
|
||||||
const netDown = document.getElementById("host-net-down");
|
const netDown = document.getElementById("host-net-down");
|
||||||
|
panel.classList.remove("hidden");
|
||||||
|
if (summaryText) summaryText.textContent = hostStatusSummaryText(data);
|
||||||
if (!data || !data.ok) {
|
if (!data || !data.ok) {
|
||||||
bar.classList.remove("hidden");
|
|
||||||
if (dot) dot.className = "host-status-dot bad";
|
if (dot) dot.className = "host-status-dot bad";
|
||||||
if (name) {
|
if (name) {
|
||||||
name.textContent = "服务器";
|
name.textContent = "服务器";
|
||||||
@@ -143,11 +199,11 @@
|
|||||||
if (netDown) netDown.textContent = "↓ —";
|
if (netDown) netDown.textContent = "↓ —";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bar.classList.remove("hidden");
|
|
||||||
const cpu = data.cpu || {};
|
const cpu = data.cpu || {};
|
||||||
const mem = data.memory || {};
|
const mem = data.memory || {};
|
||||||
const disk = data.disk || {};
|
const disk = data.disk || {};
|
||||||
const net = data.network || {};
|
const net = data.network || {};
|
||||||
|
checkHostResourceAlert(cpu, mem);
|
||||||
const levels = [
|
const levels = [
|
||||||
hostMetricLevel(cpu.percent),
|
hostMetricLevel(cpu.percent),
|
||||||
hostMetricLevel(mem.percent),
|
hostMetricLevel(mem.percent),
|
||||||
@@ -203,6 +259,7 @@
|
|||||||
|
|
||||||
function startHostStatusPoll() {
|
function startHostStatusPoll() {
|
||||||
stopHostStatusPoll();
|
stopHostStatusPoll();
|
||||||
|
initHostStatusPanel();
|
||||||
void fetchHostStatus();
|
void fetchHostStatus();
|
||||||
hostStatusTimer = setInterval(fetchHostStatus, HOST_STATUS_POLL_MS);
|
hostStatusTimer = setInterval(fetchHostStatus, HOST_STATUS_POLL_MS);
|
||||||
}
|
}
|
||||||
@@ -2585,7 +2642,6 @@
|
|||||||
opts
|
opts
|
||||||
) {
|
) {
|
||||||
const options = opts || {};
|
const options = opts || {};
|
||||||
const compact = !!options.compact;
|
|
||||||
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
||||||
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
||||||
const side = sideAttr || "long";
|
const side = sideAttr || "long";
|
||||||
@@ -2603,9 +2659,7 @@
|
|||||||
const mo = monitorOrder || {};
|
const mo = monitorOrder || {};
|
||||||
const tcBadge =
|
const tcBadge =
|
||||||
!isTrendContext(mo, trendPlan) && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
|
!isTrendContext(mo, trendPlan) && mo.time_close_enabled ? timeCloseSymbolBadgeHtml(mo) : "";
|
||||||
const actionCell = compact
|
const actionCell = `<div class="pos-action-group">
|
||||||
? `<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">
|
|
||||||
<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-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>
|
<button type="button" class="btn-close-pos btn-sm danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -61,18 +61,23 @@
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1><span class="head-tag">MON</span> 监控区</h1>
|
<h1><span class="head-tag">MON</span> 监控区</h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="host-status-bar" class="host-status-bar hidden" aria-label="服务器运行状态" aria-live="polite">
|
<details id="host-status-panel" class="host-status-panel hidden" aria-label="服务器运行状态">
|
||||||
<div class="host-status-top">
|
<summary class="host-status-summary">
|
||||||
<div class="host-status-head">
|
<span class="host-status-dot ok" id="host-status-dot" aria-hidden="true"></span>
|
||||||
<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-name" id="host-status-name" 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>
|
||||||
<div class="host-status-meta">
|
<div class="host-status-metrics">
|
||||||
<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-metric-card" id="host-metric-cpu">
|
<div class="host-metric-card" id="host-metric-cpu">
|
||||||
<div class="host-metric-head">
|
<div class="host-metric-head">
|
||||||
<span class="host-metric-label">CPU</span>
|
<span class="host-metric-label">CPU</span>
|
||||||
@@ -108,7 +113,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
<div id="monitor-alert-summary" class="monitor-alert-summary hidden" aria-live="polite"></div>
|
<div id="monitor-alert-summary" class="monitor-alert-summary hidden" aria-live="polite"></div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button type="button" id="btn-monitor-refresh" class="primary">立即刷新</button>
|
<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/dashboard.js?v=20260612-dash-monitor-count"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||||
<script src="/assets/time_close_ui.js?v=2"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
|
| **2×2 主界面** | 四所信息**完整展示**:余额、持仓表、委托/平仓、折叠委托单、下单监控、关键位、趋势/加仓摘要 |
|
||||||
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**、**风险%**、**程序监控·止盈价**、**盈亏比**,与实例策略页一致);独立卡片:**关键位**、**下单监控**、**趋势回调**(单计划 **两列**:左=币种基本信息与 3×2 指标,右=**补仓计划明细**,底=**保本偏移%** 可编辑 + **保本移交** / **结束计划**(中控直接调实例,与 `/strategy` 一致)、快照可用/计划保证金/杠杆)、**顺势加仓** |
|
| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**、**风险%**、**程序监控·止盈价**、**盈亏比**,与实例策略页一致);独立卡片:**关键位**、**下单监控**、**趋势回调**(单计划 **两列**:左=币种基本信息与 3×2 指标,右=**补仓计划明细**,底=**保本偏移%** 可编辑 + **保本移交** / **结束计划**(中控直接调实例,与 `/strategy` 一致)、快照可用/计划保证金/杠杆)、**顺势加仓** |
|
||||||
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |
|
| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** |
|
||||||
|
|||||||
Reference in New Issue
Block a user