89 lines
3.3 KiB
Go
89 lines
3.3 KiB
Go
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)
|
||
|
|
}
|