feat: 添加静态文件支持和仪表板路由,移除监控相关路由,优化代码结构和可读性

This commit is contained in:
iHeyTang
2025-08-19 15:18:41 +08:00
parent 0d8d9ffc4e
commit c2fe3585d5
6 changed files with 504 additions and 503 deletions

View File

@@ -7,4 +7,5 @@ boto3
aiosqlite aiosqlite
sqlalchemy sqlalchemy
greenlet greenlet
psutil psutil
jinja2

View File

@@ -1,13 +1,16 @@
import os import os
from pathlib import Path
from fastapi.responses import FileResponse
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from workflow_service.comfy.comfy_server import server_manager from workflow_service.comfy.comfy_server import server_manager
from workflow_service.database.api import init_db from workflow_service.database.api import init_db
from workflow_service.config import Settings from workflow_service.config import Settings
from workflow_service.routes import service, workflow, run, runx, monitor, comfy_server from workflow_service.routes import service, workflow, run, runx, comfy_server
from workflow_service.comfy.comfy_queue import queue_manager from workflow_service.comfy.comfy_queue import queue_manager
settings = Settings() settings = Settings()
@@ -23,14 +26,24 @@ web_app.add_middleware(
allow_headers=["*"], # 允许所有请求头 allow_headers=["*"], # 允许所有请求头
) )
# 挂载静态文件目录
static_dir = Path(__file__).parent / "static"
if static_dir.exists():
web_app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
web_app.include_router(service.service_router) web_app.include_router(service.service_router)
web_app.include_router(workflow.workflow_router) web_app.include_router(workflow.workflow_router)
web_app.include_router(run.run_router) web_app.include_router(run.run_router)
web_app.include_router(runx.runx_router) web_app.include_router(runx.runx_router)
web_app.include_router(monitor.monitor_router)
web_app.include_router(comfy_server.router) web_app.include_router(comfy_server.router)
@web_app.get("/")
async def dashboard():
"""监控仪表板页面"""
return FileResponse(str(Path(__file__).parent / "static" / "index.html"))
@web_app.on_event("startup") @web_app.on_event("startup")
async def startup_event(): async def startup_event():
"""服务启动时,初始化数据库。""" """服务启动时,初始化数据库。"""
@@ -40,11 +53,11 @@ async def startup_event():
print(f"检测到 {len(servers)} 个 ComfyUI 服务器配置。") print(f"检测到 {len(servers)} 个 ComfyUI 服务器配置。")
for server in servers: for server in servers:
print(f" - 服务器: {server.http_url}") print(f" - 服务器: {server.http_url}")
# 启动队列监控任务 # 启动队列监控任务
await queue_manager.start_queue_monitor() await queue_manager.start_queue_monitor()
print("队列监控任务已启动") print("队列监控任务已启动")
except ValueError as e: except ValueError as e:
print(f"错误: 无法在启动时获取服务器信息: {e}") print(f"错误: 无法在启动时获取服务器信息: {e}")

View File

@@ -1,498 +0,0 @@
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
monitor_router = APIRouter(prefix="/monitor", tags=["监控"])
@monitor_router.get("/", response_class=HTMLResponse)
async def monitor_dashboard():
"""监控仪表板页面"""
html_content = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI 工作流监控</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 24px;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.refresh-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
margin-bottom: 24px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
}
.refresh-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.stat-number {
font-size: 2.2em;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 8px 0;
}
.stat-label {
color: #7f8c8d;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.section {
background: rgba(255, 255, 255, 0.95);
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.section h3 {
margin-bottom: 20px;
color: #2c3e50;
font-size: 1.3em;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.server-status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.server-card {
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 12px;
background: #f8f9fa;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.server-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #e1e8ed;
}
.server-card.online {
border-color: #27ae60;
background: #f8fff9;
}
.server-card.online::before {
background: #27ae60;
}
.server-card.offline {
border-color: #e74c3c;
background: #fff8f8;
}
.server-card.offline::before {
background: #e74c3c;
}
.server-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.server-card h4 {
color: #2c3e50;
margin: 0;
font-size: 1em;
font-weight: 600;
white-space: nowrap;
}
.server-url {
color: #3498db;
font-size: 0.8em;
font-weight: 500;
font-family: monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.server-info {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 0.8em;
margin-top: 8px;
}
.info-item {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.info-separator {
color: #e1e8ed;
margin: 0 4px;
}
.status-icon {
font-size: 0.9em;
}
.status-text {
font-weight: 600;
color: #2c3e50;
}
.info-label {
color: #7f8c8d;
font-weight: 500;
font-size: 0.75em;
}
.info-value {
color: #2c3e50;
font-weight: 600;
font-size: 0.75em;
}
.info-value:has(span[style*="color: #e74c3c"]) {
color: #e74c3c;
}
.info-value:has(span[style*="color: #f39c12"]) {
color: #f39c12;
}
.info-value:has(span[style*="color: #27ae60"]) {
color: #27ae60;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-online { background-color: #27ae60; }
.status-offline { background-color: #e74c3c; }
.task-list {
max-height: 500px;
overflow-y: auto;
}
.task-item {
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 16px;
margin: 8px 0;
background: #f8f9fa;
transition: all 0.3s ease;
}
.task-item:hover {
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.task-name {
font-weight: 600;
color: #2c3e50;
}
.task-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
color: white;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-pending { background: #f39c12; }
.status-running { background: #3498db; }
.status-completed { background: #27ae60; }
.status-failed { background: #e74c3c; }
.task-meta {
color: #7f8c8d;
font-size: 0.85em;
}
.error-message {
color: #e74c3c;
font-size: 0.85em;
margin-top: 8px;
padding: 8px;
background: #fff5f5;
border-radius: 6px;
border-left: 3px solid #e74c3c;
}
@media (max-width: 768px) {
body { padding: 16px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.server-status { grid-template-columns: 1fr; }
.server-info { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<button class="refresh-btn" onclick="refreshData()">🔄 刷新数据</button>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="running-tasks">--</div>
<div class="stat-label">运行中任务</div>
</div>
<div class="stat-card">
<div class="stat-number" id="pending-tasks">--</div>
<div class="stat-label">等待中任务</div>
</div>
<div class="stat-card">
<div class="stat-number" id="completed-tasks">--</div>
<div class="stat-label">已完成任务</div>
</div>
<div class="stat-card">
<div class="stat-number" id="total-tasks">--</div>
<div class="stat-label">24小时总任务</div>
</div>
</div>
<div class="section">
<h3>🖥️ 服务器状态</h3>
<div class="server-status" id="server-status">
<!-- 服务器状态将在这里动态加载 -->
</div>
</div>
<div class="section">
<h3>📋 最近任务</h3>
<div class="task-list" id="recent-tasks">
<!-- 最近任务将在这里动态加载 -->
</div>
</div>
</div>
<script>
async function refreshData() {
try {
// 获取运行概览 - 更新为新的API路径
const runMetrics = await fetch('/api/run/metrics').then(r => r.json());
document.getElementById('running-tasks').textContent = runMetrics.running_tasks;
document.getElementById('pending-tasks').textContent = runMetrics.pending_tasks;
document.getElementById('completed-tasks').textContent = runMetrics.completed_tasks;
document.getElementById('total-tasks').textContent = runMetrics.total_tasks_24h;
// 获取服务器状态 - 使用现有的list接口
const serverStatus = await fetch('/api/comfy/list').then(r => r.json());
updateServerStatus(serverStatus);
// 获取运行列表 - 更新为新的API路径
const runs = await fetch('/api/run').then(r => r.json());
updateRecentTasks(runs);
} catch (error) {
console.error('刷新数据失败:', error);
}
}
function updateServerStatus(servers) {
const container = document.getElementById('server-status');
container.innerHTML = '';
if (servers.length === 0) {
container.innerHTML = '<p style="text-align: center; color: #7f8c8d; grid-column: 1/-1;">暂无服务器信息</p>';
return;
}
servers.forEach(server => {
const statusClass = server.status === 'online' ? 'online' : 'offline';
const statusColor = server.status === 'online' ? 'status-online' : 'status-offline';
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return 'N/A';
try {
const date = new Date(timeStr);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
return date.toLocaleDateString();
} catch {
return timeStr;
}
};
// 格式化健康检查时间
const formatHealthCheck = (timeStr) => {
if (!timeStr) return 'N/A';
try {
const date = new Date(timeStr);
const now = new Date();
const diff = now - date;
if (diff > 86400000) return '⚠️ 检查超时'; // 超过1天
if (diff > 3600000) return '⚠️ 检查延迟'; // 超过1小时
if (diff < 60000) return '✅ 刚刚';
if (diff < 3600000) return `✅ ${Math.floor(diff / 60000)}分钟前`;
return `✅ ${Math.floor(diff / 3600000)}小时前`;
} catch {
return timeStr;
}
};
container.innerHTML += `
<div class="server-card ${statusClass}">
<div class="server-header">
<h4>${server.name || '未命名'}</h4>
<span class="server-url">📍 ${server.http_url}</span>
</div>
<div class="server-info">
<div class="info-item">
<span class="status-icon">${server.status === 'online' ? '🟢' : '🔴'}</span>
<span class="status-text">${server.status === 'online' ? '在线' : '离线'}</span>
</div>
<span class="info-separator">|</span>
<div class="info-item">
<span class="status-text">任务: ${server.current_tasks || 0}/${server.max_concurrent_tasks || 1}</span>
</div>
<span class="info-separator">|</span>
<div class="info-item">
<span class="status-text">检查: ${formatHealthCheck(server.last_health_check)}</span>
</div>
</div>
${server.error ? `<div class="error-message">${server.error}</div>` : ''}
</div>
`;
});
}
function updateRecentTasks(tasks) {
const container = document.getElementById('recent-tasks');
container.innerHTML = '';
if (tasks.length === 0) {
container.innerHTML = '<p style="text-align: center; color: #7f8c8d;">暂无任务记录</p>';
return;
}
tasks.forEach(task => {
const statusClass = 'status-' + task.status.toLowerCase();
const statusText = {
'pending': '等待中',
'running': '运行中',
'completed': '已完成',
'failed': '失败'
}[task.status] || task.status;
container.innerHTML += `
<div class="task-item">
<div class="task-header">
<div class="task-name">${task.workflow_name || '未命名工作流'}</div>
<span class="task-status ${statusClass}">${statusText}</span>
</div>
<div class="task-meta">
<div>ID: ${task.id}</div>
<div>创建时间: ${new Date(task.created_at).toLocaleString()}</div>
</div>
</div>
`;
});
}
// 页面加载时自动刷新数据
document.addEventListener('DOMContentLoaded', refreshData);
// 每30秒自动刷新一次
setInterval(refreshData, 30000);
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content)

View File

@@ -0,0 +1,302 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 24px;
color: #2c3e50;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.refresh-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
margin-bottom: 24px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
}
.refresh-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: rgba(255, 255, 255, 0.95);
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.stat-number {
font-size: 2.2em;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 8px 0;
}
.stat-label {
color: #7f8c8d;
font-size: 0.9em;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.section {
background: rgba(255, 255, 255, 0.95);
padding: 24px;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.section h3 {
margin-bottom: 20px;
color: #2c3e50;
font-size: 1.3em;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.server-status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
}
.server-card {
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 12px;
background: #f8f9fa;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.server-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #e1e8ed;
}
.server-card.online {
border-color: #27ae60;
background: #f8fff9;
}
.server-card.online::before {
background: #27ae60;
}
.server-card.offline {
border-color: #e74c3c;
background: #fff8f8;
}
.server-card.offline::before {
background: #e74c3c;
}
.server-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}
.server-card h4 {
color: #2c3e50;
margin: 0;
font-size: 1em;
font-weight: 600;
white-space: nowrap;
}
.server-url {
color: #3498db;
font-size: 0.8em;
font-weight: 500;
font-family: monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
.server-info {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 0.8em;
margin-top: 8px;
}
.info-item {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.info-separator {
color: #e1e8ed;
margin: 0 4px;
}
.status-icon {
font-size: 0.9em;
}
.status-text {
font-weight: 600;
color: #2c3e50;
}
.info-label {
color: #7f8c8d;
font-weight: 500;
font-size: 0.75em;
}
.info-value {
color: #2c3e50;
font-weight: 600;
font-size: 0.75em;
}
.info-value:has(span[style*="color: #e74c3c"]) {
color: #e74c3c;
}
.info-value:has(span[style*="color: #f39c12"]) {
color: #f39c12;
}
.info-value:has(span[style*="color: #27ae60"]) {
color: #27ae60;
}
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-online { background-color: #27ae60; }
.status-offline { background-color: #e74c3c; }
.task-list {
max-height: 500px;
overflow-y: auto;
}
.task-item {
border: 1px solid #e1e8ed;
border-radius: 8px;
padding: 16px;
margin: 8px 0;
background: #f8f9fa;
transition: all 0.3s ease;
}
.task-item:hover {
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.task-name {
font-weight: 600;
color: #2c3e50;
}
.task-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8em;
font-weight: 600;
color: white;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-pending { background: #f39c12; }
.status-running { background: #3498db; }
.status-completed { background: #27ae60; }
.status-failed { background: #e74c3c; }
.task-meta {
color: #7f8c8d;
font-size: 0.85em;
}
.error-message {
color: #e74c3c;
font-size: 0.85em;
margin-top: 8px;
padding: 8px;
background: #fff5f5;
border-radius: 6px;
border-left: 3px solid #e74c3c;
}
@media (max-width: 768px) {
body { padding: 16px; }
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.server-status { grid-template-columns: 1fr; }
.server-info { grid-template-columns: 1fr; }
}

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI 工作流监控</title>
<link rel="stylesheet" href="/static/css/monitor.css">
</head>
<body>
<div class="container">
<button class="refresh-btn" onclick="refreshData()">🔄 刷新数据</button>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number" id="running-tasks">--</div>
<div class="stat-label">运行中任务</div>
</div>
<div class="stat-card">
<div class="stat-number" id="pending-tasks">--</div>
<div class="stat-label">等待中任务</div>
</div>
<div class="stat-card">
<div class="stat-number" id="completed-tasks">--</div>
<div class="stat-label">已完成任务</div>
</div>
<div class="stat-card">
<div class="stat-number" id="total-tasks">--</div>
<div class="stat-label">24小时总任务</div>
</div>
</div>
<div class="section">
<h3>🖥️ 服务器状态</h3>
<div class="server-status" id="server-status">
<!-- 服务器状态将在这里动态加载 -->
</div>
</div>
<div class="section">
<h3>📋 最近任务</h3>
<div class="task-list" id="recent-tasks">
<!-- 最近任务将在这里动态加载 -->
</div>
</div>
</div>
<script src="/static/js/monitor.js"></script>
</body>
</html>

View File

@@ -0,0 +1,134 @@
async function refreshData() {
try {
// 获取运行概览 - 更新为新的API路径
const runMetrics = await fetch('/api/run/metrics').then(r => r.json());
document.getElementById('running-tasks').textContent = runMetrics.running_tasks;
document.getElementById('pending-tasks').textContent = runMetrics.pending_tasks;
document.getElementById('completed-tasks').textContent = runMetrics.completed_tasks;
document.getElementById('total-tasks').textContent = runMetrics.total_tasks_24h;
// 获取服务器状态 - 使用现有的list接口
const serverStatus = await fetch('/api/comfy/list').then(r => r.json());
updateServerStatus(serverStatus);
// 获取运行列表 - 更新为新的API路径
const runs = await fetch('/api/run').then(r => r.json());
updateRecentTasks(runs);
} catch (error) {
console.error('刷新数据失败:', error);
}
}
function updateServerStatus(servers) {
const container = document.getElementById('server-status');
container.innerHTML = '';
if (servers.length === 0) {
container.innerHTML = '<p style="text-align: center; color: #7f8c8d; grid-column: 1/-1;">暂无服务器信息</p>';
return;
}
servers.forEach(server => {
const statusClass = server.status === 'online' ? 'online' : 'offline';
const statusColor = server.status === 'online' ? 'status-online' : 'status-offline';
// 格式化时间
const formatTime = (timeStr) => {
if (!timeStr) return 'N/A';
try {
const date = new Date(timeStr);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
return date.toLocaleDateString();
} catch {
return timeStr;
}
};
// 格式化健康检查时间
const formatHealthCheck = (timeStr) => {
if (!timeStr) return 'N/A';
try {
const date = new Date(timeStr);
const now = new Date();
const diff = now - date;
if (diff > 86400000) return '⚠️ 检查超时'; // 超过1天
if (diff > 3600000) return '⚠️ 检查延迟'; // 超过1小时
if (diff < 60000) return '✅ 刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
return `${Math.floor(diff / 3600000)}小时前`;
} catch {
return timeStr;
}
};
container.innerHTML += `
<div class="server-card ${statusClass}">
<div class="server-header">
<h4>${server.name || '未命名'}</h4>
<span class="server-url">📍 ${server.http_url}</span>
</div>
<div class="server-info">
<div class="info-item">
<span class="status-icon">${server.status === 'online' ? '🟢' : '🔴'}</span>
<span class="status-text">${server.status === 'online' ? '在线' : '离线'}</span>
</div>
<span class="info-separator">|</span>
<div class="info-item">
<span class="status-text">任务: ${server.current_tasks || 0}/${server.max_concurrent_tasks || 1}</span>
</div>
<span class="info-separator">|</span>
<div class="info-item">
<span class="status-text">检查: ${formatHealthCheck(server.last_health_check)}</span>
</div>
</div>
${server.error ? `<div class="error-message">${server.error}</div>` : ''}
</div>
`;
});
}
function updateRecentTasks(tasks) {
const container = document.getElementById('recent-tasks');
container.innerHTML = '';
if (tasks.length === 0) {
container.innerHTML = '<p style="text-align: center; color: #7f8c8d;">暂无任务记录</p>';
return;
}
tasks.forEach(task => {
const statusClass = 'status-' + task.status.toLowerCase();
const statusText = {
'pending': '等待中',
'running': '运行中',
'completed': '已完成',
'failed': '失败'
}[task.status] || task.status;
container.innerHTML += `
<div class="task-item">
<div class="task-header">
<div class="task-name">${task.workflow_name || '未命名工作流'}</div>
<span class="task-status ${statusClass}">${statusText}</span>
</div>
<div class="task-meta">
<div>ID: ${task.id}</div>
<div>创建时间: ${new Date(task.created_at).toLocaleString()}</div>
</div>
</div>
`;
});
}
// 页面加载时自动刷新数据
document.addEventListener('DOMContentLoaded', refreshData);
// 每30秒自动刷新一次
setInterval(refreshData, 30000);