package models import ( "context" "fmt" "log" "time" "git.ourworld.tf/herocode/heroagent/pkg/herojobs" ) // JobManager provides an interface for job management operations type JobManager interface { // GetAllJobs returns all jobs GetAllJobs() ([]*JobInfo, error) // GetJobsByCircle returns jobs for a specific circle GetJobsByCircle(circleID string) ([]*JobInfo, error) // GetJobsByTopic returns jobs for a specific topic GetJobsByTopic(topic string) ([]*JobInfo, error) // GetJobsByStatus returns jobs with a specific status GetJobsByStatus(status herojobs.JobStatus) ([]*JobInfo, error) // GetJob returns a specific job by ID GetJob(jobID uint32) (*JobInfo, error) } // JobInfo represents job information for the UI type JobInfo struct { JobID uint32 `json:"jobid"` SessionKey string `json:"sessionkey"` CircleID string `json:"circleid"` Topic string `json:"topic"` ParamsType herojobs.ParamsType `json:"params_type"` Status herojobs.JobStatus `json:"status"` TimeScheduled time.Time `json:"time_scheduled"` TimeStart time.Time `json:"time_start"` TimeEnd time.Time `json:"time_end"` Duration string `json:"duration"` Error string `json:"error"` HasError bool `json:"has_error"` } // HeroJobManager implements JobManager interface using herojobs package type HeroJobManager struct { factory *herojobs.Factory ctx context.Context } // NewHeroJobManager creates a new HeroJobManager func NewHeroJobManager(redisURL string) (*HeroJobManager, error) { factory, err := herojobs.NewFactory(redisURL) if err != nil { return nil, fmt.Errorf("failed to create job factory: %w", err) } return &HeroJobManager{ factory: factory, ctx: context.Background(), }, nil } // GetAllJobs returns all jobs from Redis func (jm *HeroJobManager) GetAllJobs() ([]*JobInfo, error) { // This is a simplified implementation // In a real-world scenario, you would need to: // 1. Get all circles and topics // 2. For each circle/topic combination, get all jobs // 3. Combine the results // For now, we'll just list all job IDs from Redis jobIDs, err := jm.factory.ListJobs(jm.ctx) if err != nil { return nil, fmt.Errorf("failed to list jobs: %w", err) } jobs := make([]*JobInfo, 0, len(jobIDs)) for _, jobID := range jobIDs { // Extract job ID from the key // Assuming the key format is "job:" jobData, err := jm.factory.GetJob(jm.ctx, jobID) if err != nil { log.Printf("Warning: failed to get job %s: %v", jobID, err) continue } // Parse job data job, err := herojobs.NewJobFromJSON(jobData) if err != nil { log.Printf("Warning: failed to parse job data for %s: %v", jobID, err) continue } // Convert to JobInfo jobInfo := convertToJobInfo(job) jobs = append(jobs, jobInfo) } return jobs, nil } // GetJobsByCircle returns jobs for a specific circle func (jm *HeroJobManager) GetJobsByCircle(circleID string) ([]*JobInfo, error) { // Implementation would filter jobs by circle ID // For now, return all jobs and filter in memory allJobs, err := jm.GetAllJobs() if err != nil { return nil, err } filteredJobs := make([]*JobInfo, 0) for _, job := range allJobs { if job.CircleID == circleID { filteredJobs = append(filteredJobs, job) } } return filteredJobs, nil } // GetJobsByTopic returns jobs for a specific topic func (jm *HeroJobManager) GetJobsByTopic(topic string) ([]*JobInfo, error) { // Implementation would filter jobs by topic // For now, return all jobs and filter in memory allJobs, err := jm.GetAllJobs() if err != nil { return nil, err } filteredJobs := make([]*JobInfo, 0) for _, job := range allJobs { if job.Topic == topic { filteredJobs = append(filteredJobs, job) } } return filteredJobs, nil } // GetJobsByStatus returns jobs with a specific status func (jm *HeroJobManager) GetJobsByStatus(status herojobs.JobStatus) ([]*JobInfo, error) { // Implementation would filter jobs by status // For now, return all jobs and filter in memory allJobs, err := jm.GetAllJobs() if err != nil { return nil, err } filteredJobs := make([]*JobInfo, 0) for _, job := range allJobs { if job.Status == status { filteredJobs = append(filteredJobs, job) } } return filteredJobs, nil } // GetJob returns a specific job by ID func (jm *HeroJobManager) GetJob(jobID uint32) (*JobInfo, error) { // Implementation would get a specific job by ID // This is a placeholder implementation allJobs, err := jm.GetAllJobs() if err != nil { return nil, err } for _, job := range allJobs { if job.JobID == jobID { return job, nil } } return nil, fmt.Errorf("job not found: %d", jobID) } // convertToJobInfo converts a herojobs.Job to a JobInfo func convertToJobInfo(job *herojobs.Job) *JobInfo { // Convert Unix timestamps to time.Time timeScheduled := time.Unix(job.TimeScheduled, 0) timeStart := time.Time{} if job.TimeStart > 0 { timeStart = time.Unix(job.TimeStart, 0) } timeEnd := time.Time{} if job.TimeEnd > 0 { timeEnd = time.Unix(job.TimeEnd, 0) } // Calculate duration var duration string if job.TimeStart > 0 { if job.TimeEnd > 0 { // Job has completed duration = time.Unix(job.TimeEnd, 0).Sub(time.Unix(job.TimeStart, 0)).String() } else { // Job is still running duration = time.Since(time.Unix(job.TimeStart, 0)).String() } } else { duration = "Not started" } return &JobInfo{ JobID: job.JobID, SessionKey: job.SessionKey, CircleID: job.CircleID, Topic: job.Topic, ParamsType: job.ParamsType, Status: job.Status, TimeScheduled: timeScheduled, TimeStart: timeStart, TimeEnd: timeEnd, Duration: duration, Error: job.Error, HasError: job.Error != "", } } // MockJobManager is a mock implementation of JobManager for testing type MockJobManager struct{} // NewMockJobManager creates a new MockJobManager func NewMockJobManager() *MockJobManager { return &MockJobManager{} } // GetAllJobs returns mock jobs func (m *MockJobManager) GetAllJobs() ([]*JobInfo, error) { return generateMockJobs(), nil } // GetJobsByCircle returns mock jobs for a circle func (m *MockJobManager) GetJobsByCircle(circleID string) ([]*JobInfo, error) { allJobs := generateMockJobs() filteredJobs := make([]*JobInfo, 0) for _, job := range allJobs { if job.CircleID == circleID { filteredJobs = append(filteredJobs, job) } } return filteredJobs, nil } // GetJobsByTopic returns mock jobs for a topic func (m *MockJobManager) GetJobsByTopic(topic string) ([]*JobInfo, error) { allJobs := generateMockJobs() filteredJobs := make([]*JobInfo, 0) for _, job := range allJobs { if job.Topic == topic { filteredJobs = append(filteredJobs, job) } } return filteredJobs, nil } // GetJobsByStatus returns mock jobs with a status func (m *MockJobManager) GetJobsByStatus(status herojobs.JobStatus) ([]*JobInfo, error) { allJobs := generateMockJobs() filteredJobs := make([]*JobInfo, 0) for _, job := range allJobs { if job.Status == status { filteredJobs = append(filteredJobs, job) } } return filteredJobs, nil } // GetJob returns a mock job by ID func (m *MockJobManager) GetJob(jobID uint32) (*JobInfo, error) { allJobs := generateMockJobs() for _, job := range allJobs { if job.JobID == jobID { return job, nil } } return nil, fmt.Errorf("job not found: %d", jobID) } // generateMockJobs generates mock jobs for testing func generateMockJobs() []*JobInfo { now := time.Now() return []*JobInfo{ { JobID: 1, CircleID: "circle1", Topic: "email", ParamsType: herojobs.ParamsTypeHeroScript, Status: herojobs.JobStatusDone, TimeScheduled: now.Add(-30 * time.Minute), TimeStart: now.Add(-29 * time.Minute), TimeEnd: now.Add(-28 * time.Minute), Duration: "1m0s", Error: "", HasError: false, }, { JobID: 2, CircleID: "circle1", Topic: "backup", ParamsType: herojobs.ParamsTypeOpenRPC, Status: herojobs.JobStatusActive, TimeScheduled: now.Add(-15 * time.Minute), TimeStart: now.Add(-14 * time.Minute), TimeEnd: time.Time{}, Duration: "14m0s", Error: "", HasError: false, }, { JobID: 3, CircleID: "circle2", Topic: "sync", ParamsType: herojobs.ParamsTypeRhaiScript, Status: herojobs.JobStatusError, TimeScheduled: now.Add(-45 * time.Minute), TimeStart: now.Add(-44 * time.Minute), TimeEnd: now.Add(-43 * time.Minute), Duration: "1m0s", Error: "Failed to connect to remote server", HasError: true, }, { JobID: 4, CircleID: "circle2", Topic: "email", ParamsType: herojobs.ParamsTypeHeroScript, Status: herojobs.JobStatusNew, TimeScheduled: now.Add(-5 * time.Minute), TimeStart: time.Time{}, TimeEnd: time.Time{}, Duration: "Not started", Error: "", HasError: false, }, { JobID: 5, CircleID: "circle3", Topic: "ai", ParamsType: herojobs.ParamsTypeAI, Status: herojobs.JobStatusDone, TimeScheduled: now.Add(-60 * time.Minute), TimeStart: now.Add(-59 * time.Minute), TimeEnd: now.Add(-40 * time.Minute), Duration: "19m0s", Error: "", HasError: false, }, } }