package api import ( "encoding/json" "errors" "fmt" "net/http" "time" "github.com/jackc/pgx/v5" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) type scanScheduleResp struct { Mode string `json:"mode"` IntervalHours *int `json:"interval_hours"` TimeOfDay *string `json:"time_of_day"` WeeklyDay *int `json:"weekly_day"` NextScheduledAt *string `json:"next_scheduled_at"` } type scanScheduleReq struct { Mode string `json:"mode"` IntervalHours *int `json:"interval_hours"` TimeOfDay *string `json:"time_of_day"` WeeklyDay *int `json:"weekly_day"` } func (h *handlers) handleGetScanSchedule(w http.ResponseWriter, r *http.Request) { q := dbq.New(h.pool) row, err := q.GetScanSchedule(r.Context()) if err != nil { if errors.Is(err, pgx.ErrNoRows) { h.logger.Error("admin: scan_schedule singleton row missing") writeErr(w, http.StatusInternalServerError, "server_error", "schedule row missing") return } h.logger.Error("admin: get scan schedule", "err", err) writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed") return } writeJSON(w, http.StatusOK, scanScheduleRespFromRow(row, h.scheduler.NextFire())) } func (h *handlers) handlePatchScanSchedule(w http.ResponseWriter, r *http.Request) { var req scanScheduleReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, http.StatusBadRequest, "bad_body", "invalid JSON") return } if err := validateScheduleReq(&req); err != nil { writeErr(w, http.StatusBadRequest, "validation", err.Error()) return } q := dbq.New(h.pool) if err := q.UpdateScanSchedule(r.Context(), dbq.UpdateScanScheduleParams{ Mode: req.Mode, IntervalHours: pgInt32Ptr(req.IntervalHours), TimeOfDay: req.TimeOfDay, WeeklyDay: pgInt32Ptr(req.WeeklyDay), }); err != nil { h.logger.Error("admin: update scan schedule", "err", err) writeErr(w, http.StatusInternalServerError, "server_error", "update failed") return } h.scheduler.Refresh() row, err := q.GetScanSchedule(r.Context()) if err != nil { h.logger.Error("admin: re-read scan schedule", "err", err) writeErr(w, http.StatusInternalServerError, "server_error", "re-read failed") return } cfg := library.ScheduleConfig{Mode: row.Mode} if row.IntervalHours != nil { cfg.IntervalHours = int(*row.IntervalHours) } if row.TimeOfDay != nil { cfg.TimeOfDay = *row.TimeOfDay } if row.WeeklyDay != nil { cfg.WeeklyDay = int(*row.WeeklyDay) } next := cfg.NextFire(time.Now()) writeJSON(w, http.StatusOK, scanScheduleRespFromRow(row, next)) } func validateScheduleReq(req *scanScheduleReq) error { switch req.Mode { case "off": req.IntervalHours = nil req.TimeOfDay = nil req.WeeklyDay = nil return nil case "interval": if req.IntervalHours == nil || *req.IntervalHours <= 0 { return fmt.Errorf("interval_hours required and > 0 when mode=interval") } req.TimeOfDay = nil req.WeeklyDay = nil return nil case "daily": if req.TimeOfDay == nil || *req.TimeOfDay == "" { return fmt.Errorf("time_of_day required when mode=daily") } req.IntervalHours = nil req.WeeklyDay = nil return nil case "weekly": if req.TimeOfDay == nil || *req.TimeOfDay == "" { return fmt.Errorf("time_of_day required when mode=weekly") } if req.WeeklyDay == nil || *req.WeeklyDay < 1 || *req.WeeklyDay > 7 { return fmt.Errorf("weekly_day required and in [1, 7] when mode=weekly") } req.IntervalHours = nil return nil default: return fmt.Errorf("invalid mode %q (want off / interval / daily / weekly)", req.Mode) } } func scanScheduleRespFromRow(row dbq.GetScanScheduleRow, nextFire time.Time) scanScheduleResp { resp := scanScheduleResp{Mode: row.Mode} if row.IntervalHours != nil { v := int(*row.IntervalHours) resp.IntervalHours = &v } if row.TimeOfDay != nil { v := *row.TimeOfDay resp.TimeOfDay = &v } if row.WeeklyDay != nil { v := int(*row.WeeklyDay) resp.WeeklyDay = &v } if !nextFire.IsZero() { s := nextFire.UTC().Format(time.RFC3339) resp.NextScheduledAt = &s } return resp } func pgInt32Ptr(src *int) *int32 { if src == nil { return nil } v := int32(*src) return &v }