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}}
| 节点 | 状态 | 管理地址 | 人脸库版本 | 资源状态 | |||||
|---|---|---|---|---|---|---|---|---|---|
| 资源名 | 分类 | 版本 | 哈希 | 大小 | |||||
|
- {{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/ 下放置资源文件后重启服务。 | |||||||||
| 设备 | + {{range .StandardResources}}{{resourceTypeLabel .ResourceType}} | {{end}} +非标资源 | +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
+ {{.DeviceName}}
+ {{.DeviceID}}
+ |
+ {{range .Cells}}
+ + {{if eq .Status "ok"}}一致 + {{else if eq .Status "mismatch"}}不一致 + {{else}}缺失{{end}} + | + {{end}} +
+ {{if gt .ExtraCount 0}}
+
+
+ {{else}}
+ 0
+ {{end}}
+ {{.ExtraCount}} 个 · 更多+
+ {{range .ExtraResources}}
+
+ {{.Name}}
+ {{end}}
+ |
+ ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 暂无设备资源状态。请先确保设备在线且 agent 实现了 GET /v1/resources/status 端点。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||