Files
cunkebao_v3/Moncter/public/database-sync-dashboard.html
2026-01-05 10:16:20 +08:00

1025 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据库同步进度看板</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 12px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 8px;
padding: 16px 20px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #333;
font-size: 24px;
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.status-idle {
background: #e0e0e0;
color: #666;
}
.status-full_sync {
background: #4caf50;
color: white;
}
.status-incremental_sync {
background: #2196f3;
color: white;
}
.status-error {
background: #f44336;
color: white;
}
.refresh-info {
color: #666;
font-size: 14px;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.card {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.card-uniform {
min-height: 200px;
display: flex;
flex-direction: column;
}
.card-uniform .card-title {
flex-shrink: 0;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.card-title {
font-size: 14px;
color: #666;
margin-bottom: 12px;
font-weight: 500;
}
.card-value {
font-size: 28px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
}
.card-label {
font-size: 14px;
color: #999;
}
.progress-card {
grid-column: 1 / -1;
}
.progress-bar-container {
background: #e0e0e0;
border-radius: 8px;
height: 24px;
overflow: hidden;
margin-top: 12px;
position: relative;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4caf50 0%, #8bc34a 100%);
border-radius: 10px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
}
.progress-bar.error {
background: linear-gradient(90deg, #f44336 0%, #e91e63 100%);
}
.progress-bar.success {
background: linear-gradient(90deg, #4caf50 0%, #8bc34a 100%);
}
.progress-bar.warning {
background: linear-gradient(90deg, #ff9800 0%, #ffc107 100%);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 8px;
margin-top: 12px;
}
.stat-item {
text-align: center;
padding: 10px;
background: #f5f5f5;
border-radius: 6px;
}
.stat-value {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.stat-label {
font-size: 12px;
color: #999;
}
.time-info {
display: flex;
justify-content: space-between;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
}
.time-item {
text-align: center;
}
.time-value {
font-size: 16px;
font-weight: 600;
color: #333;
}
.time-label {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.error-card {
background: #fff3cd;
border-left: 4px solid #ffc107;
}
.error-card .card-title {
color: #856404;
}
.error-message {
color: #856404;
margin-top: 8px;
font-size: 13px;
line-height: 1.5;
}
.actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #2196f3;
color: white;
}
.btn-primary:hover {
background: #1976d2;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #f57c00;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #2196f3;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.footer {
text-align: center;
color: white;
margin-top: 12px;
font-size: 12px;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 数据库同步进度看板</h1>
<div>
<span class="status-badge" id="statusBadge">加载中...</span>
<span class="refresh-info" id="refreshInfo">最后更新: --</span>
</div>
</div>
<div id="dashboard" class="dashboard">
<div class="loading">
<div class="spinner"></div>
<div>正在加载数据...</div>
</div>
</div>
<div class="footer">
<p>数据每 3 秒自动刷新 | 数据库同步系统</p>
</div>
</div>
<script>
let refreshInterval = null;
// 状态映射
const statusMap = {
'idle': { text: '空闲', class: 'status-idle' },
'full_sync': { text: '全量同步中', class: 'status-full_sync' },
'incremental_sync': { text: '增量同步中', class: 'status-incremental_sync' },
'error': { text: '错误', class: 'status-error' }
};
// 格式化数字
function formatNumber(num) {
if (num === null || num === undefined) return '0';
if (num >= 1000000) {
return (num / 1000000).toFixed(2) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(2) + 'K';
}
return num.toString();
}
// 格式化时间
function formatTime(seconds) {
if (!seconds || seconds === null) return '--';
if (seconds < 60) {
return Math.round(seconds) + '秒';
}
if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
return mins + '分' + secs + '秒';
}
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return hours + '小时' + mins + '分钟';
}
// 格式化字节为可读单位
function formatBytes(bytes) {
if (!bytes || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
let value = bytes;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i++;
}
return value.toFixed(2) + ' ' + units[i];
}
// 加载进度数据
async function loadProgress() {
try {
const response = await fetch('/api/database-sync/progress');
// 检查HTTP状态码
if (!response.ok) {
throw new Error('HTTP错误: ' + response.status);
}
const result = await response.json();
// API返回格式: { code: 0, msg: '...', data: {...} }
// code === 0 表示成功
if (result.code === 0 || result.code === 200) {
updateDashboard(result.data || {});
updateRefreshTime();
} else {
console.error('API返回错误:', result);
showError('获取进度失败: ' + (result.msg || result.message || '未知错误'));
}
} catch (error) {
console.error('加载进度失败:', error);
const errorMsg = error.message || '网络连接失败';
showError('网络错误: ' + errorMsg + '。请检查服务器是否运行,或稍后重试。');
}
}
// 更新看板
function updateDashboard(data) {
const dashboard = document.getElementById('dashboard');
const status = data.status || 'idle';
const statusInfo = statusMap[status] || { text: status, class: 'status-idle' };
// 更新状态徽章
const statusBadge = document.getElementById('statusBadge');
statusBadge.textContent = statusInfo.text;
statusBadge.className = 'status-badge ' + statusInfo.class;
// 构建 HTML
let html = '';
// 进度卡片
const progressPercent = Math.max(0, Math.min(100, data.progress_percent || 0));
const bytesInfo = data.bytes || {};
const bytesProcessed = bytesInfo.processed || 0;
const bytesTotal = bytesInfo.total || 0;
// 计算进度条颜色(根据进度百分比)
let progressBarClass = '';
if (status === 'error') {
progressBarClass = 'error';
} else if (progressPercent >= 90) {
progressBarClass = 'success';
} else if (progressPercent >= 50) {
progressBarClass = 'warning';
}
html += `
<div class="card progress-card">
<div class="card-title">同步进度</div>
<div class="card-value">${progressPercent.toFixed(2)}%</div>
<div class="progress-bar-container">
<div class="progress-bar ${progressBarClass}" style="width: ${progressPercent}%">
${progressPercent > 5 ? progressPercent.toFixed(1) + '%' : ''}
</div>
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value">${data.databases?.completed || 0}/${data.databases?.total || 0}</div>
<div class="stat-label">数据库</div>
</div>
<div class="stat-item">
<div class="stat-value">${data.collections?.completed || 0}/${data.collections?.total || 0}</div>
<div class="stat-label">集合</div>
</div>
<div class="stat-item">
<div class="stat-value">${formatNumber(data.documents?.processed || 0)}/${formatNumber(data.documents?.total || 0)}</div>
<div class="stat-label">文档</div>
</div>
<div class="stat-item">
<div class="stat-value">${bytesTotal > 0 ? formatBytes(bytesProcessed) + ' / ' + formatBytes(bytesTotal) : '--'}</div>
<div class="stat-label">数据量</div>
</div>
</div>
<div class="time-info">
<div class="time-item">
<div class="time-value">${data.current_database || '无'}</div>
<div class="time-label">当前数据库</div>
</div>
<div class="time-item">
<div class="time-value">${data.current_collection || '无'}</div>
<div class="time-label">当前集合</div>
</div>
<div class="time-item">
<div class="time-value">${formatTime(data.time?.elapsed_seconds)}</div>
<div class="time-label">已用时间</div>
</div>
<div class="time-item">
<div class="time-value">${formatTime(data.time?.estimated_remaining_seconds)}</div>
<div class="time-label">预计剩余</div>
</div>
</div>
</div>
`;
// 统计卡片
const stats = data.stats || {};
html += `
<div class="card card-uniform">
<div class="card-title">📈 统计信息</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" style="color: #4caf50">${formatNumber(stats.documents_inserted || 0)}</div>
<div class="stat-label">已插入</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #2196f3">${formatNumber(stats.documents_updated || 0)}</div>
<div class="stat-label">已更新</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #ff9800">${formatNumber(stats.documents_deleted || 0)}</div>
<div class="stat-label">已删除</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: ${(stats.errors || 0) > 0 ? '#f44336' : '#4caf50'}">${stats.errors || 0}</div>
<div class="stat-label">错误数</div>
</div>
</div>
</div>
`;
// 时间信息卡片
html += `
<div class="card card-uniform">
<div class="card-title">⏰ 时间信息</div>
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">开始时间</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${data.time?.start_time || '未开始'}
</div>
</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">已用时间</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatTime(data.time?.elapsed_seconds)}
</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">预计剩余时间</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatTime(data.time?.estimated_remaining_seconds) || '计算中...'}
</div>
</div>
</div>
</div>
`;
// 性能指标卡片(同步速度、吞吐量等)
const elapsedSeconds = data.time?.elapsed_seconds || 0;
const documentsProcessed = data.documents?.processed || 0;
// bytesProcessed 和 bytesTotal 已在上面声明第411-412行直接使用
// 计算同步速度
let docsPerSecond = 0;
let mbPerSecond = 0;
if (elapsedSeconds > 0) {
docsPerSecond = documentsProcessed / elapsedSeconds;
mbPerSecond = bytesProcessed / elapsedSeconds / 1024 / 1024;
}
// 计算平均速度(最近一段时间)
const recentSpeed = docsPerSecond > 0 ? docsPerSecond : 0;
html += `
<div class="card card-uniform">
<div class="card-title">⚡ 性能指标</div>
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">文档同步速度</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatNumber(Math.round(recentSpeed))} 文档/秒
</div>
</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">数据吞吐量</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${mbPerSecond > 0 ? mbPerSecond.toFixed(2) : '0.00'} MB/秒
</div>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">总处理量</div>
<div style="font-size: 15px; font-weight: 600; color: #333;">
${formatNumber(documentsProcessed)} 文档 / ${formatBytes(bytesProcessed)}
</div>
</div>
</div>
</div>
`;
// 数据库连接状态卡片(移动到性能指标之后)
if (data.connection_status) {
const connStatus = data.connection_status;
let connHtml = '';
// 源数据库连接状态
const sourceConnected = connStatus.source?.connected || false;
const sourceHost = connStatus.source?.host || '未知';
const sourcePort = connStatus.source?.port || 0;
const sourceError = connStatus.source?.error || null;
// 目标数据库连接状态
const targetConnected = connStatus.target?.connected || false;
const targetHost = connStatus.target?.host || '未知';
const targetPort = connStatus.target?.port || 0;
const targetError = connStatus.target?.error || null;
connHtml += `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin-top: 12px;">
<div style="padding: 10px; background: ${sourceConnected ? '#e8f5e9' : '#ffebee'}; border-radius: 6px; border-left: 3px solid ${sourceConnected ? '#4caf50' : '#f44336'};">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
<span style="color: ${sourceConnected ? '#4caf50' : '#f44336'}; margin-right: 6px;">${sourceConnected ? '●' : '○'}</span>
源数据库
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div><strong>地址:</strong> ${sourceHost}:${sourcePort}</div>
<div style="margin-top: 4px;"><strong>状态:</strong>
<span style="color: ${sourceConnected ? '#4caf50' : '#f44336'}; font-weight: 600;">
${sourceConnected ? '已连接' : '未连接'}
</span>
</div>
${sourceError ? `<div style="margin-top: 4px; font-size: 10px; color: #f44336;">错误: ${sourceError.substring(0, 50)}${sourceError.length > 50 ? '...' : ''}</div>` : ''}
</div>
</div>
<div style="padding: 10px; background: ${targetConnected ? '#e8f5e9' : '#ffebee'}; border-radius: 6px; border-left: 3px solid ${targetConnected ? '#4caf50' : '#f44336'};">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
<span style="color: ${targetConnected ? '#4caf50' : '#f44336'}; margin-right: 6px;">${targetConnected ? '●' : '○'}</span>
目标数据库
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div><strong>地址:</strong> ${targetHost}:${targetPort}</div>
<div style="margin-top: 4px;"><strong>状态:</strong>
<span style="color: ${targetConnected ? '#4caf50' : '#f44336'}; font-weight: 600;">
${targetConnected ? '已连接' : '未连接'}
</span>
</div>
${targetError ? `<div style="margin-top: 4px; font-size: 10px; color: #f44336;">错误: ${targetError.substring(0, 50)}${targetError.length > 50 ? '...' : ''}</div>` : ''}
</div>
</div>
</div>
`;
// 总体连接状态
const allConnected = sourceConnected && targetConnected;
const overallBgColor = allConnected ? '#e8f5e9' : '#fff3cd';
const overallBorderColor = allConnected ? '#4caf50' : '#ffc107';
const overallTextColor = allConnected ? '#2e7d32' : '#856404';
const overallStatusText = allConnected ? '✓ 所有数据库连接正常' : '⚠ 部分数据库连接异常';
connHtml += `
<div style="margin-top: 12px; padding: 10px; background: ${overallBgColor}; border-radius: 6px; border-left: 3px solid ${overallBorderColor};">
<div style="font-size: 12px; color: ${overallTextColor};">
<strong>总体状态:</strong> ${overallStatusText}
</div>
</div>
`;
html += `
<div class="card card-uniform">
<div class="card-title">🔌 数据库连接状态</div>
${connHtml}
</div>
`;
}
// 数据库列表卡片(已完成、处理中、待同步)
if (data.database_list) {
const dbList = data.database_list;
let dbListHtml = '';
// 处理中的数据库
if (dbList.processing && dbList.processing.length > 0) {
dbListHtml += `
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
处理中 (${dbList.processing.length})
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
`;
dbList.processing.forEach(db => {
dbListHtml += `
<span style="padding: 4px 8px; background: #e3f2fd; color: #1976d2; border-radius: 4px; font-size: 11px;">
${db.name}
</span>
`;
});
dbListHtml += `</div></div>`;
}
// 已完成的数据库
if (dbList.completed && dbList.completed.length > 0) {
dbListHtml += `
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
已完成 (${dbList.completed.length})
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px; max-height: 120px; overflow-y: auto;">
`;
dbList.completed.slice(0, 20).forEach(db => {
dbListHtml += `
<span style="padding: 4px 8px; background: #e8f5e9; color: #2e7d32; border-radius: 4px; font-size: 11px;">
${db.name}
</span>
`;
});
if (dbList.completed.length > 20) {
dbListHtml += `<span style="padding: 4px 8px; color: #999; font-size: 11px;">...还有 ${dbList.completed.length - 20} 个</span>`;
}
dbListHtml += `</div></div>`;
}
// 待同步的数据库
if (dbList.pending && dbList.pending.length > 0) {
dbListHtml += `
<div>
<div style="font-size: 12px; color: #666; margin-bottom: 6px; font-weight: 600;">
待同步 (${dbList.pending.length})
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px; max-height: 120px; overflow-y: auto;">
`;
dbList.pending.slice(0, 20).forEach(db => {
dbListHtml += `
<span style="padding: 4px 8px; background: #fff3e0; color: #e65100; border-radius: 4px; font-size: 11px;">
${db.name}
</span>
`;
});
if (dbList.pending.length > 20) {
dbListHtml += `<span style="padding: 4px 8px; color: #999; font-size: 11px;">...还有 ${dbList.pending.length - 20} 个</span>`;
}
dbListHtml += `</div></div>`;
}
if (dbListHtml) {
html += `
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title">📋 数据库列表</div>
<div style="margin-top: 12px;">
${dbListHtml}
</div>
</div>
`;
}
}
// 错误信息卡片
if (status === 'error') {
const errorDb = data.error_database || data.current_database || '未知';
const errorCollection = data.current_collection || (data.last_error?.collection) || '未知';
const errorMessage = data.last_error?.message || '未知错误';
const errorTime = data.last_error?.time || '未知';
const errorFile = data.last_error?.file || '';
const errorLine = data.last_error?.line || '';
html += `
<div class="card error-card">
<div class="card-title">⚠️ 错误信息</div>
<div class="error-message">
<div><strong>数据库:</strong> ${errorDb}</div>
<div><strong>集合:</strong> ${errorCollection}</div>
<div><strong>错误:</strong> ${errorMessage}</div>
<div><strong>时间:</strong> ${errorTime}</div>
${errorFile ? `<div style="margin-top: 8px; font-size: 12px; color: #666;"><strong>位置:</strong> ${errorFile}${errorLine ? ':' + errorLine : ''}</div>` : ''}
</div>
<div class="actions">
<button class="btn btn-warning" onclick="skipError()">跳过错误数据库</button>
</div>
</div>
`;
}
// Worker 状态卡片(多进程状态)
if (data.worker_status) {
const workerStatus = data.worker_status;
let workerHtml = '';
if (workerStatus.workers && workerStatus.workers.length > 0) {
// 显示每个 Worker 的详细信息
workerHtml += `
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px; font-size: 13px; color: #666;">
总 Worker 数: <strong>${workerStatus.total_workers || 0}</strong> |
活跃 Worker 数: <strong style="color: #4caf50;">${workerStatus.active_workers || 0}</strong>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; margin-top: 10px;">
`;
// 按 Worker ID 排序
const sortedWorkers = Object.values(workerStatus.workers).sort((a, b) => a.worker_id - b.worker_id);
sortedWorkers.forEach(worker => {
const dbCount = worker.databases ? worker.databases.length : 0;
workerHtml += `
<div style="padding: 10px; background: #f5f5f5; border-radius: 6px; border-left: 3px solid #4caf50;">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
Worker #${worker.worker_id}
<span style="font-size: 11px; color: #4caf50; margin-left: 6px;">● 运行中</span>
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div>数据库: <strong>${dbCount}</strong> 个</div>
<div>集合: <strong>${worker.collections || 0}</strong> 个</div>
<div>文档: <strong>${formatNumber(worker.documents_processed || 0)}</strong></div>
${dbCount > 0 ? `<div style="margin-top: 6px; font-size: 10px; color: #999;">${worker.databases.slice(0, 3).join(', ')}${dbCount > 3 ? '...' : ''}</div>` : ''}
</div>
</div>
`;
});
workerHtml += `</div>`;
} else if (workerStatus.workers && workerStatus.workers.length > 0) {
// 显示所有 Worker包括等待状态的
workerHtml += `
<div style="margin-top: 12px;">
<div style="margin-bottom: 10px; font-size: 13px; color: #666;">
总 Worker 数: <strong>${workerStatus.total_workers || 0}</strong> |
活跃 Worker 数: <strong style="color: #4caf50;">${workerStatus.active_workers || 0}</strong>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 8px; margin-top: 10px;">
`;
// 按 Worker ID 排序
const sortedWorkers = Object.values(workerStatus.workers).sort((a, b) => a.worker_id - b.worker_id);
sortedWorkers.forEach(worker => {
const dbCount = worker.databases ? worker.databases.length : 0;
const isActive = worker.status === 'active' && dbCount > 0;
const statusColor = isActive ? '#4caf50' : '#999';
const statusText = isActive ? '● 运行中' : '○ 等待中';
const borderColor = isActive ? '#4caf50' : '#e0e0e0';
workerHtml += `
<div style="padding: 10px; background: #f5f5f5; border-radius: 6px; border-left: 3px solid ${borderColor};">
<div style="font-weight: 600; color: #333; margin-bottom: 6px; font-size: 13px;">
Worker #${worker.worker_id}
<span style="font-size: 11px; color: ${statusColor}; margin-left: 6px;">${statusText}</span>
</div>
<div style="font-size: 11px; color: #666; margin-top: 6px;">
<div>数据库: <strong>${dbCount}</strong> 个</div>
<div>集合: <strong>${worker.collections || 0}</strong> 个</div>
<div>文档: <strong>${formatNumber(worker.documents_processed || 0)}</strong></div>
${dbCount > 0 ? `<div style="margin-top: 6px; font-size: 10px; color: #999;">${worker.databases.slice(0, 3).join(', ')}${dbCount > 3 ? '...' : ''}</div>` : '<div style="margin-top: 6px; font-size: 10px; color: #999;">等待分配任务...</div>'}
</div>
</div>
`;
});
workerHtml += `</div>`;
if (workerStatus.message) {
workerHtml += `
<div style="margin-top: 12px; padding: 10px; background: #fff3cd; border-radius: 6px; border-left: 3px solid #ffc107;">
<div style="color: #856404; font-size: 12px;">
${workerStatus.message}
</div>
</div>
`;
}
} else {
// 完全没有 Worker 信息时的提示
workerHtml += `
<div style="margin-top: 12px; padding: 12px; background: #fff3cd; border-radius: 6px; border-left: 3px solid #ffc107;">
<div style="color: #856404; font-size: 13px;">
${workerStatus.message || '暂无 Worker 活动信息'}
</div>
${workerStatus.total_workers > 0 ? `<div style="margin-top: 6px; font-size: 11px; color: #666;">配置的 Worker 数量: ${workerStatus.total_workers}</div>` : ''}
</div>
`;
}
if (data.progress_file_last_modified) {
const ageSeconds = data.progress_file_age_seconds || 0;
const ageText = ageSeconds < 60 ? `${ageSeconds}秒前` :
ageSeconds < 3600 ? `${Math.floor(ageSeconds / 60)}分钟前` :
`${Math.floor(ageSeconds / 3600)}小时前`;
workerHtml += `
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">
<div style="font-size: 11px; color: #999;">
进度文件最后修改: ${data.progress_file_last_modified} (${ageText})
</div>
</div>
`;
}
if (data.hint) {
workerHtml += `
<div style="margin-top: 10px; padding: 10px; background: #e3f2fd; border-radius: 6px; border-left: 3px solid #2196f3;">
<div style="font-size: 11px; color: #1976d2;">
💡 ${data.hint}
</div>
</div>
`;
}
html += `
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title">🔧 Worker 进程状态</div>
${workerHtml}
</div>
`;
}
// 操作按钮
html += `
<div class="card">
<div class="card-title">⚙️ 操作</div>
<div class="actions">
<button class="btn btn-primary" onclick="loadProgress()">🔄 刷新</button>
<button class="btn btn-danger" onclick="resetProgress()">🗑️ 重置进度</button>
${status === 'error' ? '<button class="btn btn-warning" onclick="skipError()">⏭️ 跳过错误</button>' : ''}
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e0e0e0;">
<div style="font-size: 11px; color: #999; line-height: 1.5;">
<div>💡 提示:</div>
<div style="margin-top: 2px;">• 页面每 3 秒自动刷新</div>
<div>• 重置进度将清除所有同步进度信息</div>
<div>• 跳过错误将标记当前错误数据库为已完成</div>
</div>
</div>
</div>
`;
dashboard.innerHTML = html;
}
// 显示错误
function showError(message) {
const dashboard = document.getElementById('dashboard');
dashboard.innerHTML = `
<div class="card" style="grid-column: 1 / -1;">
<div class="card-title" style="color: #f44336;">❌ 错误</div>
<div style="color: #666; margin-top: 16px;">${message}</div>
<div class="actions" style="margin-top: 20px;">
<button class="btn btn-primary" onclick="loadProgress()">重试</button>
</div>
</div>
`;
}
// 更新刷新时间
function updateRefreshTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('zh-CN');
document.getElementById('refreshInfo').textContent = '最后更新: ' + timeStr;
}
// 跳过错误数据库
async function skipError() {
if (!confirm('确定要跳过当前错误数据库吗?')) {
return;
}
try {
const response = await fetch('/api/database-sync/skip-error', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('HTTP错误: ' + response.status);
}
const result = await response.json();
// code === 0 表示成功
if (result.code === 0 || result.code === 200) {
alert('已跳过错误数据库');
loadProgress();
} else {
alert('操作失败: ' + (result.msg || result.message || '未知错误'));
}
} catch (error) {
alert('网络错误: ' + error.message);
}
}
// 重置进度
async function resetProgress() {
if (!confirm('确定要重置同步进度吗?这将清除所有进度信息。')) {
return;
}
try {
const response = await fetch('/api/database-sync/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('HTTP错误: ' + response.status);
}
const result = await response.json();
// code === 0 表示成功
if (result.code === 0 || result.code === 200) {
alert('进度已重置');
loadProgress();
} else {
alert('操作失败: ' + (result.msg || result.message || '未知错误'));
}
} catch (error) {
alert('网络错误: ' + error.message);
}
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 立即加载一次
loadProgress();
// 每3秒刷新一次
refreshInterval = setInterval(loadProgress, 3000);
});
// 页面可见性变化时暂停/恢复刷新
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
} else {
if (!refreshInterval) {
loadProgress();
refreshInterval = setInterval(loadProgress, 3000);
}
}
});
</script>
</body>
</html>