Refocus UI on device card details

This commit is contained in:
chick
2026-07-08 00:17:22 +08:00
parent 0e746f961c
commit 4f80e951d4
4 changed files with 224 additions and 196 deletions
+112 -91
View File
@@ -42,20 +42,6 @@ function instancePayloadFromForm() {
auth: { password: $('instancePasswordInput').value },
}
}
function setActiveHome() {
activeId = null
localStorage.removeItem('multi-simadmin-active')
$('loginBtn').disabled = true
$('openExternal').href = '#'
$('openExternal').classList.add('disabled')
$('emptyState').hidden = false
$('frame').hidden = true
$('frame').removeAttribute('src')
switchView('dashboard')
renderInstances()
renderOverview()
renderFeatures()
}
function statusKind(s) {
if (!s) return 'unknown'
if (!s.reachable) return 'offline'
@@ -191,11 +177,17 @@ function filteredInstances() {
})
}
function renderInstances() {
const root = $('instances')
function deviceSummary(item) {
const s = statuses.get(item.id)
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 renderHomeCards() {
const root = $('homeDeviceGrid')
renderFleetStats()
if (!instances.length) {
root.innerHTML = '<section class="empty-state mini-empty"><h3>未配置设备</h3><p>点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。</p></section>'
root.innerHTML = '<section class="empty-state mini-empty"><h3>未配置设备</h3><p>点击“添加设备”新增 SimAdmin 地址后,这里会出现设备卡片。</p></section>'
return
}
const list = filteredInstances()
@@ -204,52 +196,87 @@ function renderInstances() {
return
}
root.innerHTML = list.map(item => {
const s = statuses.get(item.id)
const sum = s?.summary || {}
const network = sum.network || {}
const sim = sum.sim || {}
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">
const { s, device, sim, network, data, sms, system, ota } = deviceSummary(item)
const dataState = firstValue(data.active, data.connected, data.enabled)
return `<article class="sim-device-card ${pillClass(s)}" data-open-detail="${escapeHtml(item.id)}" role="button" tabindex="0" aria-label="打开 ${escapeHtml(item.name || item.id)} 详情">
<div class="sim-card-head">
<div>
<div class="instance-name">${escapeHtml(item.name || item.id)}</div>
<div class="instance-url">${escapeHtml(item.url)}</div>
<p class="eyebrow">${escapeHtml(item.id)}</p>
<h2>${escapeHtml(item.name || item.id)}</h2>
<span class="instance-url">${escapeHtml(item.url)}</span>
</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 class="sim-card-model">${escapeHtml(value([device.manufacturer, device.model].filter(Boolean).join(' ') || device.imei || '未知设备'))}</div>
<div class="sim-card-kpis">
<span><em>SIM</em><b>${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}</b></span>
<span><em>信号</em><b>${escapeHtml(value(firstValue(network.signal, sim.signal)))}</b></span>
<span><em>温度</em><b>${escapeHtml(formatTemperature(system.temperature))}</b></span>
<span><em>流量</em><b>${escapeHtml(formatSpeed(system.networkSpeed))}</b></span>
<span><em>数据</em><b>${escapeHtml(dataState === true ? '已连接' : dataState === false ? '未连接' : '-')}</b></span>
<span><em>短信</em><b>${escapeHtml(sms.total != null ? `${sms.total}` : '-')}</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 class="sim-card-foot">
<span>${escapeHtml(value(network.operator || sim.operator || network.registration))}</span>
<span>${escapeHtml(value(ota.currentVersion || device.firmware || device.revision))}</span>
</div>
</article>`
}).join('')
$$('.instance-card', root).forEach(card => card.addEventListener('click', (event) => {
if (event.target.closest('button')) return
selectInstance(card.dataset.id)
}))
$$('[data-edit-id]', root).forEach(btn => btn.addEventListener('click', () => openInstanceDialog(btn.dataset.editId)))
$$('[data-delete-id]', root).forEach(btn => btn.addEventListener('click', () => deleteInstance(btn.dataset.deleteId)))
$$('[data-open-detail]', root).forEach(card => {
const open = () => openDeviceDetail(card.dataset.openDetail)
card.addEventListener('click', open)
card.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); open() }
})
})
}
function renderInstances() { renderHomeCards() }
function openDeviceDetail(id) {
const item = instances.find(x => x.id === id)
if (!item) return
activeId = id
localStorage.setItem('multi-simadmin-active', id)
$('loginBtn').disabled = false
$('openExternal').href = item.url
$('openExternal').classList.remove('disabled')
$('emptyState').hidden = true
$('frame').hidden = false
$('frame').src = item.url
switchView('detail')
renderHomeCards()
renderOverview()
renderDeviceDetails()
renderDetailActions()
}
function setActiveHome() {
activeId = null
localStorage.removeItem('multi-simadmin-active')
$('loginBtn').disabled = true
$('openExternal').href = '#'
$('openExternal').classList.add('disabled')
$('emptyState').hidden = false
$('frame').hidden = true
$('frame').removeAttribute('src')
switchView('home')
renderHomeCards()
}
function renderHero() {
const { item, s, device, sim, network, system, data, ota } = getMonitorData()
if (!item) {
$('activeKicker').textContent = 'DEVICE MONITOR'
$('activeName').textContent = '选择一个设备查看实时状态'
$('activeMeta').textContent = '主页面展示各设备的状态、温度、流量、SIM、信号、系统资源与短信统计。'
$('activeKicker').textContent = 'DEVICE DETAIL'
$('activeName').textContent = '选择一个 SimAdmin 设备'
$('activeMeta').textContent = '从首页卡片进入详情页后,可查看状态并使用功能交互。'
$('signalPanel').innerHTML = [
['状态', '等待选择'], ['设备', '-'], ['温度', '-'], ['流量速率', '-'],
].map(([k, v]) => `<div class="signal-item"><span>${k}</span><b>${v}</b></div>`).join('')
return
}
$('activeKicker').textContent = `${item.id} · ${statusText(s)}`
$('activeName').textContent = `${item.name || item.id} 设备监控`
$('activeName').textContent = `${item.name || item.id} 详情页`
$('activeMeta').textContent = `${device.manufacturer || ''} ${device.model || ''}`.trim() || item.url
$('signalPanel').innerHTML = [
['在线状态', statusText(s)],
@@ -264,18 +291,8 @@ function renderHero() {
}
function renderOverview() {
const { item, s, device, sim, network, data, sms, system } = getMonitorData()
if (!item) {
$('overview').innerHTML = [
metricCard('设备', `${instances.length}`),
metricCard('在线', [...statuses.values()].filter(x => statusKind(x) === 'online').length),
metricCard('温度', '选择设备后显示'),
metricCard('流量', '选择设备后显示'),
].join('')
renderHero()
renderDeviceDetails()
return
}
const { item, s, sim, network, data, sms, system } = getMonitorData()
if (!item) return
const dataState = firstValue(data.active, data.connected, data.enabled)
$('overview').innerHTML = [
metricCard('状态', s ? (s.reachable ? (s.authenticated === false ? '在线/未登录' : '在线') : '离线') : '未知', pillClass(s)),
@@ -288,16 +305,12 @@ function renderOverview() {
metricCard('短信', sms.total != null ? `${sms.total} / 收 ${value(sms.incoming)} / 发 ${value(sms.outgoing)}` : '-'),
].join('')
renderHero()
renderDeviceDetails()
}
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 = [
if (!item) return
$('detailDeviceGrid').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'),
@@ -316,7 +329,26 @@ function renderDeviceDetails() {
].join('')
}
function renderFeatures() { renderDeviceDetails() }
function renderDetailActions() {
const disabled = activeId ? '' : ' disabled'
const primary = [
['设备状态', '/api/device'], ['SIM 信息', '/api/sim'], ['蜂窝网络', '/api/network'], ['系统资源', '/api/stats'],
['短信统计', '/api/sms/stats'], ['通话记录', '/api/calls'], ['OTA 状态', '/api/ota/status'], ['数据连接', '/api/data'],
]
$('detailActions').innerHTML = `
<button type="button" class="detail-action raw-action" data-view="rawpage">打开原始管理页</button>
${primary.map(([label, ep]) => `<button type="button" class="detail-action"${disabled} data-detail-ep="${escapeHtml(ep)}"><span>${escapeHtml(label)}</span><code>${escapeHtml(ep)}</code></button>`).join('')}
`
$$('[data-detail-ep]', $('detailActions')).forEach(btn => btn.addEventListener('click', () => {
switchView('api')
$('endpointInput').value = btn.dataset.detailEp
$('methodSelect').value = inferMethod(btn.dataset.detailEp)
runEndpoint()
}))
$$('[data-view]', $('detailActions')).forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view)))
}
function renderFeatures() { renderDeviceDetails(); renderDetailActions() }
function renderEndpointList() {
const groups = catalog.length ? catalog : modules.map(m => ({ id: m.id, name: m.title, endpoints: m.endpoints }))
@@ -332,26 +364,13 @@ function renderEndpointList() {
}
function switchView(view) {
$$('.phone-app[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view))
if (!['home', 'detail', 'rawpage', 'api'].includes(view)) view = activeId ? 'detail' : 'home'
$$('[data-view]').forEach(btn => btn.classList.toggle('active', btn.dataset.view === view))
$$('.view').forEach(panel => panel.classList.toggle('active', panel.id === `${view}View`))
localStorage.setItem('multi-simadmin-view', view)
}
function selectInstance(id) {
const item = instances.find(x => x.id === id)
if (!item) return
activeId = id
localStorage.setItem('multi-simadmin-active', id)
$('loginBtn').disabled = false
$('openExternal').href = item.url
$('openExternal').classList.remove('disabled')
$('emptyState').hidden = true
$('frame').hidden = false
$('frame').src = item.url
renderInstances()
renderOverview()
renderFeatures()
}
function selectInstance(id) { openDeviceDetail(id) }
function openInstanceDialog(id = '') {
const item = id ? instances.find(x => x.id === id) : null
@@ -420,10 +439,8 @@ async function loadConfig({ preserveSelection = activeId } = {}) {
const saved = preserveSelection || localStorage.getItem('multi-simadmin-active')
activeId = saved && instances.some(x => x.id === saved) ? saved : null
renderEndpointList()
renderInstances()
renderOverview()
renderFeatures()
if (activeId) selectInstance(activeId)
renderHomeCards()
if (activeId) openDeviceDetail(activeId)
}
async function refreshStatus({ silent = false } = {}) {
@@ -434,9 +451,12 @@ async function refreshStatus({ silent = false } = {}) {
if (!res.ok) throw new Error(`状态读取失败:HTTP ${res.status}`)
const data = await res.json()
statuses = new Map((data.instances || []).map(item => [item.id, item]))
renderInstances()
renderOverview()
renderFeatures()
renderHomeCards()
if (activeId) {
renderOverview()
renderDeviceDetails()
renderDetailActions()
}
if (!silent) showToast(`已刷新 ${statuses.size} 个实例`)
} catch (error) {
showToast(error.message || '刷新失败', 4200)
@@ -469,7 +489,7 @@ function parseBody() {
async function runEndpoint() {
if (!activeId) {
showToast('请先选择左侧设备')
showToast('请先在首页选择设备卡片')
return
}
const endpoint = $('endpointInput').value.trim()
@@ -505,16 +525,17 @@ function bindEvents() {
$('addInstanceBtn').addEventListener('click', () => openInstanceDialog())
$('loginBtn').addEventListener('click', loginActive)
$('runEndpoint').addEventListener('click', runEndpoint)
$('instanceSearch').addEventListener('input', renderInstances)
$('instanceSearch').addEventListener('input', renderHomeCards)
$('backToFleetBtn').addEventListener('click', setActiveHome)
$('densityBtn').addEventListener('click', () => {
document.body.classList.toggle('compact')
localStorage.setItem('multi-simadmin-density', document.body.classList.contains('compact') ? 'compact' : 'normal')
})
$$('.phone-app[data-view]').forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view)))
$$('[data-view]').forEach(btn => btn.addEventListener('click', () => switchView(btn.dataset.view)))
$$('#statusFilters .tab-leaf').forEach(btn => btn.addEventListener('click', () => {
activeFilter = btn.dataset.filter
$$('#statusFilters .tab-leaf').forEach(x => x.classList.toggle('active', x === btn))
renderInstances()
renderHomeCards()
}))
$('loginForm').addEventListener('submit', async (event) => {
event.preventDefault()
@@ -543,7 +564,7 @@ function bindEvents() {
function restorePrefs() {
if (localStorage.getItem('multi-simadmin-density') === 'compact') document.body.classList.add('compact')
switchView(localStorage.getItem('multi-simadmin-view') || 'dashboard')
switchView(localStorage.getItem('multi-simadmin-view') || 'home')
}
try {
+88 -86
View File
@@ -17,86 +17,95 @@
<span class="leaf leaf-b"></span>
</div>
<div id="appShell" class="island-shell">
<aside class="nook-sidebar" aria-label="设备岛屿列表">
<header class="island-card sidebar-card pattern-default">
<div class="ribbon compact-ribbon" aria-label="Multi SimAdmin">
<span class="ribbon-back ribbon-back-left"></span>
<span class="ribbon-back ribbon-back-right"></span>
<span class="ribbon-fold ribbon-fold-left"></span>
<span class="ribbon-fold ribbon-fold-right"></span>
<span class="ribbon-front"></span>
<span class="ribbon-text">SimAdmin 岛屿</span>
</div>
<p class="sidebar-subtitle">像 NookPhone 一样管理每个 CPE 小岛</p>
<div class="sidebar-actions">
<button id="homeBtn" class="animal-btn default-btn" type="button">返回首页</button>
<button id="refreshBtn" class="animal-btn primary-btn" type="button">刷新状态</button>
</div>
<button id="addInstanceBtn" class="animal-btn primary-btn block-btn" type="button"> 添加设备地址</button>
</header>
<div id="appShell" class="sim-shell">
<header class="sim-topbar island-card pattern-app-teal">
<div>
<p class="eyebrow">MULTI SIMADMIN</p>
<h1>SimAdmin 设备卡片墙</h1>
<p class="topbar-copy">首页直接展示每个 SimAdmin 设备卡片;点击卡片进入设备详情页,再进行状态查看和功能交互。</p>
</div>
<div class="hero-actions">
<button id="homeBtn" class="animal-btn default-btn" type="button">设备首页</button>
<button id="refreshBtn" class="animal-btn primary-btn" type="button">刷新状态</button>
<button id="addInstanceBtn" class="animal-btn primary-btn" type="button"> 添加设备</button>
<button id="densityBtn" class="animal-btn default-btn" type="button">切换密度</button>
</div>
</header>
<section class="wallet-row" id="fleetStats"></section>
<label class="search-pill" aria-label="搜索设备">
<span></span>
<input id="instanceSearch" type="search" placeholder="搜索设备、标签、地址…" autocomplete="off" />
</label>
<nav class="island-tabs" id="statusFilters" aria-label="状态筛选">
<button class="tab-leaf active" data-filter="all">全部</button>
<button class="tab-leaf" data-filter="online">在线</button>
<button class="tab-leaf" data-filter="auth">需登录</button>
<button class="tab-leaf" data-filter="offline">离线</button>
</nav>
<div id="instances" class="island-list"></div>
<footer class="config-note">
<span>本地配置</span>
<code id="configPath">config.json</code>
</footer>
</aside>
<main class="nook-workspace">
<header class="workspace-hero island-card pattern-app-teal">
<div>
<p id="activeKicker" class="eyebrow">NO ISLAND SELECTED</p>
<h1 id="activeName">选择一个 SimAdmin 小岛</h1>
<p id="activeMeta">集中展示设备信息、联网状态、SIM、信号、温度、流量、系统资源、短信与版本。</p>
</div>
<div class="hero-actions">
<button id="loginBtn" type="button" class="animal-btn default-btn" disabled>登录/刷新会话</button>
<a id="openExternal" href="#" target="_blank" rel="noreferrer" class="animal-btn default-btn disabled">原站打开</a>
</div>
</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-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 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 monitor-kpis" aria-label="关键状态指标"></section>
<section class="module-board island-card pattern-default device-detail-board">
<div class="board-head">
<div class="section-ribbon color-app-green">设备详情</div>
<p class="board-caption">按设备、SIM、蜂窝网络、系统资源、温度、流量和短信分类展示真实状态。</p>
<main class="sim-main">
<section id="homeView" class="view active home-view" aria-label="SimAdmin 卡片首页">
<section class="home-toolbar island-card pattern-default">
<div>
<div class="section-ribbon color-app-green">设备卡片墙</div>
<p class="board-caption">每张卡片对应一个 SimAdmin;卡片上直接显示在线状态、SIM、信号、温度、流量和短信摘要。</p>
</div>
<div class="home-tools">
<section class="wallet-row" id="fleetStats"></section>
<label class="search-pill" aria-label="搜索设备">
<span></span>
<input id="instanceSearch" type="search" placeholder="搜索设备、标签、地址、ICCID…" autocomplete="off" />
</label>
<nav class="island-tabs" id="statusFilters" aria-label="状态筛选">
<button class="tab-leaf active" data-filter="all">全部</button>
<button class="tab-leaf" data-filter="online">在线</button>
<button class="tab-leaf" data-filter="auth">需登录</button>
<button class="tab-leaf" data-filter="offline">离线</button>
</nav>
</div>
<div id="featureGrid" class="device-detail-grid"></div>
</section>
<section id="homeDeviceGrid" class="home-device-grid" aria-label="设备卡片列表"></section>
</section>
<section id="detailView" class="view detail-view" aria-label="设备详情页">
<header class="workspace-hero island-card pattern-app-teal detail-hero">
<div>
<p id="activeKicker" class="eyebrow">DEVICE DETAIL</p>
<h1 id="activeName">选择一个 SimAdmin 设备</h1>
<p id="activeMeta">从首页设备卡片进入详情页后,可查看完整状态并使用功能入口。</p>
</div>
<div class="hero-actions">
<button id="backToFleetBtn" type="button" class="animal-btn default-btn">← 返回卡片墙</button>
<button id="loginBtn" type="button" class="animal-btn default-btn" disabled>登录/刷新会话</button>
<a id="openExternal" href="#" target="_blank" rel="noreferrer" class="animal-btn default-btn disabled">原站打开</a>
</div>
</header>
<section class="detail-layout">
<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 monitor-kpis" aria-label="关键状态指标"></section>
<section class="module-board island-card pattern-default device-detail-board">
<div class="board-head">
<div class="section-ribbon color-app-green">设备详情</div>
<p class="board-caption">设备、SIM、蜂窝网络、系统资源、温度、流量、短信与版本。</p>
</div>
<div id="detailDeviceGrid" class="device-detail-grid"></div>
</section>
<section class="island-card pattern-purple detail-actions-card">
<div class="section-ribbon color-purple">功能交互</div>
<div id="detailActions" class="detail-actions"></div>
</section>
</section>
</section>
<section id="rawpageView" class="view rawpage-view">
<div class="frame-toolbar island-card pattern-app-blue">
<button type="button" class="animal-btn default-btn" data-view="detail">← 返回详情</button>
<span id="frameHint">原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。</span>
</div>
<section id="emptyState" class="empty-state island-card pattern-default">
<div class="section-ribbon color-app-pink">等待选择</div>
<h3>还没有选择设备</h3>
<p>先在首页点击一个设备卡片,再进入原始页面。</p>
</section>
<iframe id="frame" class="frame" title="SimAdmin instance" hidden></iframe>
</section>
<section id="apiView" class="view api-layout">
<aside class="api-catalog island-card pattern-purple">
<div class="section-ribbon color-purple">API 地图</div>
<div class="section-ribbon color-purple">调试接口</div>
<div id="endpointList" class="endpoint-list"></div>
</aside>
<section class="api-console island-card pattern-default">
@@ -110,22 +119,15 @@
<button id="runEndpoint" class="animal-btn primary-btn" type="button">发送</button>
</div>
<textarea id="requestBody" class="request-body" placeholder='POST/DELETE 请求体 JSON,例如 {"enabled":true}'></textarea>
<pre id="rawOutput" class="raw-output">选择设备后可调用统一代理接口。</pre>
<pre id="rawOutput" class="raw-output">从设备详情页进入后,可对当前设备调用代理接口。</pre>
</section>
</section>
<section id="rawpageView" class="view rawpage-view">
<div class="frame-toolbar island-card pattern-app-blue">
<span id="frameHint">原始 SimAdmin 页面会在下方显示;若目标站禁止 iframe,请使用“原站打开”。</span>
</div>
<section id="emptyState" class="empty-state island-card pattern-default">
<div class="section-ribbon color-app-pink">等待登岛</div>
<h3>还没有选择设备</h3>
<p>在左侧选择一个小岛后,这里会加载原 SimAdmin 页面。</p>
</section>
<iframe id="frame" class="frame" title="SimAdmin instance" hidden></iframe>
</section>
</main>
<footer class="config-note sim-config-note">
<span>本地配置</span>
<code id="configPath">config.json</code>
</footer>
</div>
<dialog id="loginDialog">
+7 -13
View File
File diff suppressed because one or more lines are too long
+17 -6
View File
@@ -8,19 +8,30 @@ async function text(path) {
return readFile(new URL(path, root), 'utf8')
}
test('dashboard primary surface is a device monitoring board, not an API catalog', async () => {
test('home screen is a simadmin card wall and device detail owns interactions', async () => {
const html = await text('public/index.html')
const app = await text('public/app.js')
assert.match(html, /设备监控|设备详情|实时状态/)
assert.match(app, /renderDeviceDetails/)
assert.match(html, /id="homeView"/)
assert.match(html, /id="homeDeviceGrid"/)
assert.match(html, /设备卡片|设备卡片墙|SimAdmin 卡片/)
assert.match(html, /id="detailView"/)
assert.match(html, /id="backToFleetBtn"/)
assert.match(html, /id="detailActions"/)
assert.match(html, /id="detailDeviceGrid"/)
assert.match(app, /function renderHomeCards/)
assert.match(app, /function openDeviceDetail/)
assert.match(app, /function renderDeviceDetails/)
assert.match(app, /data-open-detail/)
assert.match(app, /\['home', 'detail', 'rawpage', 'api'\]/)
assert.match(app, /温度/)
assert.match(app, /流量|速率/)
assert.match(app, /内存|磁盘/)
assert.match(app, /SIM|ICCID/)
assert.match(app, /短信/)
const dashboardBlock = html.match(/<section id="dashboardView"[\s\S]*?<\/section>\s*<section id="apiView"/)
assert.ok(dashboardBlock, 'dashboardView block should exist before apiView')
assert.doesNotMatch(dashboardBlock[0], /API 地图|endpointList|接口路径/)
const homeBlock = html.match(/<section id="homeView"[\s\S]*?<\/section>\s*<section id="detailView"/)
assert.ok(homeBlock, 'homeView should exist before detailView')
assert.doesNotMatch(homeBlock[0], /API 地图|endpointList|接口路径|rawOutput/)
})