目标节点`) {
t.Fatalf("model summary should not use large stat cards")
}
}
func TestUI_DiagnosticsPageRendersLogAndMetricLinks(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/diagnostics", nil)
rr := httptest.NewRecorder()
ui.pageDiagnostics(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
body := rr.Body.String()
for _, want := range []string{"日志分析", "诊断工作台", "入口识别节点", "诊断日志", "运行指标", "高级调试"} {
if !strings.Contains(body, want) {
t.Fatalf("expected diagnostics HTML to contain %q", want)
}
}
}
func TestUI_SidebarMatchesInformationArchitecture(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/devices", nil)
rr := httptest.NewRecorder()
ui.pageDevices(rr, req)
body := rr.Body.String()
for _, want := range []string{
"设备",
"配置管理",
"操作审计",
"系统",
`href="/ui/devices"`,
`href="/ui/assets"`,
`href="/ui/audit"`,
`href="/ui/system"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected sidebar to contain %q", want)
}
}
for _, old := range []string{"配置资产", "识别配置", "模型管理", "高级调试", "日志分析"} {
if strings.Contains(body, old) {
t.Fatalf("sidebar should not contain old label %q", old)
}
}
}
func TestUI_LegacyDeviceConfigRedirectsToAssets(t *testing.T) {
ui := newTestUI(t)
req := httptest.NewRequest(http.MethodGet, "/ui/device-config", nil)
rr := httptest.NewRecorder()
ui.pageDeviceConfig(rr, req)
if rr.Code != http.StatusFound {
t.Fatalf("expected redirect, got %d", rr.Code)
}
if got := rr.Header().Get("Location"); got != "/ui/assets" {
t.Fatalf("expected redirect to /ui/assets, got %q", got)
}
}
func TestUI_DeviceControlPageShowsLiveActions(t *testing.T) {
ui := newTestUI(t)
routes, err := ui.Routes()
if err != nil {
t.Fatalf("Routes: %v", err)
}
rr := httptest.NewRecorder()
routes.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/devices/edge-01/control", nil))
body := rr.Body.String()
for _, want := range []string{
"设备控制",
"配置预览",
"配置应用",
"服务控制",
"执行结果摘要",
`action="/ui/devices/edge-01/action"`,
`action="/ui/devices/edge-01/config-candidate/apply"`,
`href="/ui/devices/edge-01/config-preview"`,
"回滚到上一份",
"打开预览器",
} {
if !strings.Contains(body, want) {
t.Fatalf("expected device control HTML to contain %q", want)
}
}
if strings.Contains(body, "重载服务") {
t.Fatalf("device control page should not contain reload action")
}
for _, forbidden := range []string{"框架版", "结构样机", "待接入", `disabled>`, `value="workshop_face_shoe_alarm" disabled`} {
if strings.Contains(body, forbidden) {
t.Fatalf("device control page should not contain placeholder marker %q", forbidden)
}
}
}
func TestUI_ActionDeviceActionCanRenderControlPage(t *testing.T) {
agentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v1/media-server/reload":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
case r.Method == http.MethodGet && r.URL.Path == "/v1/config/status":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"ok": true,
"metadata": {"config_id": "preview_edge-01", "config_version": "v2"},
"previous_config": {"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}
}`))
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("action", "reload")
form.Set("return_to", "control")
req := httptest.NewRequest(http.MethodPost, "/ui/devices/edge-01/action", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "edge-01")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
rr := httptest.NewRecorder()
ui.actionDeviceAction(rr, req)
body := rr.Body.String()
for _, want := range []string{"设备控制", "POST /v1/media-server/reload", "执行结果摘要"} {
if !strings.Contains(body, want) {
t.Fatalf("expected control page result HTML to contain %q, got:\n%s", want, body)
}
}
}
func TestUI_ActionDeviceConfigCandidateApplyCanRenderControlPage(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")
_, _ = 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"},
"previous_config": {"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}
}`))
case r.Method == http.MethodPost && r.URL.Path == "/v1/config/candidate/apply":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
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("return_to", "control")
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")
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)
if statusCalls < 2 {
t.Fatalf("expected status to be refreshed for control page, got %d calls", statusCalls)
}
body := rr.Body.String()
for _, want := range []string{"设备控制", "应用候选配置结果", "preview_edge-01", "local_3588_face_debug"} {
if !strings.Contains(body, want) {
t.Fatalf("expected control page apply result HTML to contain %q, got:\n%s", want, body)
}
}
}
func TestUI_AssetsPageDefinesConfigAssetScope(t *testing.T) {
ui := newTestUI(t)
root := t.TempDir()
writeTestFile(t, filepath.Join(root, "configs", "templates", "workshop_face_shoe_alarm.json"), `{"name":"workshop_face_shoe_alarm","description":"template","params":{"minio_endpoint":"http://10.0.0.49:9000"},"template":{"nodes":[{},{}],"edges":[[]]}}`)
writeTestFile(t, filepath.Join(root, "configs", "profiles", "local_3588_test.json"), `{"name":"local_3588_test","business_name":"A厂区视觉识别","instances":[{"name":"cam1","template":"workshop_face_shoe_alarm","params":{"display_name":"东门入口"}}]}`)
writeTestFile(t, filepath.Join(root, "configs", "overlays", "face_debug.json"), `{"description":"debug","instance_overrides":{"*":{"override":{}}}}`)
ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: root})
req := httptest.NewRequest(http.MethodGet, "/ui/assets", nil)
rr := httptest.NewRecorder()
ui.pageAssets(rr, req)
body := rr.Body.String()
for _, want := range []string{"配置管理", "总览", "模板", "业务配置", "叠加项", "local_3588_test", "workshop_face_shoe_alarm", "face_debug"} {
if !strings.Contains(body, want) {
t.Fatalf("expected assets HTML to contain %q", want)
}
}
}
func TestUI_ProfileAssetPageShowsInstanceSummary(t *testing.T) {
ui := newTestUI(t)
root := t.TempDir()
writeTestFile(t, filepath.Join(root, "configs", "templates", "workshop_face_shoe_alarm.json"), `{"name":"workshop_face_shoe_alarm","params":{"minio_endpoint":"http://10.0.0.49:9000","external_get_token_url":"http://10.0.0.49:8080/api/getToken"},"template":{"nodes":[{}],"edges":[]}}`)
writeTestFile(t, filepath.Join(root, "configs", "profiles", "local_3588_test.json"), `{
"name":"local_3588_test",
"business_name":"A厂区视觉识别",
"description":"test profile",
"queue":{"size":8,"strategy":"drop_oldest"},
"instances":[{
"name":"cam1",
"template":"workshop_face_shoe_alarm",
"params":{
"display_name":"东门入口",
"device_code":"rk3588-a-001",
"site_name":"A厂区",
"rtsp_url":"rtsp://10.0.0.1/live",
"publish_hls_path":"./web/hls/cam1/index.m3u8",
"publish_rtsp_port":8555,
"publish_rtsp_path":"/live/cam1",
"channel_no":"cam1",
"queue_debug":true
}
}]
}`)
writeTestFile(t, filepath.Join(root, "configs", "overlays", "face_debug.json"), `{"description":"debug","instance_overrides":{"*":{"override":{}}}}`)
ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: root})
req := httptest.NewRequest(http.MethodGet, "/ui/assets/profiles/local_3588_test", nil)
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, func() *chi.Context {
rctx := chi.NewRouteContext()
rctx.URLParams.Add("name", "local_3588_test")
return rctx
}()))
rr := httptest.NewRecorder()
ui.pageAssetProfile(rr, req)
body := rr.Body.String()
for _, want := range []string{"业务配置", "local_3588_test", "业务名称", "A厂区视觉识别", "站点名", "A厂区", "东门入口", "rtsp://10.0.0.1/live", "高级设置", "queue_debug"} {
if !strings.Contains(body, want) {
t.Fatalf("expected profile asset HTML to contain %q, got:\n%s", want, body)
}
}
for _, forbidden := range []string{"MinIO", "取 token 接口", "告警上报接口", "设备编号"} {
if strings.Contains(body, forbidden) {
t.Fatalf("profile asset HTML should no longer contain shared template field %q, got:\n%s", forbidden, body)
}
}
}
func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) {
cfg := &config.Config{Concurrency: 1, OfflineAfterMs: 1000000}
reg := service.NewRegistryService(cfg, nil)
reg.UpdateDevice(&models.Device{DeviceID: "edge-01", DeviceName: "入口识别节点", IP: "127.0.0.1", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.0", BuildID: "20260419.151628", GitSha: "5c04681"})
reg.UpdateDevice(&models.Device{DeviceID: "edge-02", DeviceName: "辅助节点", IP: "127.0.0.2", AgentPort: 9100, MediaPort: 9000, Online: true, Version: "1.0.1", BuildID: "20260419.151900", GitSha: "8eaf213"})
tasks := service.NewTaskService(cfg, nil, reg)
taskConfig, err := tasks.CreateTask("config_apply", []string{"edge-01", "edge-02"}, map[string]any{"config": map[string]any{}})
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
taskService, err := tasks.CreateTask("media_restart", []string{"edge-01", "edge-02"}, nil)
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
taskConfig.Mu.Lock()
taskConfig.Status = models.TaskSuccess
for _, did := range taskConfig.DeviceIDs {
if ds, ok := taskConfig.Devices[did]; ok && ds != nil {
ds.Status = models.TaskSuccess
}
}
taskConfig.Mu.Unlock()
taskService.Mu.Lock()
taskService.Status = models.TaskSuccess
for _, did := range taskService.DeviceIDs {
if ds, ok := taskService.Devices[did]; ok && ds != nil {
ds.Status = models.TaskSuccess
}
}
taskService.Mu.Unlock()
ui, err := NewUI(nil, reg, nil, tasks, nil)
if err != nil {
t.Fatalf("NewUI: %v", err)
}
rrAudit := httptest.NewRecorder()
ui.pageAudit(rrAudit, httptest.NewRequest(http.MethodGet, "/ui/audit", nil))
for _, want := range []string{"操作审计", "审计记录", "批量配置", "批量服务", "目标设备数", "2 台", "下发识别配置", "重启视频分析服务"} {
if !strings.Contains(rrAudit.Body.String(), want) {
t.Fatalf("expected audit HTML to contain %q", want)
}
}
for _, forbidden := range []string{"框架版", "后续", "任务中心", "节点执行情况", `disabled`} {
if strings.Contains(rrAudit.Body.String(), forbidden) {
t.Fatalf("audit HTML should not contain placeholder marker %q", forbidden)
}
}
for _, forbidden := range []string{"success", "failed", "running"} {
if strings.Contains(rrAudit.Body.String(), forbidden) {
t.Fatalf("audit HTML should not leak raw status enum %q", forbidden)
}
}
rrTasks := httptest.NewRecorder()
ui.pageTasks(rrTasks, httptest.NewRequest(http.MethodGet, "/ui/tasks", nil))
for _, want := range []string{"任务列表", "批量配置", "批量服务", "目标设备数"} {
if !strings.Contains(rrTasks.Body.String(), want) {
t.Fatalf("expected tasks HTML to contain %q", want)
}
}
for _, forbidden := range []string{"任务中心", "节点执行情况"} {
if strings.Contains(rrTasks.Body.String(), forbidden) {
t.Fatalf("tasks HTML should not contain placeholder marker %q", forbidden)
}
}
rrTaskConfig := httptest.NewRecorder()
reqTask := httptest.NewRequest(http.MethodGet, "/ui/tasks/"+taskConfig.ID, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", taskConfig.ID)
reqTask = reqTask.WithContext(context.WithValue(reqTask.Context(), chi.RouteCtxKey, rctx))
ui.pageTask(rrTaskConfig, reqTask)
for _, want := range []string{"批量配置", "下发识别配置", "返回任务列表"} {
if !strings.Contains(rrTaskConfig.Body.String(), want) {
t.Fatalf("expected task detail HTML to contain %q", want)
}
}
for _, forbidden := range []string{"任务中心", "返回操作审计"} {
if strings.Contains(rrTaskConfig.Body.String(), forbidden) {
t.Fatalf("task detail HTML should not contain %q", forbidden)
}
}
rrSystem := httptest.NewRecorder()
ui.pageSystem(rrSystem, httptest.NewRequest(http.MethodGet, "/ui/system", nil))
for _, want := range []string{"系统", "设备发现与注册", "Agent 访问策略", "后台健康", "当前设备数", "UDP 广播发现", "/health", "/openapi.json"} {
if !strings.Contains(rrSystem.Body.String(), want) {
t.Fatalf("expected system HTML to contain %q", want)
}
}
for _, forbidden := range []string{"框架版", "后续", "平台状态与访问入口"} {
if strings.Contains(rrSystem.Body.String(), forbidden) {
t.Fatalf("system HTML should not contain placeholder marker %q", forbidden)
}
}
}