package daemon import ( "context" "errors" "log/slog" "net/http" "strings" "time" "git.cloudinit.dev/coreci/orca/internal/model" "git.cloudinit.dev/coreci/orca/internal/store" ) // handleJobsCollection handles /v1/jobs. // - GET → list all jobs // - POST → not yet supported (job submission is CLI-only in v0.1) func (s *Server) handleJobsCollection(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() switch r.Method { case http.MethodGet: jobs, err := store.NewJobRepo(s.db).List(ctx) if err != nil { s.log.Error("list jobs", slog.String("component", "daemon"), slog.String("error", err.Error())) writeError(w, http.StatusInternalServerError, "failed to list jobs") return } if jobs == nil { jobs = []*model.Job{} } writeJSON(w, http.StatusOK, map[string]any{"jobs": jobs, "count": len(jobs)}) case http.MethodPost: // Job submission via HTTP is intentionally not exposed in v0.1. // The CLI submits jobs to the local store directly; the daemon // exists for observability and lifecycle control. writeError(w, http.StatusNotImplemented, "job submission via API is not supported in v0.1; use 'orca job run'") default: writeError(w, http.StatusMethodNotAllowed, "method not allowed") } } // handleJobsItem handles /v1/jobs/{id} and /v1/jobs/{id}/tasks. // - GET /v1/jobs/{id} → job details // - GET /v1/jobs/{id}/tasks → tasks for a job func (s *Server) handleJobsItem(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { writeError(w, http.StatusMethodNotAllowed, "method not allowed") return } ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() // Path is /v1/jobs/{id} or /v1/jobs/{id}/tasks path := strings.TrimPrefix(r.URL.Path, "/v1/jobs/") parts := strings.Split(path, "/") if len(parts) == 0 || parts[0] == "" { writeError(w, http.StatusBadRequest, "job id required") return } id := parts[0] if err := validateID(id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } // /v1/jobs/{id}/tasks if len(parts) == 2 && parts[1] == "tasks" { tasks, err := store.NewTaskRepo(s.db).ListByJob(ctx, id) if err != nil { s.log.Error("list tasks for job", slog.String("component", "daemon"), slog.String("job_id", id), slog.String("error", err.Error())) writeError(w, http.StatusInternalServerError, "failed to list tasks") return } if tasks == nil { tasks = []*model.Task{} } writeJSON(w, http.StatusOK, map[string]any{"tasks": tasks, "count": len(tasks), "job_id": id}) return } // /v1/jobs/{id} (with no further path) if len(parts) != 1 { writeError(w, http.StatusNotFound, "not found") return } job, err := store.NewJobRepo(s.db).Get(ctx, id) if err != nil { if errors.Is(err, store.ErrNotFound) { writeError(w, http.StatusNotFound, "job not found") return } s.log.Error("get job", slog.String("component", "daemon"), slog.String("job_id", id), slog.String("error", err.Error())) writeError(w, http.StatusInternalServerError, "failed to get job") return } writeJSON(w, http.StatusOK, job) }