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>
							 | 
						|
								}
							 |