diff --git a/cmd/managerd/main.go b/cmd/managerd/main.go index 334ec64..460f636 100644 --- a/cmd/managerd/main.go +++ b/cmd/managerd/main.go @@ -73,6 +73,7 @@ func main() { // Proxy routes for device actions 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}/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 b78977f..8e783e0 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -129,6 +129,9 @@ func (h *Handler) ProxyAgent(w http.ResponseWriter, r *http.Request) { agentPath = "/v1/info" case r.URL.Path == fmt.Sprintf("/api/devices/%s/config/status", id): agentPath = "/v1/config/status" + 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/reload", id): agentPath = "/v1/media-server/reload" method = "POST" diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go index 8025579..05741a1 100644 --- a/internal/api/handlers_test.go +++ b/internal/api/handlers_test.go @@ -143,3 +143,44 @@ func TestHandler_ProxyAgentMapsConfigStatus(t *testing.T) { t.Fatalf("expected config status response, got %s", rr.Body.String()) } } + +func TestHandler_ProxyAgentMapsConfigCandidate(t *testing.T) { + agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Fatalf("expected PUT, got %s", r.Method) + } + if r.URL.Path != "/v1/config/candidate" { + t.Fatalf("expected /v1/config/candidate, got %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true,"path":"/tmp/media-server.json.candidate.json"}`)) + })) + 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.Put("/api/devices/{id}/config/candidate", h.ProxyAgent) + rr := httptest.NewRecorder() + r.ServeHTTP(rr, httptest.NewRequest(http.MethodPut, "/api/devices/edge-01/config/candidate", strings.NewReader(`{"metadata":{"config_id":"preview"},"templates":{"t":{}},"instances":[]}`))) + + 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 response, got %s", rr.Body.String()) + } +} diff --git a/internal/web/ui.go b/internal/web/ui.go index a37fd28..164097d 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -181,6 +181,7 @@ func (u *UI) Routes() (chi.Router, error) { r.Get("/devices/{id}/config-friendly", u.pageDeviceConfigFriendly) 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-ui/plan", u.actionDeviceConfigUIPlan) r.Post("/devices/{id}/config-ui/apply", u.actionDeviceConfigUIApply) r.Post("/devices/{id}/face-gallery/upload", u.actionDeviceFaceGalleryUpload) @@ -876,6 +877,35 @@ func (u *UI) actionDeviceConfigPreview(w http.ResponseWriter, r *http.Request) { u.render(w, r, "config_preview", data) } +func (u *UI) actionDeviceConfigCandidate(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + dev, ok := u.findDevice(id) + if !ok { + http.NotFound(w, r) + return + } + _ = r.ParseForm() + raw := strings.TrimSpace(r.FormValue("json")) + data := u.configPreviewPageData(dev) + if raw == "" { + data.Error = "候选配置 JSON 不能为空" + u.render(w, r, "config_preview", data) + return + } + if err := json.Unmarshal([]byte(raw), new(any)); err != nil { + data.Error = "候选配置 JSON 无效: " + err.Error() + u.render(w, r, "config_preview", data) + return + } + body, code, err := u.agent.Do("PUT", dev.IP, dev.AgentPort, "/v1/config/candidate", []byte(raw)) + data.Message = fmt.Sprintf("PUT /v1/config/candidate -> %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 9ca847d..df2f13c 100644 --- a/internal/web/ui/templates/config_preview.html +++ b/internal/web/ui/templates/config_preview.html @@ -53,6 +53,7 @@
+ 查看当前运行配置
@@ -76,7 +77,19 @@

完整 JSON

+
+ + + 查看当前运行配置 +
{{.ConfigPreview.JSON}}
{{end}} + +{{if .RawText}} +
+

上传结果

+
{{.RawText}}
+
+{{end}} {{end}} diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index 7e66d43..e7a3d4a 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -331,6 +331,7 @@ func TestUI_ConfigPreviewPageShowsTemplateProfileOverlayForm(t *testing.T) { "workshop_face_shoe_alarm", "local_3588_test", "face_debug", + "上传为候选配置", } { if !strings.Contains(body, want) { t.Fatalf("expected config preview page to contain %q, got:\n%s", want, body)