feat(web): update-mode toggle and available-version badge (#8)

This commit is contained in:
2026-10-12 20:41:41 +02:00
parent 5d95a35b3b
commit 71d66846f2
6 changed files with 239 additions and 8 deletions
+66 -3
View File
@@ -32,6 +32,8 @@ var agentNamePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
type Store interface {
ListServices(ctx context.Context) ([]model.Service, error)
GetService(ctx context.Context, id int64) (model.Service, error)
// SetUpdateMode returns model.ErrNotFound for an unknown or removed service.
SetUpdateMode(ctx context.Context, id int64, mode string) error
GetJob(ctx context.Context, id int64) (model.Job, error)
ListJobs(ctx context.Context, f model.JobFilter) ([]model.Job, error)
ListAgents(ctx context.Context) ([]model.Agent, error)
@@ -46,10 +48,11 @@ type Hub interface {
Disconnect(agentID int64)
}
// Jobs starts updates and explains why a service cannot be updated.
// *jobs.Service satisfies it.
// Jobs starts updates and explains why a service cannot be updated or set
// to auto mode. *jobs.Service satisfies it.
type Jobs interface {
Updatable(svc model.Service) error
AutoAllowed(svc model.Service) error
Trigger(ctx context.Context, serviceID int64, requestedBy string) (model.Job, error)
}
@@ -104,6 +107,7 @@ func (s *Server) Register(mux *http.ServeMux, protect func(http.Handler) http.Ha
route("GET /services/{id}", s.servicePage)
route("GET /fragments/services/{id}/jobs", s.serviceJobsFragment)
route("POST /services/{id}/update", s.updateService)
route("POST /services/{id}/mode", s.setMode)
route("GET /jobs", s.jobsPage)
route("GET /fragments/jobs", s.jobsFragment)
route("GET /jobs/{id}", s.jobPage)
@@ -127,6 +131,7 @@ type pageData struct {
Service model.Service
ConfigFiles []string
Reason string // why Service cannot be updated; "" when it can
AutoBlocked string // why Service cannot be set to auto; "" when it can
Jobs []model.Job
Job model.Job
}
@@ -150,7 +155,7 @@ func (s *Server) serviceData(r *http.Request) (pageData, error) {
if err != nil {
return data, err
}
data.Groups = groupServices(services)
data.Groups = groupServices(services, s.autoBlocked)
return data, nil
}
@@ -172,6 +177,15 @@ func (s *Server) servicesFragment(w http.ResponseWriter, r *http.Request) {
s.render(w, http.StatusOK, "overview.html", "services", data)
}
// autoBlocked returns the reason svc cannot be set to auto mode, or "".
func (s *Server) autoBlocked(svc model.Service) string {
var nu *model.NotUpdatableError
if err := s.jobs.AutoAllowed(svc); errors.As(err, &nu) {
return nu.Reason
}
return ""
}
// pathID parses the {id} path value. On failure it has already answered 400.
func pathID(w http.ResponseWriter, r *http.Request) (int64, bool) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
@@ -207,6 +221,7 @@ func (s *Server) serviceDetail(r *http.Request, id int64) (pageData, error) {
} else if err != nil {
return data, err
}
data.AutoBlocked = s.autoBlocked(svc)
data.Jobs, err = s.store.ListJobs(r.Context(), model.JobFilter{ServiceID: id, Limit: serviceJobsLimit})
return data, err
}
@@ -263,6 +278,54 @@ func (s *Server) updateService(w http.ResponseWriter, r *http.Request) {
}
}
// setMode switches a service between manual and auto. Manual is always
// allowed; auto only when jobs.AutoAllowed says so.
func (s *Server) setMode(w http.ResponseWriter, r *http.Request) {
id, ok := pathID(w, r)
if !ok {
return
}
mode := r.PostFormValue("mode")
if mode != model.ModeManual && mode != model.ModeAuto {
http.Error(w, "invalid mode", http.StatusBadRequest)
return
}
if mode == model.ModeAuto {
svc, err := s.store.GetService(r.Context(), id)
if err == nil && svc.RemovedAt != nil {
err = model.ErrNotFound
}
if err != nil {
s.failLookup(w, err)
return
}
var nu *model.NotUpdatableError
if err := s.jobs.AutoAllowed(svc); errors.As(err, &nu) {
data, derr := s.serviceDetail(r, id)
if derr != nil {
s.failLookup(w, derr)
return
}
data.Error = "Cannot enable auto: " + nu.Reason + "."
s.render(w, http.StatusConflict, "service.html", "layout", data)
return
} else if err != nil {
s.fail(w, err)
return
}
}
if err := s.store.SetUpdateMode(r.Context(), id, mode); err != nil {
s.failLookup(w, err)
return
}
s.broker.Publish(events.Services)
if r.PostFormValue("return") == "overview" {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/services/"+strconv.FormatInt(id, 10), http.StatusSeeOther)
}
func (s *Server) jobsData(r *http.Request) (pageData, error) {
data := s.base(r, "Jobs")
var err error