179 lines
5.5 KiB
HTML
179 lines
5.5 KiB
HTML
{{define "devices"}}
|
||
<div class="card">
|
||
<h2>发现设备</h2>
|
||
<form method="post" action="/ui/discovery/search" class="row">
|
||
<div>
|
||
<div class="muted small">超时(毫秒)</div>
|
||
<input name="timeout_ms" value="1200" />
|
||
</div>
|
||
<div style="align-self:end">
|
||
<button type="submit">搜索(UDP 广播)</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>概览</h2>
|
||
<div class="stats">
|
||
<div class="stat"><div class="k">已登记设备</div><div class="v">{{.DeviceCount}}</div></div>
|
||
<div class="stat"><div class="k">在线</div><div class="v">{{.OnlineCount}}</div></div>
|
||
<div class="stat"><div class="k">离线</div><div class="v">{{.OfflineCount}}</div></div>
|
||
<div class="stat"><div class="k">本次发现</div><div class="v">{{.FoundCount}}</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
{{if .Found}}
|
||
<div class="card">
|
||
<h2>本次发现({{len .Found}})</h2>
|
||
<div class="muted small">提示:发现结果会同步写入下方“设备列表(Registry)”。</div>
|
||
<div class="table-wrap" style="margin-top:10px">
|
||
<table>
|
||
<thead>
|
||
<tr><th>设备ID</th><th>名称</th><th>地址</th><th>Media 端口</th><th>版本</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Found}}
|
||
<tr>
|
||
<td><a class="mono" href="/ui/devices/{{.DeviceID}}">{{.DeviceID}}</a></td>
|
||
<td>{{.DeviceName}}</td>
|
||
<td class="mono">{{.IP}}:{{.AgentPort}}</td>
|
||
<td class="mono">{{.MediaPort}}</td>
|
||
<td class="mono">{{.Version}}</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="card">
|
||
<h2>设备列表(Registry)</h2>
|
||
<div class="row" style="margin-top:10px">
|
||
<div>
|
||
<div class="muted small">筛选(设备ID / 名称 / IP)</div>
|
||
<input id="filter" placeholder="输入关键字..." />
|
||
</div>
|
||
<div class="muted small" style="align-self:end">由 discovery 与轮询自动更新</div>
|
||
</div>
|
||
|
||
<div class="table-wrap" style="margin-top:10px">
|
||
<table id="devices-table">
|
||
<thead>
|
||
<tr><th>设备ID</th><th>名称</th><th>地址</th><th>状态</th><th>主程序</th><th>最后在线</th><th>版本</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
{{range .Devices}}
|
||
<tr>
|
||
<td><a class="mono" href="/ui/devices/{{.DeviceID}}">{{.DeviceID}}</a></td>
|
||
<td>{{.DeviceName}}</td>
|
||
<td class="mono">
|
||
{{.IP}}:{{.AgentPort}}
|
||
<div class="muted small">media: {{.MediaPort}}</div>
|
||
</td>
|
||
<td>{{if .Online}}<span class="pill ok">在线</span>{{else}}<span class="pill bad">离线</span>{{end}}</td>
|
||
<td>
|
||
{{if .Online}}
|
||
<span class="pill warn" data-media-status="{{.DeviceID}}">待查询</span>
|
||
{{else}}
|
||
<span class="muted">-</span>
|
||
{{end}}
|
||
</td>
|
||
<td>
|
||
<div>{{ago .LastSeenMs}}</div>
|
||
<div class="muted small mono">{{.LastSeenMs}}</div>
|
||
</td>
|
||
<td class="mono">{{.Version}}</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(() => {
|
||
const input = document.getElementById('filter');
|
||
const table = document.getElementById('devices-table');
|
||
if(!input || !table) return;
|
||
input.addEventListener('input', () => {
|
||
const q = (input.value || '').trim().toLowerCase();
|
||
const rows = table.tBodies[0].rows;
|
||
for(const tr of rows){
|
||
const txt = tr.innerText.toLowerCase();
|
||
tr.style.display = (!q || txt.includes(q)) ? '' : 'none';
|
||
}
|
||
});
|
||
})();
|
||
|
||
(() => {
|
||
const els = Array.from(document.querySelectorAll('[data-media-status]'));
|
||
if(!els.length) return;
|
||
|
||
const labelFromHttp = (status, txt) => {
|
||
if(status === 501) return {cls:'warn', text:'未启用', title:txt};
|
||
if(status === 401) return {cls:'warn', text:'未授权', title:txt};
|
||
if(status === 404) return {cls:'warn', text:'未实现', title:txt};
|
||
return {cls:'warn', text:`HTTP ${status}`, title:txt};
|
||
};
|
||
|
||
async function fetchOne(el){
|
||
const id = el.getAttribute('data-media-status');
|
||
el.textContent = '查询中...';
|
||
el.className = 'pill warn';
|
||
el.title = '';
|
||
let res;
|
||
let txt = '';
|
||
try{
|
||
res = await fetch(`/api/devices/${encodeURIComponent(id)}/media-server/status`, { method: 'GET' });
|
||
txt = await res.text();
|
||
}catch(e){
|
||
el.className = 'pill warn';
|
||
el.textContent = '请求失败';
|
||
el.title = String(e);
|
||
return;
|
||
}
|
||
|
||
if(!res.ok){
|
||
const v = labelFromHttp(res.status, txt);
|
||
el.className = 'pill ' + v.cls;
|
||
el.textContent = v.text;
|
||
el.title = v.title || '';
|
||
return;
|
||
}
|
||
|
||
let obj;
|
||
try{ obj = JSON.parse(txt); }catch(e){}
|
||
if(!obj || typeof obj.running !== 'boolean'){
|
||
el.className = 'pill warn';
|
||
el.textContent = '未知';
|
||
el.title = txt;
|
||
return;
|
||
}
|
||
|
||
const pid = (typeof obj.pid === 'number' && obj.pid > 0) ? obj.pid : 0;
|
||
const cfg = (typeof obj.config_path === 'string') ? obj.config_path : '';
|
||
if(obj.running){
|
||
el.className = 'pill ok';
|
||
el.textContent = pid ? `运行中 PID ${pid}` : '运行中';
|
||
}else{
|
||
el.className = 'pill bad';
|
||
el.textContent = pid ? `未运行 PID ${pid}` : '未运行';
|
||
}
|
||
el.title = cfg;
|
||
}
|
||
|
||
const limit = 6;
|
||
let idx = 0;
|
||
const workers = Array.from({length: Math.min(limit, els.length)}, async () => {
|
||
while(true){
|
||
const i = idx++;
|
||
if(i >= els.length) return;
|
||
await fetchOne(els[i]);
|
||
}
|
||
});
|
||
Promise.all(workers);
|
||
})();
|
||
</script>
|
||
{{end}}
|