diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 5e4179c2a..4c0cfd261 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "sort" - "strconv" "strings" "time" @@ -33,6 +32,17 @@ import ( _ "github.com/seaweedfs/seaweedfs/weed/credential/grpc" // Register gRPC credential store ) +const ( + maxAssignmentHistoryDisplay = 50 + maxLogMessageLength = 2000 + maxLogFields = 20 + maxRelatedTasksDisplay = 50 + maxRecentTasksDisplay = 10 + defaultCacheTimeout = 10 * time.Second + defaultFilerCacheTimeout = 30 * time.Second + defaultStatsCacheTimeout = 30 * time.Second +) + // FilerConfig holds filer configuration needed for bucket operations type FilerConfig struct { BucketsPath string @@ -132,10 +142,10 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string, templateFS: templateFS, dataDir: dataDir, grpcDialOption: grpcDialOption, - cacheExpiration: 10 * time.Second, - filerCacheExpiration: 30 * time.Second, // Cache filers for 30 seconds + cacheExpiration: defaultCacheTimeout, + filerCacheExpiration: defaultFilerCacheTimeout, configPersistence: NewConfigPersistence(dataDir), - collectionStatsCacheThreshold: 30 * time.Second, + collectionStatsCacheThreshold: defaultStatsCacheTimeout, s3TablesManager: newS3TablesManager(), icebergPort: icebergPort, } @@ -779,7 +789,7 @@ func (s *AdminServer) GetClusterBrokers() (*ClusterBrokersData, error) { // ShowMaintenanceQueue displays the maintenance queue page func (as *AdminServer) ShowMaintenanceQueue(c *gin.Context) { - data, err := as.getMaintenanceQueueData() + data, err := as.GetMaintenanceQueueData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -868,7 +878,7 @@ func (as *AdminServer) TriggerMaintenanceScan(c *gin.Context) { // GetMaintenanceTasks returns all maintenance tasks func (as *AdminServer) GetMaintenanceTasks(c *gin.Context) { - tasks, err := as.getMaintenanceTasks() + tasks, err := as.GetAllMaintenanceTasks() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -1032,9 +1042,9 @@ func (as *AdminServer) UpdateMaintenanceConfigData(config *maintenance.Maintenan // Helper methods for maintenance operations -// getMaintenanceQueueData returns data for the maintenance queue UI -func (as *AdminServer) getMaintenanceQueueData() (*maintenance.MaintenanceQueueData, error) { - tasks, err := as.getMaintenanceTasks() +// GetMaintenanceQueueData returns data for the maintenance queue UI +func (as *AdminServer) GetMaintenanceQueueData() (*maintenance.MaintenanceQueueData, error) { + tasks, err := as.GetAllMaintenanceTasks() if err != nil { return nil, err } @@ -1089,14 +1099,16 @@ func (as *AdminServer) getMaintenanceQueueStats() (*maintenance.QueueStats, erro return queueStats, nil } -// getMaintenanceTasks returns all maintenance tasks -func (as *AdminServer) getMaintenanceTasks() ([]*maintenance.MaintenanceTask, error) { +// GetAllMaintenanceTasks returns all maintenance tasks +func (as *AdminServer) GetAllMaintenanceTasks() ([]*maintenance.MaintenanceTask, error) { if as.maintenanceManager == nil { return []*maintenance.MaintenanceTask{}, nil } - // Collect all tasks from memory across all statuses - allTasks := []*maintenance.MaintenanceTask{} + // 1. Collect all tasks from memory + tasksMap := make(map[string]*maintenance.MaintenanceTask) + + // Collect from memory via GetTasks loop to ensure we catch everything statuses := []maintenance.MaintenanceTaskStatus{ maintenance.TaskStatusPending, maintenance.TaskStatusAssigned, @@ -1108,29 +1120,92 @@ func (as *AdminServer) getMaintenanceTasks() ([]*maintenance.MaintenanceTask, er for _, status := range statuses { tasks := as.maintenanceManager.GetTasks(status, "", 0) - allTasks = append(allTasks, tasks...) + for _, t := range tasks { + tasksMap[t.ID] = t + } } - // Also load any persisted tasks that might not be in memory + // 2. Merge persisted tasks if as.configPersistence != nil { persistedTasks, err := as.configPersistence.LoadAllTaskStates() if err == nil { - // Add any persisted tasks not already in memory - for _, persistedTask := range persistedTasks { - found := false - for _, memoryTask := range allTasks { - if memoryTask.ID == persistedTask.ID { - found = true - break - } - } - if !found { - allTasks = append(allTasks, persistedTask) + for _, t := range persistedTasks { + if _, exists := tasksMap[t.ID]; !exists { + tasksMap[t.ID] = t } } } } + // 3. Bucketize buckets + var pendingTasks, activeTasks, finishedTasks []*maintenance.MaintenanceTask + + for _, t := range tasksMap { + switch t.Status { + case maintenance.TaskStatusPending: + pendingTasks = append(pendingTasks, t) + case maintenance.TaskStatusAssigned, maintenance.TaskStatusInProgress: + activeTasks = append(activeTasks, t) + case maintenance.TaskStatusCompleted, maintenance.TaskStatusFailed, maintenance.TaskStatusCancelled: + finishedTasks = append(finishedTasks, t) + default: + // Treat unknown as finished/archived? Or pending? + // Safest to add to finished so they appear somewhere + finishedTasks = append(finishedTasks, t) + } + } + + // 4. Sort buckets + // Pending: Newest Created First + sort.Slice(pendingTasks, func(i, j int) bool { + return pendingTasks[i].CreatedAt.After(pendingTasks[j].CreatedAt) + }) + + // Active: Newest Created First (or StartedAt?) + sort.Slice(activeTasks, func(i, j int) bool { + return activeTasks[i].CreatedAt.After(activeTasks[j].CreatedAt) + }) + + // Finished: Newest Completed First + sort.Slice(finishedTasks, func(i, j int) bool { + t1 := finishedTasks[i].CompletedAt + t2 := finishedTasks[j].CompletedAt + + // Handle nil completion times + if t1 == nil && t2 == nil { + // Both nil, fallback to CreatedAt + if !finishedTasks[i].CreatedAt.Equal(finishedTasks[j].CreatedAt) { + return finishedTasks[i].CreatedAt.After(finishedTasks[j].CreatedAt) + } + return finishedTasks[i].ID > finishedTasks[j].ID + } + if t1 == nil { + return false // t1 (nil) goes to bottom + } + if t2 == nil { + return true // t2 (nil) goes to bottom + } + + // Compare completion times + if !t1.Equal(*t2) { + return t1.After(*t2) + } + + // Fallback to CreatedAt if completion times are identical + if !finishedTasks[i].CreatedAt.Equal(finishedTasks[j].CreatedAt) { + return finishedTasks[i].CreatedAt.After(finishedTasks[j].CreatedAt) + } + + // Final tie-breaker: ID + return finishedTasks[i].ID > finishedTasks[j].ID + }) + + // 5. Recombine + allTasks := make([]*maintenance.MaintenanceTask, 0, len(tasksMap)) + allTasks = append(allTasks, pendingTasks...) + allTasks = append(allTasks, activeTasks...) + allTasks = append(allTasks, finishedTasks...) + return allTasks, nil } @@ -1181,15 +1256,25 @@ func (as *AdminServer) GetMaintenanceTaskDetail(taskID string) (*maintenance.Tas return nil, err } + // Copy task and truncate assignment history for display + displayTask := *task + displayTask.AssignmentHistory = nil // History is provided separately in taskDetail + // Create task detail structure from the loaded task taskDetail := &maintenance.TaskDetailData{ - Task: task, + Task: &displayTask, AssignmentHistory: task.AssignmentHistory, // Use assignment history from persisted task ExecutionLogs: []*maintenance.TaskExecutionLog{}, RelatedTasks: []*maintenance.MaintenanceTask{}, LastUpdated: time.Now(), } + // Truncate assignment history if it's too long (display last N only) + if len(taskDetail.AssignmentHistory) > maxAssignmentHistoryDisplay { + startIdx := len(taskDetail.AssignmentHistory) - maxAssignmentHistoryDisplay + taskDetail.AssignmentHistory = taskDetail.AssignmentHistory[startIdx:] + } + if taskDetail.AssignmentHistory == nil { taskDetail.AssignmentHistory = []*maintenance.TaskAssignmentRecord{} } @@ -1205,72 +1290,19 @@ func (as *AdminServer) GetMaintenanceTaskDetail(taskID string) (*maintenance.Tas } } - // Get execution logs from worker if task is active/completed and worker is connected - if task.Status == maintenance.TaskStatusInProgress || task.Status == maintenance.TaskStatusCompleted { - if as.workerGrpcServer != nil && task.WorkerID != "" { - workerLogs, err := as.workerGrpcServer.RequestTaskLogs(task.WorkerID, taskID, 100, "") - if err == nil && len(workerLogs) > 0 { - // Convert worker logs to maintenance logs - for _, workerLog := range workerLogs { - maintenanceLog := &maintenance.TaskExecutionLog{ - Timestamp: time.Unix(workerLog.Timestamp, 0), - Level: workerLog.Level, - Message: workerLog.Message, - Source: "worker", - TaskID: taskID, - WorkerID: task.WorkerID, - } - // carry structured fields if present - if len(workerLog.Fields) > 0 { - maintenanceLog.Fields = make(map[string]string, len(workerLog.Fields)) - for k, v := range workerLog.Fields { - maintenanceLog.Fields[k] = v - } - } - // carry optional progress/status - if workerLog.Progress != 0 { - p := float64(workerLog.Progress) - maintenanceLog.Progress = &p - } - if workerLog.Status != "" { - maintenanceLog.Status = workerLog.Status - } - taskDetail.ExecutionLogs = append(taskDetail.ExecutionLogs, maintenanceLog) - } - } else if err != nil { - // Add a diagnostic log entry when worker logs cannot be retrieved - diagnosticLog := &maintenance.TaskExecutionLog{ - Timestamp: time.Now(), - Level: "WARNING", - Message: fmt.Sprintf("Failed to retrieve worker logs: %v", err), - Source: "admin", - TaskID: taskID, - WorkerID: task.WorkerID, - } - taskDetail.ExecutionLogs = append(taskDetail.ExecutionLogs, diagnosticLog) - glog.V(1).Infof("Failed to get worker logs for task %s from worker %s: %v", taskID, task.WorkerID, err) - } + // Load execution logs from disk + if as.configPersistence != nil { + logs, err := as.configPersistence.LoadTaskExecutionLogs(taskID) + if err == nil { + taskDetail.ExecutionLogs = logs } else { - // Add diagnostic information when worker is not available - reason := "worker gRPC server not available" - if task.WorkerID == "" { - reason = "no worker assigned to task" - } - diagnosticLog := &maintenance.TaskExecutionLog{ - Timestamp: time.Now(), - Level: "INFO", - Message: fmt.Sprintf("Worker logs not available: %s", reason), - Source: "admin", - TaskID: taskID, - WorkerID: task.WorkerID, - } - taskDetail.ExecutionLogs = append(taskDetail.ExecutionLogs, diagnosticLog) + glog.V(2).Infof("No execution logs found on disk for task %s", taskID) } } // Get related tasks (other tasks on same volume/server) if task.VolumeID != 0 || task.Server != "" { - allTasks := as.maintenanceManager.GetTasks("", "", 50) // Get recent tasks + allTasks := as.maintenanceManager.GetTasks("", "", maxRelatedTasksDisplay) // Get recent tasks for _, relatedTask := range allTasks { if relatedTask.ID != taskID && (relatedTask.VolumeID == task.VolumeID || relatedTask.Server == task.Server) { @@ -1324,7 +1356,7 @@ func (as *AdminServer) getMaintenanceWorkerDetails(workerID string) (*WorkerDeta } // Get recent tasks for this worker - recentTasks := as.maintenanceManager.GetTasks(TaskStatusCompleted, "", 10) + recentTasks := as.maintenanceManager.GetTasks(TaskStatusCompleted, "", maxRecentTasksDisplay) var workerRecentTasks []*MaintenanceTask for _, task := range recentTasks { if task.WorkerID == workerID { @@ -1336,12 +1368,13 @@ func (as *AdminServer) getMaintenanceWorkerDetails(workerID string) (*WorkerDeta var totalDuration time.Duration var completedTasks, failedTasks int for _, task := range workerRecentTasks { - if task.Status == TaskStatusCompleted { + switch task.Status { + case TaskStatusCompleted: completedTasks++ if task.StartedAt != nil && task.CompletedAt != nil { totalDuration += task.CompletedAt.Sub(*task.StartedAt) } - } else if task.Status == TaskStatusFailed { + case TaskStatusFailed: failedTasks++ } } @@ -1370,31 +1403,29 @@ func (as *AdminServer) getMaintenanceWorkerDetails(workerID string) (*WorkerDeta }, nil } -// GetWorkerLogs fetches logs from a specific worker for a task +// GetWorkerLogs fetches logs from a specific worker for a task (now reads from disk) func (as *AdminServer) GetWorkerLogs(c *gin.Context) { workerID := c.Param("id") taskID := c.Query("taskId") - maxEntriesStr := c.DefaultQuery("maxEntries", "100") - logLevel := c.DefaultQuery("logLevel", "") - - maxEntries := int32(100) - if maxEntriesStr != "" { - if parsed, err := strconv.ParseInt(maxEntriesStr, 10, 32); err == nil { - maxEntries = int32(parsed) - } - } - if as.workerGrpcServer == nil { - c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Worker gRPC server not available"}) + // Check config persistence first + if as.configPersistence == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Config persistence not available"}) return } - logs, err := as.workerGrpcServer.RequestTaskLogs(workerID, taskID, maxEntries, logLevel) + // Load logs strictly from disk to avoid timeouts and network dependency + // This matches the behavior of the Task Detail page + logs, err := as.configPersistence.LoadTaskExecutionLogs(taskID) if err != nil { - c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Failed to get logs from worker: %v", err)}) - return + glog.V(2).Infof("No execution logs found on disk for task %s: %v", taskID, err) + logs = []*maintenance.TaskExecutionLog{} } + // Filter logs by workerID if strictly needed, but usually task logs are what we want + // The persistent logs struct (TaskExecutionLog) matches what the frontend expects for the detail view + // ensuring consistent display. + c.JSON(http.StatusOK, gin.H{"worker_id": workerID, "task_id": taskID, "logs": logs, "count": len(logs)}) } diff --git a/weed/admin/dash/config_persistence.go b/weed/admin/dash/config_persistence.go index 6578ee890..061abc7c9 100644 --- a/weed/admin/dash/config_persistence.go +++ b/weed/admin/dash/config_persistence.go @@ -962,7 +962,36 @@ func (cp *ConfigPersistence) CleanupCompletedTasks() error { // Sort by completion time (most recent first) sort.Slice(completedTasks, func(i, j int) bool { - return completedTasks[i].CompletedAt.After(*completedTasks[j].CompletedAt) + t1 := completedTasks[i].CompletedAt + t2 := completedTasks[j].CompletedAt + + // Handle nil completion times + if t1 == nil && t2 == nil { + // Both nil, fallback to CreatedAt + if !completedTasks[i].CreatedAt.Equal(completedTasks[j].CreatedAt) { + return completedTasks[i].CreatedAt.After(completedTasks[j].CreatedAt) + } + return completedTasks[i].ID > completedTasks[j].ID + } + if t1 == nil { + return false // t1 (nil) goes to bottom + } + if t2 == nil { + return true // t2 (nil) goes to bottom + } + + // Compare completion times + if !t1.Equal(*t2) { + return t1.After(*t2) + } + + // Fallback to CreatedAt if completion times are identical + if !completedTasks[i].CreatedAt.Equal(completedTasks[j].CreatedAt) { + return completedTasks[i].CreatedAt.After(completedTasks[j].CreatedAt) + } + + // Final tie-breaker: ID + return completedTasks[i].ID > completedTasks[j].ID }) // Keep only the most recent MaxCompletedTasks, delete the rest diff --git a/weed/admin/dash/worker_grpc_server.go b/weed/admin/dash/worker_grpc_server.go index 34e9a9284..b5841f82a 100644 --- a/weed/admin/dash/worker_grpc_server.go +++ b/weed/admin/dash/worker_grpc_server.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/seaweedfs/seaweedfs/weed/admin/maintenance" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/pb" "github.com/seaweedfs/seaweedfs/weed/pb/worker_pb" @@ -17,6 +18,15 @@ import ( "google.golang.org/grpc/peer" ) +const ( + maxLogFetchLimit = 1000 + maxLogMessageSize = 2000 + maxLogFieldsCount = 20 + logRequestTimeout = 10 * time.Second + logResponseTimeout = 30 * time.Second + logSendTimeout = 10 * time.Second +) + // WorkerGrpcServer implements the WorkerService gRPC interface type WorkerGrpcServer struct { worker_pb.UnimplementedWorkerServiceServer @@ -42,7 +52,6 @@ type LogRequestContext struct { TaskID string WorkerID string ResponseCh chan *worker_pb.TaskLogResponse - Timeout time.Time } // WorkerConnection represents an active worker connection @@ -89,8 +98,9 @@ func (s *WorkerGrpcServer) StartWithTLS(port int) error { s.listener = listener s.running = true - // Start cleanup routine + // Start background routines go s.cleanupRoutine() + go s.activeLogFetchLoop() // Start serving in a goroutine go func() { @@ -437,9 +447,90 @@ func (s *WorkerGrpcServer) handleTaskCompletion(conn *WorkerConnection, completi } else { glog.Errorf("Worker %s failed task %s: %s", conn.workerID, completion.TaskId, completion.ErrorMessage) } + + // Fetch and persist logs + go s.FetchAndSaveLogs(conn.workerID, completion.TaskId) } } +// FetchAndSaveLogs retrieves logs from a worker and saves them to disk +func (s *WorkerGrpcServer) FetchAndSaveLogs(workerID, taskID string) error { + // Add a small initial delay to allow worker to finalize and sync logs + // especially when this is called immediately after TaskComplete + time.Sleep(300 * time.Millisecond) + + var workerLogs []*worker_pb.TaskLogEntry + var err error + + // Retry a few times if fetch fails, as logs might be in the middle of a terminal sync + for attempt := 1; attempt <= 3; attempt++ { + workerLogs, err = s.RequestTaskLogs(workerID, taskID, maxLogFetchLimit, "") + if err == nil { + break + } + if attempt < 3 { + glog.V(1).Infof("Fetch logs attempt %d failed for task %s: %v. Retrying in 1s...", attempt, taskID, err) + time.Sleep(1 * time.Second) + } + } + + if err != nil { + glog.Warningf("Failed to fetch logs for task %s after 3 attempts: %v", taskID, err) + return err + } + + // Convert logs + var maintenanceLogs []*maintenance.TaskExecutionLog + for _, workerLog := range workerLogs { + maintenanceLog := &maintenance.TaskExecutionLog{ + Timestamp: time.Unix(workerLog.Timestamp, 0), + Level: workerLog.Level, + Message: workerLog.Message, + Source: "worker", + TaskID: taskID, + WorkerID: workerID, + } + + // Truncate very long messages to prevent rendering issues and disk bloat + if len(maintenanceLog.Message) > maxLogMessageSize { + maintenanceLog.Message = maintenanceLog.Message[:maxLogMessageSize] + "... (truncated)" + } + + // carry structured fields if present + if len(workerLog.Fields) > 0 { + maintenanceLog.Fields = make(map[string]string) + fieldCount := 0 + for k, v := range workerLog.Fields { + if fieldCount >= maxLogFieldsCount { + maintenanceLog.Fields["..."] = fmt.Sprintf("(%d more fields truncated)", len(workerLog.Fields)-maxLogFieldsCount) + break + } + maintenanceLog.Fields[k] = v + fieldCount++ + } + } + + // carry optional progress/status + if workerLog.Progress != 0 { + p := float64(workerLog.Progress) + maintenanceLog.Progress = &p + } + if workerLog.Status != "" { + maintenanceLog.Status = workerLog.Status + } + maintenanceLogs = append(maintenanceLogs, maintenanceLog) + } + + // Persist logs + if s.adminServer.configPersistence != nil { + if err := s.adminServer.configPersistence.SaveTaskExecutionLogs(taskID, maintenanceLogs); err != nil { + glog.Errorf("Failed to persist logs for task %s: %v", taskID, err) + return err + } + } + return nil +} + // handleTaskLogResponse processes task log responses from workers func (s *WorkerGrpcServer) handleTaskLogResponse(conn *WorkerConnection, response *worker_pb.TaskLogResponse) { requestKey := fmt.Sprintf("%s:%s", response.WorkerId, response.TaskId) @@ -575,10 +666,13 @@ func (s *WorkerGrpcServer) RequestTaskLogs(workerID, taskID string, maxEntries i TaskID: taskID, WorkerID: workerID, ResponseCh: responseCh, - Timeout: time.Now().Add(10 * time.Second), } s.logRequestsMutex.Lock() + if _, exists := s.pendingLogRequests[requestKey]; exists { + s.logRequestsMutex.Unlock() + return nil, fmt.Errorf("a log request for task %s is already in progress", taskID) + } s.pendingLogRequests[requestKey] = requestContext s.logRequestsMutex.Unlock() @@ -601,10 +695,12 @@ func (s *WorkerGrpcServer) RequestTaskLogs(workerID, taskID string, maxEntries i select { case conn.outgoing <- logRequest: glog.V(1).Infof("Log request sent to worker %s for task %s", workerID, taskID) - case <-time.After(5 * time.Second): + case <-time.After(logSendTimeout): // Clean up pending request on timeout s.logRequestsMutex.Lock() - delete(s.pendingLogRequests, requestKey) + if s.pendingLogRequests[requestKey] == requestContext { + delete(s.pendingLogRequests, requestKey) + } s.logRequestsMutex.Unlock() return nil, fmt.Errorf("timeout sending log request to worker %s", workerID) } @@ -617,10 +713,12 @@ func (s *WorkerGrpcServer) RequestTaskLogs(workerID, taskID string, maxEntries i } glog.V(1).Infof("Received %d log entries for task %s from worker %s", len(response.LogEntries), taskID, workerID) return response.LogEntries, nil - case <-time.After(10 * time.Second): + case <-time.After(logResponseTimeout): // Clean up pending request on timeout s.logRequestsMutex.Lock() - delete(s.pendingLogRequests, requestKey) + if s.pendingLogRequests[requestKey] == requestContext { + delete(s.pendingLogRequests, requestKey) + } s.logRequestsMutex.Unlock() return nil, fmt.Errorf("timeout waiting for log response from worker %s", workerID) } @@ -684,3 +782,38 @@ func findClientAddress(ctx context.Context) string { } return pr.Addr.String() } + +// activeLogFetchLoop periodically fetches logs for all in-progress tasks +func (s *WorkerGrpcServer) activeLogFetchLoop() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-s.stopChan: + return + case <-ticker.C: + if !s.running || s.adminServer == nil || s.adminServer.maintenanceManager == nil { + continue + } + + // Get all in-progress tasks + tasks := s.adminServer.maintenanceManager.GetTasks(maintenance.TaskStatusInProgress, "", 0) + if len(tasks) == 0 { + continue + } + + glog.V(2).Infof("Background log fetcher: found %d in-progress tasks", len(tasks)) + for _, task := range tasks { + if task.WorkerID != "" { + // Use a goroutine to avoid blocking the loop + go func(wID, tID string) { + if err := s.FetchAndSaveLogs(wID, tID); err != nil { + glog.V(2).Infof("Background log fetch failed for task %s on worker %s: %v", tID, wID, err) + } + }(task.WorkerID, task.ID) + } + } + } + } +} diff --git a/weed/admin/handlers/maintenance_handlers.go b/weed/admin/handlers/maintenance_handlers.go index 3c1b5e410..005f60277 100644 --- a/weed/admin/handlers/maintenance_handlers.go +++ b/weed/admin/handlers/maintenance_handlers.go @@ -39,6 +39,11 @@ func NewMaintenanceHandlers(adminServer *dash.AdminServer) *MaintenanceHandlers func (h *MaintenanceHandlers) ShowTaskDetail(c *gin.Context) { taskID := c.Param("id") + if h.adminServer == nil { + c.String(http.StatusInternalServerError, "Admin server not initialized") + return + } + taskDetail, err := h.adminServer.GetMaintenanceTaskDetail(taskID) if err != nil { glog.Errorf("DEBUG ShowTaskDetail: error getting task detail for %s: %v", taskID, err) @@ -111,6 +116,10 @@ func (h *MaintenanceHandlers) ShowMaintenanceQueue(c *gin.Context) { // ShowMaintenanceWorkers displays the maintenance workers page func (h *MaintenanceHandlers) ShowMaintenanceWorkers(c *gin.Context) { + if h.adminServer == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Admin server not initialized"}) + return + } workersData, err := h.adminServer.GetMaintenanceWorkersData() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) @@ -339,6 +348,8 @@ func (h *MaintenanceHandlers) UpdateTaskConfig(c *gin.Context) { glog.Warningf("Failed to save task config to protobuf file: %v", err) // Don't fail the request, just log the warning } + } else if h.adminServer == nil { + glog.Warningf("Failed to save task config: admin server not initialized") } // Trigger a configuration reload in the maintenance manager @@ -492,74 +503,25 @@ func (h *MaintenanceHandlers) UpdateMaintenanceConfig(c *gin.Context) { // Helper methods that delegate to AdminServer func (h *MaintenanceHandlers) getMaintenanceQueueData() (*maintenance.MaintenanceQueueData, error) { - tasks, err := h.getMaintenanceTasks() - if err != nil { - return nil, err - } - - workers, err := h.getMaintenanceWorkers() - if err != nil { - return nil, err - } - - stats, err := h.getMaintenanceQueueStats() - if err != nil { - return nil, err - } - - data := &maintenance.MaintenanceQueueData{ - Tasks: tasks, - Workers: workers, - Stats: stats, - LastUpdated: time.Now(), - } - - return data, nil -} - -func (h *MaintenanceHandlers) getMaintenanceQueueStats() (*maintenance.QueueStats, error) { - // Use the exported method from AdminServer - return h.adminServer.GetMaintenanceQueueStats() -} - -func (h *MaintenanceHandlers) getMaintenanceTasks() ([]*maintenance.MaintenanceTask, error) { - // Call the maintenance manager directly to get recent tasks (limit for performance) if h.adminServer == nil { - return []*maintenance.MaintenanceTask{}, nil + return nil, fmt.Errorf("admin server not initialized") } - - manager := h.adminServer.GetMaintenanceManager() - if manager == nil { - return []*maintenance.MaintenanceTask{}, nil - } - - // Get recent tasks only (last 100) to prevent slow page loads - // Users can view more tasks via pagination if needed - allTasks := manager.GetTasks("", "", 100) - return allTasks, nil + // Use the exported method from AdminServer used by the JSON API + return h.adminServer.GetMaintenanceQueueData() } -func (h *MaintenanceHandlers) getMaintenanceWorkers() ([]*maintenance.MaintenanceWorker, error) { - // Get workers from the admin server's maintenance manager +func (h *MaintenanceHandlers) getMaintenanceConfig() (*maintenance.MaintenanceConfigData, error) { if h.adminServer == nil { - return []*maintenance.MaintenanceWorker{}, nil + return nil, fmt.Errorf("admin server not initialized") } - - if h.adminServer.GetMaintenanceManager() == nil { - return []*maintenance.MaintenanceWorker{}, nil - } - - // Get workers from the maintenance manager - workers := h.adminServer.GetMaintenanceManager().GetWorkers() - return workers, nil -} - -func (h *MaintenanceHandlers) getMaintenanceConfig() (*maintenance.MaintenanceConfigData, error) { // Delegate to AdminServer's real persistence method return h.adminServer.GetMaintenanceConfigData() } func (h *MaintenanceHandlers) updateMaintenanceConfig(config *maintenance.MaintenanceConfig) error { + if h.adminServer == nil { + return fmt.Errorf("admin server not initialized") + } // Delegate to AdminServer's real persistence method return h.adminServer.UpdateMaintenanceConfigData(config) } diff --git a/weed/admin/maintenance/maintenance_queue.go b/weed/admin/maintenance/maintenance_queue.go index d39c96a30..8c863fcf4 100644 --- a/weed/admin/maintenance/maintenance_queue.go +++ b/weed/admin/maintenance/maintenance_queue.go @@ -587,15 +587,35 @@ func (mq *MaintenanceQueue) GetTasks(status MaintenanceTaskStatus, taskType Main continue } tasks = append(tasks, task) - if limit > 0 && len(tasks) >= limit { - break - } } - // Sort by creation time (newest first) - sort.Slice(tasks, func(i, j int) bool { - return tasks[i].CreatedAt.After(tasks[j].CreatedAt) - }) + // Sort based on status + if status == TaskStatusCompleted || status == TaskStatusFailed || status == TaskStatusCancelled { + sort.Slice(tasks, func(i, j int) bool { + t1 := tasks[i].CompletedAt + t2 := tasks[j].CompletedAt + if t1 == nil && t2 == nil { + return tasks[i].CreatedAt.After(tasks[j].CreatedAt) + } + if t1 == nil { + return false + } + if t2 == nil { + return true + } + return t1.After(*t2) + }) + } else { + // Default to creation time (newest first) + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].CreatedAt.After(tasks[j].CreatedAt) + }) + } + + // Apply limit after sorting + if limit > 0 && len(tasks) > limit { + tasks = tasks[:limit] + } return tasks } diff --git a/weed/admin/view/app/admin_templ.go b/weed/admin/view/app/admin_templ.go index cbff92c5d..c32024b95 100644 --- a/weed/admin/view/app/admin_templ.go +++ b/weed/admin/view/app/admin_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_brokers_templ.go b/weed/admin/view/app/cluster_brokers_templ.go index 18b5b0c34..f176dc0d6 100644 --- a/weed/admin/view/app/cluster_brokers_templ.go +++ b/weed/admin/view/app/cluster_brokers_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_collections_templ.go b/weed/admin/view/app/cluster_collections_templ.go index 1e0234cbd..a800f63ea 100644 --- a/weed/admin/view/app/cluster_collections_templ.go +++ b/weed/admin/view/app/cluster_collections_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_ec_shards_templ.go b/weed/admin/view/app/cluster_ec_shards_templ.go index 05a557c4f..b46111da7 100644 --- a/weed/admin/view/app/cluster_ec_shards_templ.go +++ b/weed/admin/view/app/cluster_ec_shards_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_ec_volumes_templ.go b/weed/admin/view/app/cluster_ec_volumes_templ.go index d3420d320..42996be48 100644 --- a/weed/admin/view/app/cluster_ec_volumes_templ.go +++ b/weed/admin/view/app/cluster_ec_volumes_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_filers_templ.go b/weed/admin/view/app/cluster_filers_templ.go index c61c218fc..8aad3730e 100644 --- a/weed/admin/view/app/cluster_filers_templ.go +++ b/weed/admin/view/app/cluster_filers_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_masters_templ.go b/weed/admin/view/app/cluster_masters_templ.go index b10881bc0..e68949291 100644 --- a/weed/admin/view/app/cluster_masters_templ.go +++ b/weed/admin/view/app/cluster_masters_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_volume_servers_templ.go b/weed/admin/view/app/cluster_volume_servers_templ.go index 3a47df7d9..f27f088a5 100644 --- a/weed/admin/view/app/cluster_volume_servers_templ.go +++ b/weed/admin/view/app/cluster_volume_servers_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/cluster_volumes_templ.go b/weed/admin/view/app/cluster_volumes_templ.go index 117ae8585..66817e49f 100644 --- a/weed/admin/view/app/cluster_volumes_templ.go +++ b/weed/admin/view/app/cluster_volumes_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/collection_details_templ.go b/weed/admin/view/app/collection_details_templ.go index 16e025ed2..313b535a9 100644 --- a/weed/admin/view/app/collection_details_templ.go +++ b/weed/admin/view/app/collection_details_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/ec_volume_details_templ.go b/weed/admin/view/app/ec_volume_details_templ.go index 1092f1b86..d09665c8f 100644 --- a/weed/admin/view/app/ec_volume_details_templ.go +++ b/weed/admin/view/app/ec_volume_details_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/file_browser_templ.go b/weed/admin/view/app/file_browser_templ.go index 6da7ad22d..8e8793e99 100644 --- a/weed/admin/view/app/file_browser_templ.go +++ b/weed/admin/view/app/file_browser_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/iceberg_catalog_templ.go b/weed/admin/view/app/iceberg_catalog_templ.go index 2a74583cb..dc5d76691 100644 --- a/weed/admin/view/app/iceberg_catalog_templ.go +++ b/weed/admin/view/app/iceberg_catalog_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/iceberg_namespaces_templ.go b/weed/admin/view/app/iceberg_namespaces_templ.go index 8df314ef4..4d6d973e5 100644 --- a/weed/admin/view/app/iceberg_namespaces_templ.go +++ b/weed/admin/view/app/iceberg_namespaces_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/iceberg_tables_templ.go b/weed/admin/view/app/iceberg_tables_templ.go index 87084f0b8..06e82b8ca 100644 --- a/weed/admin/view/app/iceberg_tables_templ.go +++ b/weed/admin/view/app/iceberg_tables_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/maintenance_config_schema_templ.go b/weed/admin/view/app/maintenance_config_schema_templ.go index 289590fd6..c44e37ad4 100644 --- a/weed/admin/view/app/maintenance_config_schema_templ.go +++ b/weed/admin/view/app/maintenance_config_schema_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/maintenance_config_templ.go b/weed/admin/view/app/maintenance_config_templ.go index 75017b31d..685f51b07 100644 --- a/weed/admin/view/app/maintenance_config_templ.go +++ b/weed/admin/view/app/maintenance_config_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/maintenance_queue.templ b/weed/admin/view/app/maintenance_queue.templ index fa56cdb3f..d67ab56c2 100644 --- a/weed/admin/view/app/maintenance_queue.templ +++ b/weed/admin/view/app/maintenance_queue.templ @@ -302,12 +302,7 @@ templ MaintenanceQueue(data *maintenance.MaintenanceQueueData) { // Debug output to browser console console.log("DEBUG: Maintenance Queue Template loaded"); - // Auto-refresh every 10 seconds - setInterval(function() { - if (!document.hidden) { - window.location.reload(); - } - }, 10000); + window.triggerScan = function() { console.log("triggerScan called"); diff --git a/weed/admin/view/app/maintenance_queue_templ.go b/weed/admin/view/app/maintenance_queue_templ.go index bd2f845e5..6c06e7f80 100644 --- a/weed/admin/view/app/maintenance_queue_templ.go +++ b/weed/admin/view/app/maintenance_queue_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -610,7 +610,7 @@ func MaintenanceQueue(data *maintenance.MaintenanceQueueData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -809,7 +809,7 @@ func ProgressBar(progress float64, status maintenance.MaintenanceTaskStatus) tem var templ_7745c5c3_Var35 string templ_7745c5c3_Var35, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%", progress)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_queue.templ`, Line: 390, Col: 102} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_queue.templ`, Line: 385, Col: 102} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { @@ -822,7 +822,7 @@ func ProgressBar(progress float64, status maintenance.MaintenanceTaskStatus) tem var templ_7745c5c3_Var36 string templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f%%", progress)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_queue.templ`, Line: 393, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/maintenance_queue.templ`, Line: 388, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) if templ_7745c5c3_Err != nil { diff --git a/weed/admin/view/app/maintenance_workers_templ.go b/weed/admin/view/app/maintenance_workers_templ.go index a1de325b8..8c84361a9 100644 --- a/weed/admin/view/app/maintenance_workers_templ.go +++ b/weed/admin/view/app/maintenance_workers_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/object_store_users_templ.go b/weed/admin/view/app/object_store_users_templ.go index 2c11fe0e2..34b8cf517 100644 --- a/weed/admin/view/app/object_store_users_templ.go +++ b/weed/admin/view/app/object_store_users_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/policies_templ.go b/weed/admin/view/app/policies_templ.go index da911247c..8e1de7c23 100644 --- a/weed/admin/view/app/policies_templ.go +++ b/weed/admin/view/app/policies_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/s3_buckets_templ.go b/weed/admin/view/app/s3_buckets_templ.go index 6b58b5580..7644bd750 100644 --- a/weed/admin/view/app/s3_buckets_templ.go +++ b/weed/admin/view/app/s3_buckets_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/s3tables_buckets_templ.go b/weed/admin/view/app/s3tables_buckets_templ.go index 0312c44f2..56478162c 100644 --- a/weed/admin/view/app/s3tables_buckets_templ.go +++ b/weed/admin/view/app/s3tables_buckets_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/s3tables_namespaces_templ.go b/weed/admin/view/app/s3tables_namespaces_templ.go index e18605cb9..6cf818eaf 100644 --- a/weed/admin/view/app/s3tables_namespaces_templ.go +++ b/weed/admin/view/app/s3tables_namespaces_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/s3tables_tables_templ.go b/weed/admin/view/app/s3tables_tables_templ.go index 22c6e7ca1..25c03f269 100644 --- a/weed/admin/view/app/s3tables_tables_templ.go +++ b/weed/admin/view/app/s3tables_tables_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/service_accounts_templ.go b/weed/admin/view/app/service_accounts_templ.go index 64805f5b6..6905f9ac3 100644 --- a/weed/admin/view/app/service_accounts_templ.go +++ b/weed/admin/view/app/service_accounts_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/subscribers_templ.go b/weed/admin/view/app/subscribers_templ.go index 32b743da6..103963fe1 100644 --- a/weed/admin/view/app/subscribers_templ.go +++ b/weed/admin/view/app/subscribers_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/task_config_schema.templ b/weed/admin/view/app/task_config_schema.templ index 91d46190f..1c3393854 100644 --- a/weed/admin/view/app/task_config_schema.templ +++ b/weed/admin/view/app/task_config_schema.templ @@ -430,7 +430,7 @@ func getTaskConfigStringField(config interface{}, fieldName string) string { func getTaskNumberStep(field *config.Field) string { if field.Type == config.FieldTypeFloat { - return "0.01" + return "any" } return "1" } diff --git a/weed/admin/view/app/task_config_schema_templ.go b/weed/admin/view/app/task_config_schema_templ.go index ebe65a571..80f0c2871 100644 --- a/weed/admin/view/app/task_config_schema_templ.go +++ b/weed/admin/view/app/task_config_schema_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -891,7 +891,7 @@ func getTaskConfigStringField(config interface{}, fieldName string) string { func getTaskNumberStep(field *config.Field) string { if field.Type == config.FieldTypeFloat { - return "0.01" + return "any" } return "1" } diff --git a/weed/admin/view/app/task_config_templ.go b/weed/admin/view/app/task_config_templ.go index 243b5816e..d6edc5c64 100644 --- a/weed/admin/view/app/task_config_templ.go +++ b/weed/admin/view/app/task_config_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/task_config_templ_templ.go b/weed/admin/view/app/task_config_templ_templ.go index 5b5d261b0..3e2f60aee 100644 --- a/weed/admin/view/app/task_config_templ_templ.go +++ b/weed/admin/view/app/task_config_templ_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/weed/admin/view/app/task_detail.templ b/weed/admin/view/app/task_detail.templ index aa1a45260..0e46c713d 100644 --- a/weed/admin/view/app/task_detail.templ +++ b/weed/admin/view/app/task_detail.templ @@ -942,10 +942,48 @@ templ TaskDetail(data *maintenance.TaskDetailData) { return; } + // Format and display logs with structured fields let logText = ''; + + // Helper function to format timestamps robustly + function formatTimestamp(timestamp) { + if (!timestamp) { + return 'N/A'; + } + + let date; + + // Check if timestamp is a numeric string (e.g., "1738652668") + if (typeof timestamp === 'string' && /^\d+$/.test(timestamp)) { + const numericTimestamp = parseInt(timestamp, 10); + // Treat values > 10^10 as milliseconds, otherwise as seconds + date = numericTimestamp > 10000000000 + ? new Date(numericTimestamp) + : new Date(numericTimestamp * 1000); + } else if (typeof timestamp === 'string') { + // ISO date string + date = new Date(timestamp); + } else if (typeof timestamp === 'number') { + // Numeric timestamp (seconds or milliseconds) + date = timestamp > 10000000000 + ? new Date(timestamp) + : new Date(timestamp * 1000); + } else { + return 'N/A'; + } + + // Validate the date + if (isNaN(date.getTime())) { + return 'N/A'; + } + + return date.toISOString(); + } + logs.forEach(entry => { - const timestamp = entry.timestamp ? new Date(entry.timestamp * 1000).toISOString() : 'N/A'; + const timestamp = formatTimestamp(entry.timestamp); + const level = entry.level || 'INFO'; const message = entry.message || ''; @@ -1011,7 +1049,12 @@ templ TaskDetail(data *maintenance.TaskDetailData) { 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'; + let timestamp; + if (typeof entry.timestamp === 'string') { + timestamp = new Date(entry.timestamp).toISOString(); + } else { + timestamp = entry.timestamp ? new Date(entry.timestamp * 1000).toISOString() : 'N/A'; + } const level = entry.level || 'INFO'; const message = entry.message || ''; diff --git a/weed/admin/view/app/task_detail_templ.go b/weed/admin/view/app/task_detail_templ.go index 7e0657b5e..e1211c7b6 100644 --- a/weed/admin/view/app/task_detail_templ.go +++ b/weed/admin/view/app/task_detail_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.960 +// templ: version: v0.3.977 package app //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -1617,7 +1617,7 @@ func TaskDetail(data *maintenance.TaskDetailData) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 201, "\" onclick=\"exportTaskDetail(this.getAttribute('data-task-id'))\"> Export Details