feat: lazy-load device resource status via AJAX
This commit is contained in:
parent
10c21d8fb8
commit
2289802dac
@ -610,6 +610,7 @@ 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)
|
||||
@ -1418,14 +1419,31 @@ 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
|
||||
}
|
||||
_ = store.Close()
|
||||
}
|
||||
}
|
||||
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 data.Devices {
|
||||
for _, device := range devices {
|
||||
if device == nil || !device.Online {
|
||||
continue
|
||||
}
|
||||
@ -1434,13 +1452,13 @@ func (u *UI) pageResources(w http.ResponseWriter, r *http.Request) {
|
||||
installed[device.DeviceID] = items
|
||||
}
|
||||
}
|
||||
board = service.BuildResourceStatusBoard(data.StandardResources, data.Devices, installed)
|
||||
board = service.BuildResourceStatusBoard(items, devices, installed)
|
||||
}
|
||||
_ = store.Close()
|
||||
}
|
||||
}
|
||||
data.ResourceStatusBoard = &board
|
||||
u.render(w, r, "resources", data)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(board)
|
||||
}
|
||||
|
||||
func (u *UI) actionResourceSync(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
{{define "resources"}}
|
||||
{{$board := .ResourceStatusBoard}}
|
||||
<div class="card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
@ -7,7 +6,7 @@
|
||||
<div class="form-hint">统一维护标准资源,设备侧通过任务同步。当前支持人脸库等资源类型。</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<form method="post" action="/ui/resources/sync">
|
||||
<form method="post" action="/ui/resources/sync" id="sync-form">
|
||||
{{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>
|
||||
@ -17,7 +16,7 @@
|
||||
<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 class="v" id="stat-standard">{{len .StandardResources}}</div>
|
||||
</div>
|
||||
<div class="stat accent-green">
|
||||
<div class="k metric-label">{{icon "devices"}}<span>在线设备数</span></div>
|
||||
@ -25,15 +24,15 @@
|
||||
</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 class="v" id="stat-complete">-</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 class="v" id="stat-missing">-</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 class="v" id="stat-mismatch">-</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,15 +66,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card" id="device-status-card">
|
||||
<div class="section-title">
|
||||
<div>
|
||||
<h2 class="title-with-icon">{{icon "devices"}}<span>设备资源状态</span></h2>
|
||||
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。</div>
|
||||
<div class="form-hint">按标准资源逐项比对设备已安装状态,缺失和不一致会直接标出。<span id="status-hint"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="models-status-table">
|
||||
<table class="models-status-table" id="device-status-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备</th>
|
||||
@ -83,42 +82,69 @@
|
||||
<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 id="device-status-body">
|
||||
<tr><td colspan="99" class="muted">正在加载设备资源状态…</td></tr>
|
||||
</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