package api import ( "encoding/json" "net/http" "strings" "time" "github.com/ponzischeme89/memby/server/internal/scheduler" "github.com/ponzischeme89/memby/server/internal/store" ) type adminTasksResponse struct { Tasks []scheduler.Status `json:"tasks"` Groups []string `json:"groups"` Runs []store.TaskRun `json:"runs"` } // handleAdminTasks is the scheduled-tasks page: the registry as it stands plus the most // recent runs across every task. // // The recent-runs list is included rather than being a second request because the useful // reading of this page is chronological — "what has the gateway been doing overnight" — // and per-task last-run cells alone cannot show two jobs interfering with each other. func (s *Server) handleAdminTasks(w http.ResponseWriter, r *http.Request) { response := adminTasksResponse{ Tasks: s.scheduler.Snapshot(), Groups: s.scheduler.Groups(), Runs: []store.TaskRun{}, } if runs, err := s.store.TaskRuns(r.Context(), strings.TrimSpace(r.URL.Query().Get("task")), queryInt(r, "limit", 40, 500)); err == nil { response.Runs = runs } w.Header().Set("Cache-Control", "no-store") writeJSON(w, http.StatusOK, response) } // handleAdminRunTask starts one task by hand. // // It answers as soon as the run has *started*, not when it finishes: the console watches // the run history for the outcome, and a button that blocked for the length of an // overnight job would look like a page that had hung. func (s *Server) handleAdminRunTask(w http.ResponseWriter, r *http.Request) { id := strings.TrimSpace(r.PathValue("taskID")) if err := s.scheduler.RunNow(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.loggerFor(r.Context()).Info("scheduled task started by hand", "task", id) writeJSON(w, http.StatusAccepted, map[string]any{"started": true, "taskId": id}) } type taskSettingsRequest struct { Enabled *bool `json:"enabled"` IntervalSeconds *int `json:"intervalSeconds"` } // handleAdminTaskSettings changes a task's enabled state or its cadence. // // Both fields are pointers so an unset one means "leave it alone": the console has two // separate controls on a row, and a payload that carried the whole task would have each // control silently reasserting whatever the page happened to be showing for the other. func (s *Server) handleAdminTaskSettings(w http.ResponseWriter, r *http.Request) { id := strings.TrimSpace(r.PathValue("taskID")) var req taskSettingsRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "malformed request body") return } if req.Enabled != nil { if err := s.scheduler.SetEnabled(r.Context(), id, *req.Enabled); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.loggerFor(r.Context()).Info("scheduled task switched", "task", id, "enabled", *req.Enabled) } if req.IntervalSeconds != nil { interval := time.Duration(*req.IntervalSeconds) * time.Second if err := s.scheduler.SetInterval(r.Context(), id, interval); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.loggerFor(r.Context()).Info("scheduled task interval changed", "task", id, "interval_seconds", *req.IntervalSeconds) } s.handleAdminTasks(w, r) }