// repo_maintenance.go — REST API for /api/hosts/{id}/repo-maintenance. // // Cadence rows for the three repo-wide verbs (forget / prune / check). // Edits do NOT bump host_schedule_version: the server-side maintenance // ticker drives execution (P2R-06), not the agent's local cron. package http import ( "encoding/json" "errors" stdhttp "net/http" "github.com/go-chi/chi/v5" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" ) type repoMaintenanceView struct { HostID string `json:"host_id"` ForgetCron string `json:"forget_cron"` ForgetEnabled bool `json:"forget_enabled"` PruneCron string `json:"prune_cron"` PruneEnabled bool `json:"prune_enabled"` CheckCron string `json:"check_cron"` CheckEnabled bool `json:"check_enabled"` CheckSubsetPct int `json:"check_subset_pct"` } func toRepoMaintenanceView(m store.HostRepoMaintenance) repoMaintenanceView { return repoMaintenanceView{ HostID: m.HostID, ForgetCron: m.ForgetCron, ForgetEnabled: m.ForgetEnabled, PruneCron: m.PruneCron, PruneEnabled: m.PruneEnabled, CheckCron: m.CheckCron, CheckEnabled: m.CheckEnabled, CheckSubsetPct: m.CheckSubsetPct, } } func (s *Server) handleGetRepoMaintenance(w stdhttp.ResponseWriter, r *stdhttp.Request) { if !s.authedUser(r) { writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "") return } hostID := chi.URLParam(r, "id") if _, err := s.deps.Store.GetHost(r.Context(), hostID); err != nil { writeJSONError(w, stdhttp.StatusNotFound, "host_not_found", "") return } m, err := s.deps.Store.GetRepoMaintenance(r.Context(), hostID) if err != nil { if errors.Is(err, store.ErrNotFound) { // Self-heal: seed and return the defaults so the UI never // has to handle a 404 here. Hosts enrolled before the // migration may legitimately be missing the row. if seedErr := s.deps.Store.CreateDefaultRepoMaintenance(r.Context(), hostID); seedErr != nil { writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "") return } m, err = s.deps.Store.GetRepoMaintenance(r.Context(), hostID) if err != nil { writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "") return } } else { writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "") return } } writeJSON(w, stdhttp.StatusOK, toRepoMaintenanceView(*m)) } type repoMaintenanceWriteRequest struct { ForgetCron string `json:"forget_cron"` ForgetEnabled bool `json:"forget_enabled"` PruneCron string `json:"prune_cron"` PruneEnabled bool `json:"prune_enabled"` CheckCron string `json:"check_cron"` CheckEnabled bool `json:"check_enabled"` CheckSubsetPct int `json:"check_subset_pct"` } func (s *Server) handleUpdateRepoMaintenance(w stdhttp.ResponseWriter, r *stdhttp.Request) { if !s.authedUser(r) { writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorised", "") return } hostID := chi.URLParam(r, "id") if _, err := s.deps.Store.GetHost(r.Context(), hostID); err != nil { writeJSONError(w, stdhttp.StatusNotFound, "host_not_found", "") return } var req repoMaintenanceWriteRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONError(w, stdhttp.StatusBadRequest, "invalid_json", err.Error()) return } for label, expr := range map[string]string{ "forget_cron": req.ForgetCron, "prune_cron": req.PruneCron, "check_cron": req.CheckCron, } { if expr == "" { writeJSONError(w, stdhttp.StatusBadRequest, "missing_field", label+" required") return } if _, err := cronParser.Parse(expr); err != nil { writeJSONError(w, stdhttp.StatusBadRequest, "invalid_cron", label+": "+err.Error()) return } } if req.CheckSubsetPct < 0 || req.CheckSubsetPct > 100 { writeJSONError(w, stdhttp.StatusBadRequest, "invalid_value", "check_subset_pct must be 0..100") return } // Ensure the row exists (older hosts may pre-date the auto-seed). if err := s.deps.Store.CreateDefaultRepoMaintenance(r.Context(), hostID); err != nil { writeJSONError(w, stdhttp.StatusInternalServerError, "internal", "") return } m := store.HostRepoMaintenance{ HostID: hostID, ForgetCron: req.ForgetCron, ForgetEnabled: req.ForgetEnabled, PruneCron: req.PruneCron, PruneEnabled: req.PruneEnabled, CheckCron: req.CheckCron, CheckEnabled: req.CheckEnabled, CheckSubsetPct: req.CheckSubsetPct, } if err := s.deps.Store.UpdateRepoMaintenance(r.Context(), &m); err != nil { writeJSONError(w, stdhttp.StatusInternalServerError, "internal", err.Error()) return } out, _ := s.deps.Store.GetRepoMaintenance(r.Context(), hostID) if out != nil { writeJSON(w, stdhttp.StatusOK, toRepoMaintenanceView(*out)) return } writeJSON(w, stdhttp.StatusOK, toRepoMaintenanceView(m)) }