diff --git a/internal/web/ui.go b/internal/web/ui.go index 4c47935..ea7270e 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -147,6 +147,72 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic } return v }, + "taskGroupLabel": func(v any) string { + switch fmt.Sprint(v) { + case "config_apply": + return "批量配置" + case "media_start", "media_restart", "media_stop": + return "批量服务" + case "reload", "rollback": + return "设备操作" + default: + return "其他任务" + } + }, + "taskActionLabel": func(v any) string { + switch fmt.Sprint(v) { + case "config_apply": + return "下发识别配置" + case "reload": + return "重载识别服务" + case "rollback": + return "回滚识别配置" + case "media_start": + return "启动视频分析服务" + case "media_restart": + return "重启视频分析服务" + case "media_stop": + return "停止视频分析服务" + default: + return fmt.Sprint(v) + } + }, + "taskGroupClass": func(v any) string { + switch fmt.Sprint(v) { + case "config_apply": + return "pill run" + case "media_start", "media_restart", "media_stop": + return "pill ok" + case "reload", "rollback": + return "pill warn" + default: + return "pill" + } + }, + "taskStatusLabel": func(v any) string { + switch fmt.Sprint(v) { + case "success": + return "成功" + case "failed": + return "失败" + case "running": + return "执行中" + default: + return "待执行" + } + }, + "taskStatusClass": func(v any) string { + switch fmt.Sprint(v) { + case "success": + return "pill ok" + case "failed": + return "pill bad" + case "running": + return "pill run" + default: + return "pill" + } + }, "ago": func(ms int64) string { if ms <= 0 { return "-" @@ -779,7 +845,7 @@ func (u *UI) actionDeviceMediaServerConfigUploadBatch(w http.ResponseWriter, r * } func (u *UI) pageTasks(w http.ResponseWriter, r *http.Request) { - u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices()}) + u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices()}) } func (u *UI) taskPageData(task *models.Task) PageData { @@ -837,13 +903,13 @@ func (u *UI) actionCreateTask(w http.ResponseWriter, r *http.Request) { } var payload any if err := json.Unmarshal([]byte(raw), &payload); err != nil { - u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: "高级参数 JSON 无效: " + err.Error(), RawJSON: raw, DeviceIDs: ids}) + u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: "高级参数 JSON 无效: " + err.Error(), RawJSON: raw, DeviceIDs: ids}) return } task, err := u.tasks.CreateTask(typeStr, deviceIDs, payload) if err != nil { - u.render(w, r, "tasks", PageData{Title: "任务中心", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: err.Error(), RawJSON: raw, DeviceIDs: ids}) + u.render(w, r, "tasks", PageData{Title: "任务", Tasks: u.tasks.ListTasks(), Devices: u.registry.GetDevices(), Error: err.Error(), RawJSON: raw, DeviceIDs: ids}) return } http.Redirect(w, r, "/ui/tasks/"+task.ID, http.StatusFound) diff --git a/internal/web/ui/templates/audit.html b/internal/web/ui/templates/audit.html index 50585de..51e65fd 100644 --- a/internal/web/ui/templates/audit.html +++ b/internal/web/ui/templates/audit.html @@ -18,9 +18,9 @@ + - - + @@ -28,10 +28,15 @@ {{range .Tasks}} - - - - + + + + {{else}} diff --git a/internal/web/ui/templates/task.html b/internal/web/ui/templates/task.html index 5b9d985..c573a8c 100644 --- a/internal/web/ui/templates/task.html +++ b/internal/web/ui/templates/task.html @@ -1,7 +1,7 @@ {{define "task"}}
- {{icon "audit"}}返回操作审计 + {{icon "devices"}}返回任务列表
@@ -13,28 +13,20 @@
{{.Task.ID}}
-
操作类型
+
任务类型
- {{if eq .Task.Type "config_apply"}}下发识别配置 - {{else if eq .Task.Type "reload"}}重载识别服务 - {{else if eq .Task.Type "rollback"}}回滚识别配置 - {{else if eq .Task.Type "media_start"}}启动视频分析服务 - {{else if eq .Task.Type "media_restart"}}重启视频分析服务 - {{else if eq .Task.Type "media_stop"}}停止视频分析服务 - {{else}}{{.Task.Type}}{{end}} + {{taskGroupLabel .Task.Type}} +
{{taskActionLabel .Task.Type}}
-
设备数量
-
{{len .Task.DeviceIDs}}
+
目标设备数
+
{{len .Task.DeviceIDs}} 台
当前状态
- {{if eq .Task.Status "success"}}成功 - {{else if eq .Task.Status "failed"}}失败 - {{else if eq .Task.Status "running"}}执行中 - {{else}}待执行{{end}} + {{taskStatusLabel .Task.Status}}
@@ -63,10 +55,7 @@ diff --git a/internal/web/ui/templates/tasks.html b/internal/web/ui/templates/tasks.html index 315e6f9..1c46e09 100644 --- a/internal/web/ui/templates/tasks.html +++ b/internal/web/ui/templates/tasks.html @@ -1,6 +1,6 @@ {{define "tasks"}}
-

创建批量运维任务

+

创建任务

用于向多台边缘节点下发识别配置、重载服务、回滚配置或控制视频分析服务。
@@ -33,32 +33,26 @@
-

任务中心

+

任务列表

类型 任务动作目标设备目标设备数 结果 说明
{{.ID}}{{.Type}}{{range $i, $id := .DeviceIDs}}{{if $i}}, {{end}}{{$id}}{{end}}{{.Status}} + {{taskGroupLabel .Type}} +
{{taskActionLabel .Type}}
+
+
{{.ID}}
+
{{len .DeviceIDs}} 台{{taskStatusLabel .Status}} {{if .Payload}}已记录任务参数{{else}}无附加参数{{end}}
- {{if eq .Status "success"}}成功 - {{else if eq .Status "failed"}}失败 - {{else if eq .Status "running"}}执行中 - {{else}}待执行{{end}} + {{taskStatusLabel .Status}} {{.Progress}} {{.Error}}
- + {{range .Tasks}} - + + - {{end}} diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index 21fb795..cd7ae8c 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -436,16 +436,20 @@ func writeTestFile(t *testing.T, path string, body string) { func TestUI_TaskPageRendersBatchSummaryAndDeviceResults(t *testing.T) { ui := newTestUI(t) ui.registry.UpdateDevice(&models.Device{DeviceID: "edge-02", DeviceName: "辅助节点", IP: "127.0.0.2", AgentPort: 9100, MediaPort: 9000, Online: true}) + ui.preview = service.NewConfigPreviewService(&config.Config{MediaRepoPath: createBatchConfigMediaRepo(t)}) form := url.Values{} - form.Set("action", "reload") form.Add("device_id", "edge-01") form.Add("device_id", "edge-02") - req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-action", strings.NewReader(form.Encode())) + form.Set("template", "workshop_face_shoe_alarm") + form.Set("profile", "local_3588_test") + form.Set("config_id", "batch_edge") + form.Set("config_version", "20260420.090000") + req := httptest.NewRequest(http.MethodPost, "/ui/devices/batch-config", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() - ui.actionDevicesBatchAction(rr, req) + ui.actionDeviceBatchConfig(rr, req) if rr.Code != http.StatusFound { t.Fatalf("expected redirect, got %d: %s", rr.Code, rr.Body.String()) } @@ -466,11 +470,19 @@ func TestUI_TaskPageRendersBatchSummaryAndDeviceResults(t *testing.T) { t.Fatalf("expected task page 200, got %d: %s", rrTask.Code, rrTask.Body.String()) } body := rrTask.Body.String() - for _, want := range []string{"任务详情", "返回操作审计", "设备结果表", "设备数量", "入口识别节点", "辅助节点", "edge-01", "edge-02", `id="task-status-value"`, "syncTaskStatus()"} { + for _, want := range []string{"任务详情", "返回任务列表", "设备结果表", "任务类型", "目标设备数", "批量配置", "下发识别配置", "2 台", "入口识别节点", "辅助节点", "edge-01", "edge-02", `id="task-status-value"`, "syncTaskStatus()", `href="/ui/tasks"`} { if !strings.Contains(body, want) { t.Fatalf("expected task page to contain %q, got:\n%s", want, body) } } + for _, forbidden := range []string{"任务中心", "节点执行情况"} { + if strings.Contains(body, forbidden) { + t.Fatalf("task page should not contain %q, got:\n%s", forbidden, body) + } + } + if strings.Contains(body, "返回操作审计") { + t.Fatalf("task page should not point back to audit, got:\n%s", body) + } if strings.Index(body, "edge-01") > strings.Index(body, "edge-02") { t.Fatalf("expected task devices to keep selection order, got:\n%s", body) } @@ -1521,17 +1533,32 @@ 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", 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", GitSha: "8eaf213"}) tasks := service.NewTaskService(cfg, nil, reg) - task, err := tasks.CreateTask("reload", []string{"edge-01"}, nil) + 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) } - task.Mu.Lock() - task.Status = models.TaskSuccess - if ds, ok := task.Devices["edge-01"]; ok && ds != nil { - ds.Status = models.TaskSuccess + taskService, err := tasks.CreateTask("media_restart", []string{"edge-01", "edge-02"}, nil) + if err != nil { + t.Fatalf("CreateTask: %v", err) } - task.Mu.Unlock() + 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) @@ -1539,16 +1566,51 @@ func TestUI_AuditAndSystemPagesDefineNewScopes(t *testing.T) { rrAudit := httptest.NewRecorder() ui.pageAudit(rrAudit, httptest.NewRequest(http.MethodGet, "/ui/audit", nil)) - for _, want := range []string{"操作审计", "审计记录", "谁做了什么、对哪台设备做的、结果如何", "reload", "edge-01", task.ID} { + 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`} { + 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))
任务标识操作状态节点数
任务类型目标设备数状态
{{.ID}} - {{if eq .Type "config_apply"}}下发识别配置 - {{else if eq .Type "reload"}}重载识别服务 - {{else if eq .Type "rollback"}}回滚识别配置 - {{else if eq .Type "media_start"}}启动视频分析服务 - {{else if eq .Type "media_restart"}}重启视频分析服务 - {{else if eq .Type "media_stop"}}停止视频分析服务 - {{else}}{{.Type}}{{end}} +
{{.ID}}
+
{{taskActionLabel .Type}}
- {{if eq .Status "success"}}成功 - {{else if eq .Status "failed"}}失败 - {{else if eq .Status "running"}}执行中 - {{else}}待执行{{end}} + {{taskGroupLabel .Type}} + {{len .DeviceIDs}} 台 + {{taskStatusLabel .Status}} {{len .DeviceIDs}}