From 5d3948250d7343f297e4d5c12f5ff781e6393e76 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 19 Apr 2026 12:23:27 +0800 Subject: [PATCH] Apply candidate config from preview UI --- cmd/managerd/main.go | 1 + internal/api/handlers.go | 3 ++ internal/api/handlers_test.go | 41 +++++++++++++++++++ internal/web/ui.go | 18 ++++++++ internal/web/ui/templates/config_preview.html | 2 + internal/web/ui_test.go | 1 + 6 files changed, 66 insertions(+) diff --git a/cmd/managerd/main.go b/cmd/managerd/main.go index 460f636..9326bda 100644 --- a/cmd/managerd/main.go +++ b/cmd/managerd/main.go @@ -74,6 +74,7 @@ func main() { r.Get("/devices/{id}/info", h.ProxyAgent) r.Get("/devices/{id}/config/status", h.ProxyAgent) r.Put("/devices/{id}/config/candidate", h.ProxyAgent) + r.Post("/devices/{id}/config/candidate/apply", h.ProxyAgent) r.Post("/devices/{id}/reload", h.ProxyAgent) r.Post("/devices/{id}/rollback", h.ProxyAgent) r.Get("/devices/{id}/graphs", h.ProxyAgent) diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 8e783e0..3d5d413 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -132,6 +132,9 @@ func (h *Handler) ProxyAgent(w http.ResponseWriter, r *http.Request) { case r.URL.Path == fmt.Sprintf("/api/devices/%s/config/candidate", id): agentPath = "/v1/config/candidate" method = "PUT" + case r.URL.Path == fmt.Sprintf("/api/devices/%s/config/candidate/apply", id): + agentPath = "/v1/config/candidate/apply" + method = "POST" case r.URL.Path == fmt.Sprintf("/api/devices/%s/reload", id): agentPath = "/v1/media-server/reload" method = "POST" diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 05741a1..d1dc4fd 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -184,3 +184,44 @@ func TestHandler_ProxyAgentMapsConfigCandidate(t *testing.T) { t.Fatalf("expected candidate response, got %s", rr.Body.String()) } } + +func TestHandler_ProxyAgentMapsConfigCandidateApply(t *testing.T) { + agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Fatalf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/v1/config/candidate/apply" { + t.Fatalf("expected /v1/config/candidate/apply, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true,"status":{"metadata":{"config_id":"candidate"}}}`)) + })) + defer agentServer.Close() + + host, portText, err := net.SplitHostPort(strings.TrimPrefix(agentServer.URL, "http://")) + if err != nil { + t.Fatalf("parse test server address: %v", err) + } + port, err := strconv.Atoi(portText) + if err != nil { + t.Fatalf("parse test server port: %v", err) + } + + cfg := &config.Config{} + agent := service.NewAgentClient(cfg) + reg := service.NewRegistryService(cfg, agent) + reg.UpdateDevice(&models.Device{DeviceID: "edge-01", IP: host, AgentPort: port}) + h := NewHandler(nil, reg, agent, nil, nil) + + r := chi.NewRouter() + r.Post("/api/devices/{id}/config/candidate/apply", h.ProxyAgent) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, httptest.NewRequest(http.MethodPost, "/api/devices/edge-01/config/candidate/apply", nil)) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", rr.Code, rr.Body.String()) + } + if !strings.Contains(rr.Body.String(), "candidate") { + t.Fatalf("expected candidate apply response, got %s", rr.Body.String()) + } +} diff --git a/internal/web/ui.go b/internal/web/ui.go index 164097d..35f4992 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -182,6 +182,7 @@ func (u *UI) Routes() (chi.Router, error) { r.Get("/devices/{id}/config-preview", u.pageDeviceConfigPreview) r.Post("/devices/{id}/config-preview", u.actionDeviceConfigPreview) r.Post("/devices/{id}/config-candidate", u.actionDeviceConfigCandidate) + r.Post("/devices/{id}/config-candidate/apply", u.actionDeviceConfigCandidateApply) r.Post("/devices/{id}/config-ui/plan", u.actionDeviceConfigUIPlan) r.Post("/devices/{id}/config-ui/apply", u.actionDeviceConfigUIApply) r.Post("/devices/{id}/face-gallery/upload", u.actionDeviceFaceGalleryUpload) @@ -906,6 +907,23 @@ func (u *UI) actionDeviceConfigCandidate(w http.ResponseWriter, r *http.Request) u.render(w, r, "config_preview", data) } +func (u *UI) actionDeviceConfigCandidateApply(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + dev, ok := u.findDevice(id) + if !ok { + http.NotFound(w, r) + return + } + data := u.configPreviewPageData(dev) + body, code, err := u.agent.Do("POST", dev.IP, dev.AgentPort, "/v1/config/candidate/apply", []byte(`{}`)) + data.Message = fmt.Sprintf("POST /v1/config/candidate/apply -> %d", code) + data.RawText = prettyJSON(body) + if err != nil { + data.Error = err.Error() + } + u.render(w, r, "config_preview", data) +} + func (u *UI) configPreviewPageData(dev *models.Device) PageData { sources, err := u.preview.ListSources() data := PageData{Title: "配置预览", Device: dev, ConfigSources: sources} diff --git a/internal/web/ui/templates/config_preview.html b/internal/web/ui/templates/config_preview.html index df2f13c..404aa19 100644 --- a/internal/web/ui/templates/config_preview.html +++ b/internal/web/ui/templates/config_preview.html @@ -54,6 +54,7 @@
+ 查看当前运行配置
@@ -80,6 +81,7 @@
+ 查看当前运行配置
{{.ConfigPreview.JSON}}
diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index e7a3d4a..6862c64 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -332,6 +332,7 @@ func TestUI_ConfigPreviewPageShowsTemplateProfileOverlayForm(t *testing.T) { "local_3588_test", "face_debug", "上传为候选配置", + "应用候选配置", } { if !strings.Contains(body, want) { t.Fatalf("expected config preview page to contain %q, got:\n%s", want, body)