package dashboard import ( "fmt" "net/http" ) // usedByTag names one check type that references a target group, and // whether that check type is currently enabled — rendered as a status pill // next to the group so the relationship (invisible before this page had // this column) is visible without cross-referencing /check-types by hand. type usedByTag struct { Name string Enabled bool } // targetGroupView is targetGroupDTO plus the check types that reference it, // resolved server-side from ListCheckTypes so the template stays a plain // range with no cross-referencing logic of its own. type targetGroupView struct { targetGroupDTO UsedBy []usedByTag } type targetsStats struct { Groups int TotalTargets int CheckTypesUsing int Unused int } type targetsPageData struct { PageData Items []targetGroupView Stats targetsStats } // loadTargetsPage fetches target groups and check types together and // resolves the group -> referencing-check-types relationship server-side. // A ListCheckTypes failure degrades to "no usage info" rather than hiding // the group list — the error still surfaces via the banner. func (s *Server) loadTargetsPage(r *http.Request) (targetsPageData, error) { groups, err := s.CA.ListTargetGroups(r.Context()) if err != nil { return targetsPageData{}, err } checkTypes, ctErr := s.CA.ListCheckTypes(r.Context()) usedBy := make(map[string][]usedByTag) checkTypesUsing := 0 for _, ct := range checkTypes { if len(ct.Targets) > 0 { checkTypesUsing++ } for _, g := range ct.Targets { usedBy[g] = append(usedBy[g], usedByTag{Name: ct.Name, Enabled: ct.Enabled}) } } items := make([]targetGroupView, len(groups)) totalTargets := 0 unused := 0 for i, g := range groups { items[i] = targetGroupView{targetGroupDTO: g, UsedBy: usedBy[g.Name]} totalTargets += len(g.Targets) if len(usedBy[g.Name]) == 0 { unused++ } } return targetsPageData{ Items: items, Stats: targetsStats{ Groups: len(groups), TotalTargets: totalTargets, CheckTypesUsing: checkTypesUsing, Unused: unused, }, }, ctErr } func (s *Server) handleTargetsPage(w http.ResponseWriter, r *http.Request) { data, err := s.loadTargetsPage(r) data.ActiveNav = "targets" data.Banner = bannerFor(err) s.renderPage(w, r, "targets_page", data) } // renderTargetsTable renders the #targets-table-wrap swap target plus, // appended to the same response, an out-of-band update of #targets-stats-wrap // (see targets_stats_oob in targets.html) — that block sits above the create // form, outside the swap target, so it needs its own refresh to stay live // after a create/update/delete. Can't use the shared renderFragment helper // here because of that second OOB block; otherwise mirrors it exactly, // including always appending error_banner last. func (s *Server) renderTargetsTable(w http.ResponseWriter, r *http.Request, actionErr error) { data, listErr := s.loadTargetsPage(r) if actionErr == nil { actionErr = listErr } w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := s.tmpl.ExecuteTemplate(w, "targets_table", data); err != nil { s.Log.Error("render fragment", "template", "targets_table", "err", err) return } if err := s.tmpl.ExecuteTemplate(w, "targets_stats_oob", data); err != nil { s.Log.Error("render targets stats oob", "err", err) } if err := s.tmpl.ExecuteTemplate(w, "error_banner", bannerFor(actionErr)); err != nil { s.Log.Error("render error banner", "err", err) } } func (s *Server) handleTargetCreate(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderTargetsTable(w, r, fmt.Errorf("invalid form: %w", err)) return } name := r.PostFormValue("name") targets := splitList(r.PostFormValue("targets")) if name == "" { s.renderTargetsTable(w, r, &apiErr{Status: http.StatusBadRequest, Message: "имя группы обязательно"}) return } err := s.CA.PutTargetGroup(r.Context(), name, targets) s.renderTargetsTable(w, r, err) } func (s *Server) handleTargetUpdate(w http.ResponseWriter, r *http.Request) { group := r.PathValue("group") if err := r.ParseForm(); err != nil { s.renderTargetsTable(w, r, fmt.Errorf("invalid form: %w", err)) return } targets := splitList(r.PostFormValue("targets")) err := s.CA.PutTargetGroup(r.Context(), group, targets) s.renderTargetsTable(w, r, err) } func (s *Server) handleTargetDelete(w http.ResponseWriter, r *http.Request) { group := r.PathValue("group") err := s.CA.DeleteTargetGroup(r.Context(), group) s.renderTargetsTable(w, r, err) }