package dashboard import ( "fmt" "math" "net/http" "strconv" "strings" ) type settingsPageData struct { PageData Settings orchestratorSettingsDTO Inbound inboundChecksDTO // AutoCycle is the automatic-check-cycle panel's status/parameters. AutoCycle autoCycleDTO } func (s *Server) handleSettingsPage(w http.ResponseWriter, r *http.Request) { settings, err := s.CA.GetOrchestratorSettings(r.Context()) inbound, inboundErr := s.CA.GetInboundChecks(r.Context()) if err == nil { err = inboundErr } autoCycle, autoCycleErr := s.CA.GetAutoCycle(r.Context()) if err == nil { err = autoCycleErr } data := settingsPageData{Settings: settings, Inbound: inbound, AutoCycle: autoCycle} data.ActiveNav = "settings" data.Banner = bannerFor(err) s.renderPage(w, "settings_page", data) } // renderSettingsForm re-fetches the current settings and renders the // settings_form fragment, tagging actionErr (if any) on the shared error // banner — same pattern as renderIPsTable: always reflect true current // state regardless of whether the mutation itself succeeded. func (s *Server) renderSettingsForm(w http.ResponseWriter, r *http.Request, actionErr error) { settings, getErr := s.CA.GetOrchestratorSettings(r.Context()) if actionErr == nil { actionErr = getErr } inbound, inboundErr := s.CA.GetInboundChecks(r.Context()) if actionErr == nil { actionErr = inboundErr } autoCycle, autoCycleErr := s.CA.GetAutoCycle(r.Context()) if actionErr == nil { actionErr = autoCycleErr } s.renderFragment(w, "settings_form", settingsPageData{Settings: settings, Inbound: inbound, AutoCycle: autoCycle}, actionErr) } func (s *Server) handleSettingsPut(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderSettingsForm(w, r, fmt.Errorf("invalid form: %w", err)) return } seconds, err := strconv.Atoi(r.PostFormValue("fip_settle_seconds")) if err != nil { s.renderSettingsForm(w, r, &apiErr{Status: http.StatusBadRequest, Message: "пауза должна быть целым числом секунд"}) return } retentionCycles, err := strconv.Atoi(r.PostFormValue("history_retention_cycles")) if err != nil { s.renderSettingsForm(w, r, &apiErr{Status: http.StatusBadRequest, Message: "глубина истории должна быть целым числом циклов"}) return } _, err = s.CA.PutOrchestratorSettings(r.Context(), seconds, retentionCycles) s.renderSettingsForm(w, r, err) } // handleInboundChecksPut parses the comma-separated ports field of the // second form on /settings and saves it via PUT // /api/v1/admin/config/inbound-checks. A separate form/handler from // fip_settle_seconds above — the two settings are unrelated and shouldn't // share one submit. func (s *Server) handleInboundChecksPut(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderSettingsForm(w, r, fmt.Errorf("invalid form: %w", err)) return } raw := strings.TrimSpace(r.PostFormValue("ports")) var ports []int if raw != "" { for _, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } p, err := strconv.Atoi(part) if err != nil { s.renderSettingsForm(w, r, &apiErr{Status: http.StatusBadRequest, Message: "порты должны быть целыми числами через запятую"}) return } ports = append(ports, p) } } icmp := r.PostFormValue("icmp") == "on" _, err := s.CA.PutInboundChecks(r.Context(), ports, icmp) s.renderSettingsForm(w, r, err) } // parseMinutes converts a form field holding a (possibly fractional) number // of minutes into whole seconds. func parseMinutes(raw string) (int, error) { f, err := strconv.ParseFloat(strings.TrimSpace(strings.ReplaceAll(raw, ",", ".")), 64) if err != nil || math.IsNaN(f) || math.IsInf(f, 0) || math.Abs(f) > 1e6 { return 0, fmt.Errorf("not a number of minutes: %q", raw) } return int(math.Round(f * 60)), nil } // handleAutoCyclePut saves the auto-cycle interval and maximum run duration // (entered in minutes, sent to control-api in seconds). It does not touch // the enabled flag — that's what the start/stop buttons are for. func (s *Server) handleAutoCyclePut(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderSettingsForm(w, r, fmt.Errorf("invalid form: %w", err)) return } intervalSec, err := parseMinutes(r.PostFormValue("interval_minutes")) if err != nil { s.renderSettingsForm(w, r, &apiErr{Status: http.StatusBadRequest, Message: "интервал должен быть числом минут"}) return } maxRunSec, err := parseMinutes(r.PostFormValue("max_run_minutes")) if err != nil { s.renderSettingsForm(w, r, &apiErr{Status: http.StatusBadRequest, Message: "максимальная длительность должна быть числом минут (0 — без лимита)"}) return } _, err = s.CA.PutAutoCycle(r.Context(), intervalSec, maxRunSec) s.renderSettingsForm(w, r, err) } func (s *Server) handleAutoCycleStart(w http.ResponseWriter, r *http.Request) { _, err := s.CA.StartAutoCycle(r.Context()) s.renderSettingsForm(w, r, err) } func (s *Server) handleAutoCycleStop(w http.ResponseWriter, r *http.Request) { _, err := s.CA.StopAutoCycle(r.Context()) s.renderSettingsForm(w, r, err) }