<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapReduce Progress Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 10px;
padding: 20px 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.job-id {
color: #667eea;
font-weight: 600;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.metric-card {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.metric-card:hover {
transform: translateY(-5px);
}
.metric-label {
color: #666;
font-size: 14px;
margin-bottom: 5px;
}
.metric-value {
color: #333;
font-size: 32px;
font-weight: bold;
}
.metric-unit {
color: #999;
font-size: 14px;
margin-left: 5px;
}
.overall-progress {
background: white;
border-radius: 10px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.progress-bar-container {
width: 100%;
height: 40px;
background: #f0f0f0;
border-radius: 20px;
overflow: hidden;
position: relative;
margin-top: 15px;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
border-radius: 20px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 15px;
color: white;
font-weight: bold;
font-size: 16px;
}
.agents-section {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.agents-section h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
}
.agent-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
}
.agent-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
background: #fafafa;
transition: all 0.3s ease;
}
.agent-card:hover {
background: white;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.agent-id {
font-size: 12px;
color: #666;
margin-bottom: 5px;
font-family: monospace;
}
.agent-state {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
margin-bottom: 10px;
}
.state-running {
background: #e3f2fd;
color: #1976d2;
}
.state-completed {
background: #e8f5e9;
color: #388e3c;
}
.state-failed {
background: #ffebee;
color: #c62828;
}
.state-queued {
background: #f5f5f5;
color: #616161;
}
.agent-progress {
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
margin-top: 5px;
}
.agent-progress-fill {
height: 100%;
background: #4CAF50;
transition: width 0.3s ease;
}
.connection-status {
position: fixed;
bottom: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.connected {
background: #4CAF50;
color: white;
}
.disconnected {
background: #f44336;
color: white;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
margin-left: 5px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.metrics-grid {
grid-template-columns: 1fr;
}
.agent-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>MapReduce Progress Dashboard</h1>
<div>Job ID: <span id="job-id" class="job-id">Loading...</span></div>
</div>
<div class="overall-progress">
<h2>Overall Progress</h2>
<div class="progress-bar-container">
<div id="overall-progress-bar" class="progress-bar" style="width: 0%">
<span id="progress-text">0%</span>
</div>
</div>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-label">Completed Items</div>
<div class="metric-value">
<span id="completed-items">0</span>
<span class="metric-unit">/ <span id="total-items">0</span></span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">Throughput</div>
<div class="metric-value">
<span id="throughput">0.00</span>
<span class="metric-unit">items/sec</span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">Success Rate</div>
<div class="metric-value">
<span id="success-rate">100</span>
<span class="metric-unit">%</span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">Active Agents</div>
<div class="metric-value">
<span id="active-agents">0</span>
<span class="metric-unit">agents</span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">Failed Items</div>
<div class="metric-value">
<span id="failed-items">0</span>
<span class="metric-unit">items</span>
</div>
</div>
<div class="metric-card">
<div class="metric-label">ETC</div>
<div class="metric-value" style="font-size: 18px;">
<span id="etc">Calculating...</span>
</div>
</div>
</div>
<div class="agents-section">
<h2>Agent Status <span id="agent-count"></span></h2>
<div id="agent-grid" class="agent-grid">
</div>
</div>
</div>
<div id="connection-status" class="connection-status disconnected">
Connecting... <span class="spinner"></span>
</div>
<script>
let ws = null;
let reconnectInterval = null;
let agents = {};
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
updateConnectionStatus(true);
if (reconnectInterval) {
clearInterval(reconnectInterval);
reconnectInterval = null;
}
fetchInitialData();
};
ws.onmessage = (event) => {
try {
const update = JSON.parse(event.data);
handleUpdate(update);
} catch (e) {
console.error('Failed to parse WebSocket message:', e);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('WebSocket disconnected');
updateConnectionStatus(false);
if (!reconnectInterval) {
reconnectInterval = setInterval(connectWebSocket, 2000);
}
};
}
function updateConnectionStatus(connected) {
const status = document.getElementById('connection-status');
if (connected) {
status.className = 'connection-status connected';
status.innerHTML = 'Connected';
} else {
status.className = 'connection-status disconnected';
status.innerHTML = 'Disconnected <span class="spinner"></span>';
}
}
function handleUpdate(update) {
switch (update.update_type) {
case 'agent_progress':
updateAgent(update.data);
break;
case 'metrics_update':
updateMetrics(update.data);
break;
case 'job_completed':
handleJobCompleted(update.data);
break;
case 'error':
console.error('Error update:', update.data);
break;
}
}
function updateAgent(data) {
const agentId = data.agent_id;
const progress = data.progress;
agents[agentId] = progress;
renderAgents();
}
function updateMetrics(metrics) {
document.getElementById('completed-items').textContent = metrics.completed_items || 0;
document.getElementById('failed-items').textContent = metrics.failed_items || 0;
document.getElementById('active-agents').textContent = metrics.active_agents || 0;
document.getElementById('throughput').textContent = (metrics.throughput_average || 0).toFixed(2);
document.getElementById('success-rate').textContent = (metrics.success_rate || 100).toFixed(1);
if (metrics.estimated_completion) {
const etc = new Date(metrics.estimated_completion);
const now = new Date();
const remaining = Math.max(0, (etc - now) / 1000);
document.getElementById('etc').textContent = formatDuration(remaining);
} else {
document.getElementById('etc').textContent = 'Calculating...';
}
const total = metrics.completed_items + metrics.failed_items + metrics.pending_items;
const completed = metrics.completed_items + metrics.failed_items;
const progress = total > 0 ? (completed / total * 100) : 0;
const progressBar = document.getElementById('overall-progress-bar');
progressBar.style.width = progress + '%';
document.getElementById('progress-text').textContent = progress.toFixed(1) + '%';
}
function renderAgents() {
const grid = document.getElementById('agent-grid');
grid.innerHTML = '';
const agentArray = Object.entries(agents).slice(0, 50); document.getElementById('agent-count').textContent = `(${Object.keys(agents).length} total)`;
agentArray.forEach(([id, agent]) => {
const card = document.createElement('div');
card.className = 'agent-card';
const stateClass = getStateClass(agent.state);
const stateText = getStateText(agent.state);
card.innerHTML = `
<div class="agent-id">${id.substring(0, 8)}...</div>
<span class="agent-state ${stateClass}">${stateText}</span>
<div>Step: ${agent.current_step}</div>
<div class="agent-progress">
<div class="agent-progress-fill" style="width: ${agent.progress_percentage}%"></div>
</div>
`;
grid.appendChild(card);
});
}
function getStateClass(state) {
switch (state.type) {
case 'Running': return 'state-running';
case 'Completed': return 'state-completed';
case 'Failed': return 'state-failed';
case 'Queued': return 'state-queued';
default: return 'state-queued';
}
}
function getStateText(state) {
switch (state.type) {
case 'Running': return `Running: ${state.step}`;
case 'Completed': return 'Completed';
case 'Failed': return 'Failed';
case 'Queued': return 'Queued';
default: return state.type;
}
}
function formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
function handleJobCompleted(data) {
const status = document.getElementById('connection-status');
status.className = 'connection-status connected';
status.innerHTML = '✅ Job Completed!';
}
async function fetchInitialData() {
try {
const progressResponse = await fetch('/api/progress');
const progressData = await progressResponse.json();
document.getElementById('job-id').textContent = progressData.job_id;
document.getElementById('total-items').textContent = progressData.total_items;
updateMetrics(progressData.metrics);
const agentsResponse = await fetch('/api/agents');
const agentsData = await agentsResponse.json();
agents = agentsData.agents || {};
renderAgents();
} catch (e) {
console.error('Failed to fetch initial data:', e);
}
}
connectWebSocket();
setInterval(fetchInitialData, 5000);
</script>
</body>
</html>