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.
		
		
		
		
		
			
		
			
				
					
					
						
							1118 lines
						
					
					
						
							56 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							1118 lines
						
					
					
						
							56 KiB
						
					
					
				| package app | |
| 
 | |
| import ( | |
|     "fmt" | |
|     "sort" | |
|     "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" | |
|     "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" | |
| ) | |
| 
 | |
| // sortedKeys returns the sorted keys for a string map | |
| func sortedKeys(m map[string]string) []string { | |
|     keys := make([]string, 0, len(m)) | |
|     for k := range m { | |
|         keys = append(keys, k) | |
|     } | |
|     sort.Strings(keys) | |
|     return keys | |
| } | |
| 
 | |
| templ TaskDetail(data *maintenance.TaskDetailData) { | |
|     <div class="container-fluid"> | |
|         <!-- Header --> | |
|         <div class="row mb-4"> | |
|             <div class="col-12"> | |
|                 <div class="d-flex justify-content-between align-items-center"> | |
|                     <div> | |
|                         <nav aria-label="breadcrumb"> | |
|                             <ol class="breadcrumb mb-1"> | |
|                                 <li class="breadcrumb-item"><a href="/maintenance">Maintenance</a></li> | |
|                                 <li class="breadcrumb-item active" aria-current="page">Task Detail</li> | |
|                             </ol> | |
|                         </nav> | |
|                         <h2 class="mb-0"> | |
|                             <i class="fas fa-tasks me-2"></i> | |
|                             Task Detail: {data.Task.ID} | |
|                         </h2> | |
|                     </div> | |
|                     <div class="btn-group"> | |
|                         <button type="button" class="btn btn-secondary" onclick="history.back()"> | |
|                             <i class="fas fa-arrow-left me-1"></i> | |
|                             Back | |
|                         </button> | |
|                         <button type="button" class="btn btn-secondary" onclick="refreshPage()"> | |
|                             <i class="fas fa-sync-alt me-1"></i> | |
|                             Refresh | |
|                         </button> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Task Overview Card --> | |
|         <div class="row mb-4"> | |
|             <div class="col-12"> | |
|                 <div class="card"> | |
|                     <div class="card-header"> | |
|                         <h5 class="mb-0"> | |
|                             <i class="fas fa-info-circle me-2"></i> | |
|                             Task Overview | |
|                         </h5> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         <div class="row"> | |
|                             <div class="col-md-6"> | |
|                                 <dl class="row"> | |
|                                     <dt class="col-sm-4">Task ID:</dt> | |
|                                     <dd class="col-sm-8"><code>{data.Task.ID}</code></dd> | |
|                                      | |
|                                     <dt class="col-sm-4">Type:</dt> | |
|                                     <dd class="col-sm-8"> | |
|                                         <span class="badge bg-info">{string(data.Task.Type)}</span> | |
|                                     </dd> | |
|                                      | |
|                                     <dt class="col-sm-4">Status:</dt> | |
|                                     <dd class="col-sm-8"> | |
|                                         if data.Task.Status == maintenance.TaskStatusPending { | |
|                                             <span class="badge bg-secondary">Pending</span> | |
|                                         } else if data.Task.Status == maintenance.TaskStatusAssigned { | |
|                                             <span class="badge bg-info">Assigned</span> | |
|                                         } else if data.Task.Status == maintenance.TaskStatusInProgress { | |
|                                             <span class="badge bg-warning">In Progress</span> | |
|                                         } else if data.Task.Status == maintenance.TaskStatusCompleted { | |
|                                             <span class="badge bg-success">Completed</span> | |
|                                         } else if data.Task.Status == maintenance.TaskStatusFailed { | |
|                                             <span class="badge bg-danger">Failed</span> | |
|                                         } else if data.Task.Status == maintenance.TaskStatusCancelled { | |
|                                             <span class="badge bg-dark">Cancelled</span> | |
|                                         } | |
|                                     </dd> | |
|                                      | |
|                                     <dt class="col-sm-4">Priority:</dt> | |
|                                     <dd class="col-sm-8"> | |
|                                         if data.Task.Priority == maintenance.PriorityHigh { | |
|                                             <span class="badge bg-danger">High</span> | |
|                                         } else if data.Task.Priority == maintenance.PriorityCritical { | |
|                                             <span class="badge bg-danger">Critical</span> | |
|                                         } else if data.Task.Priority == maintenance.PriorityNormal { | |
|                                             <span class="badge bg-warning">Normal</span> | |
|                                         } else { | |
|                                             <span class="badge bg-secondary">Low</span> | |
|                                         } | |
|                                     </dd> | |
|                                      | |
|                                     if data.Task.Reason != "" { | |
|                                         <dt class="col-sm-4">Reason:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             <span class="text-muted">{data.Task.Reason}</span> | |
|                                         </dd> | |
|                                     } | |
|                                 </dl> | |
|                             </div> | |
|                             <div class="col-md-6"> | |
|                                 <!-- Task Timeline --> | |
|                                 <div class="mb-3"> | |
|                                     <h6 class="text-primary mb-3"> | |
|                                         <i class="fas fa-clock me-1"></i>Task Timeline | |
|                                     </h6> | |
|                                     <div class="timeline-container"> | |
|                                         <div class="timeline-progress"> | |
|                                             <div class="timeline-step" data-step="created"> | |
|                                                 <div class="timeline-circle completed"> | |
|                                                     <i class="fas fa-plus"></i> | |
|                                                 </div> | |
|                                                 <div class="timeline-connector completed"></div> | |
|                                                 <div class="timeline-label"> | |
|                                                     <strong>Created</strong> | |
|                                                     <small class="d-block text-muted">{data.Task.CreatedAt.Format("01-02 15:04:05")}</small> | |
|                                                 </div> | |
|                                             </div> | |
|                                              | |
|                                             <div class="timeline-step" data-step="scheduled"> | |
|                                                 <div class="timeline-circle completed"> | |
|                                                     <i class="fas fa-calendar"></i> | |
|                                                 </div> | |
|                                                 if data.Task.StartedAt != nil { | |
|                                                     <div class="timeline-connector completed"></div> | |
|                                                 } else { | |
|                                                     <div class="timeline-connector"></div> | |
|                                                 } | |
|                                                 <div class="timeline-label"> | |
|                                                     <strong>Scheduled</strong> | |
|                                                     <small class="d-block text-muted">{data.Task.ScheduledAt.Format("01-02 15:04:05")}</small> | |
|                                                 </div> | |
|                                             </div> | |
|                                              | |
|                                             <div class="timeline-step" data-step="started"> | |
|                                                 if data.Task.StartedAt != nil { | |
|                                                     <div class="timeline-circle completed"> | |
|                                                         <i class="fas fa-play"></i> | |
|                                                     </div> | |
|                                                 } else { | |
|                                                     <div class="timeline-circle pending"> | |
|                                                         <i class="fas fa-clock"></i> | |
|                                                     </div> | |
|                                                 } | |
|                                                 if data.Task.CompletedAt != nil { | |
|                                                     <div class="timeline-connector completed"></div> | |
|                                                 } else { | |
|                                                     <div class="timeline-connector"></div> | |
|                                                 } | |
|                                                 <div class="timeline-label"> | |
|                                                     <strong>Started</strong> | |
|                                                     <small class="d-block text-muted"> | |
|                                                         if data.Task.StartedAt != nil { | |
|                                                             {data.Task.StartedAt.Format("01-02 15:04:05")} | |
|                                                         } else { | |
|                                                             — | |
|                                                         } | |
|                                                     </small> | |
|                                                 </div> | |
|                                             </div> | |
|                                              | |
|                                             <div class="timeline-step" data-step="completed"> | |
|                                                 if data.Task.CompletedAt != nil { | |
|                                                     <div class="timeline-circle completed"> | |
|                                                         if data.Task.Status == maintenance.TaskStatusCompleted { | |
|                                                             <i class="fas fa-check"></i> | |
|                                                         } else if data.Task.Status == maintenance.TaskStatusFailed { | |
|                                                             <i class="fas fa-times"></i> | |
|                                                         } else { | |
|                                                             <i class="fas fa-stop"></i> | |
|                                                         } | |
|                                                     </div> | |
|                                                 } else { | |
|                                                     <div class="timeline-circle pending"> | |
|                                                         <i class="fas fa-hourglass-half"></i> | |
|                                                     </div> | |
|                                                 } | |
|                                                 <div class="timeline-label"> | |
|                                                     <strong> | |
|                                                         if data.Task.Status == maintenance.TaskStatusCompleted { | |
|                                                             Completed | |
|                                                         } else if data.Task.Status == maintenance.TaskStatusFailed { | |
|                                                             Failed | |
|                                                         } else if data.Task.Status == maintenance.TaskStatusCancelled { | |
|                                                             Cancelled | |
|                                                         } else { | |
|                                                             Pending | |
|                                                         } | |
|                                                     </strong> | |
|                                                     <small class="d-block text-muted"> | |
|                                                         if data.Task.CompletedAt != nil { | |
|                                                             {data.Task.CompletedAt.Format("01-02 15:04:05")} | |
|                                                         } else { | |
|                                                             — | |
|                                                         } | |
|                                                     </small> | |
|                                                 </div> | |
|                                             </div> | |
|                                         </div> | |
|                                     </div> | |
|                                 </div> | |
|                                  | |
|                                 <!-- Additional Info --> | |
|                                 if data.Task.WorkerID != "" { | |
|                                     <dl class="row"> | |
|                                         <dt class="col-sm-4">Worker:</dt> | |
|                                         <dd class="col-sm-8"><code>{data.Task.WorkerID}</code></dd> | |
|                                     </dl> | |
|                                 } | |
|                                      | |
|                                 <dl class="row"> | |
|                                     if data.Task.TypedParams != nil && data.Task.TypedParams.VolumeSize > 0 { | |
|                                         <dt class="col-sm-4">Volume Size:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             <span class="badge bg-primary">{formatBytes(int64(data.Task.TypedParams.VolumeSize))}</span> | |
|                                         </dd> | |
|                                     } | |
|                                      | |
|                                     if data.Task.TypedParams != nil && data.Task.TypedParams.Collection != "" { | |
|                                         <dt class="col-sm-4">Collection:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             <span class="badge bg-info"><i class="fas fa-folder me-1"></i>{data.Task.TypedParams.Collection}</span> | |
|                                         </dd> | |
|                                     } | |
|                                      | |
|                                     if data.Task.TypedParams != nil && data.Task.TypedParams.DataCenter != "" { | |
|                                         <dt class="col-sm-4">Data Center:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             <span class="badge bg-secondary"><i class="fas fa-building me-1"></i>{data.Task.TypedParams.DataCenter}</span> | |
|                                         </dd> | |
|                                     } | |
|                                      | |
|                                     if data.Task.Progress > 0 { | |
|                                         <dt class="col-sm-4">Progress:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             <div class="progress" style="height: 20px;"> | |
|                                                 <div class="progress-bar" role="progressbar"  | |
|                                                      style={fmt.Sprintf("width: %.1f%%", data.Task.Progress)} | |
|                                                      aria-valuenow={fmt.Sprintf("%.1f", data.Task.Progress)}  | |
|                                                      aria-valuemin="0" aria-valuemax="100"> | |
|                                                     {fmt.Sprintf("%.1f%%", data.Task.Progress)} | |
|                                                 </div> | |
|                                             </div> | |
|                                         </dd> | |
|                                     } | |
|                                 </dl> | |
|                             </div> | |
|                         </div> | |
|                          | |
| 
 | |
|                          | |
|                         if data.Task.DetailedReason != "" { | |
|                             <div class="row mt-3"> | |
|                                 <div class="col-12"> | |
|                                     <h6>Detailed Reason:</h6> | |
|                                     <p class="text-muted">{data.Task.DetailedReason}</p> | |
|                                 </div> | |
|                             </div> | |
|                         } | |
|                          | |
|                         if data.Task.Error != "" { | |
|                             <div class="row mt-3"> | |
|                                 <div class="col-12"> | |
|                                     <h6>Error:</h6> | |
|                                     <div class="alert alert-danger"> | |
|                                         <code>{data.Task.Error}</code> | |
|                                     </div> | |
|                                 </div> | |
|                             </div> | |
|                         } | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
| 
 | |
|         <!-- Task Configuration Card --> | |
|         if data.Task.TypedParams != nil { | |
|             <div class="row mb-4"> | |
|                 <div class="col-12"> | |
|                     <div class="card"> | |
|                         <div class="card-header"> | |
|                             <h5 class="mb-0"> | |
|                                 <i class="fas fa-cog me-2"></i> | |
|                                 Task Configuration | |
|                             </h5> | |
|                         </div> | |
|                         <div class="card-body"> | |
|                             <!-- Source Servers (Unified) --> | |
|                             if len(data.Task.TypedParams.Sources) > 0 { | |
|                                 <div class="mb-4"> | |
|                                     <h6 class="text-info d-flex align-items-center"> | |
|                                         <i class="fas fa-server me-2"></i> | |
|                                         Source Servers | |
|                                         <span class="badge bg-info ms-2">{fmt.Sprintf("%d", len(data.Task.TypedParams.Sources))}</span> | |
|                                     </h6> | |
|                                     <div class="bg-light p-3 rounded"> | |
|                                         <div class="d-flex flex-column gap-2"> | |
|                                                                         for i, source := range data.Task.TypedParams.Sources { | |
|                                 <div class="d-grid" style="grid-template-columns: auto 1fr auto auto auto auto; gap: 0.5rem; align-items: center;"> | |
|                                     <span class="badge bg-primary">{fmt.Sprintf("#%d", i+1)}</span> | |
|                                     <code>{source.Node}</code> | |
|                                     <div> | |
|                                         if source.DataCenter != "" { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-building me-1"></i>{source.DataCenter} | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                     <div> | |
|                                         if source.Rack != "" { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-server me-1"></i>{source.Rack} | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                     <div> | |
|                                         if source.VolumeId > 0 { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-hdd me-1"></i>Vol:{fmt.Sprintf("%d", source.VolumeId)} | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                     <div> | |
|                                         if len(source.ShardIds) > 0 { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-puzzle-piece me-1"></i>Shards: | |
|                                                 for j, shardId := range source.ShardIds { | |
|                                                     if j > 0 { | |
|                                                         <span>, </span> | |
|                                                     } | |
|                                                     if shardId < erasure_coding.DataShardsCount { | |
|                                                         <span class="badge badge-sm bg-primary ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Data shard %d", shardId)}>{fmt.Sprintf("%d", shardId)}</span> | |
|                                                     } else { | |
|                                                         <span class="badge badge-sm bg-warning text-dark ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Parity shard %d", shardId)}>{fmt.Sprintf("P%d", shardId-erasure_coding.DataShardsCount)}</span> | |
|                                                     } | |
|                                                 } | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                 </div> | |
|                             } | |
|                                         </div> | |
|                                     </div> | |
|                                 </div> | |
|                             } | |
| 
 | |
|                             <!-- Task Flow Indicator --> | |
|                             if len(data.Task.TypedParams.Sources) > 0 || len(data.Task.TypedParams.Targets) > 0 { | |
|                                 <div class="text-center mb-3"> | |
|                                     <i class="fas fa-arrow-down text-primary" style="font-size: 1.5rem;"></i> | |
|                                     <br/> | |
|                                     <small class="text-muted">Task: {string(data.Task.Type)}</small> | |
|                                 </div> | |
|                             } | |
| 
 | |
|                             <!-- Target/Destination (Generic) --> | |
|                             if len(data.Task.TypedParams.Targets) > 0 { | |
|                                 <div class="mb-4"> | |
|                                     <h6 class="text-success d-flex align-items-center"> | |
|                                         <i class="fas fa-bullseye me-2"></i> | |
|                                         Target Servers | |
|                                         <span class="badge bg-success ms-2">{fmt.Sprintf("%d", len(data.Task.TypedParams.Targets))}</span> | |
|                                     </h6> | |
|                                     <div class="bg-light p-3 rounded"> | |
|                                         <div class="d-flex flex-column gap-2"> | |
|                                                                         for i, target := range data.Task.TypedParams.Targets { | |
|                                 <div class="d-grid" style="grid-template-columns: auto 1fr auto auto auto auto; gap: 0.5rem; align-items: center;"> | |
|                                     <span class="badge bg-success">{fmt.Sprintf("#%d", i+1)}</span> | |
|                                     <code>{target.Node}</code> | |
|                                     <div> | |
|                                         if target.DataCenter != "" { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-building me-1"></i>{target.DataCenter} | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                     <div> | |
|                                         if target.Rack != "" { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-server me-1"></i>{target.Rack} | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                     <div> | |
|                                         if target.VolumeId > 0 { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-hdd me-1"></i>Vol:{fmt.Sprintf("%d", target.VolumeId)} | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                     <div> | |
|                                         if len(target.ShardIds) > 0 { | |
|                                             <small class="text-muted"> | |
|                                                 <i class="fas fa-puzzle-piece me-1"></i>Shards: | |
|                                                 for j, shardId := range target.ShardIds { | |
|                                                     if j > 0 { | |
|                                                         <span>, </span> | |
|                                                     } | |
|                                                     if shardId < erasure_coding.DataShardsCount { | |
|                                                         <span class="badge badge-sm bg-primary ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Data shard %d", shardId)}>{fmt.Sprintf("%d", shardId)}</span> | |
|                                                     } else { | |
|                                                         <span class="badge badge-sm bg-warning text-dark ms-1" style="font-size: 0.65rem;" title={fmt.Sprintf("Parity shard %d", shardId)}>{fmt.Sprintf("P%d", shardId-erasure_coding.DataShardsCount)}</span> | |
|                                                     } | |
|                                                 } | |
|                                             </small> | |
|                                         } | |
|                                     </div> | |
|                                 </div> | |
|                             } | |
|                                         </div> | |
|                                     </div> | |
|                                 </div> | |
|                             } | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         } | |
| 
 | |
|         <!-- Worker Information Card --> | |
|         if data.WorkerInfo != nil { | |
|             <div class="row mb-4"> | |
|                 <div class="col-12"> | |
|                     <div class="card"> | |
|                         <div class="card-header"> | |
|                             <h5 class="mb-0"> | |
|                                 <i class="fas fa-server me-2"></i> | |
|                                 Worker Information | |
|                             </h5> | |
|                         </div> | |
|                         <div class="card-body"> | |
|                             <div class="row"> | |
|                                 <div class="col-md-6"> | |
|                                     <dl class="row"> | |
|                                         <dt class="col-sm-4">Worker ID:</dt> | |
|                                         <dd class="col-sm-8"><code>{data.WorkerInfo.ID}</code></dd> | |
|                                          | |
|                                         <dt class="col-sm-4">Address:</dt> | |
|                                         <dd class="col-sm-8"><code>{data.WorkerInfo.Address}</code></dd> | |
|                                          | |
|                                         <dt class="col-sm-4">Status:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             if data.WorkerInfo.Status == "active" { | |
|                                                 <span class="badge bg-success">Active</span> | |
|                                             } else if data.WorkerInfo.Status == "busy" { | |
|                                                 <span class="badge bg-warning">Busy</span> | |
|                                             } else { | |
|                                                 <span class="badge bg-secondary">Inactive</span> | |
|                                             } | |
|                                         </dd> | |
|                                     </dl> | |
|                                 </div> | |
|                                 <div class="col-md-6"> | |
|                                     <dl class="row"> | |
|                                         <dt class="col-sm-4">Last Heartbeat:</dt> | |
|                                         <dd class="col-sm-8">{data.WorkerInfo.LastHeartbeat.Format("2006-01-02 15:04:05")}</dd> | |
|                                          | |
|                                         <dt class="col-sm-4">Current Load:</dt> | |
|                                         <dd class="col-sm-8">{fmt.Sprintf("%d/%d", data.WorkerInfo.CurrentLoad, data.WorkerInfo.MaxConcurrent)}</dd> | |
|                                          | |
|                                         <dt class="col-sm-4">Capabilities:</dt> | |
|                                         <dd class="col-sm-8"> | |
|                                             for _, capability := range data.WorkerInfo.Capabilities { | |
|                                                 <span class="badge bg-info me-1">{string(capability)}</span> | |
|                                             } | |
|                                         </dd> | |
|                                     </dl> | |
|                                 </div> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         } | |
| 
 | |
|         <!-- Assignment History Card --> | |
|         if len(data.AssignmentHistory) > 0 { | |
|             <div class="row mb-4"> | |
|                 <div class="col-12"> | |
|                     <div class="card"> | |
|                         <div class="card-header"> | |
|                             <h5 class="mb-0"> | |
|                                 <i class="fas fa-history me-2"></i> | |
|                                 Assignment History | |
|                             </h5> | |
|                         </div> | |
|                         <div class="card-body"> | |
|                             <div class="table-responsive"> | |
|                                 <table class="table table-striped"> | |
|                                     <thead> | |
|                                         <tr> | |
|                                             <th>Worker ID</th> | |
|                                             <th>Worker Address</th> | |
|                                             <th>Assigned At</th> | |
|                                             <th>Unassigned At</th> | |
|                                             <th>Reason</th> | |
|                                         </tr> | |
|                                     </thead> | |
|                                     <tbody> | |
|                                         for _, assignment := range data.AssignmentHistory { | |
|                                             <tr> | |
|                                                 <td><code>{assignment.WorkerID}</code></td> | |
|                                                 <td><code>{assignment.WorkerAddress}</code></td> | |
|                                                 <td>{assignment.AssignedAt.Format("2006-01-02 15:04:05")}</td> | |
|                                                 <td> | |
|                                                     if assignment.UnassignedAt != nil { | |
|                                                         {assignment.UnassignedAt.Format("2006-01-02 15:04:05")} | |
|                                                     } else { | |
|                                                         <span class="text-muted">—</span> | |
|                                                     } | |
|                                                 </td> | |
|                                                 <td>{assignment.Reason}</td> | |
|                                             </tr> | |
|                                         } | |
|                                     </tbody> | |
|                                 </table> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         } | |
| 
 | |
|         <!-- Execution Logs Card --> | |
|         if len(data.ExecutionLogs) > 0 { | |
|             <div class="row mb-4"> | |
|                 <div class="col-12"> | |
|                     <div class="card"> | |
|                         <div class="card-header"> | |
|                             <h5 class="mb-0"> | |
|                                 <i class="fas fa-file-alt me-2"></i> | |
|                                 Execution Logs | |
|                             </h5> | |
|                         </div> | |
|                         <div class="card-body"> | |
|                             <div class="table-responsive"> | |
|                                 <table class="table table-striped table-sm"> | |
|                                     <thead> | |
|                                         <tr> | |
|                                             <th width="150">Timestamp</th> | |
|                                             <th width="80">Level</th> | |
|                                             <th>Message</th> | |
|                                             <th>Details</th> | |
|                                         </tr> | |
|                                     </thead> | |
|                                     <tbody> | |
|                                         for _, log := range data.ExecutionLogs { | |
|                                             <tr> | |
|                                                 <td><small>{log.Timestamp.Format("15:04:05")}</small></td> | |
|                                                 <td> | |
|                                                     if log.Level == "error" { | |
|                                                         <span class="badge bg-danger">{log.Level}</span> | |
|                                                     } else if log.Level == "warn" { | |
|                                                         <span class="badge bg-warning">{log.Level}</span> | |
|                                                     } else if log.Level == "info" { | |
|                                                         <span class="badge bg-info">{log.Level}</span> | |
|                                                     } else { | |
|                                                         <span class="badge bg-secondary">{log.Level}</span> | |
|                                                     } | |
|                                                 </td> | |
|                                                 <td><code>{log.Message}</code></td> | |
|                                                 <td> | |
|                                                     if log.Fields != nil && len(log.Fields) > 0 { | |
|                                                         <small> | |
|                                                             for _, k := range sortedKeys(log.Fields) { | |
|                                                                 <span class="badge bg-light text-dark me-1">{k}=<i>{log.Fields[k]}</i></span> | |
|                                                             } | |
|                                                         </small> | |
|                                                     } else if log.Progress != nil || log.Status != "" { | |
|                                                         <small> | |
|                                                             if log.Progress != nil { | |
|                                                                 <span class="badge bg-secondary me-1">progress=<i>{fmt.Sprintf("%.0f%%", *log.Progress)}</i></span> | |
|                                                             } | |
|                                                             if log.Status != "" { | |
|                                                                 <span class="badge bg-secondary">status=<i>{log.Status}</i></span> | |
|                                                             } | |
|                                                         </small> | |
|                                                     } else { | |
|                                                         <span class="text-muted">-</span> | |
|                                                     } | |
|                                                 </td> | |
|                                             </tr> | |
|                                         } | |
|                                     </tbody> | |
|                                 </table> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         } | |
| 
 | |
|         <!-- Related Tasks Card --> | |
|         if len(data.RelatedTasks) > 0 { | |
|             <div class="row mb-4"> | |
|                 <div class="col-12"> | |
|                     <div class="card"> | |
|                         <div class="card-header"> | |
|                             <h5 class="mb-0"> | |
|                                 <i class="fas fa-link me-2"></i> | |
|                                 Related Tasks | |
|                             </h5> | |
|                         </div> | |
|                         <div class="card-body"> | |
|                             <div class="table-responsive"> | |
|                                 <table class="table table-striped"> | |
|                                     <thead> | |
|                                         <tr> | |
|                                             <th>Task ID</th> | |
|                                             <th>Type</th> | |
|                                             <th>Status</th> | |
|                                             <th>Volume ID</th> | |
|                                             <th>Server</th> | |
|                                             <th>Created</th> | |
|                                         </tr> | |
|                                     </thead> | |
|                                     <tbody> | |
|                                         for _, relatedTask := range data.RelatedTasks { | |
|                                             <tr> | |
|                                                 <td> | |
|                                                     <a href={fmt.Sprintf("/maintenance/tasks/%s", relatedTask.ID)}> | |
|                                                         <code>{relatedTask.ID}</code> | |
|                                                     </a> | |
|                                                 </td> | |
|                                                 <td><span class="badge bg-info">{string(relatedTask.Type)}</span></td> | |
|                                                 <td> | |
|                                                     if relatedTask.Status == maintenance.TaskStatusCompleted { | |
|                                                         <span class="badge bg-success">Completed</span> | |
|                                                     } else if relatedTask.Status == maintenance.TaskStatusFailed { | |
|                                                         <span class="badge bg-danger">Failed</span> | |
|                                                     } else if relatedTask.Status == maintenance.TaskStatusInProgress { | |
|                                                         <span class="badge bg-warning">In Progress</span> | |
|                                                     } else { | |
|                                                         <span class="badge bg-secondary">{string(relatedTask.Status)}</span> | |
|                                                     } | |
|                                                 </td> | |
|                                                 <td> | |
|                                                     if relatedTask.VolumeID != 0 { | |
|                                                         {fmt.Sprintf("%d", relatedTask.VolumeID)} | |
|                                                     } else { | |
|                                                         <span class="text-muted">-</span> | |
|                                                     } | |
|                                                 </td> | |
|                                                 <td> | |
|                                                     if relatedTask.Server != "" { | |
|                                                         <code>{relatedTask.Server}</code> | |
|                                                     } else { | |
|                                                         <span class="text-muted">-</span> | |
|                                                     } | |
|                                                 </td> | |
|                                                 <td><small>{relatedTask.CreatedAt.Format("2006-01-02 15:04:05")}</small></td> | |
|                                             </tr> | |
|                                         } | |
|                                     </tbody> | |
|                                 </table> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         } | |
| 
 | |
|         <!-- Actions Card --> | |
|         <div class="row mb-4"> | |
|             <div class="col-12"> | |
|                 <div class="card"> | |
|                     <div class="card-header"> | |
|                         <h5 class="mb-0"> | |
|                             <i class="fas fa-cogs me-2"></i> | |
|                             Actions | |
|                         </h5> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         if data.Task.Status == maintenance.TaskStatusPending || data.Task.Status == maintenance.TaskStatusAssigned { | |
|                             <button type="button" class="btn btn-danger me-2" data-task-id={data.Task.ID} onclick="cancelTask(this.getAttribute('data-task-id'))"> | |
|                                 <i class="fas fa-times me-1"></i> | |
|                                 Cancel Task | |
|                             </button> | |
|                         } | |
|                         if data.Task.WorkerID != "" { | |
|                             <button type="button" class="btn btn-primary me-2" data-task-id={data.Task.ID} data-worker-id={data.Task.WorkerID} onclick="showTaskLogs(this.getAttribute('data-task-id'), this.getAttribute('data-worker-id'))"> | |
|                                 <i class="fas fa-file-text me-1"></i> | |
|                                 Show Task Logs | |
|                             </button> | |
|                         } | |
|                         <button type="button" class="btn btn-info" data-task-id={data.Task.ID} onclick="exportTaskDetail(this.getAttribute('data-task-id'))"> | |
|                             <i class="fas fa-download me-1"></i> | |
|                             Export Details | |
|                         </button> | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- Task Logs Modal --> | |
|     <div class="modal fade" id="taskLogsModal" tabindex="-1" aria-labelledby="taskLogsModalLabel" aria-hidden="true"> | |
|         <div class="modal-dialog modal-xl"> | |
|             <div class="modal-content"> | |
|                 <div class="modal-header"> | |
|                     <h5 class="modal-title" id="taskLogsModalLabel"> | |
|                         <i class="fas fa-file-text me-2"></i>Task Logs | |
|                     </h5> | |
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
|                 </div> | |
|                 <div class="modal-body"> | |
|                     <div id="logsLoadingSpinner" class="text-center py-4" style="display: none;"> | |
|                         <div class="spinner-border text-primary" role="status"> | |
|                             <span class="visually-hidden">Loading logs...</span> | |
|                         </div> | |
|                         <p class="mt-2">Fetching logs from worker...</p> | |
|                     </div> | |
|                      | |
|                     <div id="logsError" class="alert alert-danger" style="display: none;"> | |
|                         <i class="fas fa-exclamation-triangle me-2"></i> | |
|                         <span id="logsErrorMessage"></span> | |
|                     </div> | |
|                      | |
|                     <div id="logsContent" style="display: none;"> | |
|                         <div class="d-flex justify-content-between align-items-center mb-3"> | |
|                             <div> | |
|                                 <strong>Task:</strong> <span id="logsTaskId"></span> |  | |
|                                 <strong>Worker:</strong> <span id="logsWorkerId"></span> | | |
|                                 <strong>Entries:</strong> <span id="logsCount"></span> | |
|                             </div> | |
|                             <div class="btn-group"> | |
|                                 <button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshModalLogs()"> | |
|                                     <i class="fas fa-sync-alt me-1"></i>Refresh | |
|                                 </button> | |
|                                 <button type="button" class="btn btn-sm btn-outline-success" onclick="downloadTaskLogs()"> | |
|                                     <i class="fas fa-download me-1"></i>Download | |
|                                 </button> | |
|                             </div> | |
|                         </div> | |
|                          | |
|                         <div class="card"> | |
|                             <div class="card-header"> | |
|                                 <div class="d-flex justify-content-between align-items-center"> | |
|                                     <span>Log Entries (Last 100)</span> | |
|                                     <small class="text-muted">Newest entries first</small> | |
|                                 </div> | |
|                             </div> | |
|                             <div class="card-body p-0"> | |
|                                 <pre id="logsDisplay" class="bg-dark text-light p-3 mb-0" style="max-height: 400px; overflow-y: auto; font-size: 0.85rem; line-height: 1.4;"></pre> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
|                 <div class="modal-footer"> | |
|                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <style> | |
|     .timeline-container { | |
|         position: relative; | |
|         padding: 20px 0; | |
|     } | |
|      | |
|     .timeline-progress { | |
|         display: flex; | |
|         justify-content: space-between; | |
|         align-items: flex-start; | |
|         position: relative; | |
|         max-width: 100%; | |
|     } | |
|      | |
|     .timeline-step { | |
|         display: flex; | |
|         flex-direction: column; | |
|         align-items: center; | |
|         flex: 1; | |
|         position: relative; | |
|     } | |
|      | |
|     .timeline-circle { | |
|         width: 40px; | |
|         height: 40px; | |
|         border-radius: 50%; | |
|         display: flex; | |
|         align-items: center; | |
|         justify-content: center; | |
|         color: white; | |
|         font-weight: bold; | |
|         box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
|         z-index: 2; | |
|         position: relative; | |
|     } | |
|      | |
|     .timeline-circle.completed { | |
|         background-color: #28a745; | |
|         border: 3px solid #1e7e34; | |
|     } | |
|      | |
|     .timeline-circle.pending { | |
|         background-color: #6c757d; | |
|         border: 3px solid #495057; | |
|     } | |
|      | |
|     .timeline-connector { | |
|         position: absolute; | |
|         top: 20px; | |
|         left: 50%; | |
|         right: -50%; | |
|         height: 4px; | |
|         z-index: 1; | |
|         margin-left: 20px; | |
|         margin-right: 20px; | |
|     } | |
|      | |
|     .timeline-connector.completed { | |
|         background-color: #28a745; | |
|     } | |
|      | |
|     .timeline-connector:not(.completed) { | |
|         background-color: #dee2e6; | |
|     } | |
|      | |
|     .timeline-step:last-child .timeline-connector { | |
|         display: none; | |
|     } | |
|      | |
|     .timeline-label { | |
|         margin-top: 15px; | |
|         text-align: center; | |
|         min-height: 60px; | |
|     } | |
|      | |
|     .timeline-label strong { | |
|         display: block; | |
|         font-size: 0.9rem; | |
|         margin-bottom: 4px; | |
|     } | |
|      | |
|     .timeline-label small { | |
|         font-size: 0.75rem; | |
|         line-height: 1.2; | |
|     } | |
|      | |
|     @media (max-width: 768px) { | |
|         .timeline-progress { | |
|             flex-direction: column; | |
|             align-items: stretch; | |
|         } | |
|          | |
|         .timeline-step { | |
|             flex-direction: row; | |
|             align-items: center; | |
|             margin-bottom: 20px; | |
|         } | |
|          | |
|         .timeline-circle { | |
|             margin-right: 15px; | |
|             flex-shrink: 0; | |
|         } | |
|          | |
|         .timeline-connector { | |
|             display: none; | |
|         } | |
|          | |
|         .timeline-label { | |
|             text-align: left; | |
|             margin-top: 0; | |
|             min-height: auto; | |
|         } | |
|     } | |
|     </style> | |
|      | |
|     <script> | |
|     // Global variables for current logs modal | |
|     let currentTaskId = ''; | |
|     let currentWorkerId = ''; | |
| 
 | |
|     function refreshPage() { | |
|         location.reload(); | |
|     } | |
| 
 | |
|     function showTaskLogs(taskId, workerId) { | |
|         currentTaskId = taskId; | |
|         currentWorkerId = workerId; | |
|          | |
|         // Show the modal | |
|         const modal = new bootstrap.Modal(document.getElementById('taskLogsModal')); | |
|         modal.show(); | |
|          | |
|         // Load logs | |
|         loadTaskLogs(taskId, workerId); | |
|     } | |
| 
 | |
|     function loadTaskLogs(taskId, workerId) { | |
|         // Show loading spinner | |
|         document.getElementById('logsLoadingSpinner').style.display = 'block'; | |
|         document.getElementById('logsError').style.display = 'none'; | |
|         document.getElementById('logsContent').style.display = 'none'; | |
|          | |
|         // Update modal info | |
|         document.getElementById('logsTaskId').textContent = taskId; | |
|         document.getElementById('logsWorkerId').textContent = workerId; | |
|          | |
|         // Fetch logs from the API | |
|         fetch(`/api/maintenance/workers/${workerId}/logs?taskId=${taskId}&maxEntries=100`) | |
|         .then(response => response.json()) | |
|         .then(data => { | |
|             document.getElementById('logsLoadingSpinner').style.display = 'none'; | |
|              | |
|             if (data.error) { | |
|                 showLogsError(data.error); | |
|                 return; | |
|             } | |
|              | |
|             // Display logs | |
|             displayLogs(data.logs, data.count || 0); | |
|         }) | |
|         .catch(error => { | |
|             document.getElementById('logsLoadingSpinner').style.display = 'none'; | |
|             showLogsError('Failed to fetch logs: ' + error.message); | |
|         }); | |
|     } | |
| 
 | |
|     function displayLogs(logs, count) { | |
|         document.getElementById('logsError').style.display = 'none'; | |
|         document.getElementById('logsContent').style.display = 'block'; | |
|         document.getElementById('logsCount').textContent = count; | |
|          | |
|         const logsDisplay = document.getElementById('logsDisplay'); | |
|          | |
|         if (!logs || logs.length === 0) { | |
|             logsDisplay.textContent = 'No logs found for this task.'; | |
|             return; | |
|         } | |
|          | |
|         // Format and display logs with structured fields | |
|         let logText = ''; | |
|         logs.forEach(entry => { | |
|             const timestamp = entry.timestamp ? new Date(entry.timestamp * 1000).toISOString() : 'N/A'; | |
|             const level = entry.level || 'INFO'; | |
|             const message = entry.message || ''; | |
|              | |
|             logText += `[${timestamp}] ${level}: ${message}`; | |
|              | |
|             // Add structured fields if they exist | |
|             if (entry.fields && Object.keys(entry.fields).length > 0) { | |
|                 const fieldsStr = Object.entries(entry.fields) | |
|                     .map(([key, value]) => `${key}=${value}`) | |
|                     .join(', '); | |
|                 logText += ` | ${fieldsStr}`; | |
|             } | |
|              | |
|             // Add progress if available | |
|             if (entry.progress !== undefined && entry.progress !== null) { | |
|                 logText += ` | progress=${entry.progress}%`; | |
|             } | |
|              | |
|             // Add status if available | |
|             if (entry.status) { | |
|                 logText += ` | status=${entry.status}`; | |
|             } | |
|              | |
|             logText += '\n'; | |
|         }); | |
|          | |
|         logsDisplay.textContent = logText; | |
|          | |
|         // Scroll to top | |
|         logsDisplay.scrollTop = 0; | |
|     } | |
| 
 | |
|     function showLogsError(errorMessage) { | |
|         document.getElementById('logsError').style.display = 'block'; | |
|         document.getElementById('logsContent').style.display = 'none'; | |
|         document.getElementById('logsErrorMessage').textContent = errorMessage; | |
|     } | |
| 
 | |
|     function refreshModalLogs() { | |
|         if (currentTaskId && currentWorkerId) { | |
|             loadTaskLogs(currentTaskId, currentWorkerId); | |
|         } | |
|     } | |
| 
 | |
|     function downloadTaskLogs() { | |
|         if (!currentTaskId || !currentWorkerId) { | |
|             alert('No task logs to download'); | |
|             return; | |
|         } | |
|          | |
|         // Download all logs (without maxEntries limit) | |
|         const downloadUrl = `/api/maintenance/workers/${currentWorkerId}/logs?taskId=${currentTaskId}&maxEntries=0`; | |
|          | |
|         fetch(downloadUrl) | |
|         .then(response => response.json()) | |
|         .then(data => { | |
|             if (data.error) { | |
|                 alert('Error downloading logs: ' + data.error); | |
|                 return; | |
|             } | |
|              | |
|             // Convert logs to text format with structured fields | |
|             let logContent = ''; | |
|             if (data.logs && data.logs.length > 0) { | |
|                 data.logs.forEach(entry => { | |
|                     const timestamp = entry.timestamp ? new Date(entry.timestamp * 1000).toISOString() : 'N/A'; | |
|                     const level = entry.level || 'INFO'; | |
|                     const message = entry.message || ''; | |
|                      | |
|                     logContent += `[${timestamp}] ${level}: ${message}`; | |
|                      | |
|                     // Add structured fields if they exist | |
|                     if (entry.fields && Object.keys(entry.fields).length > 0) { | |
|                         const fieldsStr = Object.entries(entry.fields) | |
|                             .map(([key, value]) => `${key}=${value}`) | |
|                             .join(', '); | |
|                         logContent += ` | ${fieldsStr}`; | |
|                     } | |
|                      | |
|                     // Add progress if available | |
|                     if (entry.progress !== undefined && entry.progress !== null) { | |
|                         logContent += ` | progress=${entry.progress}%`; | |
|                     } | |
|                      | |
|                     // Add status if available | |
|                     if (entry.status) { | |
|                         logContent += ` | status=${entry.status}`; | |
|                     } | |
|                      | |
|                     logContent += '\n'; | |
|                 }); | |
|             } else { | |
|                 logContent = 'No logs found for this task.'; | |
|             } | |
|              | |
|             // Create and download file | |
|             const blob = new Blob([logContent], { type: 'text/plain' }); | |
|             const url = URL.createObjectURL(blob); | |
|             const link = document.createElement('a'); | |
|             link.href = url; | |
|             link.download = `task-${currentTaskId}-logs.txt`; | |
|             link.click(); | |
|             URL.revokeObjectURL(url); | |
|         }) | |
|         .catch(error => { | |
|             alert('Error downloading logs: ' + error.message); | |
|         }); | |
|     } | |
| 
 | |
|     function cancelTask(taskId) { | |
|         if (confirm('Are you sure you want to cancel this task?')) { | |
|             fetch(`/api/maintenance/tasks/${taskId}/cancel`, { | |
|                 method: 'POST', | |
|                 headers: { | |
|                     'Content-Type': 'application/json', | |
|                 }, | |
|             }) | |
|             .then(response => response.json()) | |
|             .then(data => { | |
|                 if (data.success) { | |
|                     alert('Task cancelled successfully'); | |
|                     location.reload(); | |
|                 } else { | |
|                     alert('Error cancelling task: ' + data.error); | |
|                 } | |
|             }) | |
|             .catch(error => { | |
|                 console.error('Error:', error); | |
|                 alert('Error cancelling task'); | |
|             }); | |
|         } | |
|     } | |
| 
 | |
|     function refreshTaskLogs(taskId) { | |
|         fetch(`/api/maintenance/tasks/${taskId}/detail`) | |
|         .then(response => response.json()) | |
|         .then(data => { | |
|             location.reload(); | |
|         }) | |
|         .catch(error => { | |
|             console.error('Error:', error); | |
|             alert('Error refreshing logs'); | |
|         }); | |
|     } | |
| 
 | |
|     function exportTaskDetail(taskId) { | |
|         fetch(`/api/maintenance/tasks/${taskId}/detail`) | |
|         .then(response => response.json()) | |
|         .then(data => { | |
|             const dataStr = JSON.stringify(data, null, 2); | |
|             const dataBlob = new Blob([dataStr], {type: 'application/json'}); | |
|             const url = URL.createObjectURL(dataBlob); | |
|             const link = document.createElement('a'); | |
|             link.href = url; | |
|             link.download = `task-${taskId}-detail.json`; | |
|             link.click(); | |
|             URL.revokeObjectURL(url); | |
|         }) | |
|         .catch(error => { | |
|             console.error('Error:', error); | |
|             alert('Error exporting task detail'); | |
|         }); | |
|     } | |
| 
 | |
|     // Auto-refresh every 30 seconds for active tasks | |
|     if ('{string(data.Task.Status)}' === 'in_progress') { | |
|         setInterval(refreshPage, 30000); | |
|     } | |
|     </script> | |
| }
 |