package service import ( "bufio" "encoding/json" "strings" "time" ) const progressLogPrefix = "METIS_PROGRESS " // RemoteProgressUpdate is emitted by remote workers so the UI can show // concrete stage transitions instead of relying only on elapsed-time guesses. type RemoteProgressUpdate struct { Stage string `json:"stage,omitempty"` ProgressPct float64 `json:"progress_pct,omitempty"` Message string `json:"message,omitempty"` WrittenBytes int64 `json:"written_bytes,omitempty"` TotalBytes int64 `json:"total_bytes,omitempty"` } // RemoteFlashResult captures the verified outcome of a remote flash worker run. type RemoteFlashResult struct { Node string `json:"node,omitempty"` Device string `json:"device,omitempty"` DestPath string `json:"dest_path,omitempty"` SizeBytes int64 `json:"size_bytes,omitempty"` Verified bool `json:"verified,omitempty"` VerificationKind string `json:"verification_kind,omitempty"` VerificationSummary string `json:"verification_summary,omitempty"` BootPartition string `json:"boot_partition,omitempty"` RootPartition string `json:"root_partition,omitempty"` BootLabel string `json:"boot_label,omitempty"` RootLabel string `json:"root_label,omitempty"` BootFSType string `json:"boot_fstype,omitempty"` RootFSType string `json:"root_fstype,omitempty"` CheckedFiles []string `json:"checked_files,omitempty"` } // ProgressLogLine formats a progress update for remote worker stdout. func ProgressLogLine(update RemoteProgressUpdate) string { data, err := json.Marshal(update) if err != nil { return "" } return progressLogPrefix + string(data) } func parseRemoteProgressLogs(logs string) (RemoteProgressUpdate, bool) { scanner := bufio.NewScanner(strings.NewReader(logs)) scanner.Buffer(make([]byte, 0, 4096), 1<<20) var latest RemoteProgressUpdate found := false for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if !strings.HasPrefix(line, progressLogPrefix) { continue } raw := strings.TrimSpace(strings.TrimPrefix(line, progressLogPrefix)) var update RemoteProgressUpdate if err := json.Unmarshal([]byte(raw), &update); err != nil { continue } latest = update found = true } return latest, found } func (a *App) applyRemoteProgress(jobID string, update RemoteProgressUpdate) { if strings.TrimSpace(jobID) == "" { return } a.setJob(jobID, func(j *Job) { if j == nil || j.Status != JobRunning { return } if stage := strings.TrimSpace(update.Stage); stage != "" && stage != j.Stage { j.Stage = stage j.StageStartedAt = time.Now().UTC() } if update.ProgressPct > j.ProgressPct { j.ProgressPct = update.ProgressPct } if message := strings.TrimSpace(update.Message); message != "" { j.Message = message } if update.WrittenBytes > 0 { j.Written = update.WrittenBytes } if update.TotalBytes > 0 { j.Total = update.TotalBytes } }) }