From 1731a30c5cd3ee277ba2abe8a5e5a02401f67d00 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 19 Apr 2026 13:58:35 +0800 Subject: [PATCH] Refresh config status after apply --- internal/web/ui.go | 8 ++++ internal/web/ui_test.go | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/internal/web/ui.go b/internal/web/ui.go index 00685dd..f7dbe28 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -949,6 +949,14 @@ func (u *UI) actionDeviceConfigCandidateApply(w http.ResponseWriter, r *http.Req data.ResultTitle = "应用候选配置结果" if err != nil { data.Error = err.Error() + } else { + status, _, statusErr := u.loadConfigStatus(dev) + data.ConfigStatus = status + if statusErr != nil { + data.ConfigStatusErr = statusErr.Error() + } else { + data.ConfigStatusErr = "" + } } u.render(w, r, "config_preview", data) } diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index 2dfa268..6fd65fd 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -570,6 +570,92 @@ func TestUI_ConfigPreviewShowsApplySummaryAfterApplyResult(t *testing.T) { } } +func TestUI_ActionDeviceConfigCandidateApplyReloadsStatusAfterApply(t *testing.T) { + statusCalls := 0 + agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v1/config/status": + statusCalls++ + w.Header().Set("Content-Type", "application/json") + if statusCalls == 1 { + _, _ = w.Write([]byte(`{ + "ok": true, + "metadata": {"config_id": "preview_edge-01", "config_version": "v2"}, + "candidate": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"}, + "last_good": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}}, + "media_server": {"running": true, "pid": 1810489} + }`)) + return + } + _, _ = w.Write([]byte(`{ + "ok": true, + "metadata": {"config_id": "preview_edge-01", "config_version": "v2"}, + "candidate": {"exists": false, "path": "/opt/rk3588-media-server/etc/media-server.json.candidate.json"}, + "last_good": {"exists": true, "path": "/opt/rk3588-media-server/etc/media-server.json.last_good.json", "metadata": {"config_id": "local_3588_face_debug", "config_version": "20260419.120246"}}, + "media_server": {"running": true, "pid": 1810489} + }`)) + return + case r.Method == http.MethodPost && r.URL.Path == "/v1/config/candidate/apply": + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"ok":true}`)) + return + default: + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + } + })) + 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{Concurrency: 1, OfflineAfterMs: 1000000} + agent := service.NewAgentClient(cfg) + reg := service.NewRegistryService(cfg, agent) + reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: host, AgentPort: port, MediaPort: 9000, Online: true}) + tasks := service.NewTaskService(cfg, agent, reg) + ui, err := NewUI(nil, reg, agent, tasks, nil) + if err != nil { + t.Fatalf("NewUI: %v", err) + } + + form := url.Values{} + form.Set("json", `{"templates":{"tpl":{"nodes":[],"edges":[]}},"instances":[],"metadata":{"config_id":"preview_edge-01","config_version":"v2","template":"workshop_face_shoe_alarm","profile":"local_3588_test"}}`) + req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/config-candidate/apply", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetPathValue("id", "edge-01") + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", "edge-01") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + rr := httptest.NewRecorder() + + ui.actionDeviceConfigCandidateApply(rr, req) + + body := rr.Body.String() + if statusCalls != 2 { + t.Fatalf("expected status to be loaded twice, got %d", statusCalls) + } + for _, want := range []string{ + "应用结果摘要", + "preview_edge-01 / v2", + "local_3588_face_debug / 20260419.120246", + "已清空", + "运行中", + } { + if !strings.Contains(body, want) { + t.Fatalf("expected apply result HTML to contain %q, got:\n%s", want, body) + } + } + if strings.Contains(body, "仍存在") { + t.Fatalf("expected refreshed status after apply, got stale candidate state:\n%s", body) + } +} + func TestUI_ModelDeploymentPageRendersDeviceActions(t *testing.T) { ui := newTestUI(t) req := httptest.NewRequest(http.MethodGet, "/ui/models", nil)