This commit is contained in:
aaron 2026-04-25 16:13:07 +08:00
parent 2779330ee5
commit 784c6a8fff

View File

@ -980,6 +980,277 @@
gap: 12px;
}
.top-priority-grid {
display: grid;
grid-template-columns: minmax(0, 1.08fr) minmax(280px, 0.92fr);
gap: 14px;
}
.top-priority-card {
padding: 18px;
border-radius: 18px;
border: 1px solid rgba(255,255,255,0.08);
background:
linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)),
rgba(255,255,255,0.02);
}
.top-priority-card.danger {
border-color: rgba(255, 111, 97, 0.24);
background:
linear-gradient(180deg, rgba(255, 111, 97, 0.16), rgba(255, 111, 97, 0.06)),
rgba(255, 111, 97, 0.05);
}
.top-priority-card.warn {
border-color: rgba(255, 184, 77, 0.24);
background:
linear-gradient(180deg, rgba(255, 184, 77, 0.14), rgba(255, 184, 77, 0.05)),
rgba(255, 184, 77, 0.04);
}
.top-priority-card.good {
border-color: rgba(48, 209, 88, 0.22);
background:
linear-gradient(180deg, rgba(48, 209, 88, 0.12), rgba(48, 209, 88, 0.04)),
rgba(48, 209, 88, 0.04);
}
.top-priority-kicker {
color: var(--muted);
font-size: 11px;
margin-bottom: 8px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.top-priority-title {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.03em;
color: var(--text);
margin-bottom: 10px;
}
.top-priority-detail,
.top-priority-action {
font-size: 13px;
line-height: 1.65;
color: var(--muted);
}
.top-priority-action {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255,255,255,0.08);
}
.top-priority-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.priority-mini-list {
display: grid;
gap: 10px;
}
.priority-mini-item {
padding: 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.priority-mini-item.danger {
border-color: rgba(255, 111, 97, 0.18);
background: rgba(255, 111, 97, 0.08);
}
.priority-mini-item.warn {
border-color: rgba(255, 184, 77, 0.18);
background: rgba(255, 184, 77, 0.08);
}
.priority-mini-item.good {
border-color: rgba(48, 209, 88, 0.16);
background: rgba(48, 209, 88, 0.07);
}
.priority-mini-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 6px;
}
.priority-mini-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.priority-mini-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.ops-kpi-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.ops-kpi-card {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.ops-kpi-card.warn {
border-color: rgba(255, 184, 77, 0.2);
background: rgba(255, 184, 77, 0.08);
}
.ops-kpi-card.danger {
border-color: rgba(255, 111, 97, 0.2);
background: rgba(255, 111, 97, 0.08);
}
.ops-kpi-label {
color: var(--muted);
font-size: 11px;
margin-bottom: 8px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.ops-kpi-value {
font-family: "IBM Plex Mono", monospace;
font-size: 20px;
color: var(--text);
margin-bottom: 6px;
}
.ops-kpi-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.lifecycle-list {
display: grid;
gap: 12px;
}
.lifecycle-card {
padding: 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.lifecycle-card.buy {
border-color: rgba(48, 209, 88, 0.18);
}
.lifecycle-card.sell {
border-color: rgba(255, 111, 97, 0.18);
}
.lifecycle-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 12px;
}
.lifecycle-title {
font-size: 16px;
font-weight: 700;
color: var(--text);
margin-bottom: 4px;
}
.lifecycle-meta {
color: var(--muted);
font-size: 11px;
line-height: 1.55;
font-family: "IBM Plex Mono", monospace;
}
.lifecycle-summary {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
margin-bottom: 12px;
}
.lifecycle-lanes {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.lifecycle-lane {
padding: 12px 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.lifecycle-lane.good {
border-color: rgba(48, 209, 88, 0.18);
background: rgba(48, 209, 88, 0.08);
}
.lifecycle-lane.warn {
border-color: rgba(255, 184, 77, 0.18);
background: rgba(255, 184, 77, 0.08);
}
.lifecycle-lane.danger {
border-color: rgba(255, 111, 97, 0.18);
background: rgba(255, 111, 97, 0.08);
}
.lifecycle-lane-head {
display: flex;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
align-items: center;
}
.lifecycle-lane-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.lifecycle-lane-state {
font-size: 11px;
color: var(--muted);
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
}
.lifecycle-lane-detail {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.signal-feed-card {
padding: 14px 16px;
border-radius: 16px;
@ -1959,7 +2230,9 @@
.platform-overview,
.platform-stats,
.signal-feed-stats,
.position-card-gridline {
.position-card-gridline,
.ops-kpi-grid,
.lifecycle-lanes {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@ -1996,7 +2269,10 @@
.heartbeat-grid,
.health-ribbon,
.ops-summary,
.focus-summary {
.focus-summary,
.top-priority-grid,
.ops-kpi-grid,
.lifecycle-lanes {
grid-template-columns: 1fr;
}
@ -2096,6 +2372,36 @@
<div class="tab-pane active" data-tab-pane="command" id="commandOverview">
<div class="overview-grid">
<div class="overview-main">
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Priority Layer</div>
<h3 class="block-title">当前最重要的问题</h3>
<div class="block-sub">先回答现在最需要你看的是什么,以及你该做什么</div>
</div>
<div class="top-priority-grid" id="topPriority">
<div class="loading">正在整理当前优先级...</div>
</div>
</section>
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Ops Layer</div>
<h3 class="block-title">系统运营指标</h3>
<div class="block-sub">看系统过去一段时间是否真的有分析、信号和执行产出</div>
</div>
<div class="ops-kpi-grid" id="opsKpis">
<div class="loading">正在汇总运营指标...</div>
</div>
</section>
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Lifecycle Layer</div>
<h3 class="block-title">信号生命周期</h3>
<div class="block-sub">把信号、执行结果、挂单和持仓串起来,不再分散查看</div>
</div>
<div class="lifecycle-list" id="signalLifecycle">
<div class="loading">正在整理信号生命周期...</div>
</div>
</section>
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Decision Layer</div>
@ -2121,8 +2427,8 @@
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Risk Layer</div>
<h3 class="block-title">管理待处理事项</h3>
<div class="block-sub">把需要你判断和干预的事情放到最前面</div>
<h3 class="block-title">待处理清单</h3>
<div class="block-sub">首屏的辅助核对区,完整优先级已提升到左侧顶部</div>
</div>
<div class="attention-list" id="attentionList">
<div class="loading">正在汇总待处理事项...</div>
@ -2425,6 +2731,247 @@
`;
}
function normalizeSeverity(severity) {
const value = String(severity || '').toLowerCase();
if (['danger', 'error', 'critical'].includes(value)) return 'danger';
if (['warning', 'warn', 'hold'].includes(value)) return 'warn';
return 'good';
}
function countRecentEvents(events, hours = 24, matcher = null) {
const cutoff = Date.now() - hours * 3600 * 1000;
return (events || []).filter((event) => {
const rawTime = event?.timestamp || event?.created_at || event?.opened_at;
const time = rawTime ? new Date(rawTime).getTime() : 0;
if (!time || Number.isNaN(time) || time < cutoff) return false;
return typeof matcher === 'function' ? matcher(event) : true;
}).length;
}
function inferOpsSnapshot(data) {
const signals = data.signals?.latest || [];
const events = data.execution_events || [];
const positions = data.management?.positions || [];
const orders = data.management?.orders || [];
const blocked24h = countRecentEvents(events, 24, (event) => event.event_type === 'execution_blocked_summary');
const success24h = countRecentEvents(events, 24, (event) => event.status === 'success');
const warn24h = countRecentEvents(events, 24, (event) => ['warning', 'error'].includes(event.status));
const recentSignal24h = countRecentEvents(signals, 24, () => true);
const entryOrders = orders.filter((order) => order.category === 'entry');
const protectionOrders = orders.filter((order) => order.category === 'tp_sl');
const completeProtectionCount = positions.filter((position) => position.take_profit && position.stop_loss).length;
const protectionCoverage = positions.length > 0 ? (completeProtectionCount / positions.length) * 100 : 100;
const conversionRate = recentSignal24h > 0 ? (success24h / recentSignal24h) * 100 : 0;
const staleEntryOrders = entryOrders.filter((order) => {
const created = order.created_at ? new Date(order.created_at).getTime() : 0;
return created && !Number.isNaN(created) && (Date.now() - created) > (30 * 60 * 1000);
}).length;
return {
recentSignal24h,
success24h,
warn24h,
blocked24h,
entryOrders,
protectionOrders,
protectionCoverage,
conversionRate,
staleEntryOrders,
positions,
};
}
function deriveTopPriorities(data) {
const cryptoAgent = data.crypto_agent || {};
const monitor = cryptoAgent.analysis_monitor || {};
const attentionItems = data.management?.attention_items || [];
const events = data.execution_events || [];
const positions = data.management?.positions || [];
const executionControls = cryptoAgent.target_execution_controls || {};
const haltedEntries = Object.entries(cryptoAgent.platform_halts || {}).filter(([, item]) => item?.halted);
const disabledTargets = Object.entries(executionControls).filter(([, item]) => item?.enabled === false);
const top = [];
if (!cryptoAgent.running || !monitor.last_heartbeat_at) {
top.push({
tone: 'danger',
title: !cryptoAgent.running ? 'Crypto Agent 已停止' : '分析心跳缺失',
detail: !cryptoAgent.running
? '信号层当前未运行,后续不会继续产出分析、信号或执行决策。'
: '当前没有最近分析心跳,说明调度、循环或主任务可能卡住。',
action: '优先核对进程状态、定时调度和主循环日志,确认系统是否仍在正常扫盘。',
badges: [
!cryptoAgent.running ? 'STOPPED' : 'NO HEARTBEAT',
monitor.last_heartbeat_at ? `上次 ${relativeTime(monitor.last_heartbeat_at)}` : '无时间戳',
],
});
}
if (haltedEntries.length > 0) {
const [targetKey, halt] = haltedEntries[0];
top.push({
tone: 'danger',
title: `${targetKey} 已停机`,
detail: halt?.reason || '平台已触发回撤熔断或风险停机。',
action: '检查触发原因、核对持仓是否已处理,然后决定是恢复自动执行还是保持停机。',
badges: [
`${haltedEntries.length} 个停机目标`,
halt?.halted_at ? formatTime(halt.halted_at) : '等待处理',
],
});
}
const latestBlocked = events.find((event) => event.event_type === 'execution_blocked_summary');
if (latestBlocked) {
top.push({
tone: latestBlocked.status === 'error' ? 'danger' : 'warn',
title: `${latestBlocked.symbol || '-'} 执行被阻塞`,
detail: latestBlocked.reason || '最近有信号未能落到执行层。',
action: '切到风险标签核对平台级归因,确认是价格未到、风控拦截、余额问题还是执行关闭。',
badges: [
latestBlocked.platform || 'execution',
latestBlocked.signal_timeframe_text || latestBlocked.setup_type || 'blocked',
],
});
}
const unprotected = positions.filter((item) => !item.take_profit || !item.stop_loss);
if (unprotected.length > 0) {
const sample = unprotected[0];
top.push({
tone: 'warn',
title: `${sample.symbol || '-'} 风控保护不完整`,
detail: `当前有 ${unprotected.length} 个持仓缺少止盈或止损,保护单链路可能存在异常。`,
action: '优先检查执行监管器和交易所保护单状态,确认 TP/SL 是否漏挂、失败或已失效。',
badges: [
`${unprotected.length} 个异常持仓`,
`${sample.platform || '-'} / ${sample.account_id || '-'}`,
],
});
}
if (disabledTargets.length > 0) {
const [targetKey] = disabledTargets[0];
top.push({
tone: 'warn',
title: `${targetKey} 自动交易已关闭`,
detail: `当前共有 ${disabledTargets.length} 个执行目标处于手动关闭状态。`,
action: '确认这是有意停用还是遗留配置。如果应继续执行,需要在控制台重新开启。',
badges: [`${disabledTargets.length} OFF`, 'manual control'],
});
}
attentionItems.slice(0, 2).forEach((item) => {
top.push({
tone: normalizeSeverity(item.severity),
title: item.title || '待处理事项',
detail: item.detail || '请检查对应模块状态。',
action: '根据事项详情进入风险、运行或平台区域继续下钻。',
badges: [item.timestamp ? relativeTime(item.timestamp) : 'now'],
});
});
if (!top.length) {
top.push({
tone: 'good',
title: '当前没有紧急人工接管项',
detail: '系统没有明显的停机、熔断、执行阻塞或保护单异常。',
action: '重点关注新的信号生命周期和运营指标,确认系统仍有稳定分析与执行产出。',
badges: ['CLEAR'],
});
}
return top.slice(0, 4);
}
function eventPlatformMatches(event, platform) {
const eventPlatform = String(event?.platform || '').toLowerCase();
if (platform === 'paper') {
return eventPlatform.includes('paper');
}
if (platform === 'bitget') {
return eventPlatform.includes('bitget');
}
return true;
}
function findMatchingEvent(signal, laneKeyword, events, platform) {
const signalTime = signal?.created_at ? new Date(signal.created_at).getTime() : 0;
const symbol = signal?.symbol;
const candidates = (events || []).filter((event) => {
if (event.symbol !== symbol) return false;
if (!eventPlatformMatches(event, platform)) return false;
if (laneKeyword && event.signal_timeframe_text && !String(event.signal_timeframe_text).includes(laneKeyword)) return false;
const eventTime = event?.timestamp ? new Date(event.timestamp).getTime() : 0;
if (!signalTime || !eventTime || Number.isNaN(signalTime) || Number.isNaN(eventTime)) return true;
return Math.abs(eventTime - signalTime) <= 12 * 3600 * 1000;
});
return candidates.sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0))[0] || null;
}
function findMatchingPosition(signal, platform, positions) {
return (positions || []).find((position) => {
if (position.symbol !== signal.symbol) return false;
return String(position.platform || '').toLowerCase() === String(platform || '').toLowerCase();
}) || null;
}
function findMatchingOrder(signal, platform, orders) {
return (orders || []).find((order) => {
if (order.symbol !== signal.symbol) return false;
if (String(order.platform || '').toLowerCase() !== String(platform || '').toLowerCase()) return false;
return order.category === 'entry';
}) || null;
}
function buildLifecycleLane(signal, laneName, platform, previewDecision, events, positions, orders) {
const position = findMatchingPosition(signal, platform, positions);
const order = findMatchingOrder(signal, platform, orders);
const event = findMatchingEvent(signal, signal.timeframe || signal.type || '', events, platform);
let tone = 'warn';
let state = 'pending';
const detailRows = [];
if (previewDecision?.decision) {
detailRows.push(`决策: ${previewDecision.decision}`);
}
if (position) {
tone = (!position.take_profit || !position.stop_loss) ? 'warn' : 'good';
state = 'position';
detailRows.push(`已持仓 ${position.side || '-'} / ${formatNumber(position.size || 0, 4)} / ${formatNumber(position.leverage || 0, 1)}x`);
detailRows.push(`入场 ${formatMoney(position.entry_price)} / 现价 ${formatMoney(position.mark_price)}`);
detailRows.push(`TP ${position.take_profit ? formatMoney(position.take_profit) : '-'} / SL ${position.stop_loss ? formatMoney(position.stop_loss) : '-'}`);
} else if (order) {
tone = 'warn';
state = 'entry order';
detailRows.push(`挂单 ${order.order_type || '-'} / ${formatMoney(order.price)} / ${formatNumber(order.size || 0, 4)}`);
detailRows.push(`创建 ${order.created_at ? `${relativeTime(order.created_at)} / ${formatTime(order.created_at)}` : '-'}`);
} else if (event) {
tone = event.status === 'success' ? 'good' : (event.status === 'error' ? 'danger' : 'warn');
state = event.status || event.event_type || 'event';
detailRows.push(event.reason || '已有执行事件,但未找到持仓或挂单。');
if (event.action || event.event_type) {
detailRows.push(`动作 ${event.action || '-'} / 事件 ${event.event_type || '-'}`);
}
} else if (previewDecision?.decision) {
tone = ['OPEN', 'ADD', 'CLOSE', 'CANCEL_PENDING'].includes(previewDecision.decision) ? 'warn' : 'good';
state = previewDecision.decision.toLowerCase();
detailRows.push(previewDecision.reason || previewDecision.reasoning || '已有决策预览,但尚未找到执行落地迹象。');
} else {
tone = 'good';
state = 'idle';
detailRows.push('当前没有匹配到执行结果、挂单或持仓。');
}
return {
laneName,
tone,
state,
detailRows,
};
}
function loadSensitivePreference() {
try {
revealSensitiveData = window.localStorage.getItem(SENSITIVE_VISIBILITY_KEY) === '1';
@ -2455,6 +3002,8 @@
cachedConsoleData.crypto_agent?.platform_halts,
cachedConsoleData.crypto_agent?.target_execution_controls || {}
);
renderOpsKpis(cachedConsoleData);
renderSignalLifecycle(cachedConsoleData);
}
}
@ -2995,6 +3544,129 @@
});
}
function renderTopPriority(data) {
const container = document.getElementById('topPriority');
if (!container) return;
const priorities = deriveTopPriorities(data);
const primary = priorities[0];
const rest = priorities.slice(1, 4);
container.innerHTML = `
<article class="top-priority-card ${primary.tone}">
<div class="top-priority-kicker">top priority</div>
<div class="top-priority-title">${primary.title}</div>
<div class="top-priority-detail">${primary.detail}</div>
<div class="top-priority-action"><strong style="color: var(--text);">建议动作:</strong> ${primary.action}</div>
<div class="top-priority-meta">
${(primary.badges || []).map((badge) => `<span class="event-inline-badge">${badge}</span>`).join('')}
</div>
</article>
<div class="priority-mini-list">
${rest.length ? rest.map((item) => `
<div class="priority-mini-item ${item.tone}">
<div class="priority-mini-head">
<div class="priority-mini-title">${item.title}</div>
<span class="event-inline-badge">${item.tone}</span>
</div>
<div class="priority-mini-detail">${item.detail}</div>
</div>
`).join('') : compactEmpty('没有其他更高优先级事项', '当前首要问题已经覆盖了最需要处理的点。')}
</div>
`;
}
function renderOpsKpis(data) {
const container = document.getElementById('opsKpis');
if (!container) return;
const snapshot = inferOpsSnapshot(data);
const halts = countHalted(data.crypto_agent?.platform_halts || {});
const disabled = Object.values(data.crypto_agent?.target_execution_controls || {}).filter((item) => item?.enabled === false).length;
container.innerHTML = `
<article class="ops-kpi-card ${snapshot.recentSignal24h === 0 ? 'warn' : ''}">
<div class="ops-kpi-label">24h 分析产出</div>
<div class="ops-kpi-value">${snapshot.recentSignal24h}</div>
<div class="ops-kpi-detail">最近 24 小时写入的信号数。若长期为 0需要先看运行心跳。</div>
</article>
<article class="ops-kpi-card ${snapshot.blocked24h > snapshot.success24h ? 'warn' : ''}">
<div class="ops-kpi-label">24h 执行成功率</div>
<div class="ops-kpi-value">${formatPercent(snapshot.conversionRate, 1)}</div>
<div class="ops-kpi-detail">成功 ${snapshot.success24h} / 信号 ${snapshot.recentSignal24h || 0},阻塞 ${snapshot.blocked24h}。</div>
</article>
<article class="ops-kpi-card ${snapshot.protectionCoverage < 100 ? 'warn' : ''}">
<div class="ops-kpi-label">持仓保护完整率</div>
<div class="ops-kpi-value">${formatPercent(snapshot.protectionCoverage, 0)}</div>
<div class="ops-kpi-detail">持仓 ${snapshot.positions.length} / 已完整 TP+SL ${snapshot.positions.filter((item) => item.take_profit && item.stop_loss).length}。</div>
</article>
<article class="ops-kpi-card ${(halts > 0 || disabled > 0) ? 'danger' : (snapshot.staleEntryOrders > 0 ? 'warn' : '')}">
<div class="ops-kpi-label">停机与陈旧挂单</div>
<div class="ops-kpi-value">${halts + disabled + snapshot.staleEntryOrders}</div>
<div class="ops-kpi-detail">停机 ${halts} / 关闭 ${disabled} / 超 30m 未成交入场单 ${snapshot.staleEntryOrders}。</div>
</article>
`;
}
function renderSignalLifecycle(data) {
const container = document.getElementById('signalLifecycle');
if (!container) return;
const signals = data.signals?.latest || [];
const events = data.execution_events || [];
const positions = data.management?.positions || [];
const orders = data.management?.orders || [];
const previewMap = data.crypto_agent?.last_execution_preview || {};
if (!signals.length) {
container.innerHTML = compactEmpty('暂无可追踪的信号生命周期', '当前没有最近信号,先关注运行心跳和分析状态。');
return;
}
const cards = signals.slice(0, 5).map((signal) => {
const preview = previewMap?.[signal.symbol] || {};
const paperLane = buildLifecycleLane(signal, '模拟盘', 'paper', preview.paper, events, positions, orders);
const bitgetPreview = preview.bitget_accounts?.default || preview.bitget;
const bitgetLane = buildLifecycleLane(signal, 'Bitget', 'bitget', bitgetPreview, events, positions, orders);
const tone = statusClassFromAction(signal.action);
const summary = signal.reasoning || signal.reason || signal.summary || '暂无额外说明';
return `
<article class="lifecycle-card ${tone}">
<div class="lifecycle-head">
<div>
<div class="lifecycle-title">${signal.symbol || '-'}</div>
<div class="lifecycle-meta">
${relativeTime(signal.created_at)} / ${formatTime(signal.created_at)}<br>
${signal.signal_type || '-'} / ${signal.timeframe || signal.type || '-'} / ${signal.grade || '-'} / ${formatPercent(signal.confidence || 0, 1)}
</div>
</div>
<span class="badge ${tone}">${signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '观望'}</span>
</div>
<div class="lifecycle-summary">
入场 ${formatMoney(signal.entry_price)} / 现价 ${formatMoney(signal.current_price)} / TP ${formatMoney(signal.take_profit)} / SL ${formatMoney(signal.stop_loss)}<br>
${summary}
</div>
<div class="lifecycle-lanes">
${[paperLane, bitgetLane].map((lane) => `
<div class="lifecycle-lane ${lane.tone}">
<div class="lifecycle-lane-head">
<div class="lifecycle-lane-title">${lane.laneName}</div>
<div class="lifecycle-lane-state">${lane.state}</div>
</div>
<div class="lifecycle-lane-detail">
${lane.detailRows.map((row) => `<div>${row}</div>`).join('')}
</div>
</div>
`).join('')}
</div>
</article>
`;
});
container.innerHTML = cards.join('');
}
function renderSignalStream(signals) {
const container = document.getElementById('signalStream');
if (!signals || signals.length === 0) {
@ -3669,6 +4341,9 @@
renderFocusSummary(data);
renderAlertStrip(data);
renderOpsSummary(data);
renderTopPriority(data);
renderOpsKpis(data);
renderSignalLifecycle(data);
renderPlatforms(data.platforms, data.crypto_agent?.platform_halts, data.crypto_agent?.target_execution_controls || {});
renderSignalStream(data.signals?.latest || []);
renderAgentSignals(data.crypto_agent, data.signals);