BIZ-46 Phase3: 7项 follow-up 开发完成
1. 架构解耦 — SidecarContext + FastAPI Depends 注入 - 新增 context.py: SidecarContext dataclass 收敛全部全局状态 - server.py: 移除模块级全局变量,lifespan 创建 ctx → app.state.sidecar - webui.py: 移除反向导入 server,改用 Depends(get_context) 2. Prometheus 标签基数治理 — model_id → provider - upstream_latency_seconds / upstream_errors_total label 收敛为 provider - 模型级信息保留在 structlog JSON 日志 3. SSE 快照共享缓存 - 1s TTL 共享 snapshot cache + double-check locking - 多客户端不重复构建快照 4. 部署支撑 - Dockerfile (python:3.12-slim, 非 root 用户, HEALTHCHECK) - systemd service (安全加固, 资源限制) - .env.example (完整环境变量清单) 5. Readiness HTTP Client 复用 - check_upstream() 注入主 http_client,不再每次创建新 client 6. Retreat 并发回归测试 - 5 个测试用例全部通过(死锁检测 + 状态转换 + 并发安全) 7. Dashboard UX 优化 - 队列柱状图 300ms 平滑动画 - SSE 断连 5s 半透明遮罩 - 队列图标题显示总排队数 - 页面加载同步配置 验证: mypy strict 通过 (0 errors), pytest 5/5 通过, server 导入正常 (13 routes) Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -39,9 +39,35 @@
|
||||
@keyframes fadeInOut { 0% { opacity: 0; transform: translateY(-8px); } 10% { opacity: 1; transform: translateY(0); } 80% { opacity: 1; } 100% { opacity: 0; } }
|
||||
.disconnected { background: #7f1d1d; color: #fca5a5; padding: 4px 10px; border-radius: 4px; font-size: 12px; display: inline-block; margin-left: 8px; }
|
||||
.connected { background: #065f46; color: #6ee7b7; padding: 4px 10px; border-radius: 4px; font-size: 12px; display: inline-block; margin-left: 8px; }
|
||||
|
||||
/* BIZ-46 Phase3: 队列柱状图 300ms 平滑动画 */
|
||||
.queue-bar { transition: height 0.3s ease; }
|
||||
|
||||
/* BIZ-46 Phase3: SSE 断连 5s 半透明遮罩 */
|
||||
#reconnect-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
#reconnect-mask.visible { display: flex; }
|
||||
#reconnect-mask .mask-icon { font-size: 48px; margin-bottom: 16px; }
|
||||
#reconnect-mask .mask-text { color: #94a3b8; font-size: 16px; font-weight: 500; }
|
||||
#reconnect-mask .mask-sub { color: #64748b; font-size: 13px; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- BIZ-46 Phase3: SSE 断连遮罩 -->
|
||||
<div id="reconnect-mask">
|
||||
<div class="mask-icon">⚠️</div>
|
||||
<div class="mask-text">数据暂不可用</div>
|
||||
<div class="mask-sub">SSE 连接中断,正在重连…</div>
|
||||
</div>
|
||||
|
||||
<h1>🚀 NVIDIA Sidecar 实时仪表盘
|
||||
<span id="conn-status" class="connected">已连接</span>
|
||||
</h1>
|
||||
@@ -64,7 +90,8 @@
|
||||
<canvas id="chart-tokens"></canvas>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>📈 队列深度</h2>
|
||||
<!-- BIZ-46 Phase3: 队列图标题显示总排队数 -->
|
||||
<h2>📈 队列深度 <span id="queue-total" style="font-size:13px;color:#38bdf8;">(共 0)</span></h2>
|
||||
<canvas id="chart-queue"></canvas>
|
||||
</div>
|
||||
<div class="card">
|
||||
@@ -99,7 +126,16 @@
|
||||
let evtSource = null;
|
||||
let dataHistory = { throughput: [], rates: [] };
|
||||
const MAX_HISTORY = 20;
|
||||
let latencyLog = [];
|
||||
let lastSSETime = Date.now();
|
||||
|
||||
// BIZ-46 Phase3: SSE 断连 5s 遮罩
|
||||
function checkReconnect() {
|
||||
const mask = document.getElementById('reconnect-mask');
|
||||
if (Date.now() - lastSSETime > 5000) {
|
||||
mask.classList.add('visible');
|
||||
}
|
||||
}
|
||||
setInterval(checkReconnect, 1000);
|
||||
|
||||
function connectSSE() {
|
||||
if (evtSource) evtSource.close();
|
||||
@@ -107,8 +143,10 @@ function connectSSE() {
|
||||
evtSource.onmessage = (e) => {
|
||||
try {
|
||||
const snap = JSON.parse(e.data);
|
||||
lastSSETime = Date.now();
|
||||
// 隐藏断连遮罩
|
||||
document.getElementById('reconnect-mask').classList.remove('visible');
|
||||
updateDashboard(snap);
|
||||
updateLatencies(snap);
|
||||
document.getElementById('conn-status').className = 'connected';
|
||||
document.getElementById('conn-status').textContent = '已连接';
|
||||
} catch (err) {
|
||||
@@ -130,7 +168,9 @@ const chartTokens = new Chart(ctxTokens, {
|
||||
labels: ['已用令牌', '可用令牌'],
|
||||
datasets: [{ data: [0, 40], backgroundColor: ['#ef4444', '#22c55e'], borderWidth: 0 }]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: true, cutout: '65%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
|
||||
options: { responsive: true, maintainAspectRatio: true, cutout: '65%', plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } },
|
||||
// BIZ-46 Phase3: 300ms 平滑动画
|
||||
animation: { duration: 300 } }
|
||||
});
|
||||
|
||||
const ctxQueue = document.getElementById('chart-queue').getContext('2d');
|
||||
@@ -140,7 +180,11 @@ const chartQueue = new Chart(ctxQueue, {
|
||||
labels: ['URGENT', 'HIGH', 'NORMAL', 'LOW'],
|
||||
datasets: [{ label: '排队数', data: [0, 0, 0, 0], backgroundColor: ['#ef4444', '#f59e0b', '#38bdf8', '#a78bfa'] }]
|
||||
},
|
||||
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } }, plugins: { legend: { display: false } } }
|
||||
options: { responsive: true, maintainAspectRatio: true,
|
||||
scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } },
|
||||
plugins: { legend: { display: false } },
|
||||
// BIZ-46 Phase3: 300ms 平滑动画
|
||||
animation: { duration: 300 } }
|
||||
});
|
||||
|
||||
const ctxThroughput = document.getElementById('chart-throughput').getContext('2d');
|
||||
@@ -151,7 +195,10 @@ const chartThroughput = new Chart(ctxThroughput, {
|
||||
{ label: '429', data: [], borderColor: '#f59e0b', backgroundColor: '#f59e0b20', fill: false, tension: 0.3, pointRadius: 2 },
|
||||
{ label: '直通', data: [], borderColor: '#a78bfa', backgroundColor: '#a78bfa20', fill: false, tension: 0.3, pointRadius: 2 },
|
||||
]},
|
||||
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } }, plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
|
||||
options: { responsive: true, maintainAspectRatio: true,
|
||||
scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } },
|
||||
plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } },
|
||||
animation: { duration: 300 } }
|
||||
});
|
||||
|
||||
const ctxRate = document.getElementById('chart-rate').getContext('2d');
|
||||
@@ -161,7 +208,10 @@ const chartRate = new Chart(ctxRate, {
|
||||
{ label: '有效 RPM', data: [], borderColor: '#38bdf8', fill: false, tension: 0.3, pointRadius: 2 },
|
||||
{ label: '基准 RPM', data: [], borderColor: '#64748b', fill: false, tension: 0.3, pointRadius: 2, borderDash: [4, 4] },
|
||||
]},
|
||||
options: { responsive: true, maintainAspectRatio: true, scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } }, plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } } }
|
||||
options: { responsive: true, maintainAspectRatio: true,
|
||||
scales: { y: { beginAtZero: true, ticks: { color: '#94a3b8' } }, x: { ticks: { color: '#94a3b8' } } },
|
||||
plugins: { legend: { position: 'bottom', labels: { color: '#94a3b8' } } },
|
||||
animation: { duration: 300 } }
|
||||
});
|
||||
|
||||
function updateDashboard(snap) {
|
||||
@@ -188,6 +238,7 @@ function updateDashboard(snap) {
|
||||
|
||||
const qs = snap.queue || {};
|
||||
const perPriority = qs.per_priority || {};
|
||||
const totalQueued = perPriority.URGENT + perPriority.HIGH + perPriority.NORMAL + perPriority.LOW || qs.current_size || 0;
|
||||
chartQueue.data.datasets[0].data = [
|
||||
perPriority.URGENT || 0,
|
||||
perPriority.HIGH || 0,
|
||||
@@ -196,6 +247,9 @@ function updateDashboard(snap) {
|
||||
];
|
||||
chartQueue.update();
|
||||
|
||||
// BIZ-46 Phase3: 队列图标题显示总排队数
|
||||
document.getElementById('queue-total').textContent = '(共 ' + totalQueued + ')';
|
||||
|
||||
const now = new Date().toLocaleTimeString();
|
||||
const prev = dataHistory.throughput.length > 0 ? dataHistory.throughput[dataHistory.throughput.length - 1].nvidia : 0;
|
||||
const throughput = Math.max(0, (r.nvidia || 0) - prev);
|
||||
@@ -217,10 +271,6 @@ function updateDashboard(snap) {
|
||||
chartRate.update();
|
||||
}
|
||||
|
||||
function updateLatencies(snap) {
|
||||
const tb = snap.token_bucket || {};
|
||||
}
|
||||
|
||||
function fmtDuration(s) {
|
||||
if (s < 60) return s + 's';
|
||||
if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
||||
@@ -255,6 +305,22 @@ function showToast(type, msg) {
|
||||
setTimeout(() => t.remove(), 3000);
|
||||
}
|
||||
|
||||
// BIZ-46 Phase3: 页面加载时同步当前配置值
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const resp = await fetch('/api/admin/config');
|
||||
if (resp.ok) {
|
||||
const config = await resp.json();
|
||||
document.getElementById('cfg-rate-rpm').value = config.rate_rpm || 40;
|
||||
document.getElementById('cfg-rate-val').textContent = config.rate_rpm || 40;
|
||||
document.getElementById('cfg-queue-max').value = config.queue_max_size || 500;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('配置加载失败(可能需要 Admin Token)', e);
|
||||
}
|
||||
}
|
||||
|
||||
loadConfig();
|
||||
connectSSE();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user