stock-ai-agent/frontend/console.html
2026-04-27 10:32:14 +08:00

4555 lines
184 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统总控台</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #08111a;
--panel: rgba(11, 21, 32, 0.86);
--panel-strong: rgba(8, 18, 28, 0.96);
--panel-soft: rgba(16, 28, 41, 0.72);
--line: rgba(128, 169, 202, 0.18);
--line-strong: rgba(128, 169, 202, 0.32);
--text: #edf6ff;
--muted: #8ea6bc;
--cold: #7ec8ff;
--accent: #9cff57;
--warn: #ffb84d;
--danger: #ff6f61;
--good: #30d158;
--shadow: 0 20px 80px rgba(0, 0, 0, 0.38);
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
min-height: 100%;
background:
radial-gradient(circle at top left, rgba(64, 127, 255, 0.16), transparent 28%),
radial-gradient(circle at top right, rgba(156, 255, 87, 0.12), transparent 22%),
linear-gradient(180deg, #071018 0%, #08111a 48%, #050b11 100%);
color: var(--text);
font-family: "IBM Plex Sans", sans-serif;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
background-size: 28px 28px;
mask-image: linear-gradient(180deg, rgba(0,0,0,0.45), rgba(0,0,0,0.9));
}
.console-shell {
width: min(1600px, calc(100vw - 32px));
margin: 0 auto;
padding: 24px 0 40px;
}
.console-hub {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.console-sidebar {
position: sticky;
top: 18px;
padding: 16px;
border-radius: 22px;
background: var(--panel);
border: 1px solid var(--line);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.console-sidebar::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: 22px;
background: linear-gradient(180deg, rgba(255,255,255,0.04), transparent 24%, transparent 76%, rgba(126, 200, 255, 0.04));
}
.sidebar-label {
color: var(--muted);
font-size: 11px;
margin-bottom: 12px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.sidebar-nav {
display: grid;
gap: 10px;
}
.sidebar-link {
appearance: none;
width: 100%;
text-align: left;
cursor: pointer;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.06);
background: rgba(255,255,255,0.03);
color: var(--text);
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.sidebar-link:hover {
transform: translateY(-1px);
border-color: rgba(126, 200, 255, 0.18);
}
.sidebar-link.active {
border-color: rgba(126, 200, 255, 0.28);
background: rgba(126, 200, 255, 0.10);
}
.sidebar-link-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
.sidebar-link-sub {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.sidebar-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 20px;
padding: 0 8px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
color: var(--cold);
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
}
.hub-content {
display: grid;
gap: 18px;
}
.hub-pane {
display: none;
gap: 18px;
}
.hub-pane.active {
display: grid;
}
.pane-grid {
display: grid;
gap: 18px;
}
.pane-grid.two-col {
grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr);
}
.pane-grid.equal {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.compact-panel-body {
display: grid;
gap: 14px;
}
.monitor-strip {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.monitor-card {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.monitor-card .title {
color: var(--muted);
font-size: 11px;
margin-bottom: 8px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.monitor-card .main {
font-family: "IBM Plex Mono", monospace;
font-size: 16px;
color: var(--text);
margin-bottom: 6px;
}
.monitor-card .meta {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(360px, 0.9fr);
gap: 18px;
margin-bottom: 18px;
}
.hero-card,
.panel,
.platform-card,
.stream-card,
.signal-card {
position: relative;
overflow: hidden;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 22px;
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.hero-card::after,
.panel::after,
.platform-card::after,
.stream-card::after,
.signal-card::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(135deg, rgba(255,255,255,0.04), transparent 24%, transparent 70%, rgba(158, 214, 255, 0.05));
}
.hero-main {
padding: 26px 28px 22px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
color: var(--cold);
font-family: "IBM Plex Mono", monospace;
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--accent);
box-shadow: 0 0 18px rgba(156, 255, 87, 0.72);
}
.hero-title {
margin: 0;
font-size: clamp(34px, 4.8vw, 56px);
line-height: 1;
font-weight: 700;
letter-spacing: -0.04em;
}
.hero-title strong {
color: var(--cold);
font-weight: 700;
}
.hero-subtitle {
margin: 14px 0 24px;
max-width: 860px;
color: var(--muted);
font-size: 15px;
line-height: 1.7;
}
.hero-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.hero-metric {
padding: 16px;
border-radius: 18px;
background: var(--panel-soft);
border: 1px solid rgba(126, 200, 255, 0.08);
position: relative;
overflow: hidden;
}
.hero-metric::after {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, var(--cold), transparent);
opacity: 0.9;
}
.metric-label {
color: var(--muted);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 8px;
}
.metric-value {
font-family: "IBM Plex Mono", monospace;
font-size: clamp(22px, 3vw, 30px);
font-weight: 600;
}
.metric-value.good {
color: var(--good);
}
.metric-value.danger {
color: var(--danger);
}
.metric-note {
margin-top: 6px;
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.hero-side {
padding: 22px;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 16px;
background: var(--panel-strong);
}
.status-stack {
display: grid;
gap: 12px;
}
.status-pill {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.025);
border: 1px solid rgba(255,255,255,0.06);
}
.status-pill .name {
font-size: 14px;
color: var(--muted);
}
.status-pill .value {
font-family: "IBM Plex Mono", monospace;
font-size: 13px;
color: var(--text);
}
.status-pill.halted .value {
color: var(--danger);
}
.hero-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.action-btn {
appearance: none;
border: none;
cursor: pointer;
border-radius: 14px;
padding: 12px 16px;
font-family: "IBM Plex Mono", monospace;
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
transition: transform 0.2s ease, opacity 0.2s ease, background 0.2s ease;
}
.action-btn:hover {
transform: translateY(-1px);
}
.action-primary {
background: linear-gradient(135deg, #9cff57, #63e6be);
color: #071018;
}
.action-secondary {
background: rgba(126, 200, 255, 0.12);
color: var(--cold);
border: 1px solid rgba(126, 200, 255, 0.22);
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.78fr);
gap: 18px;
}
.layout > :only-child {
grid-column: 1 / -1;
}
.left-stack,
.right-stack {
display: grid;
gap: 18px;
}
.section-label {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: rgba(126, 200, 255, 0.08);
border: 1px solid rgba(126, 200, 255, 0.16);
color: var(--cold);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.section-label::before {
content: "";
width: 8px;
height: 8px;
border-radius: 999px;
background: currentColor;
box-shadow: 0 0 10px currentColor;
}
.panel {
padding: 22px;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.panel-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.panel-title {
margin: 0;
font-size: 18px;
font-weight: 600;
letter-spacing: -0.02em;
}
.panel-sub {
color: var(--muted);
font-size: 12px;
font-family: "IBM Plex Mono", monospace;
}
.priority-layout {
display: grid;
grid-template-columns: minmax(320px, 0.9fr) minmax(0, 1.1fr);
gap: 18px;
margin-top: 18px;
}
.priority-main,
.priority-side {
display: grid;
gap: 18px;
}
.focus-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.focus-summary-card {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.focus-summary-card .kicker {
color: var(--muted);
font-size: 11px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-family: "IBM Plex Mono", monospace;
}
.focus-summary-card .headline {
font-family: "IBM Plex Mono", monospace;
font-size: 18px;
color: var(--text);
margin-bottom: 6px;
}
.focus-summary-card .headline.good {
color: var(--good);
}
.focus-summary-card .headline.warn {
color: var(--warn);
}
.focus-summary-card .headline.danger {
color: var(--danger);
}
.focus-summary-card .detail {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.alert-strip {
display: grid;
gap: 10px;
margin-top: 14px;
}
.alert-strip:empty {
display: none;
}
.alert-banner {
display: grid;
grid-template-columns: 140px 1fr auto;
gap: 12px;
align-items: center;
padding: 12px 14px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
}
.alert-banner.danger {
border-color: rgba(255, 111, 97, 0.26);
background: rgba(255, 111, 97, 0.10);
}
.alert-banner.warn {
border-color: rgba(255, 184, 77, 0.24);
background: rgba(255, 184, 77, 0.10);
}
.alert-banner.good {
border-color: rgba(48, 209, 88, 0.22);
background: rgba(48, 209, 88, 0.08);
}
.alert-kicker {
color: var(--text);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.alert-detail {
color: var(--muted);
font-size: 13px;
line-height: 1.55;
}
.alert-count {
font-family: "IBM Plex Mono", monospace;
font-size: 18px;
color: var(--text);
}
.focus-grid {
display: grid;
grid-template-columns: minmax(320px, 0.82fr) minmax(0, 1.18fr);
gap: 18px;
margin-top: 18px;
}
.focus-panel {
min-height: 100%;
}
.overview-grid {
display: grid;
grid-template-columns: minmax(0, 1.18fr) minmax(320px, 0.82fr);
gap: 14px;
}
.overview-main,
.overview-side {
display: grid;
gap: 14px;
}
.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));
gap: 12px;
margin-top: 12px;
}
.health-card {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.health-card .kicker {
color: var(--muted);
font-size: 11px;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.health-card .headline {
font-family: "IBM Plex Mono", monospace;
font-size: 14px;
color: var(--text);
}
.health-card .headline.good {
color: var(--good);
}
.health-card .headline.warn {
color: var(--warn);
}
.health-card .headline.danger {
color: var(--danger);
}
.health-card .detail {
margin-top: 6px;
color: var(--muted);
font-size: 11px;
line-height: 1.55;
font-family: "IBM Plex Mono", monospace;
}
.signal-grid,
.platform-grid {
display: grid;
gap: 14px;
}
.coord-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.coord-block {
padding: 14px;
border-radius: 18px;
background: rgba(255,255,255,0.025);
border: 1px solid rgba(255,255,255,0.06);
}
.coord-block .signal-grid {
grid-template-columns: 1fr;
}
.block-head {
margin-bottom: 12px;
}
.block-title {
margin: 10px 0 4px;
font-size: 16px;
font-weight: 600;
letter-spacing: -0.02em;
}
.block-sub {
color: var(--muted);
font-size: 12px;
font-family: "IBM Plex Mono", monospace;
}
.signal-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.signal-card {
padding: 18px;
}
.signal-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.signal-symbol {
font-size: 20px;
font-weight: 700;
letter-spacing: -0.03em;
}
.signal-meta {
color: var(--muted);
font-size: 12px;
font-family: "IBM Plex Mono", monospace;
}
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 74px;
padding: 6px 10px;
border-radius: 999px;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.06em;
border: 1px solid transparent;
}
.badge.buy {
background: rgba(48, 209, 88, 0.12);
color: var(--good);
border-color: rgba(48, 209, 88, 0.2);
}
.badge.sell {
background: rgba(255, 111, 97, 0.12);
color: var(--danger);
border-color: rgba(255, 111, 97, 0.22);
}
.badge.wait {
background: rgba(255, 184, 77, 0.12);
color: var(--warn);
border-color: rgba(255, 184, 77, 0.22);
}
.signal-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.stat-chip {
padding: 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.03);
}
.decision-chip {
border: 1px solid rgba(255,255,255,0.06);
transition: border-color 0.2s ease, background 0.2s ease;
}
.decision-chip.success {
border-color: rgba(48, 209, 88, 0.18);
background: rgba(48, 209, 88, 0.08);
}
.decision-chip.warning {
border-color: rgba(255, 184, 77, 0.18);
background: rgba(255, 184, 77, 0.08);
}
.decision-chip.error {
border-color: rgba(255, 111, 97, 0.18);
background: rgba(255, 111, 97, 0.08);
}
.decision-chip.hold {
border-color: rgba(255,255,255,0.08);
background: rgba(255,255,255,0.04);
}
.stat-chip .label {
display: block;
color: var(--muted);
font-size: 11px;
margin-bottom: 5px;
}
.stat-chip .value {
font-family: "IBM Plex Mono", monospace;
font-size: 13px;
}
.platform-grid {
grid-template-columns: 1fr;
}
.platform-grid.compact {
grid-template-columns: 1fr;
}
.platform-card {
padding: 18px;
}
.platform-card.danger {
border-color: rgba(255, 111, 97, 0.22);
background: rgba(255, 111, 97, 0.06);
}
.platform-card.warn {
border-color: rgba(255, 184, 77, 0.18);
background: rgba(255, 184, 77, 0.05);
}
.platform-top {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 10px;
}
.platform-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.platform-state {
font-family: "IBM Plex Mono", monospace;
font-size: 11px;
color: var(--muted);
}
.platform-overview {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-bottom: 12px;
}
.platform-pill-stat {
padding: 10px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.035);
border: 1px solid rgba(255,255,255,0.05);
}
.platform-pill-stat .label {
display: block;
color: var(--muted);
font-size: 10px;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-family: "IBM Plex Mono", monospace;
}
.platform-pill-stat .value {
font-family: "IBM Plex Mono", monospace;
font-size: 14px;
color: var(--text);
}
.risk-band {
margin: 8px 0 12px;
height: 8px;
border-radius: 999px;
background: rgba(255,255,255,0.06);
overflow: hidden;
}
.risk-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--good), var(--warn), var(--danger));
}
.platform-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.platform-stat {
padding: 10px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.03);
}
.platform-stat .label {
display: block;
color: var(--muted);
font-size: 11px;
margin-bottom: 4px;
}
.platform-stat .value {
font-family: "IBM Plex Mono", monospace;
font-size: 14px;
}
.platform-detail-toggle {
margin-top: 12px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
color: var(--muted);
font-size: 12px;
}
.platform-detail-toggle summary {
cursor: pointer;
list-style: none;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--cold);
}
.platform-detail-toggle summary::-webkit-details-marker {
display: none;
}
.stream-card {
padding: 18px;
}
.dense-panel .panel-header {
margin-bottom: 12px;
}
.workspace-panel {
margin-top: 18px;
}
.workspace-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 14px;
}
.workspace-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.workspace-tab {
appearance: none;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: var(--muted);
cursor: pointer;
border-radius: 999px;
padding: 9px 12px;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.06em;
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.workspace-tab.active {
color: var(--cold);
border-color: rgba(126, 200, 255, 0.28);
background: rgba(126, 200, 255, 0.12);
}
.workspace-tab.muted {
color: rgba(142, 166, 188, 0.72);
border-color: rgba(255,255,255,0.05);
}
.tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
margin-left: 6px;
padding: 0 6px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
color: inherit;
font-size: 10px;
line-height: 1;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
.stream-list {
display: grid;
gap: 10px;
}
.signal-feed-grid {
display: grid;
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;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.signal-feed-card.buy {
border-color: rgba(48, 209, 88, 0.18);
background: rgba(48, 209, 88, 0.07);
}
.signal-feed-card.sell {
border-color: rgba(255, 111, 97, 0.18);
background: rgba(255, 111, 97, 0.07);
}
.signal-feed-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 10px;
}
.signal-feed-symbol {
font-size: 16px;
font-weight: 700;
color: var(--text);
}
.signal-feed-meta {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
line-height: 1.5;
}
.signal-feed-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.signal-feed-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-bottom: 10px;
}
.signal-feed-stat {
padding: 10px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.03);
}
.signal-feed-stat .label {
display: block;
color: var(--muted);
font-size: 10px;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-family: "IBM Plex Mono", monospace;
}
.signal-feed-stat .value {
font-family: "IBM Plex Mono", monospace;
font-size: 13px;
color: var(--text);
}
.signal-feed-reason {
color: var(--muted);
font-size: 12px;
line-height: 1.65;
}
.workspace-stream {
min-height: 0;
}
.scroll-panel {
max-height: 560px;
overflow: auto;
padding-right: 4px;
}
.log-shell {
display: grid;
gap: 12px;
}
.log-head {
margin-bottom: 0;
}
.event-list {
display: grid;
gap: 10px;
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.filter-chip {
appearance: none;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: var(--muted);
cursor: pointer;
border-radius: 999px;
padding: 8px 10px;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.filter-chip.active {
border-color: rgba(126, 200, 255, 0.28);
color: var(--cold);
background: rgba(126, 200, 255, 0.10);
}
.ghost-btn {
appearance: none;
border: 1px solid rgba(255,255,255,0.08);
background: rgba(255,255,255,0.03);
color: var(--muted);
cursor: pointer;
border-radius: 999px;
padding: 8px 12px;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.ghost-btn.active {
color: var(--cold);
border-color: rgba(126, 200, 255, 0.24);
background: rgba(126, 200, 255, 0.1);
}
.event-item {
display: grid;
grid-template-columns: 120px 110px 1fr;
gap: 12px;
align-items: start;
padding: 12px 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.event-time {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.event-tag {
display: inline-flex;
justify-content: center;
align-items: center;
min-height: 28px;
padding: 6px 8px;
border-radius: 10px;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.04em;
background: rgba(126, 200, 255, 0.12);
color: var(--cold);
}
.event-tag.success {
background: rgba(48, 209, 88, 0.12);
color: var(--good);
}
.event-tag.error {
background: rgba(255, 111, 97, 0.12);
color: var(--danger);
}
.event-tag.warning {
background: rgba(255, 184, 77, 0.12);
color: var(--warn);
}
.event-tag.hold {
background: rgba(255,255,255,0.08);
color: var(--muted);
}
.event-body {
font-size: 13px;
line-height: 1.55;
}
.event-body strong {
color: var(--text);
}
.event-summary {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 4px;
}
.event-inline-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
border-radius: 999px;
background: rgba(255,255,255,0.06);
color: var(--muted);
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.event-details {
margin-top: 8px;
border-top: 1px dashed rgba(255,255,255,0.08);
padding-top: 8px;
}
.event-details summary {
cursor: pointer;
list-style: none;
color: var(--cold);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.event-details summary::-webkit-details-marker {
display: none;
}
.event-details pre {
margin: 10px 0 0;
padding: 10px 12px;
border-radius: 10px;
background: rgba(0,0,0,0.2);
border: 1px solid rgba(255,255,255,0.06);
overflow: auto;
color: #cfe1f2;
font-size: 11px;
line-height: 1.55;
font-family: "IBM Plex Mono", monospace;
white-space: pre-wrap;
word-break: break-word;
}
.stream-item {
display: grid;
grid-template-columns: 124px 1fr auto;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.stream-item .time {
color: var(--muted);
font-family: "IBM Plex Mono", monospace;
font-size: 11px;
}
.stream-item .headline {
font-size: 14px;
line-height: 1.45;
}
.stream-item .grade {
font-family: "IBM Plex Mono", monospace;
font-size: 11px;
color: var(--cold);
}
.halt-list {
display: grid;
gap: 10px;
}
.halt-item {
padding: 14px;
border-radius: 14px;
border: 1px solid rgba(255,255,255,0.05);
background: rgba(255,255,255,0.025);
}
.halt-item.active {
border-color: rgba(255, 111, 97, 0.2);
background: rgba(255, 111, 97, 0.08);
}
.halt-item.disabled {
border-color: rgba(255, 184, 77, 0.2);
background: rgba(255, 184, 77, 0.08);
}
.halt-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.halt-name {
font-size: 14px;
font-weight: 600;
}
.halt-state {
font-family: "IBM Plex Mono", monospace;
font-size: 11px;
color: var(--muted);
}
.halt-reason {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.halt-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
gap: 8px;
flex-wrap: wrap;
}
.mini-btn {
appearance: none;
border: 1px solid rgba(126, 200, 255, 0.22);
background: rgba(126, 200, 255, 0.12);
color: var(--cold);
cursor: pointer;
border-radius: 10px;
padding: 8px 10px;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.mini-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.mini-btn.warn {
border-color: rgba(255, 184, 77, 0.24);
background: rgba(255, 184, 77, 0.12);
color: var(--warn);
}
.mini-btn.good {
border-color: rgba(48, 209, 88, 0.22);
background: rgba(48, 209, 88, 0.12);
color: var(--good);
}
.footer-note {
margin-top: 16px;
color: var(--muted);
font-size: 12px;
font-family: "IBM Plex Mono", monospace;
}
.heartbeat-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.heartbeat-card {
padding: 12px;
border-radius: 12px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.heartbeat-card .label {
display: block;
color: var(--muted);
font-size: 11px;
margin-bottom: 6px;
}
.heartbeat-card .value {
font-size: 13px;
font-family: "IBM Plex Mono", monospace;
color: var(--text);
}
.analysis-log-list {
display: grid;
gap: 10px;
}
.runtime-summary-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
gap: 12px;
margin-bottom: 14px;
}
.runtime-summary-card {
padding: 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.runtime-summary-title {
color: var(--muted);
font-size: 11px;
margin-bottom: 10px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.runtime-summary-main {
font-family: "IBM Plex Mono", monospace;
font-size: 16px;
color: var(--text);
margin-bottom: 8px;
}
.runtime-summary-meta {
display: grid;
gap: 8px;
}
.runtime-summary-row {
display: flex;
justify-content: space-between;
gap: 10px;
font-size: 12px;
color: var(--muted);
}
.runtime-summary-row strong {
color: var(--text);
font-weight: 500;
}
.blocked-list {
display: grid;
gap: 10px;
}
.blocked-item {
padding: 12px 14px;
border-radius: 14px;
background: rgba(255, 184, 77, 0.08);
border: 1px solid rgba(255, 184, 77, 0.16);
}
.blocked-item-head {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
align-items: center;
}
.blocked-item-title {
font-size: 13px;
color: var(--text);
font-weight: 600;
}
.blocked-item-meta {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.blocked-platforms {
display: grid;
gap: 6px;
margin-top: 10px;
}
.blocked-platform {
display: flex;
gap: 8px;
align-items: flex-start;
font-size: 12px;
color: var(--muted);
}
.blocked-platform strong {
color: var(--text);
min-width: 92px;
}
.target-grid,
.decision-account-grid {
display: grid;
gap: 10px;
margin-top: 12px;
}
.target-grid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.decision-account-grid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.target-mini-card,
.decision-account-card {
padding: 12px 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.target-mini-card.halted,
.decision-account-card.error {
border-color: rgba(255, 111, 97, 0.22);
background: rgba(255, 111, 97, 0.08);
}
.target-mini-card.disabled {
border-color: rgba(255, 184, 77, 0.22);
background: rgba(255, 184, 77, 0.08);
}
.decision-account-card.warning {
border-color: rgba(255, 184, 77, 0.22);
background: rgba(255, 184, 77, 0.08);
}
.decision-account-card.success {
border-color: rgba(48, 209, 88, 0.20);
background: rgba(48, 209, 88, 0.08);
}
.target-mini-head,
.decision-account-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 8px;
}
.target-mini-title,
.decision-account-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.target-mini-meta,
.decision-account-meta {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.target-mini-stats,
.decision-account-detail {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.target-mini-row {
display: flex;
justify-content: space-between;
gap: 10px;
}
.target-mini-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 10px;
}
.lane-state-list {
display: grid;
gap: 8px;
margin-top: 10px;
}
.lane-state-item {
display: grid;
grid-template-columns: 76px 1fr;
gap: 10px;
padding-top: 8px;
border-top: 1px solid rgba(255,255,255,0.06);
font-size: 12px;
}
.lane-state-symbol {
font-family: "IBM Plex Mono", monospace;
color: var(--text);
}
.lane-state-detail {
color: var(--muted);
line-height: 1.5;
}
.analysis-log-item {
padding: 12px 14px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.analysis-log-item.error {
border-color: rgba(255, 111, 97, 0.2);
background: rgba(255, 111, 97, 0.08);
}
.analysis-log-item.warning,
.analysis-log-item.hold {
border-color: rgba(255, 184, 77, 0.2);
background: rgba(255, 184, 77, 0.08);
}
.analysis-log-item.success {
border-color: rgba(48, 209, 88, 0.18);
background: rgba(48, 209, 88, 0.08);
}
.analysis-log-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 6px;
}
.analysis-log-head strong {
font-size: 13px;
}
.analysis-log-meta {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.analysis-log-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.ops-grid {
display: grid;
gap: 18px;
grid-template-columns: 1fr;
margin-top: 18px;
}
.attention-list,
.unified-list {
display: grid;
gap: 10px;
}
.attention-item,
.unified-item {
padding: 14px 16px;
border-radius: 14px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.05);
}
.attention-item.danger {
border-color: rgba(255, 111, 97, 0.2);
background: rgba(255, 111, 97, 0.08);
}
.attention-item.warning {
border-color: rgba(255, 184, 77, 0.2);
background: rgba(255, 184, 77, 0.08);
}
.attention-item.info {
border-color: rgba(126, 200, 255, 0.2);
background: rgba(126, 200, 255, 0.08);
}
.attention-title,
.unified-title {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 6px;
}
.attention-title strong,
.unified-title strong {
font-size: 14px;
}
.attention-time,
.unified-time {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
}
.attention-detail,
.unified-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.table-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.toolbar-chip {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(126, 200, 255, 0.08);
color: var(--cold);
border: 1px solid rgba(126, 200, 255, 0.16);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.unified-section {
margin-top: 18px;
}
.unified-table {
overflow-x: auto;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.05);
background: rgba(255,255,255,0.02);
}
.position-card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.position-card {
padding: 16px;
border-radius: 18px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.position-card.long {
border-color: rgba(48, 209, 88, 0.18);
}
.position-card.short {
border-color: rgba(255, 111, 97, 0.18);
}
.position-card-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 12px;
}
.position-card-symbol {
font-size: 16px;
font-weight: 700;
color: var(--text);
}
.position-card-meta {
color: var(--muted);
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
line-height: 1.5;
}
.position-card-pnl {
text-align: right;
}
.position-card-pnl .amount {
font-family: "IBM Plex Mono", monospace;
font-size: 18px;
font-weight: 600;
}
.position-card-pnl .ratio {
margin-top: 4px;
font-family: "IBM Plex Mono", monospace;
font-size: 12px;
}
.position-card-gridline {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-bottom: 12px;
}
.position-card-block {
padding: 10px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.03);
}
.position-card-block .label {
display: block;
color: var(--muted);
font-size: 10px;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.05em;
font-family: "IBM Plex Mono", monospace;
}
.position-card-block .value {
font-family: "IBM Plex Mono", monospace;
font-size: 13px;
color: var(--text);
line-height: 1.55;
}
.position-card-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.unified-table table {
min-width: 980px;
}
.platform-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 92px;
padding: 5px 10px;
border-radius: 999px;
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--cold);
background: rgba(126, 200, 255, 0.1);
border: 1px solid rgba(126, 200, 255, 0.2);
}
.side-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 60px;
padding: 4px 10px;
border-radius: 999px;
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.side-pill.long {
color: var(--good);
background: rgba(48, 209, 88, 0.12);
border: 1px solid rgba(48, 209, 88, 0.2);
}
.side-pill.short {
color: var(--danger);
background: rgba(255, 111, 97, 0.12);
border: 1px solid rgba(255, 111, 97, 0.2);
}
.inline-mono {
font-family: "IBM Plex Mono", monospace;
font-size: 12px;
}
.loading,
.error-box,
.empty-box {
padding: 24px;
border-radius: 18px;
text-align: center;
color: var(--muted);
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.error-box {
color: #ffd0cb;
background: rgba(255, 111, 97, 0.08);
border-color: rgba(255, 111, 97, 0.2);
}
.empty-box.compact {
padding: 14px 16px;
text-align: left;
border-radius: 14px;
}
.empty-box.compact strong {
display: block;
color: var(--text);
font-size: 13px;
margin-bottom: 4px;
}
.empty-detail {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
@media (max-width: 1240px) {
.console-hub,
.hero,
.layout,
.priority-layout,
.focus-grid,
.overview-grid {
grid-template-columns: 1fr;
}
.console-sidebar {
position: static;
}
.sidebar-nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.platform-grid,
.signal-grid,
.ops-grid,
.coord-grid,
.position-card-grid,
.pane-grid.two-col,
.pane-grid.equal,
.monitor-strip,
.runtime-summary-grid {
grid-template-columns: 1fr;
}
.platform-overview,
.platform-stats,
.signal-feed-stats,
.position-card-gridline,
.ops-kpi-grid,
.lifecycle-lanes {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.hero-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.focus-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.console-shell {
width: min(100vw - 20px, 100%);
padding-top: 12px;
}
.sidebar-nav {
grid-template-columns: 1fr;
}
.hero-main,
.hero-side,
.panel,
.platform-card,
.signal-card,
.stream-card {
padding: 18px;
}
.hero-metrics,
.platform-stats,
.platform-overview,
.signal-feed-stats,
.position-card-gridline,
.signal-stats,
.heartbeat-grid,
.health-ribbon,
.ops-summary,
.focus-summary,
.top-priority-grid,
.ops-kpi-grid,
.lifecycle-lanes {
grid-template-columns: 1fr;
}
.alert-banner {
grid-template-columns: 1fr;
}
.stream-item {
grid-template-columns: 1fr;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="console-shell">
<section class="hero">
<div class="hero-card hero-main">
<div class="eyebrow">System Console</div>
<h1 class="hero-title">交易系统 <strong>总控台</strong></h1>
<p class="hero-subtitle">
统一观察信号流、执行层、多账户风险和平台熔断状态。
这个页面的目标不是展示“历史”,而是让你在一屏内判断系统现在是不是健康、哪里堵住了、哪里需要人工接管。
</p>
<div class="hero-metrics" id="heroMetrics">
<div class="hero-metric"><div class="metric-label">运行 Agent</div><div class="metric-value">-</div></div>
<div class="hero-metric"><div class="metric-label">30 分钟信号</div><div class="metric-value">-</div></div>
<div class="hero-metric"><div class="metric-label">活跃持仓</div><div class="metric-value">-</div></div>
<div class="hero-metric"><div class="metric-label">停机平台</div><div class="metric-value">-</div></div>
</div>
</div>
<div class="hero-card hero-side">
<div>
<div class="panel-header" style="margin-bottom: 14px;">
<h2 class="panel-title">运行态快照</h2>
<div class="panel-sub" id="lastUpdated">等待刷新</div>
</div>
<div class="status-stack" id="runSnapshot">
<div class="status-pill"><span class="name">系统启动</span><span class="value">-</span></div>
<div class="status-pill"><span class="name">Crypto Agent</span><span class="value">-</span></div>
<div class="status-pill"><span class="name">轮询模式</span><span class="value">-</span></div>
<div class="status-pill"><span class="name">监控标的</span><span class="value">-</span></div>
</div>
</div>
<div class="hero-actions">
<button class="action-btn action-primary" id="refreshBtn">立即刷新</button>
<button class="action-btn action-secondary" id="toggleAutoRefreshBtn">自动刷新: 开</button>
</div>
</div>
</section>
<div id="feedback"></div>
<section class="console-hub">
<aside class="console-sidebar">
<div class="sidebar-label">Navigation</div>
<div class="sidebar-nav" id="sidebarNav">
<button class="sidebar-link active" data-hub-target="hubOverview">
<div class="sidebar-link-title">总览 <span class="sidebar-badge" id="sidebarBadgeOverview">-</span></div>
<div class="sidebar-link-sub">系统健康、优先级、执行概览</div>
</button>
<button class="sidebar-link" data-hub-target="hubExecution">
<div class="sidebar-link-title">执行资产 <span class="sidebar-badge" id="sidebarBadgeExecution">-</span></div>
<div class="sidebar-link-sub">平台、持仓、挂单、自动交易控制</div>
</button>
<button class="sidebar-link" data-hub-target="hubSignals">
<div class="sidebar-link-title">信号决策 <span class="sidebar-badge" id="sidebarBadgeSignals">-</span></div>
<div class="sidebar-link-sub">生命周期、预览、信号缓存</div>
</button>
<button class="sidebar-link" data-hub-target="hubRuntime">
<div class="sidebar-link-title">运行监控 <span class="sidebar-badge" id="sidebarBadgeRuntime">-</span></div>
<div class="sidebar-link-sub">心跳、调度、guardian、事件触发</div>
</button>
<button class="sidebar-link" data-hub-target="hubLogs">
<div class="sidebar-link-title">日志事件 <span class="sidebar-badge" id="sidebarBadgeLogs">-</span></div>
<div class="sidebar-link-sub">执行事件、分析日志、阻塞归因</div>
</button>
</div>
</aside>
<div class="hub-content">
<section class="panel">
<div class="panel-header">
<div>
<div class="section-label">Health 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="health-ribbon" id="healthRibbon">
<div class="health-card"><div class="kicker">分析状态</div><div class="headline">-</div></div>
<div class="health-card"><div class="kicker">最近轮次</div><div class="headline">-</div></div>
<div class="health-card"><div class="kicker">人工处理</div><div class="headline">-</div></div>
</div>
<div class="focus-summary" id="focusSummary">
<div class="focus-summary-card"><div class="kicker">运行心跳</div><div class="headline">-</div></div>
<div class="focus-summary-card"><div class="kicker">待执行机会</div><div class="headline">-</div></div>
<div class="focus-summary-card"><div class="kicker">风险暴露</div><div class="headline">-</div></div>
<div class="focus-summary-card"><div class="kicker">阻塞状态</div><div class="headline">-</div></div>
</div>
<div class="alert-strip" id="alertStrip"></div>
</section>
<section class="hub-pane active" id="hubOverview">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Priority Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">当前最重要的问题</h2>
<div class="panel-sub">先回答你现在最该关注什么,再决定是否继续下钻</div>
</div>
</div>
<div class="compact-panel-body">
<div class="top-priority-grid" id="topPriority">
<div class="loading">正在整理当前优先级...</div>
</div>
<div class="ops-kpi-grid" id="opsKpis">
<div class="loading">正在汇总运营指标...</div>
</div>
</div>
</section>
<div class="pane-grid two-col">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Execution Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">平台执行概览</h2>
<div class="panel-sub">统一看平台权益、仓位、挂单、停机与自动交易状态</div>
</div>
</div>
<div class="platform-grid" id="platformGrid">
<div class="loading">正在加载平台状态...</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Attention Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">待处理清单</h2>
<div class="panel-sub">保留给人工干预和快速核对的辅助视图</div>
</div>
</div>
<div class="attention-list" id="attentionList">
<div class="loading">正在汇总待处理事项...</div>
</div>
</section>
</div>
</div>
</section>
<section class="hub-pane" id="hubExecution">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Asset Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">执行与资产</h2>
<div class="panel-sub">以后不再看 trading 页面,这里承接持仓、挂单和执行状态</div>
</div>
</div>
<div class="monitor-strip" id="executionMonitorStrip">
<div class="monitor-card">
<div class="title">价格链路</div>
<div class="main">等待刷新</div>
<div class="meta">状态加载中</div>
</div>
<div class="monitor-card">
<div class="title">模拟盘执行链路</div>
<div class="main">等待刷新</div>
<div class="meta">状态加载中</div>
</div>
</div>
</section>
<section class="panel workspace-panel unified-section">
<div class="workspace-head">
<div>
<div class="section-label">Position Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">统一持仓视图</h2>
<div class="panel-sub">跨平台持仓合并展示,优先看风险、盈亏和保护完整度</div>
</div>
</div>
<div class="table-toolbar" id="positionsToolbar"></div>
<div class="unified-table" id="positionsTable">
<div class="loading">正在整理跨平台持仓...</div>
</div>
</section>
<section class="panel workspace-panel unified-section">
<div class="workspace-head">
<div>
<div class="section-label">Order Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">统一挂单视图</h2>
<div class="panel-sub">入场单、保护单、资金占用和老化状态集中查看</div>
</div>
</div>
<div class="table-toolbar" id="ordersToolbar"></div>
<div class="unified-table" id="ordersTable">
<div class="loading">正在整理跨平台挂单...</div>
</div>
</section>
</div>
</section>
<section class="hub-pane" id="hubSignals">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Lifecycle Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">信号与决策</h2>
<div class="panel-sub">把最近信号、生命周期、预览和缓存收敛到一个入口</div>
</div>
</div>
<div class="lifecycle-list" id="signalLifecycle">
<div class="loading">正在整理信号生命周期...</div>
</div>
</section>
<div class="pane-grid two-col">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Decision Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">最近决策预览</h2>
<div class="panel-sub">看系统准备做什么,以及不同执行目标如何响应</div>
</div>
</div>
<div class="signal-grid" id="decisionPreview">
<div class="loading">正在读取决策预览...</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Signal Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">最近信号流</h2>
<div class="panel-sub">压缩展示最新机会,但保留关键信号细节</div>
</div>
</div>
<div class="stream-list workspace-stream scroll-panel" id="signalStream">
<div class="loading">正在读取信号...</div>
</div>
</section>
</div>
<div class="pane-grid equal">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Agent Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">Crypto Agent 状态</h2>
<div class="panel-sub">最近信号缓存、统计和辅助协同信息</div>
</div>
</div>
<div class="signal-grid" id="agentSignals">
<div class="loading">正在读取 Agent 状态...</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Mirror Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">补充视角</h2>
<div class="panel-sub">保留对照区,避免主区过度堆叠</div>
</div>
</div>
<div class="signal-grid" id="coordinationMirror">
<div class="empty-box compact">
<strong>主视图已集中到本菜单</strong>
<div class="empty-detail">后续如需要补充“信号转执行”专题卡片,可以继续在这里加,不影响总览区。</div>
</div>
</div>
</section>
</div>
</div>
</section>
<section class="hub-pane" id="hubRuntime">
<div class="pane-grid two-col">
<section class="panel workspace-panel">
<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>
</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="runtime-summary-grid">
<div class="runtime-summary-card" id="runtimeSummaryCard">
<div class="runtime-summary-title">运行摘要</div>
<div class="runtime-summary-main">正在整理分析状态...</div>
</div>
<div class="runtime-summary-card" id="guardianSummaryCard">
<div class="runtime-summary-title">执行监管器</div>
<div class="runtime-summary-main">正在整理执行监管状态...</div>
</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Guardian Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">执行监管目标</h2>
<div class="panel-sub">监管 target、保护单补救和持仓管理能力集中展示</div>
</div>
</div>
<div class="analysis-log-list" id="guardianTargetList">
<div class="loading">正在读取执行监管目标...</div>
</div>
</section>
</div>
</section>
<section class="hub-pane" id="hubLogs">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<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="pane-grid equal">
<div class="halt-list" id="haltList">
<div class="loading">正在读取平台停机状态...</div>
</div>
<div class="blocked-list" id="blockedSummaryList">
<div class="loading">正在读取未落单汇总...</div>
</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head log-head">
<div>
<div class="section-label">Log Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">运行与执行日志</h2>
<div class="panel-sub">日志保留在独立菜单,按需查看</div>
</div>
<div class="workspace-tabs" data-tab-group="logs">
<button class="workspace-tab active" data-tab="logs" data-target="logsExecution">执行事件</button>
<button class="workspace-tab" data-tab="logs" data-target="logsAnalysis">分析日志</button>
</div>
</div>
<div class="tab-pane active" data-tab-pane="logs" id="logsExecution">
<div class="filter-row" id="eventFilters">
<button class="filter-chip active" data-filter="all">All</button>
<button class="filter-chip" data-filter="error">Error</button>
<button class="filter-chip" data-filter="warning">Warning</button>
<button class="filter-chip" data-filter="hold">Hold</button>
<button class="filter-chip" data-filter="success">Success</button>
</div>
<div class="event-list workspace-stream scroll-panel" id="eventStream">
<div class="loading">正在读取执行事件...</div>
</div>
</div>
<div class="tab-pane" data-tab-pane="logs" id="logsAnalysis">
<div class="analysis-log-list scroll-panel" id="analysisLogList">
<div class="loading">正在读取分析日志...</div>
</div>
</div>
</section>
</div>
</section>
</div>
</section>
</div>
<script>
let autoRefresh = true;
let timer = null;
let currentEventFilter = 'all';
let cachedExecutionEvents = [];
let cachedConsoleData = null;
let revealSensitiveData = false;
const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2';
const HUB_PREFERENCE_KEY = 'console_hub_active_v1';
function formatNumber(value, digits = 2) {
const num = Number(value || 0);
return Number.isFinite(num) ? num.toLocaleString('zh-CN', {
minimumFractionDigits: digits,
maximumFractionDigits: digits,
}) : '-';
}
function formatPercent(value, digits = 1) {
const num = Number(value || 0);
return Number.isFinite(num) ? `${num.toFixed(digits)}%` : '-';
}
function formatMoney(value) {
const num = Number(value || 0);
return Number.isFinite(num) ? `$${formatNumber(num, 2)}` : '-';
}
function formatTime(value) {
if (!value) return '-';
try {
return new Date(value).toLocaleString('zh-CN', { hour12: false });
} catch {
return String(value);
}
}
function relativeTime(value) {
if (!value) return '-';
const target = new Date(value);
if (Number.isNaN(target.getTime())) return String(value);
const diff = Date.now() - target.getTime();
const sec = Math.max(0, Math.floor(diff / 1000));
if (sec < 60) return `${sec}s 前`;
if (sec < 3600) return `${Math.floor(sec / 60)}m 前`;
if (sec < 86400) return `${Math.floor(sec / 3600)}h 前`;
return `${Math.floor(sec / 86400)}d 前`;
}
function statusClassFromAction(action) {
if (action === 'buy') return 'buy';
if (action === 'sell') return 'sell';
return 'wait';
}
function riskFillStyle(current, max) {
if (!max || max <= 0) return 0;
return Math.max(0, Math.min(100, (current / max) * 100));
}
function sumPlatformPositions(platforms) {
return ['paper', 'bitget']
.map((key) => platforms?.[key]?.positions?.count || 0)
.reduce((sum, count) => sum + count, 0);
}
function countHalted(platformHalts) {
return Object.values(platformHalts || {}).filter((item) => item && item.halted).length;
}
function setFeedback(message, isError = false) {
const el = document.getElementById('feedback');
if (!message) {
el.innerHTML = '';
return;
}
el.innerHTML = `<div class="${isError ? 'error-box' : 'empty-box'}" style="margin-bottom: 18px;">${message}</div>`;
}
function compactEmpty(title, detail = '') {
return `
<div class="empty-box compact">
<strong>${title}</strong>
${detail ? `<div class="empty-detail">${detail}</div>` : ''}
</div>
`;
}
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';
} catch {
revealSensitiveData = false;
}
renderSensitiveToggle();
}
function renderSensitiveToggle() {
const button = document.getElementById('toggleSensitiveBtn');
if (!button) return;
button.textContent = `敏感数据: ${revealSensitiveData ? '显示' : '隐藏'}`;
button.classList.toggle('active', revealSensitiveData);
}
function toggleSensitiveData() {
revealSensitiveData = !revealSensitiveData;
try {
window.localStorage.setItem(SENSITIVE_VISIBILITY_KEY, revealSensitiveData ? '1' : '0');
} catch {
// ignore storage errors
}
renderSensitiveToggle();
if (cachedConsoleData) {
renderPlatforms(
cachedConsoleData.platforms,
cachedConsoleData.crypto_agent?.platform_halts,
cachedConsoleData.crypto_agent?.target_execution_controls || {}
);
renderOpsKpis(cachedConsoleData);
renderSignalLifecycle(cachedConsoleData);
}
}
function formatSensitiveMoney(value) {
return revealSensitiveData ? formatMoney(value) : '$••••';
}
function initHubNavigation() {
document.querySelectorAll('[data-hub-target]').forEach((button) => {
button.addEventListener('click', () => {
setActiveHub(button.getAttribute('data-hub-target'));
});
});
let preferredHub = 'hubOverview';
try {
preferredHub = window.localStorage.getItem(HUB_PREFERENCE_KEY) || 'hubOverview';
} catch {
preferredHub = 'hubOverview';
}
setActiveHub(preferredHub);
}
function setActiveHub(target) {
document.querySelectorAll('[data-hub-target]').forEach((button) => {
button.classList.toggle('active', button.getAttribute('data-hub-target') === target);
});
document.querySelectorAll('.hub-pane').forEach((pane) => {
pane.classList.toggle('active', pane.id === target);
});
try {
window.localStorage.setItem(HUB_PREFERENCE_KEY, target);
} catch {
// ignore storage errors
}
}
function updateSidebarBadge(id, value) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = value;
}
function initTabs() {
document.querySelectorAll('[data-tab-group]').forEach((groupEl) => {
const group = groupEl.getAttribute('data-tab-group');
const buttons = groupEl.querySelectorAll('[data-tab]');
buttons.forEach((button) => {
button.addEventListener('click', () => {
const target = button.getAttribute('data-target');
setActiveTab(group, target);
});
});
});
}
function setActiveTab(group, target) {
document.querySelectorAll(`[data-tab="${group}"]`).forEach((button) => {
button.classList.toggle('active', button.getAttribute('data-target') === target);
});
document.querySelectorAll(`[data-tab-pane="${group}"]`).forEach((pane) => {
pane.classList.toggle('active', pane.id === target);
});
}
function getActiveTabTarget(group) {
return document.querySelector(`[data-tab="${group}"].active`)?.getAttribute('data-target') || null;
}
function updateTabButton(group, target, label, count, hasData) {
const button = document.querySelector(`[data-tab="${group}"][data-target="${target}"]`);
if (!button) return;
button.innerHTML = `${label}<span class="tab-count">${count}</span>`;
button.classList.toggle('muted', !hasData);
}
function renderExecutionMonitor(data) {
const container = document.getElementById('executionMonitorStrip');
if (!container) return;
const executionLoop = data.monitoring?.execution_loop || {};
const priceMonitor = data.monitoring?.price_monitor || {};
const latestPriceCount = Object.keys(priceMonitor.latest_prices || {}).length;
container.innerHTML = `
<div class="monitor-card">
<div class="title">价格链路</div>
<div class="main">${priceMonitor.running ? 'RUNNING' : 'STOPPED'} / ${(priceMonitor.mode || 'unknown').toUpperCase()}</div>
<div class="meta">
订阅标的 ${(priceMonitor.subscribed_symbols || []).length || 0} / 缓存价格 ${latestPriceCount}<br>
最近刷新 ${priceMonitor.checked_at ? formatTime(priceMonitor.checked_at) : '-'}
</div>
</div>
<div class="monitor-card">
<div class="title">模拟盘执行链路</div>
<div class="main">${executionLoop.running ? 'RUNNING' : 'STOPPED'}</div>
<div class="meta">
心跳 ${executionLoop.last_heartbeat_at ? `${relativeTime(executionLoop.last_heartbeat_at)} / ${formatTime(executionLoop.last_heartbeat_at)}` : '-'}<br>
活跃订单 ${executionLoop.active_orders ?? 0} / 标的 ${Array.isArray(executionLoop.last_symbols) && executionLoop.last_symbols.length ? executionLoop.last_symbols.join(', ') : '-'}<br>
错误 ${executionLoop.last_error || '无'}
</div>
</div>
`;
}
function renderFocusSummary(data) {
const container = document.getElementById('focusSummary');
if (!container) return;
const cryptoAgent = data.crypto_agent || {};
const monitor = cryptoAgent.analysis_monitor || {};
const previews = Object.values(cryptoAgent.last_execution_preview || {});
const management = data.management || {};
const positions = management.positions || [];
const attentionItems = management.attention_items || [];
const haltedCount = countHalted(cryptoAgent.platform_halts || {});
const blockedEvents = (data.execution_events || []).filter((event) => event.event_type === 'execution_blocked_summary');
const actionablePreviewCount = previews.filter((preview) => {
const paperDecision = preview.paper?.decision;
const bitgetDecision = preview.bitget?.decision;
return ['OPEN', 'ADD', 'CLOSE', 'CANCEL_PENDING'].includes(paperDecision) ||
['OPEN', 'ADD', 'CLOSE', 'CANCEL_PENDING'].includes(bitgetDecision);
}).length;
const heartbeatHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳';
const heartbeatTone = toneClassForHealth(cryptoAgent.running ? (monitor.last_cycle_status || monitor.last_analysis_status) : 'stopped');
const exposureNotional = positions.reduce((sum, item) => sum + Number(item.notional || item.size || 0), 0);
const riskTone = haltedCount > 0 ? 'danger' : (positions.length > 0 ? 'warn' : 'good');
const blockedTone = blockedEvents.length > 0 || attentionItems.length > 0 ? 'warn' : 'good';
container.innerHTML = `
<div class="focus-summary-card">
<div class="kicker">运行心跳</div>
<div class="headline ${heartbeatTone}">${heartbeatHeadline}</div>
<div class="detail">状态 ${String(monitor.last_cycle_status || monitor.last_analysis_status || 'idle').toUpperCase()} / 下次 ${monitor.next_scheduled_run_at ? formatTime(monitor.next_scheduled_run_at) : '-'}</div>
</div>
<div class="focus-summary-card">
<div class="kicker">待执行机会</div>
<div class="headline ${actionablePreviewCount > 0 ? 'good' : 'warn'}">${actionablePreviewCount}</div>
<div class="detail">最近预览中可执行动作 ${actionablePreviewCount} 个</div>
</div>
<div class="focus-summary-card">
<div class="kicker">风险暴露</div>
<div class="headline ${riskTone}">${positions.length} 仓</div>
<div class="detail">名义暴露 ${formatSensitiveMoney(exposureNotional)} / 停机 ${haltedCount}</div>
</div>
<div class="focus-summary-card">
<div class="kicker">阻塞状态</div>
<div class="headline ${blockedTone}">${blockedEvents.length}</div>
<div class="detail">阻塞事件 ${blockedEvents.length} / 待处理 ${attentionItems.length}</div>
</div>
`;
}
function renderAlertStrip(data) {
const container = document.getElementById('alertStrip');
if (!container) return;
const cryptoAgent = data.crypto_agent || {};
const monitor = cryptoAgent.analysis_monitor || {};
const attentionItems = data.management?.attention_items || [];
const haltedCount = countHalted(cryptoAgent.platform_halts || {});
const disabledCount = Object.values(cryptoAgent.target_execution_controls || {}).filter((item) => item && item.enabled === false).length;
const blockedEvents = (data.execution_events || []).filter((event) => event.event_type === 'execution_blocked_summary');
const banners = [];
if (!cryptoAgent.running || !monitor.last_heartbeat_at) {
banners.push({
tone: 'danger',
kicker: 'System Halt',
detail: !cryptoAgent.running ? 'Crypto Agent 当前未运行。' : '尚未收到最近心跳,请优先确认调度与任务循环。',
count: !cryptoAgent.running ? 'STOP' : 'NO HB',
});
}
if (haltedCount > 0) {
banners.push({
tone: 'danger',
kicker: 'Platform Halt',
detail: `当前有 ${haltedCount} 个执行目标处于停机/熔断状态,需要优先恢复或核查风控触发原因。`,
count: `${haltedCount} HALT`,
});
} else if (disabledCount > 0) {
banners.push({
tone: 'warn',
kicker: 'Manual Off',
detail: `当前有 ${disabledCount} 个执行目标被人工关闭自动交易。`,
count: `${disabledCount} OFF`,
});
}
if (blockedEvents.length > 0) {
banners.push({
tone: 'warn',
kicker: 'Execution Blocked',
detail: `最近存在 ${blockedEvents.length} 条执行阻塞汇总,建议切到“风险”标签查看平台级归因。`,
count: `${blockedEvents.length} BLOCK`,
});
}
if (!banners.length && attentionItems.length === 0) {
banners.push({
tone: 'good',
kicker: 'System Clear',
detail: '当前没有停机、阻塞或明显告警,系统处于可管理状态。',
count: 'CLEAR',
});
}
container.innerHTML = banners.slice(0, 3).map((banner) => `
<div class="alert-banner ${banner.tone}">
<div class="alert-kicker">${banner.kicker}</div>
<div class="alert-detail">${banner.detail}</div>
<div class="alert-count">${banner.count}</div>
</div>
`).join('');
}
function syncTabState(data) {
const recentSignals = data.signals?.latest || [];
const executionEvents = data.execution_events || [];
const positions = data.management?.positions || [];
const orders = data.management?.orders || [];
const runtimeCount = (data.crypto_agent?.recent_analysis_events || []).length + (data.crypto_agent?.analysis_monitor?.last_heartbeat_at ? 1 : 0);
updateTabButton('logs', 'logsExecution', '执行事件', executionEvents.length, executionEvents.length > 0);
updateTabButton('logs', 'logsAnalysis', '分析日志', runtimeCount, runtimeCount > 0);
const logsCurrent = getActiveTabTarget('logs');
const logsChoices = [
{ target: 'logsExecution', hasData: executionEvents.length > 0 },
{ target: 'logsAnalysis', hasData: runtimeCount > 0 },
];
if (!logsChoices.find((item) => item.target === logsCurrent && item.hasData)) {
setActiveTab('logs', logsChoices.find((item) => item.hasData)?.target || 'logsExecution');
}
updateSidebarBadge('sidebarBadgeOverview', String((data.management?.attention_items || []).length));
updateSidebarBadge('sidebarBadgeExecution', `${positions.length}/${orders.length}`);
updateSidebarBadge('sidebarBadgeSignals', String(recentSignals.length));
updateSidebarBadge('sidebarBadgeRuntime', data.crypto_agent?.analysis_monitor?.last_heartbeat_at ? 'live' : 'idle');
updateSidebarBadge('sidebarBadgeLogs', String(executionEvents.length));
}
function toneClassForHealth(status) {
if (['error', 'failed', 'stopped'].includes(String(status || '').toLowerCase())) return 'danger';
if (['warning', 'halted', 'idle'].includes(String(status || '').toLowerCase())) return 'warn';
return 'good';
}
function renderHealthRibbon(data) {
const container = document.getElementById('healthRibbon');
const cryptoAgent = data.crypto_agent || {};
const monitor = cryptoAgent.analysis_monitor || {};
const attentionItems = data.management?.attention_items || [];
const dangerCount = attentionItems.filter((item) => item.severity === 'danger').length;
const warningCount = attentionItems.filter((item) => item.severity === 'warning').length;
const haltedCount = countHalted(cryptoAgent.platform_halts || {});
const cycleStatus = cryptoAgent.running
? String(monitor.last_cycle_status || 'waiting').toUpperCase()
: 'STOPPED';
const cycleTone = toneClassForHealth(cryptoAgent.running ? monitor.last_cycle_status : 'stopped');
const attentionTone = attentionItems.length === 0
? 'good'
: dangerCount > 0 || haltedCount > 0
? 'danger'
: 'warn';
const analysisTone = toneClassForHealth(monitor.last_analysis_status || 'idle');
const lastCycleText = monitor.last_cycle_completed_at
? `${relativeTime(monitor.last_cycle_completed_at)}`
: '尚无完成轮次';
const nextRunText = monitor.next_scheduled_run_at
? formatTime(monitor.next_scheduled_run_at)
: (monitor.current_cycle_total ? '轮次进行中' : '-');
container.innerHTML = `
<div class="health-card">
<div class="kicker">分析状态</div>
<div class="headline ${cycleTone}">${cycleStatus}</div>
<div class="detail">最近分析: ${(monitor.last_analysis_symbol || '-')} / ${(monitor.last_analysis_status || '-')}</div>
</div>
<div class="health-card">
<div class="kicker">最近轮次</div>
<div class="headline ${analysisTone}">${lastCycleText}</div>
<div class="detail">下一次运行: ${nextRunText}</div>
</div>
<div class="health-card">
<div class="kicker">人工处理</div>
<div class="headline ${attentionTone}">${attentionItems.length} 项</div>
<div class="detail">danger ${dangerCount} / warning ${warningCount} / halted ${haltedCount}</div>
</div>
`;
}
function renderHero(data) {
const heroMetrics = document.getElementById('heroMetrics');
const system = data.system || {};
const signals = data.signals || {};
const platforms = data.platforms || {};
const platformHalts = data.crypto_agent?.platform_halts || {};
const executionControls = data.crypto_agent?.target_execution_controls || {};
const haltedCount = countHalted(platformHalts);
const disabledCount = Object.values(executionControls).filter((item) => item && item.enabled === false).length;
heroMetrics.innerHTML = `
<div class="hero-metric">
<div class="metric-label">运行 Agent</div>
<div class="metric-value ${system.error_agents > 0 ? 'danger' : 'good'}">${system.running_agents || 0}/${system.total_agents || 0}</div>
<div class="metric-note">异常 ${system.error_agents || 0} 个</div>
</div>
<div class="hero-metric">
<div class="metric-label">30 分钟信号</div>
<div class="metric-value">${signals.recent_30m_count || 0}</div>
<div class="metric-note">最新分析窗口</div>
</div>
<div class="hero-metric">
<div class="metric-label">活跃持仓</div>
<div class="metric-value">${sumPlatformPositions(platforms)}</div>
<div class="metric-note">模拟盘 + Bitget 合计</div>
</div>
<div class="hero-metric">
<div class="metric-label">停机 / 关闭</div>
<div class="metric-value ${haltedCount > 0 ? 'danger' : 'good'}">${haltedCount}</div>
<div class="metric-note">停机 ${haltedCount} / 关闭 ${disabledCount}</div>
</div>
`;
const snapshot = document.getElementById('runSnapshot');
const cryptoAgent = data.crypto_agent || {};
snapshot.innerHTML = `
<div class="status-pill">
<span class="name">系统启动</span>
<span class="value">${formatTime(system.system_start_time)}</span>
</div>
<div class="status-pill ${cryptoAgent.running ? '' : 'halted'}">
<span class="name">Crypto Agent</span>
<span class="value">${cryptoAgent.running ? 'RUNNING' : 'STOPPED'}</span>
</div>
<div class="status-pill">
<span class="name">轮询模式</span>
<span class="value">${cryptoAgent.mode || '-'}</span>
</div>
<div class="status-pill">
<span class="name">监控标的</span>
<span class="value">${(cryptoAgent.symbols || []).length} symbols</span>
</div>
`;
document.getElementById('lastUpdated').textContent = `刷新时间 ${formatTime(data.generated_at)}`;
}
function renderPlatforms(platforms, platformHalts, executionControls) {
const container = document.getElementById('platformGrid');
const entries = [
{ key: 'paper', title: '模拟盘', subtitle: '执行基准 / 策略验证' },
{ key: 'bitget', title: 'Bitget', subtitle: 'U 本位实盘' },
];
container.innerHTML = entries.map(({ key, title, subtitle }) => {
const item = platforms?.[key] || { enabled: false };
const haltKey = key === 'paper'
? 'PaperTrading'
: (Object.keys(platformHalts || {}).find((targetKey) => targetKey.startsWith('Bitget:')) || 'Bitget');
const bitgetHaltActive = key === 'bitget'
? Object.entries(platformHalts || {}).some(([targetKey, haltItem]) => targetKey.startsWith('Bitget:') && haltItem?.halted)
: false;
const halt = platformHalts?.[haltKey] || {};
const platformExecutionControl = key === 'paper'
? (executionControls?.[haltKey] || {})
: null;
const enabled = item.enabled !== false;
const risk = item.risk || {};
const account = item.account || {};
const orders = item.orders || {};
const positions = item.positions || {};
const currentLeverage = risk.current_leverage || account.current_total_leverage || 0;
const maxLeverage = risk.max_leverage || account.max_total_leverage || 0;
const fill = riskFillStyle(currentLeverage, maxLeverage);
const accounts = Array.isArray(item.accounts) ? item.accounts : [];
const disabledBitgetCount = key === 'bitget'
? accounts.filter((accountItem) => executionControls?.[`Bitget:${accountItem.account_id}`]?.enabled === false).length
: 0;
const drawdownValue = risk.drawdown_percent || risk.drawdown || 0;
const thresholdValue = risk.circuit_breaker_threshold || 25;
const platformTone = (!enabled || halt.halted || bitgetHaltActive || drawdownValue >= thresholdValue)
? 'danger'
: ((key === 'paper' && platformExecutionControl?.enabled === false) || disabledBitgetCount > 0 || currentLeverage >= maxLeverage * 0.75)
? 'warn'
: '';
const accountRows = key === 'bitget' && accounts.length
? `
<details class="platform-detail-toggle">
<summary>查看账号明细</summary>
<div class="target-grid">
${accounts.slice(0, 4).map((accountItem) => {
const accountTargetKey = `Bitget:${accountItem.account_id}`;
const accountHalt = platformHalts?.[accountTargetKey] || {};
const accountControl = executionControls?.[accountTargetKey] || {};
const accountLeverage = accountItem.risk?.current_leverage || 0;
return `
<div class="target-mini-card ${accountHalt.halted ? 'halted' : (accountControl.enabled === false ? 'disabled' : '')}">
<div class="target-mini-head">
<div class="target-mini-title">${accountItem.account_id}</div>
<div class="target-mini-meta">${accountHalt.halted ? 'HALTED' : (accountControl.enabled === false ? 'DISABLED' : 'ACTIVE')}</div>
</div>
<div class="target-mini-stats">
<div class="target-mini-row"><span>权益</span><strong>${formatSensitiveMoney(accountItem.account?.account_value || 0)}</strong></div>
<div class="target-mini-row"><span>可用 / 保证金</span><strong>${formatSensitiveMoney(accountItem.account?.available_balance || 0)} / ${formatSensitiveMoney(accountItem.account?.total_margin_used || 0)}</strong></div>
<div class="target-mini-row"><span>持仓 / 挂单</span><strong>${accountItem.positions?.count || 0} / ${accountItem.orders?.count || 0}</strong></div>
<div class="target-mini-row"><span>总杠杆 / 回撤</span><strong>${formatNumber(accountLeverage, 2)}x / ${formatPercent(accountItem.risk?.drawdown_percent || 0, 1)}</strong></div>
<div class="target-mini-row"><span>自动交易</span><strong>${accountControl.enabled === false ? 'OFF' : 'ON'}</strong></div>
</div>
<div class="target-mini-actions">
<button class="mini-btn ${accountControl.enabled === false ? 'good' : 'warn'}" data-toggle-target="${accountTargetKey}" data-enabled="${accountControl.enabled === false ? 'true' : 'false'}">
${accountControl.enabled === false ? '开启自动交易' : '关闭自动交易'}
</button>
</div>
</div>
`;
}).join('')}
</div>
</details>
`
: '';
return `
<article class="platform-card ${platformTone}">
<div class="platform-top">
<div>
<div class="panel-sub">${subtitle}</div>
<h3 class="platform-title">${title}</h3>
</div>
<div class="platform-state">${!enabled ? 'DISABLED' : (halt.halted || bitgetHaltActive) ? 'HALTED' : ((key === 'paper' && platformExecutionControl?.enabled === false) ? 'MANUAL OFF' : 'ONLINE')}</div>
</div>
<div class="platform-overview">
<div class="platform-pill-stat">
<span class="label">权益</span>
<span class="value">${formatSensitiveMoney(account.current_balance || account.account_value)}</span>
</div>
<div class="platform-pill-stat">
<span class="label">可用 / 保证金</span>
<span class="value">${formatSensitiveMoney(account.available || account.available_balance)} / ${formatSensitiveMoney(account.used_margin || account.total_margin_used)}</span>
</div>
<div class="platform-pill-stat">
<span class="label">持仓 / 挂单</span>
<span class="value">${positions.count || 0} / ${orders.count || 0}</span>
</div>
<div class="platform-pill-stat">
<span class="label">自动交易</span>
<span class="value">${key === 'paper' ? (platformExecutionControl?.enabled === false ? 'OFF' : 'ON') : `${accounts.length - disabledBitgetCount}/${accounts.length} ON`}</span>
</div>
</div>
<div class="risk-band"><div class="risk-fill" style="width:${fill}%"></div></div>
<div class="platform-stats">
<div class="platform-stat">
<span class="label">开仓单 / TP-SL</span>
<span class="value">${orders.entry_orders || orders.pending_count || 0} / ${orders.tp_sl_orders || 0}</span>
</div>
<div class="platform-stat">
<span class="label">总杠杆</span>
<span class="value">${formatNumber(currentLeverage, 2)}x / ${formatNumber(maxLeverage, 2)}x</span>
</div>
<div class="platform-stat">
<span class="label">回撤</span>
<span class="value" style="color:${drawdownValue >= thresholdValue ? 'var(--danger)' : 'var(--text)'};">
${formatPercent(drawdownValue)}
</span>
</div>
<div class="platform-stat">
<span class="label">风险阈值</span>
<span class="value">${formatPercent(thresholdValue, 1)}</span>
</div>
<div class="platform-stat">
<span class="label">账号数量</span>
<span class="value">${key === 'bitget' ? accounts.length : 1}</span>
</div>
</div>
${key === 'paper' ? `
<div class="halt-actions" style="margin-top: 14px; justify-content: flex-start;">
<button class="mini-btn ${platformExecutionControl?.enabled === false ? 'good' : 'warn'}" data-toggle-target="${haltKey}" data-enabled="${platformExecutionControl?.enabled === false ? 'true' : 'false'}">
${platformExecutionControl?.enabled === false ? '开启自动交易' : '关闭自动交易'}
</button>
</div>
` : ''}
${accountRows}
</article>
`;
}).join('');
container.querySelectorAll('[data-toggle-target]').forEach((button) => {
button.addEventListener('click', async () => {
const targetKey = button.getAttribute('data-toggle-target');
const enabled = button.getAttribute('data-enabled') === 'true';
await updateExecutionControl(targetKey, enabled, button);
});
});
}
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) {
container.innerHTML = compactEmpty('最近没有可展示信号', '等待新的分析结果写入,运行状态可继续看上方心跳。');
return;
}
container.innerHTML = `
<div class="signal-feed-grid">
${signals.slice(0, 5).map((signal) => `
<article class="signal-feed-card ${signal.action || 'wait'}">
<div class="signal-feed-head">
<div>
<div class="signal-feed-symbol">${signal.symbol || '-'}</div>
<div class="signal-feed-meta">${relativeTime(signal.created_at)} / ${formatTime(signal.created_at)}</div>
</div>
<span class="badge ${statusClassFromAction(signal.action)}">${signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '观望'}</span>
</div>
<div class="signal-feed-tags">
<span class="event-inline-badge">${signal.signal_type || '-'}</span>
<span class="event-inline-badge">${signal.timeframe || signal.type || '-'}</span>
${signal.grade ? `<span class="event-inline-badge">${signal.grade}</span>` : ''}
${signal.setup_type ? `<span class="event-inline-badge">${signal.setup_type}</span>` : ''}
</div>
<div class="signal-feed-stats">
<div class="signal-feed-stat">
<span class="label">信心度</span>
<span class="value">${formatPercent(signal.confidence || 0, 1)}</span>
</div>
<div class="signal-feed-stat">
<span class="label">价格</span>
<span class="value">入场 ${formatMoney(signal.entry_price)} / 现价 ${formatMoney(signal.current_price)}</span>
</div>
<div class="signal-feed-stat">
<span class="label">止盈止损</span>
<span class="value">${formatMoney(signal.take_profit)} / ${formatMoney(signal.stop_loss)}</span>
</div>
<div class="signal-feed-stat">
<span class="label">仓位 / 入场方式</span>
<span class="value">${signal.position_size || '-'} / ${signal.entry_type || '-'}</span>
</div>
</div>
<div class="signal-feed-reason">${signal.reasoning || signal.reason || signal.summary || '暂无详细分析理由'}</div>
</article>
`).join('')}
</div>
`;
}
function renderAgentSignals(cryptoAgent, signalStats) {
const container = document.getElementById('agentSignals');
const lastSignals = cryptoAgent?.last_signals || {};
const entries = Object.entries(lastSignals);
const cards = [];
cards.push(`
<article class="signal-card">
<div class="signal-head">
<div>
<div class="signal-symbol">7D 信号统计</div>
<div class="signal-meta">crypto</div>
</div>
<span class="badge wait">stats</span>
</div>
<div class="signal-stats">
<div class="stat-chip"><span class="label">Crypto</span><span class="value">${signalStats?.stats_7d?.crypto?.total || 0}</span></div>
<div class="stat-chip"><span class="label">Buy</span><span class="value">${signalStats?.stats_7d?.crypto?.buy || 0}</span></div>
<div class="stat-chip"><span class="label">Sell</span><span class="value">${signalStats?.stats_7d?.crypto?.sell || 0}</span></div>
</div>
</article>
`);
if (entries.length === 0) {
cards.push(compactEmpty('Crypto Agent 暂无最近信号缓存', '当前没有缓存到最近信号,可切到信号流或执行流继续查看。'));
} else {
entries.slice(0, 5).forEach(([symbol, sig]) => {
cards.push(`
<article class="signal-card">
<div class="signal-head">
<div>
<div class="signal-symbol">${symbol}</div>
<div class="signal-meta">${sig.type || '-'} / ${sig.grade || '-'}</div>
</div>
<span class="badge ${statusClassFromAction(sig.action)}">${sig.action || 'wait'}</span>
</div>
<div class="signal-stats">
<div class="stat-chip"><span class="label">动作</span><span class="value">${sig.action || '-'}</span></div>
<div class="stat-chip"><span class="label">置信度</span><span class="value">${formatPercent(sig.confidence || 0, 1)}</span></div>
<div class="stat-chip"><span class="label">等级</span><span class="value">${sig.grade || '-'}</span></div>
</div>
</article>
`);
});
}
container.innerHTML = cards.join('');
}
function renderAnalysisHeartbeat(analysisMonitor, analysisEvents) {
const heartbeat = document.getElementById('analysisHeartbeat');
const logList = document.getElementById('analysisLogList');
const summaryCard = document.getElementById('runtimeSummaryCard');
const guardianCard = document.getElementById('guardianSummaryCard');
const guardianTargetList = document.getElementById('guardianTargetList');
const monitor = analysisMonitor || {};
const notifications = cachedConsoleData?.crypto_agent?.analysis_notifications || {};
const schedule = cachedConsoleData?.crypto_agent?.llm_schedule || {};
const laneState = cachedConsoleData?.crypto_agent?.lane_analysis_state || {};
const eventState = cachedConsoleData?.crypto_agent?.event_analysis_state || {};
const guardian = cachedConsoleData?.crypto_agent?.execution_guardian || {};
const cycleStatus = monitor.last_cycle_status || 'idle';
const progressText = monitor.current_cycle_total
? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total} ${monitor.current_cycle_symbol || ''}`
: '-';
heartbeat.innerHTML = `
<div class="heartbeat-card">
<span class="label">最近心跳</span>
<span class="value">${monitor.last_heartbeat_at ? `${relativeTime(monitor.last_heartbeat_at)} / ${formatTime(monitor.last_heartbeat_at)}` : '-'}</span>
</div>
<div class="heartbeat-card">
<span class="label">最近轮次</span>
<span class="value">${cycleStatus}${monitor.last_cycle_completed_at ? ` / ${relativeTime(monitor.last_cycle_completed_at)}` : ''}</span>
</div>
<div class="heartbeat-card">
<span class="label">当前进度</span>
<span class="value">${progressText}</span>
</div>
<div class="heartbeat-card">
<span class="label">下一次运行</span>
<span class="value">${monitor.next_scheduled_run_at ? formatTime(monitor.next_scheduled_run_at) : '-'}</span>
</div>
`;
const heartbeatSentAt = notifications.last_heartbeat_notified_at;
const lastSignalAt = notifications.last_signal_at;
const lastSignalSymbol = notifications.last_signal_symbol || '-';
const laneRows = Object.entries(laneState).slice(0, 4).map(([symbol, state]) => `
<div class="lane-state-item">
<div class="lane-state-symbol">${symbol}</div>
<div class="lane-state-detail">
日内 ${state.last_intraday_at ? relativeTime(state.last_intraday_at) : '-'} /
趋势 ${state.last_trend_at ? relativeTime(state.last_trend_at) : '-'}
${state.last_force_reason ? `<br>强触发: ${state.last_force_reason}` : ''}
</div>
</div>
`).join('');
const latestEventTrigger = Object.entries(eventState)
.map(([symbol, state]) => ({ symbol, ...state }))
.filter((state) => state.last_triggered_at)
.sort((a, b) => new Date(b.last_triggered_at) - new Date(a.last_triggered_at))[0];
summaryCard.innerHTML = `
<div class="runtime-summary-title">运行摘要</div>
<div class="runtime-summary-main">${monitor.last_analysis_status || 'idle'} / ${monitor.last_analysis_symbol || '-'}</div>
<div class="runtime-summary-meta">
<div class="runtime-summary-row"><span>最近分析说明</span><strong>${monitor.last_analysis_detail || '-'}</strong></div>
<div class="runtime-summary-row"><span>最近信号</span><strong>${lastSignalAt ? `${lastSignalSymbol} / ${relativeTime(lastSignalAt)}` : '近 60 分钟无信号'}</strong></div>
<div class="runtime-summary-row"><span>上次心跳通知</span><strong>${heartbeatSentAt ? `${relativeTime(heartbeatSentAt)} / ${formatTime(heartbeatSentAt)}` : '尚未发送'}</strong></div>
<div class="runtime-summary-row"><span>LLM 冷却</span><strong>日内 ${schedule.intraday_cooldown_minutes || '-'}m / 趋势 ${schedule.trend_cooldown_minutes || '-'}m</strong></div>
<div class="runtime-summary-row"><span>事件触发</span><strong>${schedule.event_analysis_enabled ? `${schedule.event_analysis_window_minutes || '-'}m / ${formatPercent(schedule.event_analysis_price_change_percent || 0, 1)} / 冷却${schedule.event_analysis_cooldown_minutes || '-'}m` : '关闭'}</strong></div>
<div class="runtime-summary-row"><span>最近异动分析</span><strong>${latestEventTrigger ? `${latestEventTrigger.symbol} / ${relativeTime(latestEventTrigger.last_triggered_at)}` : '暂无'}</strong></div>
</div>
<div class="lane-state-list">${laneRows || '<div class="analysis-log-detail">暂无 lane 状态,等待下一轮分析。</div>'}</div>
`;
const guardianTargets = Array.isArray(guardian.targets) ? guardian.targets : [];
const guardianActions = Array.isArray(guardian.last_actions) ? guardian.last_actions : [];
const guardianTargetRows = guardianTargets.slice(0, 4).map((target) => `
<div class="runtime-summary-row">
<span>${target.target_key}</span>
<strong>${target.supports_tpsl_repair ? '监管 + 保护单' : '监管'}</strong>
</div>
`).join('');
const latestGuardianAction = guardianActions[0];
if (guardianCard) {
guardianCard.innerHTML = `
<div class="runtime-summary-title">执行监管器</div>
<div class="runtime-summary-main">${guardian.last_status || 'idle'} / ${guardian.last_run_at ? relativeTime(guardian.last_run_at) : '-'}</div>
<div class="runtime-summary-meta">
<div class="runtime-summary-row"><span>监管目标</span><strong>${guardianTargets.length}</strong></div>
<div class="runtime-summary-row"><span>最近错误</span><strong>${guardian.last_error || '无'}</strong></div>
<div class="runtime-summary-row"><span>最近动作</span><strong>${latestGuardianAction ? `${latestGuardianAction.action_type} / ${latestGuardianAction.symbol || latestGuardianAction.platform}` : '暂无'}</strong></div>
</div>
<div class="lane-state-list">${guardianTargetRows || '<div class="analysis-log-detail">暂无已注册监管目标。</div>'}</div>
`;
}
if (guardianTargetList) {
const bitgetAccounts = cachedConsoleData?.platforms?.bitget?.accounts || [];
const bitgetAccountMap = Object.fromEntries(bitgetAccounts.map((account) => [String(account.account_id), account]));
guardianTargetList.innerHTML = guardianTargets.length
? guardianTargets.map((target) => {
const account = target.platform === 'Bitget' ? (bitgetAccountMap[String(target.account_id)] || {}) : {};
const positionsCount = account.positions?.count || 0;
const ordersCount = account.orders?.count || 0;
const isHalted = !!(cachedConsoleData?.crypto_agent?.platform_halts?.[target.target_key]?.halted);
return `
<div class="target-mini-card ${isHalted ? 'halted' : ''}">
<div class="target-mini-head">
<div class="target-mini-title">${target.target_key}</div>
<div class="target-mini-meta">${target.platform} / account=${target.account_id}</div>
</div>
<div class="target-mini-stats">
<div class="target-mini-row"><span>挂单超时</span><strong>${target.supports_pending_timeout ? 'on' : 'off'}</strong></div>
<div class="target-mini-row"><span>持仓管理</span><strong>${target.supports_position_management ? 'on' : 'off'}</strong></div>
<div class="target-mini-row"><span>保护单补救</span><strong>${target.supports_tpsl_repair ? 'on' : 'off'}</strong></div>
<div class="target-mini-row"><span>仓位 / 挂单</span><strong>${positionsCount} / ${ordersCount}</strong></div>
</div>
</div>
`;
}).join('')
: compactEmpty('暂无执行监管目标', '注册多个交易所或多个账号后,这里会展开显示每个 target。');
}
if (!analysisEvents || analysisEvents.length === 0) {
logList.innerHTML = compactEmpty('最近还没有分析日志', '等待下一轮分析或新的运行事件写入。');
return;
}
logList.innerHTML = analysisEvents.map((event) => `
<div class="analysis-log-item ${event.status || 'info'}">
<div class="analysis-log-head">
<strong>${event.symbol || 'SYSTEM'} · ${event.event_type || '-'}</strong>
<span class="analysis-log-meta">${relativeTime(event.timestamp)} / ${formatTime(event.timestamp)}</span>
</div>
<div class="analysis-log-detail">${event.detail || '-'}</div>
</div>
`).join('');
}
function renderBlockedSummaries(events = cachedExecutionEvents) {
const container = document.getElementById('blockedSummaryList');
const blockedEvents = (Array.isArray(events) ? events : [])
.filter((event) => event.event_type === 'execution_blocked_summary')
.slice(0, 3);
if (!blockedEvents.length) {
container.innerHTML = compactEmpty('最近没有未落单阻塞', '出现执行阻塞时,这里会按平台归类展示原因。');
return;
}
container.innerHTML = blockedEvents.map((event) => {
const blockedPlatforms = event.blocked_platforms || [];
return `
<div class="blocked-item">
<div class="blocked-item-head">
<div class="blocked-item-title">${event.symbol || '-'} · ${event.signal_action_text || '-'} / ${event.signal_timeframe_text || '-'}</div>
<div class="blocked-item-meta">${relativeTime(event.timestamp)} / ${formatTime(event.timestamp)}</div>
</div>
<div class="analysis-log-detail">建议价 ${formatMoney(event.entry_price)} / 现价 ${formatMoney(event.current_price)} / 信心 ${formatPercent(event.confidence || 0, 1)}</div>
<div class="blocked-platforms">
${blockedPlatforms.map((item) => `
<div class="blocked-platform">
<strong>${item.target_key || item.platform}</strong>
<span>${item.tag} | ${item.detail}</span>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
}
function summarizeDecision(decision) {
if (!decision) return { label: '-', detail: '无数据', tone: 'hold' };
const decisionType = decision.decision || decision.action || 'HOLD';
let tone = 'hold';
if (['OPEN', 'ADD', 'CLOSE'].includes(decisionType)) {
tone = 'success';
} else if (['CANCEL_PENDING', 'FLIP', 'ROLL', 'CLOSE_OPPOSITE', 'CANCEL_AND_OPEN'].includes(decisionType)) {
tone = 'warning';
} else if (['ERROR', 'FAILED'].includes(decisionType)) {
tone = 'error';
}
return {
label: decisionType,
detail: decision.reason || decision.reasoning || '无说明',
tone,
};
}
function renderDecisionPreview(previewMap) {
const container = document.getElementById('decisionPreview');
const entries = Object.entries(previewMap || {});
if (entries.length === 0) {
container.innerHTML = compactEmpty('暂无最近决策预览', '还没有形成最近一轮执行预览,通常意味着还没出现可执行信号。');
return;
}
const cards = [];
entries.slice(0, 4).forEach(([symbol, preview]) => {
const paper = summarizeDecision(preview.paper);
const bitget = summarizeDecision(preview.bitget);
const bitgetAccounts = Object.entries(preview.bitget_accounts || {});
const bitgetAccountDetails = bitgetAccounts.length
? bitgetAccounts.map(([accountId, decision]) => {
const summary = summarizeDecision(decision);
return `
<div class="decision-account-card ${summary.tone}">
<div class="decision-account-head">
<div class="decision-account-title">${accountId}</div>
<div class="decision-account-meta">${summary.label}</div>
</div>
<div class="decision-account-detail">${summary.detail}</div>
</div>
`;
}).join('')
: `
<div class="decision-account-card ${bitget.tone}">
<div class="decision-account-head">
<div class="decision-account-title">default</div>
<div class="decision-account-meta">${bitget.label}</div>
</div>
<div class="decision-account-detail">${bitget.detail}</div>
</div>
`;
cards.push(`
<article class="signal-card">
<div class="signal-head">
<div>
<div class="signal-symbol">${symbol}</div>
<div class="signal-meta">${formatTime(preview.timestamp)} | 现价 ${formatMoney(preview.current_price)}</div>
</div>
<span class="badge wait">preview</span>
</div>
<div class="signal-stats">
<div class="stat-chip decision-chip ${paper.tone}"><span class="label">模拟盘</span><span class="value">${paper.label}</span></div>
<div class="stat-chip decision-chip ${bitget.tone}"><span class="label">Bitget</span><span class="value">${bitget.label}</span></div>
</div>
${(preview.paper?.setup_type || preview.bitget?.setup_type)
? `
<div style="margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap;">
${preview.paper?.setup_type ? `<span class="event-inline-badge">Paper ${preview.paper.setup_type}</span>` : ''}
${preview.bitget?.setup_type ? `<span class="event-inline-badge">Bitget ${preview.bitget.setup_type}</span>` : ''}
</div>
`
: ''}
${(preview.paper?.entry_basis || preview.paper?.setup_basis || preview.bitget?.entry_basis || preview.bitget?.setup_basis)
? `
<div style="margin-top: 10px;" class="analysis-log-detail">
${preview.paper?.entry_basis || preview.paper?.setup_basis ? `<div>模拟盘: ${preview.paper.entry_basis || preview.paper.setup_basis}</div>` : ''}
${preview.bitget?.entry_basis || preview.bitget?.setup_basis ? `<div>Bitget: ${preview.bitget.entry_basis || preview.bitget.setup_basis}</div>` : ''}
</div>
`
: ''}
<div style="margin-top: 12px; color: var(--muted); font-size: 12px; line-height: 1.6;">
模拟盘: ${paper.detail}
</div>
<div class="decision-account-grid">
${bitgetAccountDetails}
</div>
</article>
`);
});
container.innerHTML = cards.join('');
}
function renderHalts(platformHalts, executionControls) {
const container = document.getElementById('haltList');
const bitgetTargets = Object.keys(platformHalts || {})
.filter((key) => key.startsWith('Bitget:'))
.sort();
const entries = [
['PaperTrading', '模拟盘'],
...bitgetTargets.map((key) => [key, key]),
];
container.innerHTML = entries.map(([key, label]) => {
const halt = platformHalts?.[key] || {};
const active = !!halt.halted;
const control = executionControls?.[key] || {};
const disabled = control.enabled === false;
return `
<div class="halt-item ${active ? 'active' : (disabled ? 'disabled' : '')}">
<div class="halt-top">
<div class="halt-name">${label}</div>
<div class="halt-state">${active ? 'HALTED' : (disabled ? 'MANUAL OFF' : 'ACTIVE')}</div>
</div>
<div class="halt-reason">
${active
? `${halt.reason || '已触发平台停机'}<br>回撤: ${formatPercent(halt.drawdown_pct || 0)} | 触发时间: ${formatTime(halt.halted_at)}`
: disabled
? `${control.reason || '已被人工关闭自动交易'}<br>更新时间: ${formatTime(control.updated_at)}`
: '运行正常,未触发平台停机或熔断。'}
</div>
<div class="halt-actions">
<button class="mini-btn ${disabled ? 'good' : 'warn'}" data-toggle-target="${key}" data-enabled="${disabled ? 'true' : 'false'}">
${disabled ? '开启自动交易' : '关闭自动交易'}
</button>
${active ? `<button class="mini-btn" data-platform="${key}" data-target-key="${key}">恢复执行</button>` : ''}
</div>
</div>
`;
}).join('');
container.querySelectorAll('[data-platform]').forEach((button) => {
button.addEventListener('click', async () => {
const platform = button.getAttribute('data-platform');
const targetKey = button.getAttribute('data-target-key');
await resumePlatform(platform, button, targetKey);
});
});
container.querySelectorAll('[data-toggle-target]').forEach((button) => {
button.addEventListener('click', async () => {
const targetKey = button.getAttribute('data-toggle-target');
const enabled = button.getAttribute('data-enabled') === 'true';
await updateExecutionControl(targetKey, enabled, button);
});
});
}
function renderExecutionEvents(events = cachedExecutionEvents) {
const container = document.getElementById('eventStream');
cachedExecutionEvents = Array.isArray(events) ? events : [];
const filtered = cachedExecutionEvents.filter((event) => currentEventFilter === 'all' || event.status === currentEventFilter);
if (!filtered || filtered.length === 0) {
container.innerHTML = compactEmpty('最近没有匹配的执行事件', currentEventFilter === 'all' ? '当前没有新的执行结果或异常事件。' : `筛选条件为 ${currentEventFilter.toUpperCase()},当前暂无匹配记录。`);
return;
}
container.innerHTML = filtered.map((event) => `
<div class="event-item">
<div class="event-time">${relativeTime(event.timestamp)}<br>${formatTime(event.timestamp)}</div>
<div class="event-tag ${event.status || 'hold'}">${event.platform || '-'}<br>${event.event_type || '-'}</div>
<div class="event-body">
<div class="event-summary">
<strong>${event.symbol || '-'}</strong>
${event.account_id ? `<span class="event-inline-badge">acct ${event.account_id}</span>` : ''}
${event.target_key ? `<span class="event-inline-badge">${event.target_key}</span>` : ''}
${event.decision ? `<span class="event-inline-badge">${event.decision}</span>` : ''}
${event.action ? `<span class="event-inline-badge">${event.action}</span>` : ''}
${event.signal_timeframe_text ? `<span class="event-inline-badge">${event.signal_timeframe_text}</span>` : ''}
${event.setup_type ? `<span class="event-inline-badge">${event.setup_type}</span>` : ''}
</div>
<span style="color: var(--muted);">${event.reason || '无说明'}</span>
${event.setup_basis || event.entry_basis ? `
<div class="analysis-log-detail" style="margin-top: 8px;">
${event.setup_basis ? `<div>Setup: ${event.setup_basis}</div>` : ''}
${event.entry_basis ? `<div>Entry: ${event.entry_basis}</div>` : ''}
</div>
` : ''}
${event.event_type === 'execution_blocked_summary' && Array.isArray(event.blocked_platforms) && event.blocked_platforms.length > 0 ? `
<div class="blocked-platforms" style="margin-top: 10px;">
${event.blocked_platforms.map((item) => `
<div class="blocked-platform">
<strong>${item.platform}</strong>
<span>${item.tag} | ${item.detail}</span>
</div>
`).join('')}
</div>
` : ''}
${(event.reason || '').length > 90 ? `
<details class="event-details">
<summary>查看完整详情</summary>
<pre>${String(event.reason || '无说明')}</pre>
</details>
` : ''}
</div>
</div>
`).join('');
}
function renderAttentionItems(items) {
const container = document.getElementById('attentionList');
if (!items || items.length === 0) {
container.innerHTML = compactEmpty('当前没有需要人工处理的事项', '系统暂无明显风险、停机或待干预问题。');
return;
}
container.innerHTML = items.map((item) => `
<div class="attention-item ${item.severity || 'info'}">
<div class="attention-title">
<strong>${item.title || '-'}</strong>
<span class="attention-time">${item.timestamp ? relativeTime(item.timestamp) : 'now'}</span>
</div>
<div class="attention-detail">${item.detail || '-'}</div>
</div>
`).join('');
}
function renderUnifiedPositions(positions) {
const toolbar = document.getElementById('positionsToolbar');
const container = document.getElementById('positionsTable');
const total = positions || [];
const platformCounts = ['paper', 'bitget'].map((platform) => {
const count = total.filter((item) => item.platform === platform).length;
return `<span class="toolbar-chip">${platform}: ${count}</span>`;
}).join('');
toolbar.innerHTML = `${platformCounts}<span class="toolbar-chip">total: ${total.length}</span>`;
if (!total.length) {
container.innerHTML = compactEmpty('当前没有跨平台持仓', '没有活动持仓时,这里会保持紧凑,不再占用大块留白。');
return;
}
container.innerHTML = `
<div class="position-card-grid">
${total.map((item) => `
<article class="position-card ${item.side === 'long' ? 'long' : 'short'}">
<div class="position-card-head">
<div>
<div class="position-card-symbol">${item.symbol || '-'}</div>
<div class="position-card-meta">${item.platform || '-'} / ${item.account_id || '-'} / ${item.opened_at ? `${relativeTime(item.opened_at)} / ${formatTime(item.opened_at)}` : '-'}</div>
</div>
<div class="position-card-pnl">
<div class="amount" style="color:${(item.unrealized_pnl || 0) >= 0 ? 'var(--good)' : 'var(--danger)'}">${formatMoney(item.unrealized_pnl)}</div>
<div class="ratio" style="color:${(item.pnl_percent || 0) >= 0 ? 'var(--good)' : 'var(--danger)'}">${formatPercent(item.pnl_percent, 2)}</div>
</div>
</div>
<div class="position-card-tags">
<span class="platform-pill">${item.platform}</span>
<span class="side-pill ${item.side === 'long' ? 'long' : 'short'}">${item.side === 'long' ? 'long' : 'short'}</span>
${item.setup_type ? `<span class="event-inline-badge">${item.setup_type}</span>` : ''}
</div>
<div class="position-card-gridline">
<div class="position-card-block">
<span class="label">入场 / 现价</span>
<span class="value">${formatMoney(item.entry_price)} / ${formatMoney(item.mark_price)}</span>
</div>
<div class="position-card-block">
<span class="label">仓位 / 杠杆</span>
<span class="value">${formatNumber(item.size, 4)} / ${formatNumber(item.leverage, 1)}x</span>
</div>
<div class="position-card-block">
<span class="label">止盈 / 止损</span>
<span class="value">${item.take_profit ? formatMoney(item.take_profit) : '-'} / ${item.stop_loss ? formatMoney(item.stop_loss) : '-'}</span>
</div>
<div class="position-card-block">
<span class="label">Setup / Entry</span>
<span class="value">${item.setup_type || '-'}${item.entry_basis || item.setup_basis ? `<br>${item.entry_basis || item.setup_basis}` : ''}</span>
</div>
</div>
</article>
`).join('')}
</div>
`;
}
function renderUnifiedOrders(orders) {
const toolbar = document.getElementById('ordersToolbar');
const container = document.getElementById('ordersTable');
const total = orders || [];
const entryCount = total.filter((item) => item.category === 'entry').length;
const protectionCount = total.filter((item) => item.category === 'tp_sl').length;
toolbar.innerHTML = `
<span class="toolbar-chip">entry: ${entryCount}</span>
<span class="toolbar-chip">tp/sl: ${protectionCount}</span>
<span class="toolbar-chip">total: ${total.length}</span>
`;
if (!total.length) {
container.innerHTML = compactEmpty('当前没有跨平台挂单', '没有入场单或保护单时,这里会保持紧凑展示。');
return;
}
container.innerHTML = `
<table>
<thead>
<tr>
<th>平台</th>
<th>账号</th>
<th>交易对</th>
<th>方向</th>
<th>类别</th>
<th>价格</th>
<th>数量 / 杠杆</th>
<th>信号</th>
<th>Setup</th>
<th>时间</th>
</tr>
</thead>
<tbody>
${total.map((item) => `
<tr>
<td><span class="platform-pill">${item.platform}</span></td>
<td class="inline-mono">${item.account_id || '-'}</td>
<td><strong>${item.symbol || '-'}</strong></td>
<td><span class="side-pill ${item.side === 'long' ? 'long' : 'short'}">${item.side === 'long' ? 'long' : 'short'}</span></td>
<td class="inline-mono">${item.category === 'tp_sl' ? 'TP/SL' : 'ENTRY'}</td>
<td class="inline-mono">${formatMoney(item.price)}</td>
<td class="inline-mono">${formatNumber(item.size, 4)} / ${item.leverage ? `${formatNumber(item.leverage, 1)}x` : '-'}</td>
<td class="inline-mono">${item.signal_grade || '-'} ${item.signal_type || ''} ${item.confidence ? `/ ${formatPercent(item.confidence, 1)}` : ''}</td>
<td>${item.setup_type ? `<div class="inline-mono">${item.setup_type}</div><div class="analysis-log-detail">${item.entry_basis || item.setup_basis || '-'}</div>` : '-'}</td>
<td class="inline-mono">${item.created_at ? relativeTime(item.created_at) : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
async function resumePlatform(platform, button, targetKey = null) {
const platformMap = {
PaperTrading: 'PaperTrading',
Bitget: 'Bitget',
};
const normalized = platformMap[platform] || platform;
try {
button.disabled = true;
button.textContent = '恢复中...';
const response = await fetch('/api/trading/platform-halts/resume', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ platform: normalized, target_key: targetKey || normalized }),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.detail || result.message || '恢复失败');
}
setFeedback(`${normalized} 已恢复执行`);
await loadConsole();
} catch (error) {
setFeedback(`恢复平台失败: ${error.message}`, true);
button.disabled = false;
button.textContent = '恢复执行';
}
}
async function updateExecutionControl(targetKey, enabled, button) {
try {
button.disabled = true;
button.textContent = enabled ? '开启中...' : '关闭中...';
const response = await fetch('/api/trading/execution-controls', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_key: targetKey,
enabled,
reason: enabled ? '控制台手动开启自动交易' : '控制台手动关闭自动交易',
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.detail || result.message || '更新失败');
}
setFeedback(`${targetKey} 自动交易已${enabled ? '开启' : '关闭'}`);
await loadConsole();
} catch (error) {
setFeedback(`更新自动交易开关失败: ${error.message}`, true);
button.disabled = false;
button.textContent = enabled ? '开启自动交易' : '关闭自动交易';
}
}
async function loadConsole() {
try {
setFeedback('');
const response = await fetch('/api/system/console');
const result = await response.json();
if (result.status !== 'success') {
throw new Error(result.detail || '总控台数据加载失败');
}
const data = result.data || {};
cachedConsoleData = data;
syncTabState(data);
renderHero(data);
renderHealthRibbon(data);
renderFocusSummary(data);
renderAlertStrip(data);
renderTopPriority(data);
renderOpsKpis(data);
renderExecutionMonitor(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);
renderAnalysisHeartbeat(
data.crypto_agent?.analysis_monitor || {},
data.crypto_agent?.recent_analysis_events || []
);
renderDecisionPreview(data.crypto_agent?.last_execution_preview || {});
renderHalts(data.crypto_agent?.platform_halts || {}, data.crypto_agent?.target_execution_controls || {});
renderExecutionEvents(data.execution_events || []);
renderBlockedSummaries(data.execution_events || []);
renderAttentionItems(data.management?.attention_items || []);
renderUnifiedPositions(data.management?.positions || []);
renderUnifiedOrders(data.management?.orders || []);
} catch (error) {
console.error(error);
setFeedback(`总控台加载失败: ${error.message}`, true);
}
}
function applyAutoRefreshState() {
const btn = document.getElementById('toggleAutoRefreshBtn');
btn.textContent = `自动刷新: ${autoRefresh ? '开' : '关'}`;
if (timer) {
clearInterval(timer);
timer = null;
}
if (autoRefresh) {
timer = setInterval(loadConsole, 15000);
}
}
document.getElementById('refreshBtn').addEventListener('click', loadConsole);
document.getElementById('toggleAutoRefreshBtn').addEventListener('click', () => {
autoRefresh = !autoRefresh;
applyAutoRefreshState();
});
document.getElementById('toggleSensitiveBtn').addEventListener('click', toggleSensitiveData);
document.getElementById('eventFilters').querySelectorAll('[data-filter]').forEach((button) => {
button.addEventListener('click', () => {
currentEventFilter = button.getAttribute('data-filter');
document.getElementById('eventFilters').querySelectorAll('[data-filter]').forEach((item) => {
item.classList.toggle('active', item === button);
});
renderExecutionEvents();
});
});
loadSensitivePreference();
initTabs();
initHubNavigation();
applyAutoRefreshState();
loadConsole();
</script>
</body>
</html>