2761 lines
108 KiB
HTML
2761 lines
108 KiB
HTML
<!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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
.platform-grid.compact {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.platform-card {
|
||
padding: 18px;
|
||
}
|
||
|
||
.platform-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.platform-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.platform-state {
|
||
font-family: "IBM Plex Mono", monospace;
|
||
font-size: 11px;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.risk-band {
|
||
margin: 12px 0 14px;
|
||
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(2, 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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.workspace-stream {
|
||
min-height: 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-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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
.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) {
|
||
.hero,
|
||
.layout,
|
||
.priority-layout {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.platform-grid,
|
||
.signal-grid,
|
||
.ops-grid,
|
||
.coord-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.hero-metrics {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 720px) {
|
||
.console-shell {
|
||
width: min(100vw - 20px, 100%);
|
||
padding-top: 12px;
|
||
}
|
||
|
||
.hero-main,
|
||
.hero-side,
|
||
.panel,
|
||
.platform-card,
|
||
.signal-card,
|
||
.stream-card {
|
||
padding: 18px;
|
||
}
|
||
|
||
.hero-metrics,
|
||
.platform-stats,
|
||
.signal-stats,
|
||
.heartbeat-grid,
|
||
.health-ribbon,
|
||
.ops-summary {
|
||
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="layout">
|
||
<div class="left-stack">
|
||
<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>
|
||
<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>
|
||
</section>
|
||
|
||
<section class="priority-layout">
|
||
<div class="priority-main">
|
||
<section class="panel">
|
||
<div class="panel-header">
|
||
<div>
|
||
<div class="section-label">Risk Layer</div>
|
||
<h2 class="panel-title" style="margin-top: 12px;">管理待处理事项</h2>
|
||
<div class="panel-sub">把需要你判断和干预的事情放到最前面</div>
|
||
</div>
|
||
</div>
|
||
<div class="attention-list" id="attentionList">
|
||
<div class="loading">正在汇总待处理事项...</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<div class="priority-side">
|
||
<section class="panel dense-panel">
|
||
<div class="workspace-head">
|
||
<div>
|
||
<div class="section-label">Operations Layer</div>
|
||
<h2 class="panel-title" style="margin-top: 12px;">运行监控面板</h2>
|
||
<div class="panel-sub">把平台概览、分析心跳和停机熔断收进一个运行面板,避免首屏纵向堆叠</div>
|
||
</div>
|
||
<div class="panel-actions">
|
||
<button class="ghost-btn" id="toggleSensitiveBtn">敏感数据: 隐藏</button>
|
||
<div class="workspace-tabs" data-tab-group="ops">
|
||
<button class="workspace-tab active" data-tab="ops" data-target="opsPlatform">平台概览</button>
|
||
<button class="workspace-tab" data-tab="ops" data-target="opsRuntime">心跳日志</button>
|
||
<button class="workspace-tab" data-tab="ops" data-target="opsRisk">停机熔断</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ops-summary" id="opsSummary">
|
||
<button class="ops-summary-card active" data-ops-target="opsPlatform">
|
||
<div class="ops-summary-kicker">平台概览</div>
|
||
<div class="ops-summary-headline">-</div>
|
||
<div class="ops-summary-detail">正在汇总平台状态...</div>
|
||
</button>
|
||
<button class="ops-summary-card" data-ops-target="opsRuntime">
|
||
<div class="ops-summary-kicker">心跳日志</div>
|
||
<div class="ops-summary-headline">-</div>
|
||
<div class="ops-summary-detail">正在汇总分析状态...</div>
|
||
</button>
|
||
<button class="ops-summary-card" data-ops-target="opsRisk">
|
||
<div class="ops-summary-kicker">停机熔断</div>
|
||
<div class="ops-summary-headline">-</div>
|
||
<div class="ops-summary-detail">正在汇总风险状态...</div>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="ops-panel-body">
|
||
<div class="tab-pane active ops-pane" data-tab-pane="ops" id="opsPlatform">
|
||
<div class="block-head">
|
||
<div class="section-label">Platform Layer</div>
|
||
<h3 class="block-title">平台执行概览</h3>
|
||
<div class="block-sub">资金、杠杆、持仓、挂单、回撤阈值</div>
|
||
</div>
|
||
<div class="platform-grid" id="platformGrid">
|
||
<div class="loading">正在加载平台状态...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane ops-pane" data-tab-pane="ops" id="opsRuntime">
|
||
<div class="block-head">
|
||
<div class="section-label">Runtime Layer</div>
|
||
<h3 class="block-title">分析心跳与日志</h3>
|
||
<div class="block-sub">没有信号时,也能确认系统仍在正常扫盘</div>
|
||
</div>
|
||
<div class="heartbeat-grid" id="analysisHeartbeat">
|
||
<div class="heartbeat-card"><span class="label">最近心跳</span><span class="value">-</span></div>
|
||
<div class="heartbeat-card"><span class="label">最近轮次</span><span class="value">-</span></div>
|
||
<div class="heartbeat-card"><span class="label">当前进度</span><span class="value">-</span></div>
|
||
<div class="heartbeat-card"><span class="label">下一次运行</span><span class="value">-</span></div>
|
||
</div>
|
||
<div class="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">
|
||
<div class="runtime-summary-title">最近阻塞原因</div>
|
||
<div class="blocked-list" id="blockedSummaryList">
|
||
<div class="loading">正在读取未落单汇总...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="analysis-log-list" id="analysisLogList">
|
||
<div class="loading">正在读取分析日志...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane ops-pane" data-tab-pane="ops" id="opsRisk">
|
||
<div class="block-head">
|
||
<div class="section-label">Risk Layer</div>
|
||
<h3 class="block-title">平台停机 / 熔断</h3>
|
||
<div class="block-sub">风险触发后,这里应当最先看到</div>
|
||
</div>
|
||
<div class="halt-list" id="haltList">
|
||
<div class="loading">正在读取平台停机状态...</div>
|
||
</div>
|
||
<div class="footer-note">建议挂在大屏或副屏,默认每 15 秒刷新。</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel workspace-panel">
|
||
<div class="workspace-head">
|
||
<div>
|
||
<div class="section-label">Workspace Layer</div>
|
||
<h2 class="panel-title" style="margin-top: 12px;">交易工作区</h2>
|
||
<div class="panel-sub">次级明细放进 tab,保留一个干净的管理视角</div>
|
||
</div>
|
||
<div class="workspace-tabs" data-tab-group="workspace">
|
||
<button class="workspace-tab active" data-tab="workspace" data-target="workspaceCoordination">协同</button>
|
||
<button class="workspace-tab" data-tab="workspace" data-target="workspaceSignals">信号流</button>
|
||
<button class="workspace-tab" data-tab="workspace" data-target="workspaceExecution">执行流</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane active" data-tab-pane="workspace" id="workspaceCoordination">
|
||
<div class="coord-grid">
|
||
<div class="coord-block">
|
||
<div class="block-head">
|
||
<div class="section-label">Agent Layer</div>
|
||
<h3 class="block-title">Crypto Agent 状态</h3>
|
||
<div class="block-sub">最近信号、平台停机、执行层状态</div>
|
||
</div>
|
||
<div class="signal-grid" id="agentSignals">
|
||
<div class="loading">正在读取 Agent 状态...</div>
|
||
</div>
|
||
</div>
|
||
<div class="coord-block">
|
||
<div class="block-head">
|
||
<div class="section-label">Decision Layer</div>
|
||
<h3 class="block-title">最近决策预览</h3>
|
||
<div class="block-sub">最近一轮各平台准备执行的动作</div>
|
||
</div>
|
||
<div class="signal-grid" id="decisionPreview">
|
||
<div class="loading">正在读取决策预览...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane" data-tab-pane="workspace" id="workspaceSignals">
|
||
<div class="block-head">
|
||
<div class="section-label">Market Layer</div>
|
||
<h3 class="block-title">最近信号流</h3>
|
||
<div class="block-sub">数据库最新信号与等级、置信度、时间分布</div>
|
||
</div>
|
||
<div class="stream-list workspace-stream" id="signalStream">
|
||
<div class="loading">正在读取信号...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane" data-tab-pane="workspace" id="workspaceExecution">
|
||
<div class="block-head">
|
||
<div class="section-label">Execution Layer</div>
|
||
<h3 class="block-title">执行事件流</h3>
|
||
<div class="block-sub">最近执行结果、未执行原因、平台异常</div>
|
||
</div>
|
||
<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" id="eventStream">
|
||
<div class="loading">正在读取执行事件...</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel workspace-panel unified-section">
|
||
<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">统一持仓和统一挂单改为切换查看,减少表格同时占屏</div>
|
||
</div>
|
||
<div class="workspace-tabs" data-tab-group="asset">
|
||
<button class="workspace-tab active" data-tab="asset" data-target="assetPositions">持仓</button>
|
||
<button class="workspace-tab" data-tab="asset" data-target="assetOrders">挂单</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane active" data-tab-pane="asset" id="assetPositions">
|
||
<div class="block-head">
|
||
<div class="section-label">Position Layer</div>
|
||
<h3 class="block-title">统一持仓视图</h3>
|
||
<div class="block-sub">三端持仓合并,优先看风险与盈亏</div>
|
||
</div>
|
||
<div class="table-toolbar" id="positionsToolbar"></div>
|
||
<div class="unified-table" id="positionsTable">
|
||
<div class="loading">正在整理跨平台持仓...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-pane" data-tab-pane="asset" id="assetOrders">
|
||
<div class="block-head">
|
||
<div class="section-label">Order Layer</div>
|
||
<h3 class="block-title">统一挂单视图</h3>
|
||
<div class="block-sub">入场单、保护单、资金占用一屏观察</div>
|
||
</div>
|
||
<div class="table-toolbar" id="ordersToolbar"></div>
|
||
<div class="unified-table" id="ordersTable">
|
||
<div class="loading">正在整理跨平台挂单...</div>
|
||
</div>
|
||
</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';
|
||
|
||
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', 'hyperliquid']
|
||
.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 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);
|
||
}
|
||
}
|
||
|
||
function formatSensitiveMoney(value) {
|
||
return revealSensitiveData ? formatMoney(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);
|
||
});
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-ops-target]').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
setActiveTab('ops', button.getAttribute('data-ops-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);
|
||
});
|
||
if (group === 'ops') {
|
||
document.querySelectorAll('[data-ops-target]').forEach((card) => {
|
||
card.classList.toggle('active', card.getAttribute('data-ops-target') === 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 renderOpsSummary(data) {
|
||
const container = document.getElementById('opsSummary');
|
||
if (!container) return;
|
||
|
||
const platforms = data.platforms || {};
|
||
const cryptoAgent = data.crypto_agent || {};
|
||
const monitor = cryptoAgent.analysis_monitor || {};
|
||
const halts = cryptoAgent.platform_halts || {};
|
||
const enabledPlatforms = ['paper', 'bitget', 'hyperliquid'].filter((key) => platforms?.[key]?.enabled !== false);
|
||
const haltedCount = countHalted(halts);
|
||
const runtimeTone = toneClassForHealth(cryptoAgent.running ? monitor.last_cycle_status || monitor.last_analysis_status : 'stopped');
|
||
const riskTone = haltedCount > 0 ? 'danger' : ((data.management?.attention_items || []).some((item) => item.severity === 'danger' || item.severity === 'warning') ? 'warn' : 'good');
|
||
const platformTone = haltedCount > 0 ? 'warn' : 'good';
|
||
const platformHeadline = `${enabledPlatforms.length} 平台`;
|
||
const platformDetail = `${sumPlatformPositions(platforms)} 持仓 / ${['paper', 'bitget', 'hyperliquid'].map((key) => platforms?.[key]?.orders?.count || 0).reduce((a, b) => a + b, 0)} 挂单`;
|
||
const runtimeHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳';
|
||
const runtimeDetail = `状态 ${String(monitor.last_cycle_status || monitor.last_analysis_status || 'idle').toUpperCase()} / ${monitor.current_cycle_total ? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total}` : '待机'}`;
|
||
const riskHeadline = haltedCount > 0 ? `${haltedCount} 停机` : '无停机';
|
||
const attentionItems = data.management?.attention_items || [];
|
||
const riskDetail = haltedCount > 0
|
||
? '已有平台触发停机或熔断,建议优先查看。'
|
||
: attentionItems.length > 0
|
||
? `待处理 ${attentionItems.length} 项,建议检查风险与执行事件。`
|
||
: '当前没有明显风险阻塞。';
|
||
|
||
container.innerHTML = `
|
||
<button class="ops-summary-card ${platformTone}" data-ops-target="opsPlatform">
|
||
<div class="ops-summary-kicker">平台概览</div>
|
||
<div class="ops-summary-headline ${platformTone}">${platformHeadline}</div>
|
||
<div class="ops-summary-detail">${platformDetail}</div>
|
||
</button>
|
||
<button class="ops-summary-card ${runtimeTone}" data-ops-target="opsRuntime">
|
||
<div class="ops-summary-kicker">心跳日志</div>
|
||
<div class="ops-summary-headline ${runtimeTone}">${runtimeHeadline}</div>
|
||
<div class="ops-summary-detail">${runtimeDetail}</div>
|
||
</button>
|
||
<button class="ops-summary-card ${riskTone}" data-ops-target="opsRisk">
|
||
<div class="ops-summary-kicker">停机熔断</div>
|
||
<div class="ops-summary-headline ${riskTone}">${riskHeadline}</div>
|
||
<div class="ops-summary-detail">${riskDetail}</div>
|
||
</button>
|
||
`;
|
||
|
||
container.querySelectorAll('[data-ops-target]').forEach((button) => {
|
||
button.addEventListener('click', () => {
|
||
setActiveTab('ops', button.getAttribute('data-ops-target'));
|
||
});
|
||
});
|
||
|
||
setActiveTab('ops', getActiveTabTarget('ops') || 'opsPlatform');
|
||
}
|
||
|
||
function syncTabState(data) {
|
||
const recentSignals = data.signals?.latest || [];
|
||
const executionEvents = data.execution_events || [];
|
||
const positions = data.management?.positions || [];
|
||
const orders = data.management?.orders || [];
|
||
const platformCount = ['paper', 'bitget', 'hyperliquid']
|
||
.filter((key) => data.platforms?.[key]?.enabled !== false)
|
||
.length;
|
||
const lastSignals = Object.keys(data.crypto_agent?.last_signals || {}).length;
|
||
const previews = Object.keys(data.crypto_agent?.last_execution_preview || {}).length;
|
||
const statsTotal = Number(data.signals?.stats_7d?.total || 0);
|
||
const coordinationCount = lastSignals + previews + (statsTotal > 0 ? 1 : 0);
|
||
const haltedCount = countHalted(data.crypto_agent?.platform_halts || {});
|
||
const runtimeCount = (data.crypto_agent?.recent_analysis_events || []).length + (data.crypto_agent?.analysis_monitor?.last_heartbeat_at ? 1 : 0);
|
||
|
||
updateTabButton('ops', 'opsPlatform', '平台概览', platformCount, platformCount > 0);
|
||
updateTabButton('ops', 'opsRuntime', '心跳日志', runtimeCount, runtimeCount > 0);
|
||
updateTabButton('ops', 'opsRisk', '停机熔断', haltedCount, haltedCount > 0);
|
||
updateTabButton('workspace', 'workspaceCoordination', '协同', coordinationCount, coordinationCount > 0);
|
||
updateTabButton('workspace', 'workspaceSignals', '信号流', recentSignals.length, recentSignals.length > 0);
|
||
updateTabButton('workspace', 'workspaceExecution', '执行流', executionEvents.length, executionEvents.length > 0);
|
||
updateTabButton('asset', 'assetPositions', '持仓', positions.length, positions.length > 0);
|
||
updateTabButton('asset', 'assetOrders', '挂单', orders.length, orders.length > 0);
|
||
|
||
const opsCurrent = getActiveTabTarget('ops');
|
||
const opsChoices = [
|
||
{ target: 'opsRisk', hasData: haltedCount > 0 },
|
||
{ target: 'opsRuntime', hasData: runtimeCount > 0 },
|
||
{ target: 'opsPlatform', hasData: platformCount > 0 },
|
||
];
|
||
if (!opsChoices.find((item) => item.target === opsCurrent && item.hasData)) {
|
||
setActiveTab('ops', opsChoices.find((item) => item.hasData)?.target || 'opsPlatform');
|
||
}
|
||
|
||
const workspaceCurrent = getActiveTabTarget('workspace');
|
||
const workspaceChoices = [
|
||
{ target: 'workspaceCoordination', hasData: coordinationCount > 0 },
|
||
{ target: 'workspaceExecution', hasData: executionEvents.length > 0 },
|
||
{ target: 'workspaceSignals', hasData: recentSignals.length > 0 },
|
||
];
|
||
if (!workspaceChoices.find((item) => item.target === workspaceCurrent && item.hasData)) {
|
||
setActiveTab('workspace', workspaceChoices.find((item) => item.hasData)?.target || 'workspaceCoordination');
|
||
}
|
||
|
||
const assetCurrent = getActiveTabTarget('asset');
|
||
const assetChoices = [
|
||
{ target: 'assetPositions', hasData: positions.length > 0 },
|
||
{ target: 'assetOrders', hasData: orders.length > 0 },
|
||
];
|
||
if (!assetChoices.find((item) => item.target === assetCurrent && item.hasData)) {
|
||
setActiveTab('asset', assetChoices.find((item) => item.hasData)?.target || 'assetPositions');
|
||
}
|
||
}
|
||
|
||
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 haltedCount = countHalted(platformHalts);
|
||
|
||
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">三端持仓合计</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">熔断或人工停机</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) {
|
||
const container = document.getElementById('platformGrid');
|
||
const entries = [
|
||
{ key: 'paper', title: '模拟盘', subtitle: '执行基准 / 策略验证' },
|
||
{ key: 'bitget', title: 'Bitget', subtitle: 'U 本位实盘' },
|
||
{ key: 'hyperliquid', title: 'Hyperliquid', subtitle: '链上实盘' },
|
||
];
|
||
|
||
container.innerHTML = entries.map(({ key, title, subtitle }) => {
|
||
const item = platforms?.[key] || { enabled: false };
|
||
const halt = platformHalts?.[key === 'paper' ? 'PaperTrading' : key === 'bitget' ? 'Bitget' : 'Hyperliquid'] || {};
|
||
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);
|
||
|
||
return `
|
||
<article class="platform-card">
|
||
<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 ? 'HALTED' : 'ONLINE'}</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">权益</span>
|
||
<span class="value">${formatSensitiveMoney(account.current_balance || account.account_value)}</span>
|
||
</div>
|
||
<div class="platform-stat">
|
||
<span class="label">可用</span>
|
||
<span class="value">${formatSensitiveMoney(account.available || account.available_balance)}</span>
|
||
</div>
|
||
<div class="platform-stat">
|
||
<span class="label">持仓</span>
|
||
<span class="value">${positions.count || 0}</span>
|
||
</div>
|
||
<div class="platform-stat">
|
||
<span class="label">挂单</span>
|
||
<span class="value">${orders.count || 0}</span>
|
||
</div>
|
||
<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:${(risk.drawdown_percent || risk.drawdown || 0) >= (risk.circuit_breaker_threshold || 25) ? 'var(--danger)' : 'var(--text)'};">
|
||
${formatPercent(risk.drawdown_percent || risk.drawdown || 0)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderSignalStream(signals) {
|
||
const container = document.getElementById('signalStream');
|
||
if (!signals || signals.length === 0) {
|
||
container.innerHTML = compactEmpty('最近没有可展示信号', '等待新的分析结果写入,运行状态可继续看上方心跳。');
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = signals.map((signal) => `
|
||
<div class="stream-item">
|
||
<div class="time">${relativeTime(signal.created_at)}<br>${formatTime(signal.created_at)}</div>
|
||
<div class="headline">
|
||
<strong>${signal.symbol || '-'}</strong>
|
||
${signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '观望'}
|
||
<span style="color: var(--muted);">| ${signal.signal_type || '-'} | ${signal.timeframe || signal.type || '-'}</span>
|
||
</div>
|
||
<div class="grade">${signal.grade || '-'} / ${formatPercent(signal.confidence || 0, 1)}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
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 / stock 聚合</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">Stock</span><span class="value">${signalStats?.stats_7d?.stock?.total || 0}</span></div>
|
||
<div class="stat-chip"><span class="label">Total</span><span class="value">${signalStats?.stats_7d?.total || 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 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 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>
|
||
`;
|
||
|
||
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.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 hyperliquid = summarizeDecision(preview.hyperliquid);
|
||
const bitget = summarizeDecision(preview.bitget);
|
||
|
||
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 ${hyperliquid.tone}"><span class="label">Hyperliquid</span><span class="value">${hyperliquid.label}</span></div>
|
||
<div class="stat-chip decision-chip ${bitget.tone}"><span class="label">Bitget</span><span class="value">${bitget.label}</span></div>
|
||
</div>
|
||
<div style="margin-top: 12px; color: var(--muted); font-size: 12px; line-height: 1.6;">
|
||
模拟盘: ${paper.detail}<br>
|
||
Hyperliquid: ${hyperliquid.detail}<br>
|
||
Bitget: ${bitget.detail}
|
||
</div>
|
||
</article>
|
||
`);
|
||
});
|
||
|
||
container.innerHTML = cards.join('');
|
||
}
|
||
|
||
function renderHalts(platformHalts) {
|
||
const container = document.getElementById('haltList');
|
||
const entries = [
|
||
['PaperTrading', '模拟盘'],
|
||
['Bitget', 'Bitget'],
|
||
['Hyperliquid', 'Hyperliquid'],
|
||
];
|
||
|
||
container.innerHTML = entries.map(([key, label]) => {
|
||
const halt = platformHalts?.[key] || {};
|
||
const active = !!halt.halted;
|
||
return `
|
||
<div class="halt-item ${active ? 'active' : ''}">
|
||
<div class="halt-top">
|
||
<div class="halt-name">${label}</div>
|
||
<div class="halt-state">${active ? 'HALTED' : 'ACTIVE'}</div>
|
||
</div>
|
||
<div class="halt-reason">
|
||
${active
|
||
? `${halt.reason || '已触发平台停机'}<br>回撤: ${formatPercent(halt.drawdown_pct || 0)} | 触发时间: ${formatTime(halt.halted_at)}`
|
||
: '运行正常,未触发平台停机或熔断。'}
|
||
</div>
|
||
${active ? `
|
||
<div class="halt-actions">
|
||
<button class="mini-btn" data-platform="${key}">恢复执行</button>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
container.querySelectorAll('[data-platform]').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
const platform = button.getAttribute('data-platform');
|
||
await resumePlatform(platform, 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.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>` : ''}
|
||
</div>
|
||
<span style="color: var(--muted);">${event.reason || '无说明'}</span>
|
||
${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', 'hyperliquid'].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 = `
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>平台</th>
|
||
<th>交易对</th>
|
||
<th>方向</th>
|
||
<th>入场 / 现价</th>
|
||
<th>仓位 / 杠杆</th>
|
||
<th>止盈 / 止损</th>
|
||
<th>未实现盈亏</th>
|
||
<th>盈亏比例</th>
|
||
<th>时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${total.map((item) => `
|
||
<tr>
|
||
<td><span class="platform-pill">${item.platform}</span></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">${formatMoney(item.entry_price)} / ${formatMoney(item.mark_price)}</td>
|
||
<td class="inline-mono">${formatNumber(item.size, 4)} / ${formatNumber(item.leverage, 1)}x</td>
|
||
<td class="inline-mono">${item.take_profit ? formatMoney(item.take_profit) : '-'} / ${item.stop_loss ? formatMoney(item.stop_loss) : '-'}</td>
|
||
<td style="color:${(item.unrealized_pnl || 0) >= 0 ? 'var(--good)' : 'var(--danger)'}">${formatMoney(item.unrealized_pnl)}</td>
|
||
<td style="color:${(item.pnl_percent || 0) >= 0 ? 'var(--good)' : 'var(--danger)'}">${formatPercent(item.pnl_percent, 2)}</td>
|
||
<td class="inline-mono">${item.opened_at ? relativeTime(item.opened_at) : '-'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
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>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${total.map((item) => `
|
||
<tr>
|
||
<td><span class="platform-pill">${item.platform}</span></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 class="inline-mono">${item.created_at ? relativeTime(item.created_at) : '-'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
async function resumePlatform(platform, button) {
|
||
const platformMap = {
|
||
PaperTrading: 'PaperTrading',
|
||
Bitget: 'Bitget',
|
||
Hyperliquid: 'Hyperliquid',
|
||
};
|
||
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 }),
|
||
});
|
||
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 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);
|
||
renderOpsSummary(data);
|
||
renderPlatforms(data.platforms, data.crypto_agent?.platform_halts);
|
||
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 || {});
|
||
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();
|
||
applyAutoRefreshState();
|
||
loadConsole();
|
||
</script>
|
||
</body>
|
||
</html>
|