Refocus dashboard on device monitoring
This commit is contained in:
+140
-69
@@ -99,6 +99,77 @@ function proxiedUrl(endpoint) {
|
||||
return `/api/proxy/${encodeURIComponent(activeId)}${normalized}`
|
||||
}
|
||||
|
||||
function firstValue(...items) {
|
||||
for (const item of items) if (item !== undefined && item !== null && item !== '') return item
|
||||
return null
|
||||
}
|
||||
function formatBytes(n) {
|
||||
if (n === undefined || n === null || n === '') return '-'
|
||||
const num = Number(n)
|
||||
if (!Number.isFinite(num)) return value(n)
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = Math.abs(num)
|
||||
let i = 0
|
||||
while (size >= 1024 && i < units.length - 1) { size /= 1024; i += 1 }
|
||||
const signed = num < 0 ? -size : size
|
||||
return `${signed.toFixed(size >= 10 || i === 0 ? 0 : 1)} ${units[i]}`
|
||||
}
|
||||
function formatSpeed(speed) {
|
||||
if (!speed || typeof speed !== 'object') return '-'
|
||||
if (Array.isArray(speed.interfaces)) {
|
||||
return speed.interfaces.map(item => `${item.interface || 'net'} ↓ ${formatBytes(firstValue(item.rx_bytes_per_sec, item.rx, item.download))}/s · ↑ ${formatBytes(firstValue(item.tx_bytes_per_sec, item.tx, item.upload))}/s`).join(' | ')
|
||||
}
|
||||
const rx = firstValue(speed.rx, speed.download, speed.recv, speed.received, speed.rx_bytes_per_sec)
|
||||
const tx = firstValue(speed.tx, speed.upload, speed.sent, speed.tx_bytes_per_sec)
|
||||
return `↓ ${formatBytes(rx)}/s · ↑ ${formatBytes(tx)}/s`
|
||||
}
|
||||
function formatTemperature(temp) {
|
||||
if (temp === undefined || temp === null || temp === '') return '-'
|
||||
if (typeof temp === 'number') return `${temp}℃`
|
||||
if (typeof temp === 'string') return temp.includes('°') || temp.includes('℃') ? temp : `${temp}℃`
|
||||
if (Array.isArray(temp)) {
|
||||
return temp.map(item => `${item.type || item.zone || 'temp'}: ${value(item.temperature)}℃`).join(' | ') || '-'
|
||||
}
|
||||
if (typeof temp === 'object') {
|
||||
return Object.entries(temp).map(([k, v]) => `${k}: ${typeof v === 'number' ? `${v}℃` : value(v)}`).join(' · ') || '-'
|
||||
}
|
||||
return value(temp)
|
||||
}
|
||||
function formatPercentPair(obj) {
|
||||
if (!obj || typeof obj !== 'object') return '-'
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => `${item.mount_point || item.name || 'disk'} ${formatPercentPair(item)}`).join(' | ')
|
||||
}
|
||||
const used = Number(firstValue(obj.used, obj.used_bytes))
|
||||
const total = Number(firstValue(obj.total, obj.total_bytes))
|
||||
const percent = Number(firstValue(obj.used_percent, obj.percent))
|
||||
if (Number.isFinite(used) && Number.isFinite(total) && total > 0) return `${formatBytes(used)} / ${formatBytes(total)} (${Math.round(Number.isFinite(percent) ? percent : used / total * 100)}%)`
|
||||
const available = firstValue(obj.available, obj.available_bytes, obj.free, obj.free_bytes)
|
||||
if (available != null && total) return `可用 ${formatBytes(available)} / 总 ${formatBytes(total)}`
|
||||
return Object.entries(obj).map(([k, v]) => `${k}: ${typeof v === 'number' ? formatBytes(v) : value(v)}`).join(' · ') || '-'
|
||||
}
|
||||
function formatUptime(seconds) {
|
||||
if (seconds && typeof seconds === 'object') return seconds.uptime_formatted || seconds.formatted || formatUptime(seconds.uptime_seconds)
|
||||
const n = Number(seconds)
|
||||
if (!Number.isFinite(n)) return value(seconds)
|
||||
const d = Math.floor(n / 86400)
|
||||
const h = Math.floor((n % 86400) / 3600)
|
||||
const m = Math.floor((n % 3600) / 60)
|
||||
return [d ? `${d}天` : '', h ? `${h}小时` : '', `${m}分`].filter(Boolean).join(' ')
|
||||
}
|
||||
function metricCard(label, val, tone = '') {
|
||||
return `<div class="metric monitor-metric ${tone}"><span>${escapeHtml(label)}</span><b title="${escapeHtml(value(val))}">${escapeHtml(value(val))}</b></div>`
|
||||
}
|
||||
function detailCard(title, rows, tone = '') {
|
||||
return `<article class="device-info-card ${tone}"><h4>${escapeHtml(title)}</h4><dl>${rows.map(([k, v]) => `<div><dt>${escapeHtml(k)}</dt><dd title="${escapeHtml(value(v))}">${escapeHtml(value(v))}</dd></div>`).join('')}</dl></article>`
|
||||
}
|
||||
function getMonitorData() {
|
||||
const item = activeInstance()
|
||||
const s = activeStatus()
|
||||
const sum = s?.summary || {}
|
||||
return { item, s, sum, device: sum.device || {}, sim: sum.sim || {}, network: sum.network || {}, data: sum.data || {}, sms: sum.sms || {}, ota: sum.ota || {}, system: sum.system || {} }
|
||||
}
|
||||
|
||||
function renderFleetStats() {
|
||||
const total = instances.length
|
||||
const counts = { online: 0, auth: 0, offline: 0, unknown: 0 }
|
||||
@@ -137,7 +208,8 @@ function renderInstances() {
|
||||
const sum = s?.summary || {}
|
||||
const network = sum.network || {}
|
||||
const sim = sum.sim || {}
|
||||
return `<article class="instance-card ${item.id === activeId ? 'active' : ''}" data-id="${escapeHtml(item.id)}">
|
||||
const system = sum.system || {}
|
||||
return `<article class="instance-card monitor-device-card ${item.id === activeId ? 'active' : ''}" data-id="${escapeHtml(item.id)}">
|
||||
<div class="instance-top">
|
||||
<div>
|
||||
<div class="instance-name">${escapeHtml(item.name || item.id)}</div>
|
||||
@@ -145,17 +217,16 @@ function renderInstances() {
|
||||
</div>
|
||||
<span class="status-pill ${pillClass(s)}">${escapeHtml(statusText(s))}</span>
|
||||
</div>
|
||||
<div class="mini-grid monitor-mini-grid">
|
||||
<span>信号<b>${escapeHtml(value(network.signal || sim.signal))}</b></span>
|
||||
<span>温度<b>${escapeHtml(formatTemperature(system.temperature))}</b></span>
|
||||
<span>速率<b>${escapeHtml(formatSpeed(system.networkSpeed))}</b></span>
|
||||
<span>SIM<b>${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}</b></span>
|
||||
</div>
|
||||
<div class="instance-card-actions">
|
||||
<button type="button" class="mini-action" data-edit-id="${escapeHtml(item.id)}">编辑</button>
|
||||
<button type="button" class="mini-action danger" data-delete-id="${escapeHtml(item.id)}">删除</button>
|
||||
</div>
|
||||
${item.description ? `<p class="instance-desc">${escapeHtml(item.description)}</p>` : ''}
|
||||
<div class="mini-grid">
|
||||
<span>认证<b>${item.auth?.hasPassword ? '密码会话' : '无密码'}</b></span>
|
||||
<span>运营商<b>${escapeHtml(value(network.operator || sim.operator))}</b></span>
|
||||
<span>网络<b>${escapeHtml(value(network.accessTechnology || network.registration))}</b></span>
|
||||
<span>ICCID<b>${escapeHtml(value(sim.iccid))}</b></span>
|
||||
</div>
|
||||
</article>`
|
||||
}).join('')
|
||||
$$('.instance-card', root).forEach(card => card.addEventListener('click', (event) => {
|
||||
@@ -167,85 +238,85 @@ function renderInstances() {
|
||||
}
|
||||
|
||||
function renderHero() {
|
||||
const item = activeInstance()
|
||||
const s = activeStatus()
|
||||
const sum = s?.summary || {}
|
||||
const { item, s, device, sim, network, system, data, ota } = getMonitorData()
|
||||
if (!item) {
|
||||
$('activeKicker').textContent = 'NO DEVICE SELECTED'
|
||||
$('activeName').textContent = '选择一个 SimAdmin 实例'
|
||||
$('activeMeta').textContent = '统一查看设备、SIM、网络、短信、eSIM、OTA 与原始管理页面。'
|
||||
$('activeKicker').textContent = 'DEVICE MONITOR'
|
||||
$('activeName').textContent = '选择一个设备查看实时状态'
|
||||
$('activeMeta').textContent = '主页面展示各设备的状态、温度、流量、SIM、信号、系统资源与短信统计。'
|
||||
$('signalPanel').innerHTML = [
|
||||
['状态', '等待选择'], ['当前实例', '-'], ['会话', '-'], ['刷新', '-'],
|
||||
['状态', '等待选择'], ['设备', '-'], ['温度', '-'], ['流量速率', '-'],
|
||||
].map(([k, v]) => `<div class="signal-item"><span>${k}</span><b>${v}</b></div>`).join('')
|
||||
return
|
||||
}
|
||||
$('activeKicker').textContent = item.id
|
||||
$('activeName').textContent = item.name || item.id
|
||||
$('activeMeta').textContent = item.description ? `${item.description} · ${item.url}` : item.url
|
||||
$('activeKicker').textContent = `${item.id} · ${statusText(s)}`
|
||||
$('activeName').textContent = `${item.name || item.id} 设备监控`
|
||||
$('activeMeta').textContent = `${device.manufacturer || ''} ${device.model || ''}`.trim() || item.url
|
||||
$('signalPanel').innerHTML = [
|
||||
['状态', statusText(s)],
|
||||
['设备', value(sum.device?.model || sum.device?.name)],
|
||||
['运营商', value(sum.network?.operator || sum.sim?.operator)],
|
||||
['网络', value(sum.network?.accessTechnology || sum.network?.registration)],
|
||||
['短信', sum.sms ? `总 ${value(sum.sms.total)} / 未读 ${value(sum.sms.unread)}` : '-'],
|
||||
['版本', value(sum.ota?.currentVersion || sum.device?.firmwareVersion)],
|
||||
].map(([k, v]) => `<div class="signal-item"><span>${escapeHtml(k)}</span><b>${escapeHtml(v)}</b></div>`).join('')
|
||||
['在线状态', statusText(s)],
|
||||
['SIM / 运营商', `${sim.present === false ? '未插卡' : '已插卡'} · ${value(network.operator || sim.operator)}`],
|
||||
['信号 / 制式', `${value(network.signal || sim.signal)} · ${value(network.accessTechnology || network.registration)}`],
|
||||
['温度', formatTemperature(system.temperature)],
|
||||
['流量速率', formatSpeed(system.networkSpeed)],
|
||||
['数据连接', firstValue(data.active, data.connected, data.enabled) === true ? '已连接' : firstValue(data.active, data.connected, data.enabled) === false ? '未连接' : '-'],
|
||||
['系统负载', Array.isArray(system.cpuLoad) ? system.cpuLoad.join(' / ') : value(system.cpuLoad)],
|
||||
['版本', value(ota.currentVersion || device.firmware || device.revision)],
|
||||
].map(([k, v]) => `<div class="signal-item"><span>${escapeHtml(k)}</span><b title="${escapeHtml(value(v))}">${escapeHtml(value(v))}</b></div>`).join('')
|
||||
}
|
||||
|
||||
function renderOverview() {
|
||||
const item = activeInstance()
|
||||
const s = activeStatus()
|
||||
const { item, s, device, sim, network, data, sms, system } = getMonitorData()
|
||||
if (!item) {
|
||||
$('overview').innerHTML = '<div class="metric"><span>提示</span><b>先选择左侧设备</b></div>'
|
||||
$('overview').innerHTML = [
|
||||
metricCard('设备', `${instances.length} 台`),
|
||||
metricCard('在线', [...statuses.values()].filter(x => statusKind(x) === 'online').length),
|
||||
metricCard('温度', '选择设备后显示'),
|
||||
metricCard('流量', '选择设备后显示'),
|
||||
].join('')
|
||||
renderHero()
|
||||
renderDeviceDetails()
|
||||
return
|
||||
}
|
||||
const sum = s?.summary || {}
|
||||
const metrics = [
|
||||
['状态', s ? (s.reachable ? (s.authenticated === false ? '在线/未登录' : '在线') : `离线`) : '未知'],
|
||||
['延迟', s?.latencyMs != null ? `${s.latencyMs}ms` : '-'],
|
||||
['型号', sum.device?.model || sum.device?.name],
|
||||
['IMEI', sum.device?.imei],
|
||||
['ICCID', sum.sim?.iccid],
|
||||
['号码', sum.sim?.phoneNumber || sum.sim?.msisdn],
|
||||
['运营商', sum.network?.operator || sum.sim?.operator],
|
||||
['网络', sum.network?.accessTechnology || sum.network?.registration],
|
||||
['数据连接', sum.data ? `${boolText(sum.data.enabled)} / ${boolText(sum.data.connected)}` : '-'],
|
||||
['短信', sum.sms ? `总 ${value(sum.sms.total)} / 未读 ${value(sum.sms.unread)}` : '-'],
|
||||
]
|
||||
$('overview').innerHTML = metrics.map(([k, v]) => `<div class="metric"><span>${escapeHtml(k)}</span><b title="${escapeHtml(value(v))}">${escapeHtml(value(v))}</b></div>`).join('')
|
||||
const dataState = firstValue(data.active, data.connected, data.enabled)
|
||||
$('overview').innerHTML = [
|
||||
metricCard('状态', s ? (s.reachable ? (s.authenticated === false ? '在线/未登录' : '在线') : '离线') : '未知', pillClass(s)),
|
||||
metricCard('延迟', s?.latencyMs != null ? `${s.latencyMs}ms` : '-'),
|
||||
metricCard('信号', firstValue(network.signal, sim.signal), 'signal'),
|
||||
metricCard('温度', formatTemperature(system.temperature), 'temperature'),
|
||||
metricCard('流量速率', formatSpeed(system.networkSpeed), 'traffic'),
|
||||
metricCard('数据连接', dataState === true ? '已连接' : dataState === false ? '未连接' : '-'),
|
||||
metricCard('SIM', sim.present === false ? '未插卡' : value(sim.iccid)),
|
||||
metricCard('短信', sms.total != null ? `总 ${sms.total} / 收 ${value(sms.incoming)} / 发 ${value(sms.outgoing)}` : '-'),
|
||||
].join('')
|
||||
renderHero()
|
||||
renderDeviceDetails()
|
||||
}
|
||||
|
||||
function renderModuleTabs() {
|
||||
const tabs = [{ id: 'all', title: '全部' }, ...modules.map(m => ({ id: m.id, title: m.title.split(/[ /]/)[0] }))]
|
||||
$('moduleTabs').innerHTML = tabs.map(t => `<button class="${t.id === activeModule ? 'active' : ''}" data-module="${escapeHtml(t.id)}">${escapeHtml(t.title)}</button>`).join('')
|
||||
$$('button[data-module]', $('moduleTabs')).forEach(btn => btn.addEventListener('click', () => {
|
||||
activeModule = btn.dataset.module
|
||||
renderFeatures()
|
||||
}))
|
||||
function renderDeviceDetails() {
|
||||
const { item, device, sim, network, data, sms, ota, system, s } = getMonitorData()
|
||||
if (!item) {
|
||||
$('featureGrid').innerHTML = `<section class="empty-state mini-empty"><h3>设备监控大屏</h3><p>选择左侧任一设备后,这里会按分类展示真实设备信息、温度、流量、系统资源、SIM 与短信状态。</p></section>`
|
||||
return
|
||||
}
|
||||
$('featureGrid').innerHTML = [
|
||||
detailCard('设备信息', [
|
||||
['名称', item.name || item.id], ['地址', item.url], ['型号', device.model], ['厂商', device.manufacturer], ['IMEI', device.imei], ['电源', boolText(device.powered)], ['在线', boolText(device.online)], ['固件/版本', ota.currentVersion || device.firmware || device.revision], ['Commit', ota.currentCommit],
|
||||
], 'device'),
|
||||
detailCard('SIM 卡', [
|
||||
['状态', sim.present === false ? '未插卡' : sim.present === true ? '已插卡' : '-'], ['ICCID', sim.iccid], ['IMSI', sim.imsi], ['号码', sim.phoneNumber], ['短信中心', sim.smsCenter], ['MCC/MNC', [network.mcc || sim.mcc, network.mnc || sim.mnc].filter(Boolean).join('/') || '-'],
|
||||
], 'sim'),
|
||||
detailCard('蜂窝网络', [
|
||||
['运营商', network.operator || sim.operator], ['注册状态', network.registration], ['网络制式', network.accessTechnology], ['信号强度', firstValue(network.signal, sim.signal)], ['数据连接', firstValue(data.active, data.connected, data.enabled) === true ? '已连接' : firstValue(data.active, data.connected, data.enabled) === false ? '未连接' : '-'], ['漫游', boolText(data.roaming)],
|
||||
], 'network'),
|
||||
detailCard('温度 / 系统', [
|
||||
['温度', formatTemperature(system.temperature)], ['CPU 负载', Array.isArray(system.cpuLoad) ? system.cpuLoad.join(' / ') : system.cpuLoad], ['内存', formatPercentPair(system.memory)], ['磁盘', formatPercentPair(system.disk)], ['运行时间', formatUptime(system.uptime)], ['系统', system.info ? Object.values(system.info).filter(Boolean).join(' · ') : '-'],
|
||||
], 'system'),
|
||||
detailCard('流量 / 短信', [
|
||||
['实时速率', formatSpeed(system.networkSpeed)], ['短信总数', sms.total], ['接收短信', sms.incoming], ['发送短信', sms.outgoing], ['推送成功', sms.pushed], ['通话记录', s?.summary?.calls?.calls?.length ?? '-'],
|
||||
], 'traffic'),
|
||||
].join('')
|
||||
}
|
||||
|
||||
function renderFeatures() {
|
||||
renderModuleTabs()
|
||||
const disabled = activeId ? '' : ' disabled'
|
||||
const visible = activeModule === 'all' ? modules : modules.filter(m => m.id === activeModule)
|
||||
$('featureGrid').innerHTML = visible.map(m => `<article class="feature">
|
||||
<div class="feature-head">
|
||||
<div><p class="eyebrow ${m.tone === 'danger' ? '' : 'accent'}">${escapeHtml(m.id)}</p><h4>${escapeHtml(m.title)}</h4></div>
|
||||
<span class="status-pill ${m.tone === 'danger' ? 'bad' : m.tone === 'warn' ? 'warn' : m.tone === 'ok' ? 'ok' : ''}">${m.endpoints.length} API</span>
|
||||
</div>
|
||||
<p>${escapeHtml(m.desc)}</p>
|
||||
${m.tone === 'danger' ? '<div class="danger-note">包含写操作/网络切换类接口,执行前确认目标设备。</div>' : ''}
|
||||
<div class="endpoint-pills">${m.endpoints.map(ep => `<button${disabled} data-ep="${escapeHtml(ep)}">${escapeHtml(ep)}</button>`).join('')}</div>
|
||||
</article>`).join('')
|
||||
$$('button[data-ep]', $('featureGrid')).forEach(btn => btn.addEventListener('click', () => {
|
||||
switchView('api')
|
||||
$('endpointInput').value = btn.dataset.ep
|
||||
$('methodSelect').value = inferMethod(btn.dataset.ep)
|
||||
runEndpoint()
|
||||
}))
|
||||
}
|
||||
function renderFeatures() { renderDeviceDetails() }
|
||||
|
||||
function renderEndpointList() {
|
||||
const groups = catalog.length ? catalog : modules.map(m => ({ id: m.id, name: m.title, endpoints: m.endpoints }))
|
||||
|
||||
+11
-11
@@ -62,7 +62,7 @@
|
||||
<div>
|
||||
<p id="activeKicker" class="eyebrow">NO ISLAND SELECTED</p>
|
||||
<h1 id="activeName">选择一个 SimAdmin 小岛</h1>
|
||||
<p id="activeMeta">查看设备、SIM、网络、短信、eSIM、OTA 与原始管理页面。</p>
|
||||
<p id="activeMeta">集中展示设备信息、联网状态、SIM、信号、温度、流量、系统资源、短信与版本。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button id="loginBtn" type="button" class="animal-btn default-btn" disabled>登录/刷新会话</button>
|
||||
@@ -71,26 +71,26 @@
|
||||
</header>
|
||||
|
||||
<nav class="phone-dock" aria-label="主视图">
|
||||
<button class="phone-app active app-teal" data-view="dashboard"><span>🏝️</span><b>总览</b></button>
|
||||
<button class="phone-app app-yellow" data-view="api"><span>{}</span><b>接口</b></button>
|
||||
<button class="phone-app active app-teal" data-view="dashboard"><span>📡</span><b>设备监控</b></button>
|
||||
<button class="phone-app app-blue" data-view="rawpage"><span>↗</span><b>原页</b></button>
|
||||
<button class="phone-app app-yellow" data-view="api"><span>{}</span><b>调试接口</b></button>
|
||||
<button id="densityBtn" class="phone-app app-green" type="button"><span>↕</span><b>密度</b></button>
|
||||
</nav>
|
||||
|
||||
<section id="dashboardView" class="view active dashboard-view">
|
||||
<section class="signal-board island-card pattern-app-yellow">
|
||||
<div class="section-ribbon color-app-yellow">小岛告示牌</div>
|
||||
<section id="dashboardView" class="view active dashboard-view device-dashboard" aria-label="设备监控大屏">
|
||||
<section class="signal-board island-card pattern-app-yellow monitor-summary-card">
|
||||
<div class="section-ribbon color-app-yellow">实时状态</div>
|
||||
<div id="signalPanel" class="signal-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="overview" class="nook-metrics"></section>
|
||||
<section id="overview" class="nook-metrics monitor-kpis" aria-label="关键状态指标"></section>
|
||||
|
||||
<section class="module-board island-card pattern-default">
|
||||
<section class="module-board island-card pattern-default device-detail-board">
|
||||
<div class="board-head">
|
||||
<div class="section-ribbon color-app-green">功能应用</div>
|
||||
<div id="moduleTabs" class="module-tabs"></div>
|
||||
<div class="section-ribbon color-app-green">设备详情</div>
|
||||
<p class="board-caption">按设备、SIM、蜂窝网络、系统资源、温度、流量和短信分类展示真实状态。</p>
|
||||
</div>
|
||||
<div id="featureGrid" class="app-grid"></div>
|
||||
<div id="featureGrid" class="device-detail-grid"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user