This commit is contained in:
aaron 2026-04-22 12:00:30 +08:00
parent 8abdd23987
commit bbe27d8d1f

View File

@ -271,6 +271,10 @@
gap: 18px;
}
.layout > :only-child {
grid-column: 1 / -1;
}
.left-stack,
.right-stack {
display: grid;
@ -335,7 +339,7 @@
.priority-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.84fr);
grid-template-columns: minmax(320px, 0.9fr) minmax(0, 1.1fr);
gap: 18px;
margin-top: 18px;
}
@ -346,6 +350,94 @@
gap: 18px;
}
.ops-panel-body {
min-height: 320px;
}
.ops-pane {
display: grid;
gap: 14px;
}
.ops-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-bottom: 14px;
}
.ops-summary-card {
appearance: none;
width: 100%;
text-align: left;
cursor: pointer;
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
color: var(--text);
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.ops-summary-card:hover {
transform: translateY(-1px);
border-color: rgba(126, 200, 255, 0.16);
}
.ops-summary-card.active {
border-color: rgba(126, 200, 255, 0.26);
background: rgba(126, 200, 255, 0.10);
}
.ops-summary-card.danger {
border-color: rgba(255, 111, 97, 0.22);
background: rgba(255, 111, 97, 0.08);
}
.ops-summary-card.warn {
border-color: rgba(255, 184, 77, 0.20);
background: rgba(255, 184, 77, 0.08);
}
.ops-summary-kicker {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
}
.ops-summary-headline {
font-family: "IBM Plex Mono", monospace;
font-size: 18px;
color: var(--text);
margin-bottom: 6px;
}
.ops-summary-headline.good {
color: var(--good);
}
.ops-summary-headline.warn {
color: var(--warn);
}
.ops-summary-headline.danger {
color: var(--danger);
}
.ops-summary-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.ops-pane .analysis-log-list,
.ops-pane .halt-list {
margin-top: 2px;
}
.health-ribbon {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@ -1268,7 +1360,8 @@
.platform-stats,
.signal-stats,
.heartbeat-grid,
.health-ribbon {
.health-ribbon,
.ops-summary {
grid-template-columns: 1fr;
}
@ -1350,62 +1443,89 @@
<div class="loading">正在汇总待处理事项...</div>
</div>
</section>
<section class="panel dense-panel">
<div class="panel-header">
<div>
<div class="section-label">Platform Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">平台执行概览</h2>
<div class="panel-sub">资金、杠杆、持仓、挂单、回撤阈值</div>
</div>
<div class="panel-actions">
<button class="ghost-btn" id="toggleSensitiveBtn">敏感数据: 隐藏</button>
</div>
</div>
<div class="platform-grid" id="platformGrid">
<div class="loading">正在加载平台状态...</div>
</div>
</section>
</div>
<div class="priority-side">
<section class="panel dense-panel">
<div class="panel-header">
<div class="workspace-head">
<div>
<div class="section-label">Runtime Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">分析心跳与日志</h2>
<div class="panel-sub">没有信号时,也能确认系统仍在正常扫盘</div>
<div class="section-label">Operations Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">运行监控面板</h2>
<div class="panel-sub">把平台概览、分析心跳和停机熔断收进一个运行面板,避免首屏纵向堆叠</div>
</div>
<div class="panel-actions">
<button class="ghost-btn" id="toggleSensitiveBtn">敏感数据: 隐藏</button>
<div class="workspace-tabs" data-tab-group="ops">
<button class="workspace-tab active" data-tab="ops" data-target="opsPlatform">平台概览</button>
<button class="workspace-tab" data-tab="ops" data-target="opsRuntime">心跳日志</button>
<button class="workspace-tab" data-tab="ops" data-target="opsRisk">停机熔断</button>
</div>
</div>
</div>
<div class="heartbeat-grid" id="analysisHeartbeat">
<div class="heartbeat-card"><span class="label">最近心跳</span><span class="value">-</span></div>
<div class="heartbeat-card"><span class="label">最近轮次</span><span class="value">-</span></div>
<div class="heartbeat-card"><span class="label">当前进度</span><span class="value">-</span></div>
<div class="heartbeat-card"><span class="label">下一次运行</span><span class="value">-</span></div>
<div class="ops-summary" id="opsSummary">
<button class="ops-summary-card active" data-ops-target="opsPlatform">
<div class="ops-summary-kicker">平台概览</div>
<div class="ops-summary-headline">-</div>
<div class="ops-summary-detail">正在汇总平台状态...</div>
</button>
<button class="ops-summary-card" data-ops-target="opsRuntime">
<div class="ops-summary-kicker">心跳日志</div>
<div class="ops-summary-headline">-</div>
<div class="ops-summary-detail">正在汇总分析状态...</div>
</button>
<button class="ops-summary-card" data-ops-target="opsRisk">
<div class="ops-summary-kicker">停机熔断</div>
<div class="ops-summary-headline">-</div>
<div class="ops-summary-detail">正在汇总风险状态...</div>
</button>
</div>
<div class="analysis-log-list" id="analysisLogList">
<div class="loading">正在读取分析日志...</div>
<div class="ops-panel-body">
<div class="tab-pane active ops-pane" data-tab-pane="ops" id="opsPlatform">
<div class="block-head">
<div class="section-label">Platform Layer</div>
<h3 class="block-title">平台执行概览</h3>
<div class="block-sub">资金、杠杆、持仓、挂单、回撤阈值</div>
</div>
<div class="platform-grid" id="platformGrid">
<div class="loading">正在加载平台状态...</div>
</div>
</div>
<div class="tab-pane ops-pane" data-tab-pane="ops" id="opsRuntime">
<div class="block-head">
<div class="section-label">Runtime Layer</div>
<h3 class="block-title">分析心跳与日志</h3>
<div class="block-sub">没有信号时,也能确认系统仍在正常扫盘</div>
</div>
<div class="heartbeat-grid" id="analysisHeartbeat">
<div class="heartbeat-card"><span class="label">最近心跳</span><span class="value">-</span></div>
<div class="heartbeat-card"><span class="label">最近轮次</span><span class="value">-</span></div>
<div class="heartbeat-card"><span class="label">当前进度</span><span class="value">-</span></div>
<div class="heartbeat-card"><span class="label">下一次运行</span><span class="value">-</span></div>
</div>
<div class="analysis-log-list" id="analysisLogList">
<div class="loading">正在读取分析日志...</div>
</div>
</div>
<div class="tab-pane ops-pane" data-tab-pane="ops" id="opsRisk">
<div class="block-head">
<div class="section-label">Risk Layer</div>
<h3 class="block-title">平台停机 / 熔断</h3>
<div class="block-sub">风险触发后,这里应当最先看到</div>
</div>
<div class="halt-list" id="haltList">
<div class="loading">正在读取平台停机状态...</div>
</div>
<div class="footer-note">建议挂在大屏或副屏,默认每 15 秒刷新。</div>
</div>
</div>
</section>
</div>
</section>
</div>
<div class="right-stack">
<section class="panel dense-panel">
<div class="panel-header">
<div>
<div class="section-label">Risk Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">平台停机 / 熔断</h2>
<div class="panel-sub">风险触发后,这里应当最先看到</div>
</div>
</div>
<div class="halt-list" id="haltList">
<div class="loading">正在读取平台停机状态...</div>
</div>
<div class="footer-note">建议挂在大屏或副屏,默认每 15 秒刷新。</div>
</section>
</div>
</section>
<section class="panel workspace-panel">
@ -1647,6 +1767,12 @@
});
});
});
document.querySelectorAll('[data-ops-target]').forEach((button) => {
button.addEventListener('click', () => {
setActiveTab('ops', button.getAttribute('data-ops-target'));
});
});
}
function setActiveTab(group, target) {
@ -1656,6 +1782,11 @@
document.querySelectorAll(`[data-tab-pane="${group}"]`).forEach((pane) => {
pane.classList.toggle('active', pane.id === target);
});
if (group === 'ops') {
document.querySelectorAll('[data-ops-target]').forEach((card) => {
card.classList.toggle('active', card.getAttribute('data-ops-target') === target);
});
}
}
function getActiveTabTarget(group) {
@ -1669,22 +1800,92 @@
button.classList.toggle('muted', !hasData);
}
function renderOpsSummary(data) {
const container = document.getElementById('opsSummary');
if (!container) return;
const platforms = data.platforms || {};
const cryptoAgent = data.crypto_agent || {};
const monitor = cryptoAgent.analysis_monitor || {};
const halts = cryptoAgent.platform_halts || {};
const enabledPlatforms = ['paper', 'bitget', 'hyperliquid'].filter((key) => platforms?.[key]?.enabled !== false);
const haltedCount = countHalted(halts);
const runtimeTone = toneClassForHealth(cryptoAgent.running ? monitor.last_cycle_status || monitor.last_analysis_status : 'stopped');
const riskTone = haltedCount > 0 ? 'danger' : ((data.management?.attention_items || []).some((item) => item.severity === 'danger' || item.severity === 'warning') ? 'warn' : 'good');
const platformTone = haltedCount > 0 ? 'warn' : 'good';
const platformHeadline = `${enabledPlatforms.length} 平台`;
const platformDetail = `${sumPlatformPositions(platforms)} 持仓 / ${['paper', 'bitget', 'hyperliquid'].map((key) => platforms?.[key]?.orders?.count || 0).reduce((a, b) => a + b, 0)} 挂单`;
const runtimeHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳';
const runtimeDetail = `状态 ${String(monitor.last_cycle_status || monitor.last_analysis_status || 'idle').toUpperCase()} / ${monitor.current_cycle_total ? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total}` : '待机'}`;
const riskHeadline = haltedCount > 0 ? `${haltedCount} 停机` : '无停机';
const attentionItems = data.management?.attention_items || [];
const riskDetail = haltedCount > 0
? '已有平台触发停机或熔断,建议优先查看。'
: attentionItems.length > 0
? `待处理 ${attentionItems.length} 项,建议检查风险与执行事件。`
: '当前没有明显风险阻塞。';
container.innerHTML = `
<button class="ops-summary-card ${platformTone}" data-ops-target="opsPlatform">
<div class="ops-summary-kicker">平台概览</div>
<div class="ops-summary-headline ${platformTone}">${platformHeadline}</div>
<div class="ops-summary-detail">${platformDetail}</div>
</button>
<button class="ops-summary-card ${runtimeTone}" data-ops-target="opsRuntime">
<div class="ops-summary-kicker">心跳日志</div>
<div class="ops-summary-headline ${runtimeTone}">${runtimeHeadline}</div>
<div class="ops-summary-detail">${runtimeDetail}</div>
</button>
<button class="ops-summary-card ${riskTone}" data-ops-target="opsRisk">
<div class="ops-summary-kicker">停机熔断</div>
<div class="ops-summary-headline ${riskTone}">${riskHeadline}</div>
<div class="ops-summary-detail">${riskDetail}</div>
</button>
`;
container.querySelectorAll('[data-ops-target]').forEach((button) => {
button.addEventListener('click', () => {
setActiveTab('ops', button.getAttribute('data-ops-target'));
});
});
setActiveTab('ops', getActiveTabTarget('ops') || 'opsPlatform');
}
function syncTabState(data) {
const recentSignals = data.signals?.latest || [];
const executionEvents = data.execution_events || [];
const positions = data.management?.positions || [];
const orders = data.management?.orders || [];
const platformCount = ['paper', 'bitget', 'hyperliquid']
.filter((key) => data.platforms?.[key]?.enabled !== false)
.length;
const lastSignals = Object.keys(data.crypto_agent?.last_signals || {}).length;
const previews = Object.keys(data.crypto_agent?.last_execution_preview || {}).length;
const statsTotal = Number(data.signals?.stats_7d?.total || 0);
const coordinationCount = lastSignals + previews + (statsTotal > 0 ? 1 : 0);
const haltedCount = countHalted(data.crypto_agent?.platform_halts || {});
const runtimeCount = (data.crypto_agent?.recent_analysis_events || []).length + (data.crypto_agent?.analysis_monitor?.last_heartbeat_at ? 1 : 0);
updateTabButton('ops', 'opsPlatform', '平台概览', platformCount, platformCount > 0);
updateTabButton('ops', 'opsRuntime', '心跳日志', runtimeCount, runtimeCount > 0);
updateTabButton('ops', 'opsRisk', '停机熔断', haltedCount, haltedCount > 0);
updateTabButton('workspace', 'workspaceCoordination', '协同', coordinationCount, coordinationCount > 0);
updateTabButton('workspace', 'workspaceSignals', '信号流', recentSignals.length, recentSignals.length > 0);
updateTabButton('workspace', 'workspaceExecution', '执行流', executionEvents.length, executionEvents.length > 0);
updateTabButton('asset', 'assetPositions', '持仓', positions.length, positions.length > 0);
updateTabButton('asset', 'assetOrders', '挂单', orders.length, orders.length > 0);
const opsCurrent = getActiveTabTarget('ops');
const opsChoices = [
{ target: 'opsRisk', hasData: haltedCount > 0 },
{ target: 'opsRuntime', hasData: runtimeCount > 0 },
{ target: 'opsPlatform', hasData: platformCount > 0 },
];
if (!opsChoices.find((item) => item.target === opsCurrent && item.hasData)) {
setActiveTab('ops', opsChoices.find((item) => item.hasData)?.target || 'opsPlatform');
}
const workspaceCurrent = getActiveTabTarget('workspace');
const workspaceChoices = [
{ target: 'workspaceCoordination', hasData: coordinationCount > 0 },
@ -2283,6 +2484,7 @@
syncTabState(data);
renderHero(data);
renderHealthRibbon(data);
renderOpsSummary(data);
renderPlatforms(data.platforms, data.crypto_agent?.platform_halts);
renderSignalStream(data.signals?.latest || []);
renderAgentSignals(data.crypto_agent, data.signals);