[verified] refactor: establish service and frontend boundaries
This commit is contained in:
+48
-52
@@ -1,9 +1,13 @@
|
||||
import { escapeHtml, value, formatBytes } from './domain/formatters.js'
|
||||
import { createApiClient, normalizeTargetUrl } from './infrastructure/api-client.js'
|
||||
import { RequestCoordinator } from './state/request-coordinator.js'
|
||||
import { createViewRouter, resolveView } from './router/view-router.js'
|
||||
|
||||
let instances = []
|
||||
let statuses = new Map()
|
||||
let catalog = []
|
||||
let activeId = null
|
||||
let activeFilter = 'all'
|
||||
let activeModule = 'all'
|
||||
let configPath = 'config.json'
|
||||
let autoRefreshTimer = null
|
||||
|
||||
@@ -20,10 +24,10 @@ const modules = [
|
||||
{ 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) }
|
||||
const requests = new RequestCoordinator()
|
||||
const api = createApiClient()
|
||||
const routeView = createViewRouter({ controls: () => $$('[data-view]'), panels: () => $$('.view') })
|
||||
|
||||
function boolText(v) {
|
||||
if (v === true) return '开启'
|
||||
if (v === false) return '关闭'
|
||||
@@ -79,27 +83,10 @@ 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)) {
|
||||
@@ -236,6 +223,7 @@ function renderInstances() { renderHomeCards() }
|
||||
function openDeviceDetail(id) {
|
||||
const item = instances.find(x => x.id === id)
|
||||
if (!item) return
|
||||
if (activeId !== id) { requests.invalidate('api-console'); requests.invalidate('login') }
|
||||
activeId = id
|
||||
localStorage.setItem('multi-simadmin-active', id)
|
||||
$('loginBtn').disabled = false
|
||||
@@ -252,6 +240,7 @@ function openDeviceDetail(id) {
|
||||
}
|
||||
|
||||
function setActiveHome() {
|
||||
requests.invalidate('api-console'); requests.invalidate('login')
|
||||
activeId = null
|
||||
localStorage.removeItem('multi-simadmin-active')
|
||||
$('loginBtn').disabled = true
|
||||
@@ -364,10 +353,8 @@ function renderEndpointList() {
|
||||
}
|
||||
|
||||
function switchView(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)
|
||||
view = resolveView(view, Boolean(activeId))
|
||||
routeView(view)
|
||||
}
|
||||
|
||||
function selectInstance(id) { openDeviceDetail(id) }
|
||||
@@ -399,11 +386,8 @@ async function reloadConfigAndRefresh({ selectId = activeId, 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}`)
|
||||
payload.url = normalizeTargetUrl(payload.url)
|
||||
const data = originalId ? await api.updateInstance(originalId, payload) : await api.createInstance(payload)
|
||||
$('instanceDialog').close()
|
||||
await reloadConfigAndRefresh({ selectId: data.instance?.id || payload.id, toast: originalId ? '设备地址已更新' : '设备地址已添加' })
|
||||
}
|
||||
@@ -412,12 +396,7 @@ 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
|
||||
}
|
||||
try { await api.deleteInstance(id) } catch (error) { showToast(error.message, 4200); return }
|
||||
await reloadConfigAndRefresh({ selectId: null, toast: '设备地址已删除' })
|
||||
}
|
||||
|
||||
@@ -429,13 +408,11 @@ async function deleteCurrentInstanceFromDialog() {
|
||||
}
|
||||
|
||||
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()
|
||||
const [data, catalogData] = await Promise.all([api.config(), api.catalog().catch(() => ({ groups: [] }))])
|
||||
instances = data.instances || []
|
||||
configPath = data.configPath || 'config.json'
|
||||
$('configPath').textContent = configPath
|
||||
if (catalogRes.ok) catalog = (await catalogRes.json()).groups || []
|
||||
catalog = catalogData.groups || []
|
||||
const saved = preserveSelection || localStorage.getItem('multi-simadmin-active')
|
||||
activeId = saved && instances.some(x => x.id === saved) ? saved : null
|
||||
renderEndpointList()
|
||||
@@ -447,9 +424,12 @@ 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()
|
||||
const result = await requests.run('fleet-status', 'fleet', () => api.status())
|
||||
if (!result.accepted || result.error) {
|
||||
if (result.accepted && result.error) showToast(result.error.message || '刷新失败', 4200)
|
||||
return
|
||||
}
|
||||
const data = result.value
|
||||
statuses = new Map((data.instances || []).map(item => [item.id, item]))
|
||||
renderHomeCards()
|
||||
if (activeId) {
|
||||
@@ -471,13 +451,16 @@ async function loginActive() {
|
||||
if (!item) return
|
||||
// If config already has a saved password, try direct refresh first; otherwise ask for password.
|
||||
if (item.auth?.hasPassword) {
|
||||
const loginOwnerId = activeId
|
||||
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)
|
||||
const result = await requests.run('login', loginOwnerId, () => api.login(loginOwnerId))
|
||||
if (!result.accepted || result.ownerId !== activeId) return
|
||||
if (result.error) { showToast(result.error.message || '登录失败', 4200); return }
|
||||
await refreshStatus({ silent: true })
|
||||
return
|
||||
}
|
||||
$('loginDialog').showModal()
|
||||
$('loginDialog').dataset.instanceId = activeId
|
||||
setTimeout(() => $('passwordInput').focus(), 50)
|
||||
}
|
||||
|
||||
@@ -492,6 +475,7 @@ async function runEndpoint() {
|
||||
showToast('请先在首页选择设备卡片')
|
||||
return
|
||||
}
|
||||
const requestOwnerId = activeId
|
||||
const endpoint = $('endpointInput').value.trim()
|
||||
const method = $('methodSelect').value
|
||||
if (!endpoint.startsWith('/')) {
|
||||
@@ -507,9 +491,19 @@ async function runEndpoint() {
|
||||
if (body !== undefined) init.body = JSON.stringify(body)
|
||||
}
|
||||
const started = performance.now()
|
||||
const res = await fetch(proxiedUrl(endpoint), init)
|
||||
const result = await requests.run('api-console', requestOwnerId, async () => {
|
||||
const normalized = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
|
||||
const res = await api.proxy(requestOwnerId, normalized, init)
|
||||
return { res, text: await res.text() }
|
||||
})
|
||||
if (!result.accepted || result.ownerId !== activeId) return
|
||||
if (result.error) {
|
||||
$('rawOutput').textContent = result.error.message || String(result.error)
|
||||
showToast('接口调用失败', 4200)
|
||||
return
|
||||
}
|
||||
const { res, text } = result.value
|
||||
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}`
|
||||
@@ -539,13 +533,15 @@ function bindEvents() {
|
||||
}))
|
||||
$('loginForm').addEventListener('submit', async (event) => {
|
||||
event.preventDefault()
|
||||
if (!activeId) return
|
||||
const loginInstanceId = $('loginDialog').dataset.instanceId
|
||||
if (!loginInstanceId) 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('会话已刷新')
|
||||
const result = await requests.run('login', loginInstanceId, () => api.login(loginInstanceId, password))
|
||||
if (!result.accepted || result.ownerId !== activeId) return
|
||||
if (result.error) { showToast(result.error.message || '登录失败', 4200); return }
|
||||
showToast('会话已刷新')
|
||||
await refreshStatus({ silent: true })
|
||||
})
|
||||
$('instanceForm').addEventListener('submit', async (event) => {
|
||||
|
||||
Reference in New Issue
Block a user