1cc7eb6cad
Rewrite writeErr(w, err) to wrap *apierror.Error via apierror.From,
preserving the existing {"error": {"code", "message"}} wire envelope.
Add writeErrWithLog helper for 500-class errors that need an operator
log line. Migrate all 13 admin_*.go handler files (~76 call sites) to
the new signature; T3 will sweep the remaining api package.
The old 4-arg writeErr is removed, so non-admin call sites in
internal/api will not compile until T3 lands. This is by design — T2
and T3 are paired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
|
"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, apierror.Internal(errors.New("schedule row missing")))
|
|
return
|
|
}
|
|
writeErrWithLog(w, h.logger, "admin: get scan schedule", apierror.Internal(err))
|
|
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, apierror.BadRequest("bad_body", "invalid JSON"))
|
|
return
|
|
}
|
|
|
|
if err := validateScheduleReq(&req); err != nil {
|
|
writeErr(w, apierror.BadRequest("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 {
|
|
writeErrWithLog(w, h.logger, "admin: update scan schedule", apierror.Internal(err))
|
|
return
|
|
}
|
|
|
|
h.scheduler.Refresh()
|
|
|
|
row, err := q.GetScanSchedule(r.Context())
|
|
if err != nil {
|
|
writeErrWithLog(w, h.logger, "admin: re-read scan schedule", apierror.Internal(err))
|
|
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
|
|
}
|