package app import ( "errors" "fmt" ) // OperationPhase describes coordination state independently of any terminal UI. type OperationPhase uint8 const ( OperationRunning OperationPhase = iota + 1 OperationAwaitingConfirmation OperationSucceeded OperationFailed OperationBlocked ) // StatusKind gives presentation layers semantics without requiring them to // inspect user-facing status text. type StatusKind uint8 const ( StatusProgress StatusKind = iota + 1 StatusSuccess StatusError StatusBlocked StatusConfirmation ) // OperationStatus is the current concise status plus optional expandable // detail. TargetID relates a result to the Target it can explicitly resolve. type OperationStatus struct { Kind StatusKind Summary string Detail string TargetID string } // Confirmation is a structured, safe-default decision. Presentation layers // render these fields but never infer confirmation state from their wording. type Confirmation struct { Title string Summary string Details []string ConfirmLabel string CancelLabel string HighImpact bool } // TargetProgressPhase describes one Target's part in an operation stream. type TargetProgressPhase uint8 const ( TargetProgressRunning TargetProgressPhase = iota + 1 TargetProgressSucceeded TargetProgressFailed ) // OperationUpdate is the common progressive contract used by interactive // detection, Target mutation, and Active Profile transition operations. type OperationUpdate struct { Snapshot Snapshot Phase OperationPhase Status OperationStatus Progress []TargetProgress Confirmation *Confirmation FirstFailedTargetID string Err error } // OperationStream exposes the useful initial frame synchronously and all // subsequent structured changes through Updates. Updates closes after a // terminal phase. type OperationStream struct { Snapshot Snapshot Updates <-chan OperationUpdate } // InteractiveModule is the presentation-independent Application seam used by // the TUI. Module.Dispatch remains available for synchronous callers. type InteractiveModule interface { Module StartOperation(Intent) OperationStream } // StartOperation rejects overlap instead of queuing hidden interactive work. // Synchronous Dispatch callers retain the established serialized behavior. func (a *Application) StartOperation(intent Intent) OperationStream { a.interactiveMu.Lock() if a.interactiveOperation { a.interactiveMu.Unlock() updates := make(chan OperationUpdate, 1) updates <- OperationUpdate{ Snapshot: a.Snapshot(), Phase: OperationBlocked, Status: OperationStatus{Kind: StatusBlocked, Summary: "Another system operation is already in progress", Detail: "Wait for the current operation to finish before starting another modification."}, } close(updates) return OperationStream{Snapshot: a.Snapshot(), Updates: updates} } a.interactiveOperation = true a.interactiveMu.Unlock() if intent.Kind == RefreshTargets { return a.startDetectionOperation() } initial := a.Snapshot() updates := make(chan OperationUpdate, 2*len(a.targets)+2) go func() { defer close(updates) defer a.finishInteractiveOperation() final := a.dispatch(intent, func(result Result) { updates <- operationUpdate(result, false) }) updates <- operationUpdate(final, true) }() return OperationStream{Snapshot: initial, Updates: updates} } func (a *Application) startDetectionOperation() OperationStream { initial, detection := a.BeginTargetDetection() updates := make(chan OperationUpdate, len(a.targets)+1) go func() { defer close(updates) defer a.finishInteractiveOperation() var failures []error firstFailed := "" for item := range detection { if item.Complete { update := OperationUpdate{ Snapshot: item.Snapshot, Phase: OperationSucceeded, Status: OperationStatus{Kind: StatusSuccess, Summary: "Target availability and state checks completed"}, } if len(failures) > 0 { update.Phase = OperationFailed update.Err = errors.Join(failures...) update.FirstFailedTargetID = firstFailed update.Status = OperationStatus{Kind: StatusError, Summary: "Some Target checks failed", Detail: update.Err.Error(), TargetID: firstFailed} } updates <- update continue } targetSnapshot, name := targetByID(item.Snapshot, item.TargetID) progress := TargetProgress{TargetID: item.TargetID, State: targetSnapshot.State, Evidence: targetSnapshot.Evidence, Phase: TargetProgressSucceeded} status := OperationStatus{Kind: StatusProgress, Summary: "Checked " + name + " Target", TargetID: item.TargetID} if targetSnapshot.DetectionPhase == TargetDetectionFailed { progress.Phase = TargetProgressFailed progress.Err = targetSnapshot.DetectionErr status = OperationStatus{Kind: StatusError, Summary: "Could not check " + name + " Target", TargetID: item.TargetID} if targetSnapshot.DetectionErr != nil { status.Detail = targetSnapshot.DetectionErr.Error() failures = append(failures, fmt.Errorf("%s Target: %w", name, targetSnapshot.DetectionErr)) } if firstFailed == "" { firstFailed = item.TargetID } } updates <- OperationUpdate{Snapshot: item.Snapshot, Phase: OperationRunning, Status: status, Progress: []TargetProgress{progress}, Err: progress.Err} } }() return OperationStream{Snapshot: initial, Updates: updates} } func (a *Application) finishInteractiveOperation() { a.interactiveMu.Lock() a.interactiveOperation = false a.interactiveMu.Unlock() } func targetByID(snapshot Snapshot, id string) (TargetSnapshot, string) { for _, item := range snapshot.Targets { if item.ID == id { name := item.Description.Name if name == "" { name = id } return item, name } } return TargetSnapshot{ID: id}, id } func operationUpdate(result Result, terminal bool) OperationUpdate { update := OperationUpdate{ Snapshot: result.Snapshot, Phase: OperationRunning, Status: result.Status, Progress: result.Progress, Confirmation: result.ConfirmationRequest, FirstFailedTargetID: result.FirstFailedTargetID, Err: result.Err, } if result.ConfirmationRequest != nil { update.Phase = OperationAwaitingConfirmation if update.Status.Kind == 0 { update.Status = OperationStatus{Kind: StatusConfirmation, Summary: result.ConfirmationRequest.Title, Detail: result.ConfirmationRequest.Summary} } return update } if update.Status.Kind == 0 { switch { case result.Err != nil: update.Status = OperationStatus{Kind: StatusError, Summary: "Operation failed", Detail: result.Err.Error()} case result.Notice != "": update.Status = OperationStatus{Kind: StatusSuccess, Summary: result.Notice} } } if !terminal { return update } if result.Err != nil { update.Phase = OperationFailed if update.Status.Kind != StatusError { update.Status = OperationStatus{Kind: StatusError, Summary: "Operation failed", Detail: result.Err.Error(), TargetID: update.Status.TargetID} } } else { update.Phase = OperationSucceeded if update.Status.Kind == StatusProgress || update.Status.Kind == 0 { update.Status.Kind = StatusSuccess } } return update }