From 4df7af7f05e8215e51f77975d2db9d5114402e55 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Wed, 6 May 2026 12:40:16 +0800 Subject: [PATCH] feat: add resource management page handler and sync action; rewrite resources template --- internal/web/ui.go | 93 ++++++++++++++++- internal/web/ui/templates/resources.html | 121 +++++++++++++++++++---- internal/web/ui_test.go | 2 +- 3 files changed, 194 insertions(+), 22 deletions(-) diff --git a/internal/web/ui.go b/internal/web/ui.go index 34695c8..be451c8 100644 --- a/internal/web/ui.go +++ b/internal/web/ui.go @@ -32,6 +32,7 @@ type UI struct { stateRepo *storage.DeviceConfigStateRepo auditRepo *storage.AuditLogsRepo dbPath string + resourcesRepo *storage.ResourcesRepo tpl *template.Template } @@ -75,6 +76,8 @@ type PageData struct { TaskDeviceRows []TaskDeviceRow StandardModels []storage.StandardModelRecord ModelStatusBoard *service.ModelStatusBoard + StandardResources []storage.StandardResourceRecord + ResourceStatusBoard *service.ResourceStatusBoard Templates []service.Template Template *service.Template AssetTab string @@ -229,6 +232,18 @@ func NewUI(discovery *service.DiscoveryService, registry *service.RegistryServic return "-" } }, + "resourceTypeLabel": func(v string) string { + switch strings.TrimSpace(v) { + case "face_gallery": + return "人脸库" + case "dataset": + return "数据集" + case "calibration": + return "标定文件" + default: + return v + } + }, "displayDeviceName": func(dev *models.Device, status *ConfigStatusView) string { if dev == nil { return "-" @@ -479,6 +494,13 @@ func (u *UI) SetDBPath(path string) { u.dbPath = strings.TrimSpace(path) } +func (u *UI) SetResourcesRepo(repo *storage.ResourcesRepo) { + if u == nil { + return + } + u.resourcesRepo = repo +} + func tablerIconSVG(name string) string { icons := map[string]string{ "devices": ``, @@ -627,6 +649,7 @@ func (u *UI) Routes() (chi.Router, error) { r.Get("/templates/{name}", u.pageTemplate) r.Get("/models", u.pageModels) r.Post("/models/sync", u.actionModelSync) + r.Post("/resources/sync", u.actionResourceSync) r.Get("/diagnostics", u.pageDiagnostics) r.Get("/recognition", u.pageRecognition) r.Get("/logs", u.pageLogs) @@ -1385,7 +1408,75 @@ func (u *UI) pageDiagnostics(w http.ResponseWriter, r *http.Request) { } func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) { - u.render(w, r, "resources", PageData{Title: "资源状态", Devices: u.registry.GetDevices()}) + u.ensureDevicesLoaded() + data := PageData{Title: "资源管理", Devices: u.registry.GetDevices()} + for _, dev := range data.Devices { + if dev == nil { + continue + } + if dev.Online { + data.OnlineCount++ + } + } + board := service.ResourceStatusBoard{} + if strings.TrimSpace(u.dbPath) != "" { + if store, err := storage.OpenSQLite(u.dbPath); err == nil { + resourcesRepo := storage.NewResourcesRepo(store.DB()) + if items, err := resourcesRepo.List(); err == nil { + data.StandardResources = items + installed := map[string][]service.InstalledResourceStatus{} + for _, device := range data.Devices { + if device == nil || !device.Online { + continue + } + items, err := service.FetchInstalledResourceStatuses(u.agent, device) + if err == nil { + installed[device.DeviceID] = items + } + } + board = service.BuildResourceStatusBoard(data.StandardResources, data.Devices, installed) + } + _ = store.Close() + } + } + data.ResourceStatusBoard = &board + u.render(w, r, "resources", data) +} + +func (u *UI) actionResourceSync(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + action := strings.TrimSpace(r.FormValue("action")) + if action == "" { + action = "resource_sync_all" + } + if action != "resource_sync_one" && action != "resource_sync_all" { + http.Error(w, "invalid action", http.StatusBadRequest) + return + } + deviceIDs := make([]string, 0) + for _, id := range r.Form["device_id"] { + id = strings.TrimSpace(id) + if id != "" { + deviceIDs = append(deviceIDs, id) + } + } + if len(deviceIDs) == 0 { + http.Error(w, "missing device_id", http.StatusBadRequest) + return + } + payload := map[string]any{} + if resourceName := strings.TrimSpace(r.FormValue("resource_name")); resourceName != "" { + payload["resource_name"] = resourceName + } + task, err := u.tasks.CreateTask(action, deviceIDs, payload) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/ui/tasks/"+url.PathEscape(task.ID), http.StatusFound) } func (u *UI) pageRecognition(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/ui/templates/resources.html b/internal/web/ui/templates/resources.html index 2349180..81258f0 100644 --- a/internal/web/ui/templates/resources.html +++ b/internal/web/ui/templates/resources.html @@ -1,40 +1,121 @@ {{define "resources"}} +{{$board := .ResourceStatusBoard}}
-

资源状态

-
统一维护人脸库与通用资源,设备侧只显示当前版本与同步状态。
+

{{icon "assets"}}资源概览

+
统一维护标准资源,设备侧通过任务同步。当前支持人脸库等资源类型。
+
+
+
+ {{range .Devices}}{{if .Online}}{{end}}{{end}} + + +
-
-
人脸库版本
统一管理
由平台集中维护与发布
-
通用资源
统一管理
标定、字典与业务资源统一维护
-
设备资源状态
{{len .Devices}}
纳管设备资源版本覆盖
-
同步方式
任务下发
与设备分配、模型同步一致
+
+
+
{{icon "template"}}标准资源总数
+
{{if $board}}{{$board.Summary.StandardResources}}{{else}}0{{end}}
+
+
+
{{icon "devices"}}在线设备数
+
{{.OnlineCount}}
+
+
+
{{icon "assets"}}完整设备数
+
{{if $board}}{{$board.Summary.CompleteDevices}}{{else}}0{{end}}
+
+
+
{{icon "warn"}}缺失设备数
+
{{if $board}}{{$board.Summary.MissingDevices}}{{else}}0{{end}}
+
+
+
{{icon "service"}}不一致设备数
+
{{if $board}}{{$board.Summary.MismatchDevices}}{{else}}0{{end}}
+
-

设备资源状态

-
+
+
+

{{icon "template"}}标准资源

+
版本号用于展示,设备一致性以资源哈希校验。
+
+
+
- + - {{range .Devices}} + {{range .StandardResources}} - - - - - + + + + + {{else}} - + + {{end}} + +
节点状态管理地址人脸库版本资源状态
资源名分类版本哈希大小
- {{if .DeviceName}}{{.DeviceName}}{{else}}{{.DeviceID}}{{end}} -
{{.DeviceID}}
-
{{if .Online}}在线{{else}}离线{{end}}{{.IP}}:{{.AgentPort}}待上报{{if .Online}}待同步{{else}}设备离线{{end}}{{.Name}}{{resourceTypeLabel .ResourceType}}{{if .Version}}{{.Version}}{{else}}auto{{end}}{{shortHash .SHA256}}{{.SizeBytes}}
暂无设备。请先在“设备”页扫描或手动添加。
标准资源目录为空,请在 resources/standard_resources/ 下放置资源文件后重启服务。
+
+
+ +
+
+
+

{{icon "devices"}}设备资源状态

+
按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。
+
+
+
+ + + + + {{range .StandardResources}}{{end}} + + + + + {{if and $board (gt (len $board.Rows) 0)}} + {{range $board.Rows}} + + + {{range .Cells}} + + {{end}} + + + {{end}} + {{else}} + {{end}}
设备{{resourceTypeLabel .ResourceType}}非标资源
+
{{.DeviceName}}
+
{{.DeviceID}}
+
+ {{if eq .Status "ok"}}一致 + {{else if eq .Status "mismatch"}}不一致 + {{else}}缺失{{end}} + + {{if gt .ExtraCount 0}} +
+ {{.ExtraCount}} 个 · 更多 +
+ {{range .ExtraResources}} +
{{.Name}}
+ {{end}} +
+
+ {{else}} + 0 + {{end}} +
暂无设备资源状态。请先确保设备在线且 agent 实现了 GET /v1/resources/status 端点。
diff --git a/internal/web/ui_test.go b/internal/web/ui_test.go index f96a64b..e277de5 100644 --- a/internal/web/ui_test.go +++ b/internal/web/ui_test.go @@ -2045,7 +2045,7 @@ func TestUI_ResourcesPageShowsUnifiedResourceStatus(t *testing.T) { ui := newTestUI(t) html := renderPage(t, ui, "/ui/resources") - for _, want := range []string{"资源状态", "人脸库版本", "设备资源状态", "入口识别节点"} { + for _, want := range []string{"资源管理", "资源概览", "标准资源", "设备资源状态"} { if !strings.Contains(html, want) { t.Fatalf("expected resources HTML to contain %q", want) }