route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
811 lines (709 loc) • 26.9 kB
HTML
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code Router - Statistics Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #2d2d2d 0%, #1a1a1a 100%);
min-height: 100vh;
padding: 20px;
color: #f5f5f5;
}
.dashboard {
max-width: 1400px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #f5f5f5;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
margin-bottom: 10px;
}
.header p {
color: #b0b0b0;
font-size: 1.1rem;
}
.refresh-info {
text-align: center;
color: #888;
margin-bottom: 20px;
font-size: 0.9rem;
}
/* Bento Grid Layout */
.bento-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-auto-rows: auto;
gap: 20px;
grid-template-areas:
"summary summary summary"
"realtime providers models"
"distribution distribution distribution"
"performance failures-stats failures-trends"
"daily history trends";
}
.card {
background: rgba(45, 45, 45, 0.9);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(128, 128, 128, 0.2);
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #dc2626, #b91c1c, #991b1b);
}
.card-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
color: #f5f5f5;
display: flex;
align-items: center;
gap: 8px;
}
.card-icon {
font-size: 1.5rem;
}
/* Specific card styling */
.summary-card {
grid-area: summary;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
}
.summary-card .card-title {
color: white;
}
.realtime-card {
grid-area: realtime;
}
.providers-card {
grid-area: providers;
}
.models-card {
grid-area: models;
}
.distribution-card {
grid-area: distribution;
}
.performance-card {
grid-area: performance;
}
.daily-card {
grid-area: daily;
}
.history-card {
grid-area: history;
}
.failures-stats-card {
grid-area: failures-stats;
}
.failures-trends-card {
grid-area: failures-trends;
}
.trends-card {
grid-area: trends;
}
/* Stats display */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-top: 16px;
}
.stat-item {
text-align: center;
padding: 16px;
background: rgba(128, 128, 128, 0.1);
border-radius: 12px;
backdrop-filter: blur(5px);
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #dc2626;
margin-bottom: 4px;
}
.summary-card .stat-value {
color: white;
}
.stat-label {
font-size: 0.85rem;
color: #b0b0b0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.summary-card .stat-label {
color: rgba(255,255,255,0.8);
}
/* Progress bars */
.progress-bar {
width: 100%;
height: 8px;
background: #404040;
border-radius: 4px;
overflow: hidden;
margin: 8px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #dc2626, #b91c1c);
border-radius: 4px;
transition: width 0.3s ease;
}
/* Provider list */
.provider-list {
margin-top: 16px;
}
.provider-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin: 8px 0;
background: #3a3a3a;
border-radius: 8px;
border-left: 4px solid #dc2626;
}
.provider-name {
font-weight: 600;
color: #f5f5f5;
}
.provider-stats {
font-size: 0.9rem;
color: #b0b0b0;
}
/* Status indicators */
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.status-healthy {
background: #10b981;
}
.status-warning {
background: #f59e0b;
}
.status-error {
background: #dc2626;
}
/* Failure Analysis specific styles */
.failure-trend {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
}
.trend-up {
color: #dc2626;
}
.trend-down {
color: #10b981;
}
.trend-stable {
color: #888;
}
.error-category {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin: 4px 0;
background: #3a3a3a;
border-radius: 6px;
border-left: 3px solid #dc2626;
}
.error-category.warning {
border-left-color: #f59e0b;
}
.error-category.info {
border-left-color: #888;
}
/* Chart placeholder */
.chart-placeholder {
height: 200px;
background: #3a3a3a;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #b0b0b0;
font-style: italic;
margin-top: 16px;
}
/* Loading state */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
color: #b0b0b0;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #404040;
border-top: 2px solid #dc2626;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive design */
@media (max-width: 768px) {
.bento-grid {
grid-template-columns: 1fr;
grid-template-areas:
"summary"
"realtime"
"providers"
"models"
"distribution"
"performance"
"failures-stats"
"failures-trends"
"daily"
"history";
}
.header h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}
}
</style>
</head>
<body>
<div class="dashboard">
<div class="header">
<h1>📊 Claude Code Router Dashboard</h1>
<p>实时监控统计 · 负载均衡分析 · 性能指标</p>
</div>
<div class="refresh-info">
<span id="lastUpdate">最后更新: --</span> |
<span id="nextRefresh">下次刷新: 5秒</span>
</div>
<div class="bento-grid">
<!-- 总览卡片 -->
<div class="card summary-card">
<div class="card-title">
<span class="card-icon">🎯</span>
系统总览
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="totalRequests">--</div>
<div class="stat-label">总请求数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalProviders">--</div>
<div class="stat-label">活跃Providers</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalModels">--</div>
<div class="stat-label">使用模型数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="successRate">--%</div>
<div class="stat-label">成功率</div>
</div>
</div>
</div>
<!-- 实时状态 -->
<div class="card realtime-card">
<div class="card-title">
<span class="card-icon">⚡</span>
实时状态
</div>
<div id="realtimeContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- Provider统计 -->
<div class="card providers-card">
<div class="card-title">
<span class="card-icon">🏭</span>
Provider分布
</div>
<div id="providersContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- 模型统计 -->
<div class="card models-card">
<div class="card-title">
<span class="card-icon">🤖</span>
模型使用
</div>
<div id="modelsContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- 权重分布 -->
<div class="card distribution-card">
<div class="card-title">
<span class="card-icon">⚖️</span>
权重效果分析
</div>
<div id="distributionContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- 性能指标 -->
<div class="card performance-card">
<div class="card-title">
<span class="card-icon">📈</span>
性能指标
</div>
<div id="performanceContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- 每日统计 -->
<div class="card daily-card">
<div class="card-title">
<span class="card-icon">📅</span>
今日统计
</div>
<div class="chart-placeholder">
今日请求趋势图 (开发中)
</div>
</div>
<!-- 失败统计 -->
<div class="card failures-stats-card">
<div class="card-title">
<span class="card-icon">⚠️</span>
失败分析
</div>
<div id="failuresStatsContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- 失败趋势 -->
<div class="card failures-trends-card">
<div class="card-title">
<span class="card-icon">📉</span>
失败趋势
</div>
<div id="failuresTrendsContent" class="loading">
<div class="spinner"></div>
加载中...
</div>
</div>
<!-- 历史记录 -->
<div class="card history-card">
<div class="card-title">
<span class="card-icon">📚</span>
历史记录
</div>
<div class="chart-placeholder">
历史统计图表 (开发中)
</div>
</div>
</div>
</div>
<script>
let refreshTimer;
let nextRefreshCountdown = 5;
// 更新倒计时
function updateCountdown() {
document.getElementById('nextRefresh').textContent = `下次刷新: ${nextRefreshCountdown}秒`;
if (nextRefreshCountdown > 0) {
nextRefreshCountdown--;
setTimeout(updateCountdown, 1000);
}
}
// 获取统计数据
async function fetchStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
updateDashboard(data);
document.getElementById('lastUpdate').textContent =
`最后更新: ${new Date().toLocaleTimeString()}`;
nextRefreshCountdown = 5;
updateCountdown();
} catch (error) {
console.error('Failed to fetch stats:', error);
showError('获取统计数据失败');
}
}
// 更新仪表板
function updateDashboard(data) {
// 更新总览数据
if (data.summary) {
document.getElementById('totalRequests').textContent = data.summary.totalRequests || 0;
document.getElementById('totalProviders').textContent = data.summary.totalProviders || 0;
document.getElementById('totalModels').textContent = data.summary.totalModels || 0;
document.getElementById('successRate').textContent =
data.summary.overallSuccessRate ? `${data.summary.overallSuccessRate.toFixed(1)}%` : '0%';
}
// 更新实时状态
updateRealtimeStatus(data.concurrency);
// 更新Provider分布
updateProvidersContent(data.providers);
// 更新模型使用
updateModelsContent(data.models);
// 更新权重分布
updateDistributionContent(data.distribution);
// 更新性能指标
updatePerformanceContent(data.performance);
// 更新失败分析
updateFailuresContent(data.failures);
}
// 更新实时状态
function updateRealtimeStatus(concurrency) {
const container = document.getElementById('realtimeContent');
if (!concurrency) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">无并发数据</div>';
return;
}
let html = '';
for (const [providerId, state] of Object.entries(concurrency)) {
const statusClass = state.isAvailable ? 'status-healthy' : 'status-error';
html += `
<div class="provider-item">
<div>
<span class="status-indicator ${statusClass}"></span>
<span class="provider-name">${providerId}</span>
</div>
<div class="provider-stats">
${state.activeConnections}/${state.maxConcurrency} (${state.utilizationRate})
</div>
</div>
`;
}
container.innerHTML = html || '<div style="text-align: center; color: #b0b0b0;">暂无活动</div>';
}
// 更新Provider内容
function updateProvidersContent(providers) {
const container = document.getElementById('providersContent');
if (!providers || Object.keys(providers).length === 0) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无数据</div>';
return;
}
let html = '<div class="provider-list">';
for (const [providerId, count] of Object.entries(providers)) {
html += `
<div class="provider-item">
<div class="provider-name">${providerId}</div>
<div class="provider-stats">${count} 次请求</div>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
}
// 更新模型内容
function updateModelsContent(models) {
const container = document.getElementById('modelsContent');
if (!models || Object.keys(models).length === 0) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无数据</div>';
return;
}
let html = '<div class="provider-list">';
for (const [modelName, count] of Object.entries(models)) {
const shortName = modelName.length > 30 ? modelName.substring(0, 30) + '...' : modelName;
html += `
<div class="provider-item">
<div class="provider-name" title="${modelName}">${shortName}</div>
<div class="provider-stats">${count} 次使用</div>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
}
// 更新权重分布内容
function updateDistributionContent(distribution) {
const container = document.getElementById('distributionContent');
if (!distribution || Object.keys(distribution).length === 0) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无权重数据</div>';
return;
}
let html = '<div class="provider-list">';
const total = Object.values(distribution).reduce((sum, count) => sum + count, 0);
for (const [key, count] of Object.entries(distribution)) {
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : 0;
html += `
<div class="provider-item">
<div class="provider-name">${key}</div>
<div class="provider-stats">
${count} 次 (${percentage}%)
<div class="progress-bar">
<div class="progress-fill" style="width: ${percentage}%"></div>
</div>
</div>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
}
// 更新性能指标
function updatePerformanceContent(performance) {
const container = document.getElementById('performanceContent');
if (!performance) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无性能数据</div>';
return;
}
const html = `
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${performance.avgResponseTime || 0}ms</div>
<div class="stat-label">平均响应时间</div>
</div>
<div class="stat-item">
<div class="stat-value">${performance.requestsPerMinute || 0}</div>
<div class="stat-label">每分钟请求</div>
</div>
</div>
`;
container.innerHTML = html;
}
// 更新失败分析内容
function updateFailuresContent(failuresData) {
updateFailuresStats(failuresData?.stats);
updateFailuresTrends(failuresData?.trends);
}
// 更新失败统计
function updateFailuresStats(stats) {
const container = document.getElementById('failuresStatsContent');
if (!stats) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无失败数据</div>';
return;
}
let html = `
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${stats.totalFailures || 0}</div>
<div class="stat-label">24H失败总数</div>
</div>
<div class="stat-item">
<div class="stat-value">${stats.avgFailureDuration || 0}ms</div>
<div class="stat-label">平均失败时长</div>
</div>
</div>
`;
// 按错误类型显示失败
if (stats.failuresByError && Object.keys(stats.failuresByError).length > 0) {
html += '<div style="margin-top: 16px;"><strong>错误类型分布:</strong></div>';
for (const [errorType, count] of Object.entries(stats.failuresByError)) {
const severity = errorType.includes('Server') ? 'error' :
errorType.includes('Rate') ? 'warning' : 'info';
html += `
<div class="error-category ${severity}">
<span>${errorType}</span>
<span>${count} 次</span>
</div>
`;
}
}
container.innerHTML = html;
}
// 更新失败趋势
function updateFailuresTrends(trends) {
const container = document.getElementById('failuresTrendsContent');
if (!trends) {
container.innerHTML = '<div style="text-align: center; color: #b0b0b0;">暂无趋势数据</div>';
return;
}
const trendIcon = trends.trendAnalysis?.isIncreasing ? '📈' : '📉';
const trendClass = trends.trendAnalysis?.isIncreasing ? 'trend-up' : 'trend-down';
const changePercentage = trends.trendAnalysis?.changePercentage || 0;
let html = `
<div class="failure-trend">
<span style="font-size: 1.5rem;">${trendIcon}</span>
<div>
<div class="${trendClass}" style="font-weight: 600;">
${changePercentage > 0 ? '+' : ''}${changePercentage}%
</div>
<div style="font-size: 0.8rem; color: #b0b0b0;">7天趋势变化</div>
</div>
</div>
<div style="margin-top: 12px; padding: 12px; background: #3a3a3a; border-radius: 8px; font-size: 0.9rem;">
<strong>建议:</strong> ${trends.trendAnalysis?.recommendation || '无建议'}
</div>
`;
// 显示每日失败数据
if (trends.dailyFailures) {
html += '<div style="margin-top: 16px;"><strong>每日失败统计:</strong></div>';
const dates = Object.keys(trends.dailyFailures).sort().slice(-3); // 显示最近3天
for (const date of dates) {
const count = trends.dailyFailures[date];
const shortDate = date.substring(5); // MM-DD
html += `
<div class="provider-item">
<span>${shortDate}</span>
<span>${count} 次失败</span>
</div>
`;
}
}
container.innerHTML = html;
}
// 显示错误
function showError(message) {
const containers = ['realtimeContent', 'providersContent', 'modelsContent', 'distributionContent', 'failuresStatsContent', 'failuresTrendsContent'];
containers.forEach(id => {
const element = document.getElementById(id);
if (element && element.classList.contains('loading')) {
element.innerHTML = `<div style="text-align: center; color: #dc2626;">${message}</div>`;
}
});
}
// 启动定时刷新
function startAutoRefresh() {
fetchStats(); // 立即加载一次
refreshTimer = setInterval(fetchStats, 5000); // 每5秒刷新
}
// 页面加载完成后启动
window.addEventListener('load', startAutoRefresh);
// 页面关闭前清理定时器
window.addEventListener('beforeunload', () => {
if (refreshTimer) {
clearInterval(refreshTimer);
}
});
</script>
</body>
</html>