// run_group.go — per-source-group Run-now endpoint. // // POST /hosts/{id}/source-groups/{gid}/run dispatches a backup job // against the resolved includes/excludes/retention/tag of the named // group. Replaces the old per-host /hosts/{id}/run-backup route (now // 410 Gone). package http import ( "errors" stdhttp "net/http" "github.com/go-chi/chi/v5" "gitea.dcglab.co.uk/steve/restic-manager/internal/api" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" ) func (s *Server) handleRunSourceGroup(w stdhttp.ResponseWriter, r *stdhttp.Request) { user, ok := s.requireUser(r) if !ok { // HTML callers redirect to login; for JSON return 401. if wantsHTML(r) { stdhttp.Redirect(w, r, "/login", stdhttp.StatusSeeOther) return } writeJSONError(w, stdhttp.StatusUnauthorized, "unauthorized", "") return } hostID := chi.URLParam(r, "id") groupID := chi.URLParam(r, "gid") g, err := s.deps.Store.GetSourceGroup(r.Context(), hostID, groupID) if err != nil { if errors.Is(err, store.ErrNotFound) { s.runGroupError(w, r, stdhttp.StatusNotFound, "group_not_found", "source group not found on this host") return } s.runGroupError(w, r, stdhttp.StatusInternalServerError, "internal", "") return } // Backup invocations don't consume RetentionPolicy — that lives on // forget. Sending the resolved set here would just be dead weight. res, status, code, msg := s.dispatchJobWithPayload(r.Context(), user, hostID, api.JobBackup, api.CommandRunPayload{ Includes: g.Includes, Excludes: g.Excludes, Tag: g.Name, }) if code != "" { s.runGroupError(w, r, status, code, msg) return } if wantsHTML(r) { // HTMX action: redirect to the live job log so the operator // sees streaming output immediately. w.Header().Set("HX-Redirect", "/jobs/"+res.JobID) w.WriteHeader(stdhttp.StatusNoContent) return } writeJSON(w, stdhttp.StatusAccepted, res) } // runGroupError dispatches an error to JSON callers as the standard // envelope; HTMX callers get a 4xx with a plain text body so the // browser surfaces it via the existing toast handler. func (s *Server) runGroupError(w stdhttp.ResponseWriter, r *stdhttp.Request, status int, code, msg string) { if wantsHTML(r) { stdhttp.Error(w, msg, status) return } writeJSONError(w, status, code, msg) } // wantsHTML keys off HX-Request only. Browsers sending a default // Accept (or curl's `*/*`) get the JSON shape, which is the safer // default for non-htmx clients. HTMX always sets HX-Request=true on // its action POSTs, so the form path is unambiguous. func wantsHTML(r *stdhttp.Request) bool { return r.Header.Get("HX-Request") == "true" }