package board import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // TaskState represents the actual state of a task type TaskState struct { TaskID string `json:"task_id"` Status string `json:"status"` // "pending", "in_progress", "completed", "failed" CompletedAt time.Time `json:"completed_at,omitempty"` FailedAt time.Time `json:"failed_at,omitempty"` Error string `json:"error,omitempty"` Branch string `json:"branch,omitempty"` Metrics map[string]interface{} `json:"metrics,omitempty"` } // StateTracker tracks actual task states type StateTracker struct { mu sync.RWMutex basePath string states map[string]*TaskState lastUpdate time.Time } // NewStateTracker creates a new state tracker func NewStateTracker(basePath string) *StateTracker { return &StateTracker{ basePath: basePath, states: make(map[string]*TaskState), } } // UpdateTaskState updates the state of a task func (st *StateTracker) UpdateTaskState(taskID, status, branch string, err error) error { st.mu.Lock() defer st.mu.Unlock() errorMsg := "" if err != nil { errorMsg = err.Error() } state := &TaskState{ TaskID: taskID, Status: status, Branch: branch, Error: errorMsg, Metrics: make(map[string]interface{}), } if status == "completed" { state.CompletedAt = time.Now() } else if status == "failed" { state.FailedAt = time.Now() } st.states[taskID] = state st.lastUpdate = time.Now() return st.persistLocked() } // GetTaskState retrieves the state of a task func (st *StateTracker) GetTaskState(taskID string) *TaskState { st.mu.RLock() defer st.mu.RUnlock() return st.states[taskID] } // GetAllStates returns all task states func (st *StateTracker) GetAllStates() map[string]*TaskState { st.mu.RLock() defer st.mu.RUnlock() // Return a copy copy := make(map[string]*TaskState) for k, v := range st.states { copy[k] = v } return copy } // GetCompletedTasks returns all completed tasks func (st *StateTracker) GetCompletedTasks() []string { st.mu.RLock() defer st.mu.RUnlock() completed := make([]string, 0) for _, state := range st.states { if state.Status == "completed" { completed = append(completed, state.TaskID) } } return completed } // GetFailedTasks returns all failed tasks func (st *StateTracker) GetFailedTasks() []string { st.mu.RLock() defer st.mu.RUnlock() failed := make([]string, 0) for _, state := range st.states { if state.Status == "failed" { failed = append(failed, state.TaskID) } } return failed } // GetPendingTasks returns all pending tasks func (st *StateTracker) GetPendingTasks() []string { st.mu.RLock() defer st.mu.RUnlock() pending := make([]string, 0) for _, state := range st.states { if state.Status == "pending" || state.Status == "in_progress" { pending = append(pending, state.TaskID) } } return pending } // AddMetric adds a metric to a task func (st *StateTracker) AddMetric(taskID, metricName string, value interface{}) error { st.mu.Lock() defer st.mu.Unlock() state, exists := st.states[taskID] if !exists { return fmt.Errorf("task state not found: %s", taskID) } state.Metrics[metricName] = value st.lastUpdate = time.Now() return st.persistLocked() } // Load loads state from disk func (st *StateTracker) Load() error { st.mu.Lock() defer st.mu.Unlock() statePath := filepath.Join(st.basePath, "board", "state.json") data, err := os.ReadFile(statePath) if err != nil { if os.IsNotExist(err) { return nil // File doesn't exist yet } return err } var states []TaskState if err := json.Unmarshal(data, &states); err != nil { return err } st.states = make(map[string]*TaskState) for i := range states { st.states[states[i].TaskID] = &states[i] } return nil } // persistLocked saves state to disk (must be called with lock held) func (st *StateTracker) persistLocked() error { states := make([]TaskState, 0) for _, state := range st.states { states = append(states, *state) } data, err := json.MarshalIndent(states, "", " ") if err != nil { return err } statePath := filepath.Join(st.basePath, "board", "state.json") // Create directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil { return err } return os.WriteFile(statePath, data, 0644) } // GetAsCompletionMap returns task completion status as a boolean map func (st *StateTracker) GetAsCompletionMap() map[string]bool { st.mu.RLock() defer st.mu.RUnlock() completion := make(map[string]bool) for taskID, state := range st.states { completion[taskID] = state.Status == "completed" } return completion } // GetLastUpdate returns the last time state was updated func (st *StateTracker) GetLastUpdate() time.Time { st.mu.RLock() defer st.mu.RUnlock() return st.lastUpdate } // GetStats returns statistics about task states func (st *StateTracker) GetStats() map[string]interface{} { st.mu.RLock() defer st.mu.RUnlock() stats := make(map[string]interface{}) counts := make(map[string]int) for _, state := range st.states { counts[state.Status]++ } stats["total"] = len(st.states) stats["counts"] = counts stats["last_update"] = st.lastUpdate return stats } // Reset clears all state func (st *StateTracker) Reset() error { st.mu.Lock() defer st.mu.Unlock() st.states = make(map[string]*TaskState) st.lastUpdate = time.Time{} return st.persistLocked() }