package usecase import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/emil28092005/SciMesh/coordinator/internal/domain" ) // UIReadRepository is a read-only projection source for the local operator UI. // It intentionally exposes no storage paths or credentials. type UIReadRepository interface { GetJob(ctx context.Context, jobID uuid.UUID) (*domain.Job, error) ListJobs(ctx context.Context, limit int) ([]domain.Job, error) ListTasksByJob(ctx context.Context, jobID uuid.UUID) ([]domain.Task, error) ListTasksByJobs(ctx context.Context, jobIDs []uuid.UUID) (map[uuid.UUID][]domain.Task, error) ListWorkers(ctx context.Context, limit int) ([]domain.Worker, error) ListArtifactsByJob(ctx context.Context, jobID uuid.UUID) ([]domain.Artifact, error) } type JobCard struct { ID string `json:"id"` Workload string `json:"workload"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` CompletedAt *time.Time `json:"completed_at,omitempty"` ReducerStartedAt *time.Time `json:"reducer_started_at,omitempty"` ErrorCode string `json:"error_code,omitempty"` ErrorMessage string `json:"error_message,omitempty"` Total int `json:"total"` Pending int `json:"pending"` Leased int `json:"leased"` Running int `json:"running"` Completed int `json:"completed"` Failed int `json:"failed"` Cancelled int `json:"cancelled"` } type TaskCard struct { ID string `json:"id"` ChunkIndex int `json:"chunk_index"` Status string `json:"status"` Attempt int `json:"attempt"` MaxAttempts int `json:"max_attempts"` LeaseOwner string `json:"lease_owner,omitempty"` LeaseExpiresAt *time.Time `json:"lease_expires_at,omitempty"` StartedAt *time.Time `json:"started_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` ErrorCode string `json:"error_code,omitempty"` ErrorMessage string `json:"error_message,omitempty"` } // ParameterCard is an intentionally small allowlist of run configuration that // helps an operator verify what is being computed without exposing arbitrary // job payloads to the browser. type ParameterCard struct { Label string `json:"label"` Value string `json:"value"` } type ArtifactCard struct { ID string `json:"id"` Kind string `json:"kind"` Filename string `json:"filename"` SizeBytes int64 `json:"size_bytes"` SHA256 string `json:"sha256"` Downloadable bool `json:"downloadable"` Diagnostic bool `json:"diagnostic"` } type WorkerCard struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` Capabilities []string `json:"capabilities"` LastHeartbeatAt time.Time `json:"last_heartbeat_at"` } type DashboardView struct { Jobs []JobCard `json:"jobs"` Workers []WorkerCard `json:"workers"` ActiveJobs int `json:"active_jobs"` FinishedJobs int `json:"finished_jobs"` OnlineWorkers int `json:"online_workers"` } type JobDetailView struct { JobCard Tasks []TaskCard `json:"tasks"` Artifacts []ArtifactCard `json:"artifacts"` Parameters []ParameterCard `json:"parameters"` FinalResultAvailable bool `json:"final_result_available"` } type Dashboard struct{ read UIReadRepository } func NewDashboard(read UIReadRepository) *Dashboard { return &Dashboard{read: read} } func (d *Dashboard) Overview(ctx context.Context, limit int) (DashboardView, error) { jobs, err := d.read.ListJobs(ctx, limit) if err != nil { return DashboardView{}, err } workers, err := d.read.ListWorkers(ctx, limit) if err != nil { return DashboardView{}, err } out := DashboardView{Jobs: make([]JobCard, 0, len(jobs)), Workers: make([]WorkerCard, 0, len(workers))} jobIDs := make([]uuid.UUID, 0, len(jobs)) for _, job := range jobs { jobIDs = append(jobIDs, job.ID) } tasksByJob, err := d.read.ListTasksByJobs(ctx, jobIDs) if err != nil { return DashboardView{}, err } for _, job := range jobs { card := jobCard(job, tasksByJob[job.ID]) out.Jobs = append(out.Jobs, card) switch card.Status { case string(domain.JobCompleted), string(domain.JobFailed), string(domain.JobCancelled): out.FinishedJobs++ default: out.ActiveJobs++ } } for _, worker := range workers { out.Workers = append(out.Workers, WorkerCard{ID: worker.ID.String(), Name: worker.Name, Status: string(worker.Status), Capabilities: worker.Capabilities, LastHeartbeatAt: worker.LastHeartbeatAt}) if worker.Status == domain.WorkerOnline || worker.Status == domain.WorkerBusy { out.OnlineWorkers++ } } return out, nil } func (d *Dashboard) JobDetail(ctx context.Context, jobID uuid.UUID) (JobDetailView, error) { job, err := d.read.GetJob(ctx, jobID) if err != nil { return JobDetailView{}, err } tasks, err := d.read.ListTasksByJob(ctx, jobID) if err != nil { return JobDetailView{}, err } artifacts, err := d.read.ListArtifactsByJob(ctx, jobID) if err != nil { return JobDetailView{}, err } workers, err := d.read.ListWorkers(ctx, 100) if err != nil { return JobDetailView{}, err } workerNames := make(map[string]string, len(workers)) for _, worker := range workers { workerNames[worker.ID.String()] = worker.Name } out := JobDetailView{ JobCard: jobCard(*job, tasks), Tasks: make([]TaskCard, 0, len(tasks)), Artifacts: make([]ArtifactCard, 0, len(artifacts)), Parameters: uiParameters(job.Parameters), } for _, task := range tasks { card := TaskCard{ID: task.ID.String(), ChunkIndex: task.ChunkIndex, Status: string(task.Status), Attempt: task.Attempt, MaxAttempts: task.MaxAttempts, LeaseExpiresAt: task.LeaseExpiresAt, StartedAt: task.StartedAt, CompletedAt: task.CompletedAt} if task.LeaseOwner != nil { card.LeaseOwner = workerNames[*task.LeaseOwner] if card.LeaseOwner == "" { card.LeaseOwner = "Worker " + shortID(*task.LeaseOwner) } } if task.ErrorCode != nil { card.ErrorCode = *task.ErrorCode } if task.ErrorMessage != nil { card.ErrorMessage = *task.ErrorMessage } out.Tasks = append(out.Tasks, card) } for _, artifact := range artifacts { diagnostic := artifact.Kind == domain.ArtifactPartialResult downloadable := previewableArtifact(*job, artifact) out.Artifacts = append(out.Artifacts, ArtifactCard{ID: artifact.ID.String(), Kind: string(artifact.Kind), Filename: artifact.Filename, SizeBytes: artifact.SizeBytes, SHA256: artifact.SHA256, Downloadable: downloadable, Diagnostic: diagnostic}) if artifact.Kind == domain.ArtifactFinalResult && downloadable { out.FinalResultAvailable = true } } return out, nil } // DownloadableArtifactBelongsToJob applies the same policy used by the UI // projection: partial diagnostics and the persisted final result are public to // the operator; source inputs and shards are not exposed through a guessed UI // URL. func (d *Dashboard) DownloadableArtifactBelongsToJob(ctx context.Context, jobID, artifactID uuid.UUID) (bool, error) { job, err := d.read.GetJob(ctx, jobID) if err != nil { return false, err } artifacts, err := d.read.ListArtifactsByJob(ctx, jobID) if err != nil { return false, err } for _, a := range artifacts { if a.ID == artifactID && previewableArtifact(*job, a) { return true, nil } } return false, nil } func jobCard(job domain.Job, tasks []domain.Task) JobCard { c := JobCard{ID: job.ID.String(), Workload: job.Workload, CreatedAt: job.CreatedAt, CompletedAt: job.CompletedAt, ReducerStartedAt: job.ReducerStartedAt} if job.ErrorCode != nil { c.ErrorCode = *job.ErrorCode } if job.ErrorMessage != nil { c.ErrorMessage = *job.ErrorMessage } for _, task := range tasks { c.Total++ switch task.Status { case domain.TaskPending: c.Pending++ case domain.TaskLeased: c.Leased++ case domain.TaskRunning: c.Running++ case domain.TaskCompleted: c.Completed++ case domain.TaskFailed: c.Failed++ case domain.TaskCancelled: c.Cancelled++ } } p := domain.JobProgress{Job: job, Total: c.Total, Pending: c.Pending, Leased: c.Leased + c.Running, Done: c.Completed, Failed: c.Failed, Cancelled: c.Cancelled} c.Status = string(p.DeriveStatus()) return c } func uiParameters(parameters map[string]any) []ParameterCard { keys := []struct { key string label string }{ {"query_smiles", "Target SMILES"}, {"query_id", "Target ChEMBL ID"}, {"top_k", "Global top-k"}, {"threshold", "Similarity threshold"}, {"threshold_direction", "Threshold direction"}, } out := make([]ParameterCard, 0, len(keys)) for _, entry := range keys { value, ok := parameters[entry.key] if !ok { continue } formatted, ok := formatUIParameter(value) if ok { out = append(out, ParameterCard{Label: entry.label, Value: formatted}) } } return out } func formatUIParameter(value any) (string, bool) { switch typed := value.(type) { case string: return typed, true case int, int64, float64, bool: return fmt.Sprint(typed), true default: return "", false } } func shortID(value string) string { if len(value) <= 8 { return value } return value[:8] }