You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							338 lines
						
					
					
						
							18 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							338 lines
						
					
					
						
							18 KiB
						
					
					
				
								package app
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
								    "fmt"
							 | 
						|
								    "github.com/seaweedfs/seaweedfs/weed/admin/dash"
							 | 
						|
								    "time"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								templ MaintenanceWorkers(data *dash.MaintenanceWorkersData) {
							 | 
						|
								    <div class="container-fluid">
							 | 
						|
								        <div class="row">
							 | 
						|
								            <div class="col-12">
							 | 
						|
								                <div class="d-flex justify-content-between align-items-center mb-4">
							 | 
						|
								                    <div>
							 | 
						|
								                        <h1 class="h3 mb-0 text-gray-800">Maintenance Workers</h1>
							 | 
						|
								                        <p class="text-muted">Monitor and manage maintenance workers</p>
							 | 
						|
								                    </div>
							 | 
						|
								                    <div class="text-end">
							 | 
						|
								                        <small class="text-muted">Last updated: { data.LastUpdated.Format("2006-01-02 15:04:05") }</small>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								        </div>
							 | 
						|
								
							 | 
						|
								        <!-- Summary Cards -->
							 | 
						|
								        <div class="row mb-4">
							 | 
						|
								            <div class="col-xl-3 col-md-6 mb-4">
							 | 
						|
								                <div class="card border-left-primary shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
							 | 
						|
								                                    Total Workers
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">{ fmt.Sprintf("%d", len(data.Workers)) }</div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-users fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-3 col-md-6 mb-4">
							 | 
						|
								                <div class="card border-left-success shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
							 | 
						|
								                                    Active Workers
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    { fmt.Sprintf("%d", data.ActiveWorkers) }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-check-circle fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-3 col-md-6 mb-4">
							 | 
						|
								                <div class="card border-left-info shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-info text-uppercase mb-1">
							 | 
						|
								                                    Busy Workers
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    { fmt.Sprintf("%d", data.BusyWorkers) }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-spinner fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div class="col-xl-3 col-md-6 mb-4">
							 | 
						|
								                <div class="card border-left-warning shadow h-100 py-2">
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        <div class="row no-gutters align-items-center">
							 | 
						|
								                            <div class="col mr-2">
							 | 
						|
								                                <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
							 | 
						|
								                                    Total Load
							 | 
						|
								                                </div>
							 | 
						|
								                                <div class="h5 mb-0 font-weight-bold text-gray-800">
							 | 
						|
								                                    { fmt.Sprintf("%d", data.TotalLoad) }
							 | 
						|
								                                </div>
							 | 
						|
								                            </div>
							 | 
						|
								                            <div class="col-auto">
							 | 
						|
								                                <i class="fas fa-tasks fa-2x text-gray-300"></i>
							 | 
						|
								                            </div>
							 | 
						|
								                        </div>
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								        </div>
							 | 
						|
								
							 | 
						|
								        <!-- Workers Table -->
							 | 
						|
								        <div class="row">
							 | 
						|
								            <div class="col-12">
							 | 
						|
								                <div class="card shadow mb-4">
							 | 
						|
								                    <div class="card-header py-3">
							 | 
						|
								                        <h6 class="m-0 font-weight-bold text-primary">Worker Details</h6>
							 | 
						|
								                    </div>
							 | 
						|
								                    <div class="card-body">
							 | 
						|
								                        if len(data.Workers) == 0 {
							 | 
						|
								                            <div class="text-center py-4">
							 | 
						|
								                                <i class="fas fa-users fa-3x text-gray-300 mb-3"></i>
							 | 
						|
								                                <h5 class="text-gray-600">No Workers Found</h5>
							 | 
						|
								                            <p class="text-muted">No maintenance workers are currently registered.</p>
							 | 
						|
								                            <div class="alert alert-info mt-3">
							 | 
						|
								                                <strong>Tip:</strong> To start a worker, run:
							 | 
						|
								                                <br><code>weed worker -admin=<admin_server> -capabilities=vacuum,ec,replication</code>
							 | 
						|
								                            </div>
							 | 
						|
								                            </div>
							 | 
						|
								                        } else {
							 | 
						|
								                            <div class="table-responsive">
							 | 
						|
								                                <table class="table table-bordered table-hover" id="workersTable">
							 | 
						|
								                                    <thead class="table-light">
							 | 
						|
								                                        <tr>
							 | 
						|
								                                            <th>Worker ID</th>
							 | 
						|
								                                            <th>Address</th>
							 | 
						|
								                                            <th>Status</th>
							 | 
						|
								                                            <th>Capabilities</th>
							 | 
						|
								                                            <th>Load</th>
							 | 
						|
								                                            <th>Current Tasks</th>
							 | 
						|
								                                            <th>Performance</th>
							 | 
						|
								                                            <th>Last Heartbeat</th>
							 | 
						|
								                                            <th>Actions</th>
							 | 
						|
								                                        </tr>
							 | 
						|
								                                    </thead>
							 | 
						|
								                                    <tbody>
							 | 
						|
								                                        for _, worker := range data.Workers {
							 | 
						|
								                                            <tr>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    <code>{ worker.Worker.ID }</code>
							 | 
						|
								                                                </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    <code>{ worker.Worker.Address }</code>
							 | 
						|
								                                                </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    if worker.Worker.Status == "active" {
							 | 
						|
								                                                        <span class="badge bg-success">Active</span>
							 | 
						|
								                                                    } else if worker.Worker.Status == "busy" {
							 | 
						|
								                                                        <span class="badge bg-warning">Busy</span>
							 | 
						|
								                                                    } else {
							 | 
						|
								                                                        <span class="badge bg-danger">Inactive</span>
							 | 
						|
								                                                    }
							 | 
						|
								                                                </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    <div class="d-flex flex-wrap gap-1">
							 | 
						|
								                                                        for _, capability := range worker.Worker.Capabilities {
							 | 
						|
								                                                            <span class="badge bg-secondary rounded-pill">{ string(capability) }</span>
							 | 
						|
								                                                        }
							 | 
						|
								                                                    </div>
							 | 
						|
								                                                </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    <div class="progress" style="height: 20px;">
							 | 
						|
								                                                        if worker.Worker.MaxConcurrent > 0 {
							 | 
						|
								                                                            <div class="progress-bar" role="progressbar" 
							 | 
						|
								                                                                 style={ fmt.Sprintf("width: %d%%", (worker.Worker.CurrentLoad*100)/worker.Worker.MaxConcurrent) }
							 | 
						|
								                                                                 aria-valuenow={ fmt.Sprintf("%d", worker.Worker.CurrentLoad) } 
							 | 
						|
								                                                                 aria-valuemin="0" 
							 | 
						|
								                                                                 aria-valuemax={ fmt.Sprintf("%d", worker.Worker.MaxConcurrent) }>
							 | 
						|
								                                                                { fmt.Sprintf("%d/%d", worker.Worker.CurrentLoad, worker.Worker.MaxConcurrent) }
							 | 
						|
								                                                            </div>
							 | 
						|
								                                                        } else {
							 | 
						|
								                                                            <div class="progress-bar" role="progressbar" style="width: 0%">0/0</div>
							 | 
						|
								                                                        }
							 | 
						|
								                                                    </div>
							 | 
						|
								                                                </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    { fmt.Sprintf("%d", len(worker.CurrentTasks)) }
							 | 
						|
								                                                </td>
							 | 
						|
								                                            <td>
							 | 
						|
								                                                <small>
							 | 
						|
								                                                    <div>Completed: { fmt.Sprintf("%d", worker.Performance.TasksCompleted) }</div>
							 | 
						|
								                                                    <div>Failed: { fmt.Sprintf("%d", worker.Performance.TasksFailed) }</div>
							 | 
						|
								                                                    <div>Success Rate: { fmt.Sprintf("%.1f%%", worker.Performance.SuccessRate) }</div>
							 | 
						|
								                                                </small>
							 | 
						|
								                                            </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    if time.Since(worker.Worker.LastHeartbeat) < 2*time.Minute {
							 | 
						|
								                                                        <span class="text-success">
							 | 
						|
								                                                            <i class="fas fa-heartbeat"></i>
							 | 
						|
								                                                            { worker.Worker.LastHeartbeat.Format("15:04:05") }
							 | 
						|
								                                                        </span>
							 | 
						|
								                                                    } else {
							 | 
						|
								                                                        <span class="text-danger">
							 | 
						|
								                                                            <i class="fas fa-exclamation-triangle"></i>
							 | 
						|
								                                                            { worker.Worker.LastHeartbeat.Format("15:04:05") }
							 | 
						|
								                                                        </span>
							 | 
						|
								                                                    }
							 | 
						|
								                                                </td>
							 | 
						|
								                                                <td>
							 | 
						|
								                                                    <div class="btn-group btn-group-sm" role="group">
							 | 
						|
								                                                        <button type="button" class="btn btn-outline-info" onclick="showWorkerDetails(event)" data-worker-id={ worker.Worker.ID }>
							 | 
						|
								                                                            <i class="fas fa-info-circle"></i>
							 | 
						|
								                                                        </button>
							 | 
						|
								                                                        if worker.Worker.Status == "active" {
							 | 
						|
								                                                            <button type="button" class="btn btn-outline-warning" onclick="pauseWorker(event)" data-worker-id={ worker.Worker.ID }>
							 | 
						|
								                                                                <i class="fas fa-pause"></i>
							 | 
						|
								                                                            </button>
							 | 
						|
								                                                        }
							 | 
						|
								                                                    </div>
							 | 
						|
								                                                </td>
							 | 
						|
								                                            </tr>
							 | 
						|
								                                        }
							 | 
						|
								                                    </tbody>
							 | 
						|
								                                </table>
							 | 
						|
								                            </div>
							 | 
						|
								                        }
							 | 
						|
								                    </div>
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								        </div>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <!-- Worker Details Modal -->
							 | 
						|
								    <div class="modal fade" id="workerDetailsModal" tabindex="-1" aria-labelledby="workerDetailsModalLabel" aria-hidden="true">
							 | 
						|
								        <div class="modal-dialog modal-lg">
							 | 
						|
								            <div class="modal-content">
							 | 
						|
								                <div class="modal-header">
							 | 
						|
								                    <h5 class="modal-title" id="workerDetailsModalLabel">Worker Details</h5>
							 | 
						|
								                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
							 | 
						|
								                </div>
							 | 
						|
								                <div class="modal-body" id="workerDetailsContent">
							 | 
						|
								                    <!-- Content will be loaded dynamically -->
							 | 
						|
								                </div>
							 | 
						|
								            </div>
							 | 
						|
								        </div>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <script>
							 | 
						|
								    function showWorkerDetails(event) {
							 | 
						|
								        const workerID = event.target.closest('button').getAttribute('data-worker-id');
							 | 
						|
								        
							 | 
						|
								        // Show modal
							 | 
						|
								        var modal = new bootstrap.Modal(document.getElementById('workerDetailsModal'));
							 | 
						|
								        
							 | 
						|
								        // Load worker details
							 | 
						|
								        fetch('/api/maintenance/workers/' + workerID)
							 | 
						|
								            .then(response => response.json())
							 | 
						|
								            .then(data => {
							 | 
						|
								                const content = document.getElementById('workerDetailsContent');
							 | 
						|
								                content.innerHTML = '<div class="row">' +
							 | 
						|
								                    '<div class="col-md-6">' +
							 | 
						|
								                        '<h6>Worker Information</h6>' +
							 | 
						|
								                        '<ul class="list-unstyled">' +
							 | 
						|
								                            '<li><strong>ID:</strong> ' + data.worker.id + '</li>' +
							 | 
						|
								                            '<li><strong>Address:</strong> ' + data.worker.address + '</li>' +
							 | 
						|
								                            '<li><strong>Status:</strong> ' + data.worker.status + '</li>' +
							 | 
						|
								                            '<li><strong>Max Concurrent:</strong> ' + data.worker.max_concurrent + '</li>' +
							 | 
						|
								                            '<li><strong>Current Load:</strong> ' + data.worker.current_load + '</li>' +
							 | 
						|
								                        '</ul>' +
							 | 
						|
								                    '</div>' +
							 | 
						|
								                    '<div class="col-md-6">' +
							 | 
						|
								                        '<h6>Performance Metrics</h6>' +
							 | 
						|
								                        '<ul class="list-unstyled">' +
							 | 
						|
								                            '<li><strong>Tasks Completed:</strong> ' + data.performance.tasks_completed + '</li>' +
							 | 
						|
								                            '<li><strong>Tasks Failed:</strong> ' + data.performance.tasks_failed + '</li>' +
							 | 
						|
								                            '<li><strong>Success Rate:</strong> ' + data.performance.success_rate.toFixed(1) + '%</li>' +
							 | 
						|
								                            '<li><strong>Average Task Time:</strong> ' + formatDuration(data.performance.average_task_time) + '</li>' +
							 | 
						|
								                            '<li><strong>Uptime:</strong> ' + formatDuration(data.performance.uptime) + '</li>' +
							 | 
						|
								                        '</ul>' +
							 | 
						|
								                    '</div>' +
							 | 
						|
								                '</div>' +
							 | 
						|
								                '<hr>' +
							 | 
						|
								                '<h6>Current Tasks</h6>' +
							 | 
						|
								                (data.current_tasks.length === 0 ? 
							 | 
						|
								                    '<p class="text-muted">No current tasks</p>' :
							 | 
						|
								                    data.current_tasks.map(task => 
							 | 
						|
								                        '<div class="card mb-2">' +
							 | 
						|
								                            '<div class="card-body py-2">' +
							 | 
						|
								                                '<div class="d-flex justify-content-between">' +
							 | 
						|
								                                    '<span><strong>' + task.type + '</strong> - Volume ' + task.volume_id + '</span>' +
							 | 
						|
								                                    '<span class="badge bg-info">' + task.status + '</span>' +
							 | 
						|
								                                '</div>' +
							 | 
						|
								                                '<small class="text-muted">' + task.reason + '</small>' +
							 | 
						|
								                            '</div>' +
							 | 
						|
								                        '</div>'
							 | 
						|
								                    ).join('')
							 | 
						|
								                );
							 | 
						|
								                modal.show();
							 | 
						|
								            })
							 | 
						|
								            .catch(error => {
							 | 
						|
								                console.error('Error loading worker details:', error);
							 | 
						|
								                const content = document.getElementById('workerDetailsContent');
							 | 
						|
								                content.innerHTML = '<div class="alert alert-danger">Failed to load worker details</div>';
							 | 
						|
								                modal.show();
							 | 
						|
								            });
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    function pauseWorker(event) {
							 | 
						|
								        const workerID = event.target.closest('button').getAttribute('data-worker-id');
							 | 
						|
								        
							 | 
						|
								        if (confirm('Are you sure you want to pause this worker?')) {
							 | 
						|
								            fetch('/api/maintenance/workers/' + workerID + '/pause', {
							 | 
						|
								                method: 'POST'
							 | 
						|
								            })
							 | 
						|
								            .then(response => response.json())
							 | 
						|
								            .then(data => {
							 | 
						|
								                if (data.success) {
							 | 
						|
								                    location.reload();
							 | 
						|
								                } else {
							 | 
						|
								                    alert('Failed to pause worker: ' + data.error);
							 | 
						|
								                }
							 | 
						|
								            })
							 | 
						|
								            .catch(error => {
							 | 
						|
								                console.error('Error pausing worker:', error);
							 | 
						|
								                alert('Failed to pause worker');
							 | 
						|
								            });
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    function formatDuration(nanoseconds) {
							 | 
						|
								        const seconds = Math.floor(nanoseconds / 1000000000);
							 | 
						|
								        const minutes = Math.floor(seconds / 60);
							 | 
						|
								        const hours = Math.floor(minutes / 60);
							 | 
						|
								        
							 | 
						|
								        if (hours > 0) {
							 | 
						|
								            return hours + 'h ' + (minutes % 60) + 'm';
							 | 
						|
								        } else if (minutes > 0) {
							 | 
						|
								            return minutes + 'm ' + (seconds % 60) + 's';
							 | 
						|
								        } else {
							 | 
						|
								            return seconds + 's';
							 | 
						|
								        }
							 | 
						|
								    }
							 | 
						|
								    </script>
							 | 
						|
								} 
							 |