563 lines
18 KiB
HTML
563 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<title>系统状态 - Stock Agent</title>
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
<style>
|
|
/* 防止横向滚动 */
|
|
html, body {
|
|
overflow-x: hidden;
|
|
max-width: 100vw;
|
|
}
|
|
|
|
/* 覆盖全局 #app 样式 */
|
|
#app {
|
|
height: auto;
|
|
display: block;
|
|
align-items: initial;
|
|
justify-content: initial;
|
|
padding: 0;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.status-page {
|
|
min-height: 100vh;
|
|
background: var(--bg-primary);
|
|
padding: 20px;
|
|
}
|
|
|
|
.status-container {
|
|
max-width: 1400px;
|
|
min-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* 固定顶部区域 */
|
|
.sticky-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
background: var(--bg-primary);
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.status-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding: 10px 0 20px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
.status-title {
|
|
font-size: 24px;
|
|
font-weight: 300;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.status-title span {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.refresh-btn {
|
|
padding: 8px 16px;
|
|
background: transparent;
|
|
border: 1px solid var(--accent);
|
|
color: var(--accent);
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
background: var(--accent);
|
|
color: var(--bg-primary);
|
|
}
|
|
|
|
.last-update {
|
|
text-align: right;
|
|
color: var(--text-secondary);
|
|
font-size: 12px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
/* 统计卡片 */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 24px;
|
|
font-weight: 300;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stat-value.running {
|
|
color: #00ff41;
|
|
}
|
|
|
|
.stat-value.error {
|
|
color: #ff4444;
|
|
}
|
|
|
|
/* Agent 部分 */
|
|
.agents-section {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: 300;
|
|
color: var(--text-primary);
|
|
margin-bottom: 16px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.agents-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.agent-card {
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 20px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.agent-card:hover {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.agent-card.status-running {
|
|
border-left: 3px solid #00ff41;
|
|
}
|
|
|
|
.agent-card.status-error {
|
|
border-left: 3px solid #ff4444;
|
|
}
|
|
|
|
.agent-card.status-stopped {
|
|
border-left: 3px solid #ffaa00;
|
|
}
|
|
|
|
.agent-card.status-starting {
|
|
border-left: 3px solid var(--accent);
|
|
}
|
|
|
|
.agent-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.agent-name {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.agent-status {
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.agent-status.running {
|
|
background: rgba(0, 255, 65, 0.1);
|
|
color: #00ff41;
|
|
}
|
|
|
|
.agent-status.error {
|
|
background: rgba(255, 68, 68, 0.1);
|
|
color: #ff4444;
|
|
}
|
|
|
|
.agent-status.stopped {
|
|
background: rgba(255, 170, 0, 0.1);
|
|
color: #ffaa00;
|
|
}
|
|
|
|
.agent-status.starting {
|
|
background: rgba(0, 122, 255, 0.1);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.agent-info {
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.agent-info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.agent-info-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.agent-info-label {
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.agent-info-value {
|
|
color: var(--text-primary);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.agent-info-value.error {
|
|
color: #ff4444;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.error-message {
|
|
background: rgba(255, 68, 68, 0.1);
|
|
color: #ff4444;
|
|
padding: 12px 16px;
|
|
border-radius: 4px;
|
|
margin-bottom: 16px;
|
|
border: 1px solid #ff4444;
|
|
}
|
|
|
|
/* 响应式 */
|
|
@media (max-width: 1200px) {
|
|
.status-container {
|
|
min-width: auto;
|
|
}
|
|
|
|
.agents-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
/* 符号列表样式 */
|
|
.symbols-list {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
|
|
.symbol-tag {
|
|
background: var(--bg-primary);
|
|
padding: 2px 8px;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<div class="status-page">
|
|
<div class="status-container">
|
|
<div class="sticky-header">
|
|
<div class="status-header">
|
|
<h1 class="status-title">系统状态 <span>监控</span></h1>
|
|
<button class="refresh-btn" onclick="loadStatus()">🔄 刷新</button>
|
|
</div>
|
|
<div class="last-update" id="lastUpdate">加载中...</div>
|
|
</div>
|
|
|
|
<div id="errorContainer"></div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">运行时长</div>
|
|
<div class="stat-value" id="uptime">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Agent 总数</div>
|
|
<div class="stat-value" id="totalAgents">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">运行中</div>
|
|
<div class="stat-value running" id="runningAgents">-</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">错误</div>
|
|
<div class="stat-value error" id="errorAgents">-</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="agents-section">
|
|
<h2 class="section-title">🤖 Agent 详情</h2>
|
|
<div id="agentsContainer">
|
|
<div class="loading">加载中...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let refreshInterval;
|
|
|
|
function formatUptime(seconds) {
|
|
if (seconds < 60) {
|
|
return `${Math.floor(seconds)} 秒`;
|
|
} else if (seconds < 3600) {
|
|
return `${Math.floor(seconds / 60)} 分钟`;
|
|
} else if (seconds < 86400) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
return `${hours}小时 ${minutes}分`;
|
|
} else {
|
|
const days = Math.floor(seconds / 86400);
|
|
const hours = Math.floor((seconds % 86400) / 3600);
|
|
return `${days}天 ${hours}小时`;
|
|
}
|
|
}
|
|
|
|
function getStatusIcon(status) {
|
|
const icons = {
|
|
'运行中': '✅',
|
|
'错误': '❌',
|
|
'已停止': '⏸️',
|
|
'启动中': '🔄',
|
|
'未初始化': '⚪'
|
|
};
|
|
return icons[status] || '❓';
|
|
}
|
|
|
|
function getStatusClass(status) {
|
|
const classes = {
|
|
'运行中': 'running',
|
|
'错误': 'error',
|
|
'已停止': 'stopped',
|
|
'启动中': 'starting',
|
|
'未初始化': 'stopped'
|
|
};
|
|
return classes[status] || '';
|
|
}
|
|
|
|
function formatSymbols(symbols) {
|
|
if (!symbols) return '-';
|
|
if (Array.isArray(symbols)) {
|
|
return '<div class="symbols-list">' +
|
|
symbols.map(s => `<span class="symbol-tag">${s}</span>`).join('') +
|
|
'</div>';
|
|
}
|
|
if (typeof symbols === 'string') {
|
|
return symbols;
|
|
}
|
|
return '-';
|
|
}
|
|
|
|
function isCryptoAgent(type) {
|
|
return type === 'crypto';
|
|
}
|
|
|
|
function formatStockAgentConfig(config) {
|
|
if (!config) return '';
|
|
|
|
let html = '';
|
|
|
|
// 显示美股和港股数量
|
|
if (config.us_count !== undefined || config.hk_count !== undefined) {
|
|
const usCount = config.us_count || 0;
|
|
const hkCount = config.hk_count || 0;
|
|
html += `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">股票分布</span>
|
|
<span class="agent-info-value">🇺🇸 美股 ${usCount}只 | 🇭🇰 港股 ${hkCount}只</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 显示美股列表
|
|
if (config.us_symbols && config.us_symbols.length > 0) {
|
|
html += `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">美股列表</span>
|
|
<span class="agent-info-value">${formatSymbols(config.us_symbols)}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 显示港股列表
|
|
if (config.hk_symbols && config.hk_symbols.length > 0) {
|
|
html += `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">港股列表</span>
|
|
<span class="agent-info-value">${formatSymbols(config.hk_symbols)}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 显示分析间隔
|
|
if (config.analysis_interval) {
|
|
html += `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">分析间隔</span>
|
|
<span class="agent-info-value">${config.analysis_interval}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
async function loadStatus() {
|
|
try {
|
|
const response = await fetch('/api/system/status');
|
|
const result = await response.json();
|
|
|
|
if (result.status === 'success') {
|
|
displayStatus(result.data);
|
|
} else {
|
|
showError('加载状态失败: ' + (result.message || '未知错误'));
|
|
}
|
|
|
|
// 更新最后刷新时间
|
|
const now = new Date();
|
|
document.getElementById('lastUpdate').textContent =
|
|
`最后更新: ${now.toLocaleTimeString('zh-CN')}`;
|
|
|
|
} catch (error) {
|
|
console.error('加载状态失败:', error);
|
|
showError('加载状态失败: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function displayStatus(data) {
|
|
// 更新概览统计
|
|
document.getElementById('uptime').textContent = formatUptime(data.uptime_seconds);
|
|
document.getElementById('totalAgents').textContent = data.total_agents;
|
|
document.getElementById('runningAgents').textContent = data.running_agents;
|
|
document.getElementById('errorAgents').textContent = data.error_agents;
|
|
|
|
// 更新 Agent 列表
|
|
const container = document.getElementById('agentsContainer');
|
|
const agents = Object.entries(data.agents);
|
|
|
|
if (agents.length === 0) {
|
|
container.innerHTML = '<div class="loading">暂无 Agent 运行</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '<div class="agents-grid">' + agents.map(([id, agent]) => {
|
|
const statusClass = getStatusClass(agent.status);
|
|
|
|
// 判断 Agent 类型
|
|
const isStockAgent = agent.type === 'stock';
|
|
const isCryptoAgent = agent.type === 'crypto';
|
|
|
|
return `
|
|
<div class="agent-card status-${statusClass}">
|
|
<div class="agent-header">
|
|
<div class="agent-name">${getStatusIcon(agent.status)} ${agent.name}</div>
|
|
<div class="agent-status ${statusClass}">${agent.status}</div>
|
|
</div>
|
|
<div class="agent-info">
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">类型</span>
|
|
<span class="agent-info-value">${agent.type || '-'}</span>
|
|
</div>
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">启动时间</span>
|
|
<span class="agent-info-value">${agent.start_time || '-'}</span>
|
|
</div>
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">最后活动</span>
|
|
<span class="agent-info-value">${agent.last_activity || '-'}</span>
|
|
</div>
|
|
${isStockAgent ? formatStockAgentConfig(agent.config) : `
|
|
${agent.config?.symbols && agent.config.symbols.length > 0 ? `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">监控标的</span>
|
|
<span class="agent-info-value">${formatSymbols(agent.config.symbols)}</span>
|
|
</div>
|
|
` : ''}
|
|
${isCryptoAgent && agent.config?.auto_trading_enabled !== undefined ? `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">自动交易</span>
|
|
<span class="agent-info-value">${agent.config.auto_trading_enabled ? '✅ 已启用' : '❌ 未启用'}</span>
|
|
</div>
|
|
` : ''}
|
|
${agent.config?.analysis_interval ? `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">分析间隔</span>
|
|
<span class="agent-info-value">${agent.config.analysis_interval}</span>
|
|
</div>
|
|
` : ''}
|
|
`}
|
|
${agent.error_message ? `
|
|
<div class="agent-info-row">
|
|
<span class="agent-info-label">错误信息</span>
|
|
<span class="agent-info-value error">${agent.error_message}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('') + '</div>';
|
|
}
|
|
|
|
function showError(message) {
|
|
const container = document.getElementById('errorContainer');
|
|
container.innerHTML = `<div class="error-message">${message}</div>`;
|
|
setTimeout(() => {
|
|
container.innerHTML = '';
|
|
}, 5000);
|
|
}
|
|
|
|
// 页面加载时获取状态
|
|
loadStatus();
|
|
|
|
// 每10秒自动刷新
|
|
refreshInterval = setInterval(loadStatus, 10000);
|
|
|
|
// 页面卸载时清除定时器
|
|
window.addEventListener('beforeunload', () => {
|
|
if (refreshInterval) {
|
|
clearInterval(refreshInterval);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|