package fleet import ( "sync" "time" "github.com/google/uuid" ) // BatchJob tracks a crucible batch dispatch. type BatchJob struct { ID string `json:"id"` Command string `json:"command"` HostIDs []string `json:"host_ids"` Results []BatchResult `json:"results"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` } // BatchResult is one host outcome in a batch job. type BatchResult struct { HostID string `json:"host_id"` Hostname string `json:"hostname,omitempty"` Status string `json:"status"` Message string `json:"message,omitempty"` CommandID string `json:"command_id,omitempty"` } // CrucibleStore holds in-memory batch job history. type CrucibleStore struct { mu sync.RWMutex jobs map[string]*BatchJob max int } func NewCrucibleStore(maxHistory int) *CrucibleStore { if maxHistory <= 0 { maxHistory = 100 } return &CrucibleStore{ jobs: make(map[string]*BatchJob), max: maxHistory, } } func (c *CrucibleStore) Create(command string, hostIDs []string) *BatchJob { job := &BatchJob{ ID: uuid.NewString(), Command: command, HostIDs: append([]string(nil), hostIDs...), Results: make([]BatchResult, 0, len(hostIDs)), Status: "running", CreatedAt: time.Now().UTC(), } c.mu.Lock() c.jobs[job.ID] = job c.trimLocked() c.mu.Unlock() return job } func (c *CrucibleStore) Get(id string) (*BatchJob, bool) { c.mu.RLock() defer c.mu.RUnlock() job, ok := c.jobs[id] return job, ok } func (c *CrucibleStore) AddResult(jobID string, result BatchResult) { c.mu.Lock() defer c.mu.Unlock() job, ok := c.jobs[jobID] if !ok { return } job.Results = append(job.Results, result) } func (c *CrucibleStore) Complete(jobID, status string) { c.mu.Lock() defer c.mu.Unlock() if job, ok := c.jobs[jobID]; ok { job.Status = status } } func (c *CrucibleStore) History(limit int) []*BatchJob { if limit <= 0 { limit = 20 } c.mu.RLock() defer c.mu.RUnlock() jobs := make([]*BatchJob, 0, len(c.jobs)) for _, j := range c.jobs { jobs = append(jobs, j) } // Sort by created_at desc (simple bubble for small sets) for i := 0; i < len(jobs); i++ { for j := i + 1; j < len(jobs); j++ { if jobs[j].CreatedAt.After(jobs[i].CreatedAt) { jobs[i], jobs[j] = jobs[j], jobs[i] } } } if len(jobs) > limit { jobs = jobs[:limit] } return jobs } func (c *CrucibleStore) trimLocked() { if len(c.jobs) <= c.max { return } oldest := "" var oldestTime time.Time for id, j := range c.jobs { if oldest == "" || j.CreatedAt.Before(oldestTime) { oldest = id oldestTime = j.CreatedAt } } if oldest != "" { delete(c.jobs, oldest) } }