183 lines
6.1 KiB
Go
183 lines
6.1 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"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"`
|
|
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"`
|
|
ErrorCode string `json:"error_code,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
}
|
|
|
|
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
|
|
Workers []WorkerCard
|
|
}
|
|
type JobDetailView struct {
|
|
JobCard
|
|
Tasks []TaskCard `json:"tasks"`
|
|
Artifacts []ArtifactCard `json:"artifacts"`
|
|
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 {
|
|
out.Jobs = append(out.Jobs, jobCard(job, tasksByJob[job.ID]))
|
|
}
|
|
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})
|
|
}
|
|
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
|
|
}
|
|
out := JobDetailView{JobCard: jobCard(*job, tasks), Tasks: make([]TaskCard, 0, len(tasks)), Artifacts: make([]ArtifactCard, 0, len(artifacts))}
|
|
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}
|
|
if task.LeaseOwner != nil {
|
|
card.LeaseOwner = *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 := diagnostic || (artifact.Kind == domain.ArtifactFinalResult && out.Status == string(domain.JobCompleted))
|
|
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
|
|
}
|
|
|
|
func (d *Dashboard) ArtifactBelongsToJob(ctx context.Context, jobID, artifactID uuid.UUID) (bool, error) {
|
|
artifacts, err := d.read.ListArtifactsByJob(ctx, jobID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, a := range artifacts {
|
|
if a.ID == artifactID {
|
|
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}
|
|
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
|
|
}
|