feat: concurrent device status fetch for resources page, matching models pattern
This commit is contained in:
parent
2289802dac
commit
038b5b2cfe
@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"3588AdminBackend/internal/models"
|
||||
"3588AdminBackend/internal/storage"
|
||||
@ -220,3 +222,57 @@ func BuildResourceStatusBoard(standardResources []storage.StandardResourceRecord
|
||||
})
|
||||
return board
|
||||
}
|
||||
|
||||
// FetchAndBuildResourceBoard 并发查询所有在线设备的资源状态并构建面板,总超时 5 秒。
|
||||
func FetchAndBuildResourceBoard(agent *AgentClient, devices []*models.Device, standardResources []storage.StandardResourceRecord) ResourceStatusBoard {
|
||||
board := ResourceStatusBoard{
|
||||
Summary: ResourceStatusSummary{
|
||||
StandardResources: len(standardResources),
|
||||
Devices: len(devices),
|
||||
},
|
||||
Rows: make([]ResourceStatusRow, 0, len(devices)),
|
||||
}
|
||||
if agent == nil || len(standardResources) == 0 {
|
||||
return board
|
||||
}
|
||||
|
||||
installed := make(map[string][]InstalledResourceStatus)
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 总超时:最多等 5 秒
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
for _, device := range devices {
|
||||
if device == nil || !device.Online {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(dev *models.Device) {
|
||||
defer wg.Done()
|
||||
items, err := FetchInstalledResourceStatuses(agent, dev)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
installed[dev.DeviceID] = items
|
||||
mu.Unlock()
|
||||
}
|
||||
}(device)
|
||||
}
|
||||
|
||||
// Wait with timeout
|
||||
waitCh := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
select {
|
||||
case <-waitCh:
|
||||
case <-done:
|
||||
}
|
||||
|
||||
return BuildResourceStatusBoard(standardResources, devices, installed)
|
||||
}
|
||||
|
||||
@ -610,7 +610,6 @@ func (u *UI) Routes() (chi.Router, error) {
|
||||
r.Get("/system", u.pageSystem)
|
||||
r.Get("/system/db-backup", u.pageSystemDBBackup)
|
||||
r.Get("/resources", u.pageResources)
|
||||
r.Get("/api/resources/device-status", u.apiResourceDeviceStatus)
|
||||
r.Post("/system/db-restore", u.actionSystemDBRestore)
|
||||
r.Get("/api/graph-node-types", u.apiGraphNodeTypes)
|
||||
r.Get("/device-config", u.pageDeviceConfig)
|
||||
@ -1419,48 +1418,21 @@ func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
board = service.FetchAndBuildResourceBoard(u.agent, data.Devices, items)
|
||||
}
|
||||
_ = store.Close()
|
||||
}
|
||||
}
|
||||
data.ResourceStatusBoard = &board
|
||||
u.render(w, r, "resources", data)
|
||||
}
|
||||
|
||||
func (u *UI) apiResourceDeviceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
u.ensureDevicesLoaded()
|
||||
devices := u.registry.GetDevices()
|
||||
board := service.ResourceStatusBoard{
|
||||
Summary: service.ResourceStatusSummary{Devices: len(devices)},
|
||||
}
|
||||
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 {
|
||||
board.Summary.StandardResources = len(items)
|
||||
installed := map[string][]service.InstalledResourceStatus{}
|
||||
for _, device := range devices {
|
||||
if device == nil || !device.Online {
|
||||
continue
|
||||
}
|
||||
items, err := service.FetchInstalledResourceStatuses(u.agent, device)
|
||||
if err == nil {
|
||||
installed[device.DeviceID] = items
|
||||
}
|
||||
}
|
||||
board = service.BuildResourceStatusBoard(items, devices, installed)
|
||||
}
|
||||
_ = store.Close()
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(board)
|
||||
}
|
||||
|
||||
func (u *UI) actionResourceSync(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{{define "resources"}}
|
||||
{{$board := .ResourceStatusBoard}}
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
@ -6,7 +7,7 @@
|
||||
<div class="form-hint">统一维护标准资源,设备侧通过任务同步。当前支持人脸库等资源类型。</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<form method="post" action="/ui/resources/sync" id="sync-form">
|
||||
<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>
|
||||
@ -16,7 +17,7 @@
|
||||
<div class="stats">
|
||||
<div class="stat accent-teal">
|
||||
<div class="k metric-label">{{icon "template"}}<span>标准资源总数</span></div>
|
||||
<div class="v" id="stat-standard">{{len .StandardResources}}</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>
|
||||
@ -24,15 +25,15 @@
|
||||
</div>
|
||||
<div class="stat accent-teal">
|
||||
<div class="k metric-label">{{icon "assets"}}<span>完整设备数</span></div>
|
||||
<div class="v" id="stat-complete">-</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" id="stat-missing">-</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" id="stat-mismatch">-</div>
|
||||
<div class="v">{{if $board}}{{$board.Summary.MismatchDevices}}{{else}}0{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,15 +67,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="device-status-card">
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "devices"}}<span>设备资源状态</span></h2>
|
||||
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。<span id="status-hint"></span></div>
|
||||
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="models-status-table" id="device-status-table">
|
||||
<table class="models-status-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备</th>
|
||||
@ -82,69 +83,42 @@
|
||||
<th class="model-extra-col">非标资源</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="device-status-body">
|
||||
<tr><td colspan="99" class="muted">正在加载设备资源状态…</td></tr>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var cols = {{len .StandardResources}};
|
||||
var typeLabels = {};
|
||||
{{range .StandardResources}}
|
||||
typeLabels["{{.Name}}"] = "{{resourceTypeLabel .ResourceType}}";
|
||||
{{end}}
|
||||
|
||||
function pill(cls, text) {
|
||||
var m = {"ok":"一致","mismatch":"不一致","missing":"缺失"};
|
||||
return '<span class="pill ' + cls + '">' + (m[text]||text) + '</span>';
|
||||
}
|
||||
|
||||
function renderBoard(board) {
|
||||
document.getElementById("stat-complete").textContent = board.summary.complete_devices;
|
||||
document.getElementById("stat-missing").textContent = board.summary.missing_devices;
|
||||
document.getElementById("stat-mismatch").textContent = board.summary.mismatch_devices;
|
||||
|
||||
var body = document.getElementById("device-status-body");
|
||||
if (!board.rows || board.rows.length === 0) {
|
||||
body.innerHTML = '<tr><td colspan="99" class="muted">暂无设备资源状态。请先确保设备在线且 agent 实现了 GET /v1/resources/status 端点。</td></tr>';
|
||||
return;
|
||||
}
|
||||
var html = '';
|
||||
for (var i = 0; i < board.rows.length; i++) {
|
||||
var row = board.rows[i];
|
||||
html += '<tr>';
|
||||
html += '<td><div class="table-key">' + esc(row.device_name) + '</div><div class="muted small mono">' + esc(row.device_id) + '</div></td>';
|
||||
for (var j = 0; j < row.cells.length; j++) {
|
||||
var c = row.cells[j];
|
||||
html += '<td>' + pill(c.status === "ok" ? "ok" : c.status === "mismatch" ? "warn" : "bad", c.status) + '</td>';
|
||||
}
|
||||
html += '<td>';
|
||||
if (row.extra_resource_count > 0) {
|
||||
html += '<details class="mini-details"><summary>' + row.extra_resource_count + ' 个 · 更多</summary><div class="mini-details-body">';
|
||||
for (var k = 0; k < row.extra_resources.length; k++) {
|
||||
html += '<div class="mini-details-item mono">' + esc(row.extra_resources[k].name) + '</div>';
|
||||
}
|
||||
html += '</div></details>';
|
||||
} else {
|
||||
html += '<span class="muted">0</span>';
|
||||
}
|
||||
html += '</td></tr>';
|
||||
}
|
||||
body.innerHTML = html;
|
||||
document.getElementById("status-hint").textContent = ' (已刷新)';
|
||||
}
|
||||
|
||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
fetch('/ui/api/resources/device-status')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(renderBoard)
|
||||
.catch(function(e) {
|
||||
document.getElementById("device-status-body").innerHTML = '<tr><td colspan="99" class="muted">加载失败,请刷新页面重试。</td></tr>';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user