From 2a994b7220e7bdbdcc4d6d4a1ca4d5a742ef68b6 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Mon, 20 Apr 2026 01:15:55 +0800
Subject: [PATCH] Align batch operations with audit workflow
---
internal/web/ui.go | 72 ++++++++++++++++++++++-
internal/web/ui/templates/audit.html | 17 ++++--
internal/web/ui/templates/task.html | 27 +++------
internal/web/ui/templates/tasks.html | 26 ++++-----
internal/web/ui_test.go | 86 ++++++++++++++++++++++++----
5 files changed, 172 insertions(+), 56 deletions(-)
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}}
- | {{.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}} |
{{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"}}
@@ -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 @@
- {{if eq .Status "success"}}成功
- {{else if eq .Status "failed"}}失败
- {{else if eq .Status "running"}}执行中
- {{else}}待执行{{end}}
+ {{taskStatusLabel .Status}}
|
{{.Progress}} |
{{.Error}} |
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 @@
-
任务中心
+
任务列表
- | 任务标识 | 操作 | 状态 | 节点数 |
+ | 任务 | 类型 | 目标设备数 | 状态 |
{{range .Tasks}}
- | {{.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}}
+
+ {{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}} |
{{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))