let instances = [] let statuses = new Map() let catalog = [] let activeId = null let activeFilter = 'all' let activeModule = 'all' let configPath = 'config.json' let autoRefreshTimer = null const $ = (id) => document.getElementById(id) const $$ = (selector, root = document) => [...root.querySelectorAll(selector)] const modules = [ { id: 'overview', title: '设备总览', desc: '设备、SIM、网络、连接状态与流量统计。适合日常巡检和资产核对。', tone: 'ok', endpoints: ['/api/device', '/api/sim', '/api/network', '/api/stats', '/api/connectivity'] }, { id: 'sms', title: '短信中心', desc: '短信统计、列表、会话读取与短信发送。发送类接口请先在 API 工作台确认请求体。', tone: 'warn', endpoints: ['/api/sms/stats', '/api/sms/list', '/api/sms/conversation', '/api/sms/send'] }, { id: 'network', title: '蜂窝网络', desc: '数据开关、漫游、飞行模式、无线制式、频段/小区锁定与 APN。', tone: 'danger', endpoints: ['/api/data', '/api/roaming', '/api/airplane-mode', '/api/radio-mode', '/api/band-lock', '/api/cell-lock', '/api/apn', '/api/cells'] }, { id: 'device-network', title: '设备网络', desc: 'WLAN、DDNS、连接地址与局域网访问能力。', tone: 'neutral', endpoints: ['/api/device-network/wlan/status', '/api/device-network/wlan/profiles', '/api/device-network/ddns/status', '/api/device-network/ddns/config', '/api/network/connection-addresses'] }, { id: 'calls', title: '电话能力', desc: '通话列表、历史记录、IMS 与语音信箱状态。', tone: 'neutral', endpoints: ['/api/calls', '/api/call/history', '/api/ims/status', '/api/voicemail/status'] }, { id: 'esim', title: 'eSIM / 工作模式', desc: 'eUICC、profile、LPAC 状态、工作模式与 eSIM 配置。', tone: 'warn', endpoints: ['/api/work-mode', '/api/esim/config', '/api/esim/lpac/status', '/api/esim/euicc', '/api/esim/profiles'] }, { id: 'notify-auto-ota', title: '通知 / 自动化 / OTA', desc: '通知配置与日志、自动化任务日志、在线升级状态。', tone: 'neutral', endpoints: ['/api/notifications/config', '/api/notifications/logs', '/api/automation/config', '/api/automation/logs', '/api/ota/status'] }, ] function escapeHtml(text) { return String(text ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])) } function value(v) { return v === undefined || v === null || v === '' ? '-' : String(v) } function boolText(v) { if (v === true) return '开启' if (v === false) return '关闭' return value(v) } function activeInstance() { return instances.find(x => x.id === activeId) } function activeStatus() { return statuses.get(activeId) } function tagsFromInput(text) { return String(text || '').split(',').map(x => x.trim()).filter(Boolean) } function instancePayloadFromForm() { return { id: $('instanceIdInput').value.trim(), name: $('instanceNameInput').value.trim(), url: $('instanceUrlInput').value.trim(), description: $('instanceDescInput').value.trim(), tags: tagsFromInput($('instanceTagsInput').value), 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' if (s.authenticated === false) return 'auth' return 'online' } function statusText(s) { const kind = statusKind(s) if (kind === 'online') return `在线${s.latencyMs != null ? ` ${s.latencyMs}ms` : ''}` if (kind === 'auth') return '需登录' if (kind === 'offline') return '离线' return '未知' } function pillClass(s) { const kind = statusKind(s) if (kind === 'online') return 'ok' if (kind === 'auth') return 'warn' if (kind === 'offline') return 'bad' return '' } function showToast(message, timeout = 2600) { const toast = $('toast') toast.textContent = message toast.hidden = false clearTimeout(showToast._timer) showToast._timer = setTimeout(() => { toast.hidden = true }, timeout) } function flattenEndpoints() { return [...new Set([ ...catalog.flatMap(g => g.endpoints || []), ...modules.flatMap(m => m.endpoints || []), ])].sort() } function inferMethod(endpoint) { if (/send|config|data$|roaming$|airplane-mode$|radio-mode$|band-lock$|cell-lock$|work-mode|login|logout/i.test(endpoint)) return 'POST' return 'GET' } function proxiedUrl(endpoint) { if (!activeId) return null const normalized = endpoint.startsWith('/') ? endpoint : `/${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 `
${escapeHtml(label)}${escapeHtml(value(val))}
` } function detailCard(title, rows, tone = '') { return `

${escapeHtml(title)}

${rows.map(([k, v]) => `
${escapeHtml(k)}
${escapeHtml(value(v))}
`).join('')}
` } 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 } for (const inst of instances) counts[statusKind(statuses.get(inst.id))] += 1 $('fleetStats').innerHTML = [ ['全部', total], ['在线', counts.online], ['需登录', counts.auth], ['离线', counts.offline], ].map(([label, num]) => `
${num}${label}
`).join('') } function filteredInstances() { const q = $('instanceSearch')?.value.trim().toLowerCase() || '' return instances.filter(item => { const s = statuses.get(item.id) const kind = statusKind(s) if (activeFilter !== 'all' && kind !== activeFilter) return false if (!q) return true const haystack = [item.id, item.name, item.url, item.description, JSON.stringify(s?.summary || {})].join(' ').toLowerCase() return haystack.includes(q) }) } function renderInstances() { const root = $('instances') renderFleetStats() if (!instances.length) { root.innerHTML = '

未配置设备

点击上方“添加设备地址”即可新增 CPE / SimAdmin 地址。

' return } const list = filteredInstances() if (!list.length) { root.innerHTML = '

没有匹配设备

调整搜索关键字或状态筛选。

' 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 `
${escapeHtml(item.name || item.id)}
${escapeHtml(item.url)}
${escapeHtml(statusText(s))}
信号${escapeHtml(value(network.signal || sim.signal))} 温度${escapeHtml(formatTemperature(system.temperature))} 速率${escapeHtml(formatSpeed(system.networkSpeed))} SIM${escapeHtml(sim.present === false ? '未插卡' : value(sim.iccid))}
` }).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))) } function renderHero() { const { item, s, device, sim, network, system, data, ota } = getMonitorData() if (!item) { $('activeKicker').textContent = 'DEVICE MONITOR' $('activeName').textContent = '选择一个设备查看实时状态' $('activeMeta').textContent = '主页面展示各设备的状态、温度、流量、SIM、信号、系统资源与短信统计。' $('signalPanel').innerHTML = [ ['状态', '等待选择'], ['设备', '-'], ['温度', '-'], ['流量速率', '-'], ].map(([k, v]) => `
${k}${v}
`).join('') return } $('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)], ['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]) => `
${escapeHtml(k)}${escapeHtml(value(v))}
`).join('') } 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 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 renderDeviceDetails() { const { item, device, sim, network, data, sms, ota, system, s } = getMonitorData() if (!item) { $('featureGrid').innerHTML = `

设备监控大屏

选择左侧任一设备后,这里会按分类展示真实设备信息、温度、流量、系统资源、SIM 与短信状态。

` 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() { renderDeviceDetails() } function renderEndpointList() { const groups = catalog.length ? catalog : modules.map(m => ({ id: m.id, name: m.title, endpoints: m.endpoints })) $('endpointList').innerHTML = groups.map(group => `

${escapeHtml(group.name || group.id)}

${(group.endpoints || []).map(ep => ``).join('')}
`).join('') $$('.endpoint-row', $('endpointList')).forEach(row => row.addEventListener('click', () => { $('endpointInput').value = row.dataset.ep $('methodSelect').value = inferMethod(row.dataset.ep) $$('.endpoint-row', $('endpointList')).forEach(x => x.classList.toggle('active', x === row)) })) } function switchView(view) { $$('.phone-app[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 openInstanceDialog(id = '') { const item = id ? instances.find(x => x.id === id) : null $('instanceOriginalId').value = item?.id || '' $('instanceFormRibbon').textContent = item ? '编辑设备地址' : '添加设备地址' $('instanceIdInput').value = item?.id || '' $('instanceNameInput').value = item?.name || '' $('instanceUrlInput').value = item?.url || '' $('instanceDescInput').value = item?.description || '' $('instanceTagsInput').value = (item?.tags || []).join(', ') $('instancePasswordInput').value = '' $('instancePasswordInput').placeholder = item?.auth?.hasPassword ? '留空将清除已保存密码;输入新密码可替换' : '留空表示无密码' $('deleteInstanceBtn').hidden = !item $('instanceDialog').showModal() setTimeout(() => (item ? $('instanceNameInput') : $('instanceIdInput')).focus(), 50) } async function reloadConfigAndRefresh({ selectId = activeId, toast = '' } = {}) { await loadConfig({ preserveSelection: selectId }) await refreshStatus({ silent: true }) if (selectId && instances.some(x => x.id === selectId)) selectInstance(selectId) else setActiveHome() if (toast) showToast(toast) } async function saveInstanceFromForm() { const originalId = $('instanceOriginalId').value const payload = instancePayloadFromForm() const method = originalId ? 'PUT' : 'POST' const url = originalId ? `/api/instances/${encodeURIComponent(originalId)}` : '/api/instances' const res = await fetch(url, { method, headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) }) const data = await res.json().catch(() => ({})) if (!res.ok) throw new Error(data.error || `保存失败:HTTP ${res.status}`) $('instanceDialog').close() await reloadConfigAndRefresh({ selectId: data.instance?.id || payload.id, toast: originalId ? '设备地址已更新' : '设备地址已添加' }) } async function deleteInstance(id) { const item = instances.find(x => x.id === id) if (!item) return if (!confirm(`确定删除设备“${item.name || item.id}”?\n配置会立即写入 config.json。`)) return const res = await fetch(`/api/instances/${encodeURIComponent(id)}`, { method: 'DELETE' }) const data = await res.json().catch(() => ({})) if (!res.ok) { showToast(data.error || `删除失败:HTTP ${res.status}`, 4200) return } await reloadConfigAndRefresh({ selectId: null, toast: '设备地址已删除' }) } async function deleteCurrentInstanceFromDialog() { const id = $('instanceOriginalId').value if (!id) return $('instanceDialog').close() await deleteInstance(id) } async function loadConfig({ preserveSelection = activeId } = {}) { const [configRes, catalogRes] = await Promise.all([fetch('/api/config'), fetch('/api/catalog')]) if (!configRes.ok) throw new Error(`配置读取失败:HTTP ${configRes.status}`) const data = await configRes.json() instances = data.instances || [] configPath = data.configPath || 'config.json' $('configPath').textContent = configPath if (catalogRes.ok) catalog = (await catalogRes.json()).groups || [] 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) } async function refreshStatus({ silent = false } = {}) { $('refreshBtn').disabled = true if (!silent) showToast('正在刷新全部实例…', 1200) try { const res = await fetch('/api/status') 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() if (!silent) showToast(`已刷新 ${statuses.size} 个实例`) } catch (error) { showToast(error.message || '刷新失败', 4200) } finally { $('refreshBtn').disabled = false } } async function loginActive() { if (!activeId) return const item = activeInstance() if (!item) return // If config already has a saved password, try direct refresh first; otherwise ask for password. if (item.auth?.hasPassword) { showToast('正在使用本地配置密码刷新会话…', 1600) const res = await fetch(`/api/instances/${encodeURIComponent(activeId)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}) }) if (!res.ok) showToast(`登录失败:HTTP ${res.status}`, 4200) await refreshStatus({ silent: true }) return } $('loginDialog').showModal() setTimeout(() => $('passwordInput').focus(), 50) } function parseBody() { const raw = $('requestBody').value.trim() if (!raw) return undefined return JSON.parse(raw) } async function runEndpoint() { if (!activeId) { showToast('请先选择左侧设备') return } const endpoint = $('endpointInput').value.trim() const method = $('methodSelect').value if (!endpoint.startsWith('/')) { showToast('接口路径必须以 / 开头') return } $('rawOutput').textContent = '调用中…' try { const init = { method } if (method !== 'GET') { init.headers = { 'content-type': 'application/json' } const body = parseBody() if (body !== undefined) init.body = JSON.stringify(body) } const started = performance.now() const res = await fetch(proxiedUrl(endpoint), init) const elapsed = Math.round(performance.now() - started) const text = await res.text() let formatted = text try { formatted = JSON.stringify(JSON.parse(text), null, 2) } catch {} $('rawOutput').textContent = `HTTP ${res.status} · ${elapsed}ms · ${method} ${endpoint}\n\n${formatted}` } catch (error) { $('rawOutput').textContent = error.message || String(error) showToast('接口调用失败', 4200) } } function bindEvents() { $('refreshBtn').addEventListener('click', () => refreshStatus()) $('homeBtn').addEventListener('click', setActiveHome) $('addInstanceBtn').addEventListener('click', () => openInstanceDialog()) $('loginBtn').addEventListener('click', loginActive) $('runEndpoint').addEventListener('click', runEndpoint) $('instanceSearch').addEventListener('input', renderInstances) $('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))) $$('#statusFilters .tab-leaf').forEach(btn => btn.addEventListener('click', () => { activeFilter = btn.dataset.filter $$('#statusFilters .tab-leaf').forEach(x => x.classList.toggle('active', x === btn)) renderInstances() })) $('loginForm').addEventListener('submit', async (event) => { event.preventDefault() if (!activeId) return const password = $('passwordInput').value $('passwordInput').value = '' $('loginDialog').close() const res = await fetch(`/api/instances/${encodeURIComponent(activeId)}/login`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ password }) }) if (!res.ok) showToast(`登录失败:HTTP ${res.status}`, 4200) else showToast('会话已刷新') await refreshStatus({ silent: true }) }) $('instanceForm').addEventListener('submit', async (event) => { event.preventDefault() try { await saveInstanceFromForm() } catch (error) { showToast(error.message || '保存失败', 5200) } }) $('deleteInstanceBtn').addEventListener('click', deleteCurrentInstanceFromDialog) $('endpointInput').addEventListener('keydown', (event) => { if (event.key === 'Enter') runEndpoint() }) } function restorePrefs() { if (localStorage.getItem('multi-simadmin-density') === 'compact') document.body.classList.add('compact') switchView(localStorage.getItem('multi-simadmin-view') || 'dashboard') } try { restorePrefs() bindEvents() await loadConfig() await refreshStatus({ silent: true }) autoRefreshTimer = setInterval(() => refreshStatus({ silent: true }), 30000) window.addEventListener('beforeunload', () => clearInterval(autoRefreshTimer)) } catch (error) { console.error(error) showToast(error.message || '初始化失败', 8000) $('rawOutput').textContent = error.stack || String(error) }