feat: add resource management page handler and sync action; rewrite resources template
This commit is contained in:
parent
1ff391f055
commit
4df7af7f05
@ -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": `<svg xmlns="http://www.w3.org/2000/svg" class="icon ui-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><rect x="3" y="4" width="18" height="12" rx="1"/><path d="M7 20h10"/><path d="M9 16v4"/><path d="M15 16v4"/></svg>`,
|
||||
@ -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) {
|
||||
|
||||
@ -1,40 +1,121 @@
|
||||
{{define "resources"}}
|
||||
{{$board := .ResourceStatusBoard}}
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2>资源状态</h2>
|
||||
<div class="muted small">统一维护人脸库与通用资源,设备侧只显示当前版本与同步状态。</div>
|
||||
<h2 class="title-with-icon">{{icon "assets"}}<span>资源概览</span></h2>
|
||||
<div class="form-hint">统一维护标准资源,设备侧通过任务同步。当前支持人脸库等资源类型。</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<form method="post" action="/ui/resources/sync">
|
||||
{{range .Devices}}{{if .Online}}<input type="hidden" name="device_id" value="{{.DeviceID}}">{{end}}{{end}}
|
||||
<input type="hidden" name="action" value="resource_sync_all">
|
||||
<button class="btn" type="submit">同步全部资源</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-summary">
|
||||
<div class="summary-item"><div class="summary-label">人脸库版本</div><div class="summary-value">统一管理</div><div class="summary-hint">由平台集中维护与发布</div></div>
|
||||
<div class="summary-item"><div class="summary-label">通用资源</div><div class="summary-value">统一管理</div><div class="summary-hint">标定、字典与业务资源统一维护</div></div>
|
||||
<div class="summary-item"><div class="summary-label">设备资源状态</div><div class="summary-value">{{len .Devices}}</div><div class="summary-hint">纳管设备资源版本覆盖</div></div>
|
||||
<div class="summary-item"><div class="summary-label">同步方式</div><div class="summary-value">任务下发</div><div class="summary-hint">与设备分配、模型同步一致</div></div>
|
||||
<div class="stats">
|
||||
<div class="stat accent-teal">
|
||||
<div class="k metric-label">{{icon "template"}}<span>标准资源总数</span></div>
|
||||
<div class="v">{{if $board}}{{$board.Summary.StandardResources}}{{else}}0{{end}}</div>
|
||||
</div>
|
||||
<div class="stat accent-green">
|
||||
<div class="k metric-label">{{icon "devices"}}<span>在线设备数</span></div>
|
||||
<div class="v">{{.OnlineCount}}</div>
|
||||
</div>
|
||||
<div class="stat accent-teal">
|
||||
<div class="k metric-label">{{icon "assets"}}<span>完整设备数</span></div>
|
||||
<div class="v">{{if $board}}{{$board.Summary.CompleteDevices}}{{else}}0{{end}}</div>
|
||||
</div>
|
||||
<div class="stat accent-amber">
|
||||
<div class="k metric-label">{{icon "warn"}}<span>缺失设备数</span></div>
|
||||
<div class="v">{{if $board}}{{$board.Summary.MissingDevices}}{{else}}0{{end}}</div>
|
||||
</div>
|
||||
<div class="stat accent-amber">
|
||||
<div class="k metric-label">{{icon "service"}}<span>不一致设备数</span></div>
|
||||
<div class="v">{{if $board}}{{$board.Summary.MismatchDevices}}{{else}}0{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>设备资源状态</h2>
|
||||
<div class="table-wrap" style="margin-top:10px">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "template"}}<span>标准资源</span></h2>
|
||||
<div class="form-hint">版本号用于展示,设备一致性以资源哈希校验。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>节点</th><th>状态</th><th>管理地址</th><th>人脸库版本</th><th>资源状态</th></tr>
|
||||
<tr><th>资源名</th><th>分类</th><th>版本</th><th>哈希</th><th>大小</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Devices}}
|
||||
{{range .StandardResources}}
|
||||
<tr>
|
||||
<td>
|
||||
<a class="mono" href="/ui/devices/{{.DeviceID}}">{{if .DeviceName}}{{.DeviceName}}{{else}}{{.DeviceID}}{{end}}</a>
|
||||
<div class="muted small mono">{{.DeviceID}}</div>
|
||||
</td>
|
||||
<td>{{if .Online}}<span class="pill ok">在线</span>{{else}}<span class="pill bad">离线</span>{{end}}</td>
|
||||
<td class="mono">{{.IP}}:{{.AgentPort}}</td>
|
||||
<td class="mono">待上报</td>
|
||||
<td>{{if .Online}}<span class="pill warn">待同步</span>{{else}}<span class="pill bad">设备离线</span>{{end}}</td>
|
||||
<td><span class="mono table-key">{{.Name}}</span></td>
|
||||
<td>{{resourceTypeLabel .ResourceType}}</td>
|
||||
<td>{{if .Version}}{{.Version}}{{else}}auto{{end}}</td>
|
||||
<td class="mono">{{shortHash .SHA256}}</td>
|
||||
<td>{{.SizeBytes}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5" class="muted">暂无设备。请先在“设备”页扫描或手动添加。</td></tr>
|
||||
<tr><td colspan="5" class="muted">标准资源目录为空,请在 resources/standard_resources/ 下放置资源文件后重启服务。</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "devices"}}<span>设备资源状态</span></h2>
|
||||
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="models-status-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备</th>
|
||||
{{range .StandardResources}}<th class="model-status-col" title="{{.Name}}"><span class="model-status-label">{{resourceTypeLabel .ResourceType}}</span></th>{{end}}
|
||||
<th class="model-extra-col">非标资源</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{if and $board (gt (len $board.Rows) 0)}}
|
||||
{{range $board.Rows}}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="table-key">{{.DeviceName}}</div>
|
||||
<div class="muted small mono">{{.DeviceID}}</div>
|
||||
</td>
|
||||
{{range .Cells}}
|
||||
<td>
|
||||
{{if eq .Status "ok"}}<span class="pill ok">一致</span>
|
||||
{{else if eq .Status "mismatch"}}<span class="pill warn">不一致</span>
|
||||
{{else}}<span class="pill bad">缺失</span>{{end}}
|
||||
</td>
|
||||
{{end}}
|
||||
<td>
|
||||
{{if gt .ExtraCount 0}}
|
||||
<details class="mini-details">
|
||||
<summary>{{.ExtraCount}} 个 · 更多</summary>
|
||||
<div class="mini-details-body">
|
||||
{{range .ExtraResources}}
|
||||
<div class="mini-details-item mono">{{.Name}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
{{else}}
|
||||
<span class="muted">0</span>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr><td colspan="99" class="muted">暂无设备资源状态。请先确保设备在线且 agent 实现了 GET /v1/resources/status 端点。</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user