package store import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" "github.com/will/k8s-agent-dashboard/internal/models" ) // Store manages persistent state for the dashboard type Store struct { dataDir string mu sync.RWMutex // In-memory cache status *models.ClusterStatus pending []models.PendingAction history []models.ActionHistory workflows []models.Workflow } // New creates a new store instance func New(dataDir string) (*Store, error) { // Ensure data directory exists if err := os.MkdirAll(dataDir, 0755); err != nil { return nil, fmt.Errorf("failed to create data dir: %w", err) } s := &Store{ dataDir: dataDir, pending: make([]models.PendingAction, 0), history: make([]models.ActionHistory, 0), workflows: make([]models.Workflow, 0), } // Load existing data if err := s.load(); err != nil { return nil, err } return s, nil } func (s *Store) load() error { // Load pending actions pendingPath := filepath.Join(s.dataDir, "pending.json") if data, err := os.ReadFile(pendingPath); err == nil { if err := json.Unmarshal(data, &s.pending); err != nil { return fmt.Errorf("failed to parse pending.json: %w", err) } } // Load history historyPath := filepath.Join(s.dataDir, "history.json") if data, err := os.ReadFile(historyPath); err == nil { if err := json.Unmarshal(data, &s.history); err != nil { return fmt.Errorf("failed to parse history.json: %w", err) } } // Load status statusPath := filepath.Join(s.dataDir, "status.json") if data, err := os.ReadFile(statusPath); err == nil { s.status = &models.ClusterStatus{} if err := json.Unmarshal(data, s.status); err != nil { return fmt.Errorf("failed to parse status.json: %w", err) } } return nil } func (s *Store) save(filename string, data interface{}) error { path := filepath.Join(s.dataDir, filename) bytes, err := json.MarshalIndent(data, "", " ") if err != nil { return err } return os.WriteFile(path, bytes, 0644) } // GetClusterStatus returns the current cluster status func (s *Store) GetClusterStatus() *models.ClusterStatus { s.mu.RLock() defer s.mu.RUnlock() if s.status == nil { // Return demo status if none exists return &models.ClusterStatus{ Health: "Unknown", UpdatedAt: time.Now(), Nodes: []models.NodeStatus{}, Alerts: []models.Alert{}, Apps: []models.AppStatus{}, } } return s.status } // UpdateClusterStatus updates the cluster status func (s *Store) UpdateClusterStatus(status *models.ClusterStatus) error { s.mu.Lock() defer s.mu.Unlock() status.UpdatedAt = time.Now() s.status = status return s.save("status.json", status) } // GetPendingActions returns all pending actions func (s *Store) GetPendingActions() []models.PendingAction { s.mu.RLock() defer s.mu.RUnlock() return s.pending } // AddPendingAction adds a new pending action func (s *Store) AddPendingAction(action models.PendingAction) error { s.mu.Lock() defer s.mu.Unlock() action.CreatedAt = time.Now() s.pending = append(s.pending, action) return s.save("pending.json", s.pending) } // ApproveAction approves a pending action func (s *Store) ApproveAction(id string, reason string) (*models.PendingAction, error) { s.mu.Lock() defer s.mu.Unlock() for i, action := range s.pending { if action.ID == id { // Remove from pending s.pending = append(s.pending[:i], s.pending[i+1:]...) // Add to history historyEntry := models.ActionHistory{ ID: action.ID, Timestamp: time.Now(), Agent: action.Agent, Action: action.Action, Description: action.Description, Details: action.Details, Result: "approved", AutoApproved: false, Workflow: action.Workflow, } s.history = append([]models.ActionHistory{historyEntry}, s.history...) // Keep only last 100 history entries if len(s.history) > 100 { s.history = s.history[:100] } // Save both files s.save("pending.json", s.pending) s.save("history.json", s.history) return &action, nil } } return nil, fmt.Errorf("action not found: %s", id) } // RejectAction rejects a pending action func (s *Store) RejectAction(id string, reason string) (*models.PendingAction, error) { s.mu.Lock() defer s.mu.Unlock() for i, action := range s.pending { if action.ID == id { // Remove from pending s.pending = append(s.pending[:i], s.pending[i+1:]...) // Add to history as rejected historyEntry := models.ActionHistory{ ID: action.ID, Timestamp: time.Now(), Agent: action.Agent, Action: action.Action, Description: action.Description + " (REJECTED: " + reason + ")", Details: action.Details, Result: "rejected", AutoApproved: false, Workflow: action.Workflow, } s.history = append([]models.ActionHistory{historyEntry}, s.history...) if len(s.history) > 100 { s.history = s.history[:100] } s.save("pending.json", s.pending) s.save("history.json", s.history) return &action, nil } } return nil, fmt.Errorf("action not found: %s", id) } // GetActionHistory returns the action history func (s *Store) GetActionHistory(limit int) []models.ActionHistory { s.mu.RLock() defer s.mu.RUnlock() if limit <= 0 || limit > len(s.history) { return s.history } return s.history[:limit] } // GetWorkflows returns all defined workflows func (s *Store) GetWorkflows() []models.Workflow { s.mu.RLock() defer s.mu.RUnlock() // Return predefined workflows based on what we have in ~/.claude/workflows return []models.Workflow{ { Name: "cluster-health-check", Description: "Comprehensive cluster health assessment", Triggers: []string{"schedule: 0 */6 * * *", "manual"}, Status: "idle", }, { Name: "deploy-app", Description: "Deploy or update an application", Triggers: []string{"manual"}, Status: "idle", }, { Name: "pod-crashloop-remediation", Description: "Diagnose and remediate pods in CrashLoopBackOff", Triggers: []string{"alert: KubePodCrashLooping", "manual"}, Status: "idle", }, } }