safesight-control/internal/web/ui/templates/dashboard.html

109 lines
4.9 KiB
HTML

{{define "dashboard"}}
{{if .AlarmRecords}}
<div class="card" style="border-left:3px solid var(--red)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h3 class="title-with-icon" style="margin:0">{{icon "bell"}}<span>最近告警</span></h3>
<a class="btn ghost" href="/ui/alarms" style="font-size:11px">查看全部</a>
</div>
<div style="display:flex;flex-wrap:wrap;gap:8px">
{{range .AlarmRecords}}
<div style="padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft);min-width:200px">
<div style="font-weight:500;font-size:12px;margin-bottom:2px">{{.RuleName}}</div>
<div class="muted small mono">{{.Timestamp}}</div>
</div>
{{end}}
</div>
</div>
{{end}}
<div class="stats">
<div class="stat"><div class="k">设备总数</div><div class="v">{{.DeviceCount}}</div><div class="hint">已纳管的边缘设备</div></div>
<div class="stat"><div class="k">在线率</div><div class="v">{{.OnlineCount}} / {{.DeviceCount}}</div><div class="hint">在线设备占比</div></div>
<div class="stat"><div class="k">今日告警</div><div class="v">{{.TodayAlarmCount}}</div><div class="hint">今日告警总数</div></div>
<div class="stat"><div class="k">执行中任务</div><div class="v">{{.RunningTaskCount}}</div><div class="hint">正在下发或控制节点</div></div>
</div>
<div class="card" id="metrics-card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<h3 class="title-with-icon" style="margin:0">{{icon "heartbeat"}}<span>设备负载</span></h3>
<span class="muted small" id="metrics-time"></span>
</div>
<div id="metrics-body" style="display:flex;flex-wrap:wrap;gap:12px"><span class="muted">加载中…</span></div>
</div>
<div class="card">
<h2>异常设备</h2>
<div class="table-wrap" style="margin-top:10px">
<table>
<thead><tr><th>节点</th><th>状态</th><th>管理地址</th><th>最后心跳</th><th>操作</th></tr></thead>
<tbody>
{{range .AttentionDevices}}
<tr>
<td><a class="mono" href="/ui/devices/{{.DeviceID}}">{{.DeviceName}}</a><div class="muted small mono">{{.DeviceID}}</div></td>
<td><span class="pill bad">离线</span></td>
<td class="mono">{{.IP}}:{{.AgentPort}}</td>
<td>{{ago .LastSeenMs}}</td>
<td><a class="btn ghost" href="/ui/devices/{{.DeviceID}}">查看</a></td>
</tr>
{{else}}
<tr><td colspan="5" class="muted">暂无需要关注的节点。</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div class="card">
<h2>最近任务</h2>
<div class="table-wrap" style="margin-top:10px">
<table>
<thead><tr><th>任务标识</th><th>操作</th><th>状态</th><th>节点数</th></tr></thead>
<tbody>
{{range .Tasks}}
<tr>
<td><a class="mono" href="/ui/tasks/{{.ID}}">{{.ID}}</a></td>
<td>{{taskActionLabel .Type}}</td>
<td><span class="{{taskStatusClass .Status}}">{{taskStatusLabel .Status}}</span></td>
<td>{{len .DeviceIDs}}</td>
</tr>
{{else}}
<tr><td colspan="4" class="muted">暂无任务记录。</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
<script>
(function(){
var body=document.getElementById('metrics-body'),time=document.getElementById('metrics-time');
function load(){
time.textContent='加载中…';
var devs=[{{range .Devices}}{{if .Online}}{ip:"{{.IP}}",port:{{.AgentPort}},name:"{{.DisplayName}}"},{{end}}{{end}}];
if(!devs.length){body.innerHTML='<span class="muted">无在线设备</span>';return}
Promise.all(devs.map(function(d){
return fetch('http://'+d.ip+':'+d.port+'/v1/metrics',{headers:{'X-RK-Token':''}}).then(function(r){return r.json()}).catch(function(){return null}).then(function(m){return{name:d.name,data:m}});
})).then(function(r){
var html='';
r.forEach(function(item){
var m=item.data;
var cpu=m&&m.cpu?m.cpu.usage_pct:'-';
var mem=m&&m.memory?((m.memory.used_mb||0)/(m.memory.total_mb||1)*100).toFixed(0):'-';
html+='<div style="padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-soft);min-width:160px">';
html+='<div style="font-weight:500;font-size:12px;margin-bottom:4px">'+esc(item.name)+'</div>';
html+='<div style="display:flex;gap:12px;font-size:11px">';
html+='<span>CPU <strong>'+cpu+'%</strong></span>';
html+='<span>内存 <strong>'+mem+'%</strong></span>';
html+='</div></div>';
});
body.innerHTML=html;
var now=new Date();
time.textContent=now.getHours()+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
});
}
function esc(s){return(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
load();setInterval(load,10000);
})();
</script>
{{end}}