package api import ( "encoding/json" "errors" "net/http" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/reacquisition" ) // reacquisitionSettingsResp is the admin card's payload (milestone #290). // // Carries more than the stored settings: [UnnameableAlbums] is the count of // albums with missing files that can never be auto-requested because neither // they nor their artist has an MBID. The card states that number rather than // leaving the operator to wonder why some rows never get a request — a // feature that silently does nothing for part of its input reads as broken. type reacquisitionSettingsResp struct { Enabled bool `json:"enabled"` GraceHours int32 `json:"grace_hours"` BackoffBaseHours int32 `json:"backoff_base_hours"` BackoffMaxHours int32 `json:"backoff_max_hours"` MaxAttempts int32 `json:"max_attempts"` MaxPerPass int32 `json:"max_per_pass"` AutoApprove bool `json:"auto_approve"` UnnameableAlbums int64 `json:"unnameable_albums"` } // handleGetReacquisitionSettings implements GET /api/admin/library/reacquisition. func (h *handlers) handleGetReacquisitionSettings(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, h.reacquisitionPayload(r)) } // handleUpdateReacquisitionSettings implements PUT /api/admin/library/reacquisition. func (h *handlers) handleUpdateReacquisitionSettings(w http.ResponseWriter, r *http.Request) { var req reacquisitionSettingsResp if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, apierror.BadRequest("invalid_body", "malformed JSON")) return } _, err := h.reacqSettings.Set(r.Context(), reacquisition.Settings{ Enabled: req.Enabled, GraceHours: req.GraceHours, BackoffBaseHours: req.BackoffBaseHours, BackoffMaxHours: req.BackoffMaxHours, MaxAttempts: req.MaxAttempts, MaxPerPass: req.MaxPerPass, AutoApprove: req.AutoApprove, }) if err != nil { // The Go-side validation mirrors migration 0056's CHECKs so the // operator gets a readable message naming the field, rather than a // constraint-violation string leaking through as a 500. if errors.Is(err, reacquisition.ErrOutOfRange) { writeErr(w, apierror.BadRequest("invalid_setting", err.Error())) return } writeErrWithLog(w, h.logger, "admin reacquisition: update failed", apierror.Internal(err)) return } // Echo the payload recomputed under the new values so the card reflects // what it just did without a reload. writeJSON(w, http.StatusOK, h.reacquisitionPayload(r)) } func (h *handlers) reacquisitionPayload(r *http.Request) reacquisitionSettingsResp { cur := h.reacqSettings.Get() out := reacquisitionSettingsResp{ Enabled: cur.Enabled, GraceHours: cur.GraceHours, BackoffBaseHours: cur.BackoffBaseHours, BackoffMaxHours: cur.BackoffMaxHours, MaxAttempts: cur.MaxAttempts, MaxPerPass: cur.MaxPerPass, AutoApprove: cur.AutoApprove, } // Best-effort: the settings are the point of this endpoint, and failing // the whole card because a count query hiccuped would be the wrong trade. if n, err := dbq.New(h.pool).CountAlbumsMissingWithoutMbid(r.Context()); err == nil { out.UnnameableAlbums = n } else { h.logger.Warn("admin reacquisition: unnameable count failed", "err", err) } return out }