const statusView = document.getElementById('status-view'); const settingsEditor = document.getElementById('settings-editor'); const resultView = document.getElementById('result-view'); const dashboardSummary = document.getElementById('dashboard-summary'); const eventsList = document.getElementById('events-list'); const eventsEmpty = document.getElementById('events-empty'); const refreshDashboardButton = document.getElementById('refresh-dashboard'); const refreshStatusButton = document.getElementById('refresh-status'); const refreshEventsButton = document.getElementById('refresh-events'); const reloadSettingsButton = document.getElementById('reload-settings'); const saveSettingsButton = document.getElementById('save-settings'); const smsForm = document.getElementById('sms-form'); const deliveryModeSelect = document.getElementById('sms-delivery-mode'); const modeHint = document.getElementById('mode-hint'); const eventSearchInput = document.getElementById('event-search'); const eventResultFilter = document.getElementById('event-result-filter'); const metricEventCount = document.getElementById('metric-event-count'); const metricManualSendCount = document.getElementById('metric-manual-send-count'); const metricFailureCount = document.getElementById('metric-failure-count'); const metricLastPhone = document.getElementById('metric-last-phone'); const deliveryModeLabelMap = { dispatch: '通知转发', real_sms: '真实短信', }; let latestEvents = []; function getDeliveryModeLabel(mode) { return deliveryModeLabelMap[mode] || mode || '未知'; } function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function formatText(value, fallback = '-') { const text = String(value ?? '').trim(); return text || fallback; } function getResultLabel(result) { if (result === 'success') { return '成功'; } if (result === 'failed') { return '失败'; } return formatText(result, '未知'); } function updateModeHint() { const deliveryMode = deliveryModeSelect?.value || 'dispatch'; if (!modeHint) { return; } if (deliveryMode === 'real_sms') { modeHint.textContent = '当前为真实短信:需要已配置 ModemManager 发送能力;若发送失败,将展示错误分类与失败原因。'; modeHint.classList.add('mode-hint-warning'); return; } modeHint.textContent = '当前为通知转发:会按已启用通道分发,并写入事件留痕。'; modeHint.classList.remove('mode-hint-warning'); } async function fetchJson(url, options = {}) { const response = await fetch(url, options); const text = await response.text(); let data; try { data = text ? JSON.parse(text) : {}; } catch (error) { throw new Error(`响应不是合法 JSON: ${text}`); } if (!response.ok) { const failure = new Error(data.detail || data.error || `HTTP ${response.status}`); failure.payload = data; failure.status = response.status; throw failure; } return data; } function renderJson(element, payload) { element.textContent = JSON.stringify(payload, null, 2); } function renderResult(payload, options = {}) { const status = options.status || 'success'; const title = options.title || (status === 'failed' ? '发送失败' : '操作成功'); const rows = Object.entries(payload || {}).filter(([, value]) => value !== undefined && value !== null && value !== ''); if (!rows.length) { renderJson(resultView, payload || {}); return; } resultView.innerHTML = `
${escapeHtml(title)}
${rows .map( ([label, value]) => `
${escapeHtml(label)} ${escapeHtml(formatText(value))}
` ) .join('')}
`; } function updateMetric(element, value, fallback = '-') { element.textContent = value ?? fallback; } function renderSummaryItem(label, value) { return `
${escapeHtml(label)} ${escapeHtml(formatText(value))}
`; } function renderDashboard(payload) { const { summary = {}, runtime = {} } = payload; updateMetric(metricEventCount, summary.event_count ?? 0, '0'); updateMetric(metricManualSendCount, summary.manual_send_count ?? 0, '0'); updateMetric(metricFailureCount, summary.failure_count ?? 0, '0'); updateMetric(metricLastPhone, summary.last_sms_phone_number || '-'); dashboardSummary.innerHTML = [ renderSummaryItem('服务状态', runtime.status || 'unknown'), renderSummaryItem('启动时间', runtime.started_at || '-'), renderSummaryItem('请求次数', runtime.request_count ?? 0), renderSummaryItem('配置文件', runtime.config_path || '未落盘'), renderSummaryItem('运行模式', runtime.daemon?.mode || '-'), renderSummaryItem('监听地址', `${runtime.daemon?.host || '-'}:${runtime.daemon?.port || '-'}`), renderSummaryItem('日志级别', runtime.daemon?.log_level || '-'), renderSummaryItem('设备名', runtime.forwarding?.device_name || '-'), renderSummaryItem('启用通道', (runtime.notifications?.enabled_channels || []).join(', ') || '无'), renderSummaryItem('短信源', runtime.sms_source?.kind || '-'), renderSummaryItem('源路径', runtime.sms_source?.path || '-'), renderSummaryItem('轮询间隔', runtime.sms_source?.poll_interval_seconds ?? '-'), renderSummaryItem('当前事件数', summary.filtered_event_count ?? summary.event_count ?? 0), ].join(''); } function getEventSearchText(event) { return [ event.phone_number, event.content, event.detail, event.error_category, getDeliveryModeLabel(event.delivery_mode || ((event.channels || []).includes('real_sms') ? 'real_sms' : 'dispatch')), (event.channels || []).join(' '), ] .filter(Boolean) .join(' ') .toLowerCase(); } function getFilteredEvents(events = latestEvents) { const keyword = (eventSearchInput?.value || '').trim().toLowerCase(); const resultFilter = eventResultFilter?.value || 'all'; return events.filter((event) => { const normalizedResult = event.result === 'success' || event.result === 'failed' ? event.result : 'unknown'; if (resultFilter !== 'all' && normalizedResult !== resultFilter) { return false; } if (!keyword) { return true; } return `${normalizedResult} ${getResultLabel(normalizedResult)} ${getEventSearchText(event)}`.includes(keyword); }); } function renderEvents(events = latestEvents) { latestEvents = Array.isArray(events) ? events : []; const filteredEvents = getFilteredEvents(latestEvents); if (!filteredEvents.length) { eventsList.innerHTML = ''; eventsEmpty.style.display = 'block'; eventsEmpty.textContent = latestEvents.length ? '没有匹配当前筛选条件的事件,换个关键字或筛选项试试。' : '还没有事件,先去下面的短信测试台发一条试试。'; return; } eventsEmpty.style.display = 'none'; eventsList.innerHTML = filteredEvents .map((event) => { const channels = (event.channels || []).join(', ') || '无'; const normalizedResult = event.result === 'success' || event.result === 'failed' ? event.result : 'unknown'; const statusClass = normalizedResult === 'success' ? 'status-success' : 'status-failed'; const deliveryMode = event.delivery_mode || ((event.channels || []).includes('real_sms') ? 'real_sms' : 'dispatch'); const deliveryModeLabel = getDeliveryModeLabel(deliveryMode); const errorCategory = formatText(event.error_category, ''); return `
${escapeHtml(formatText(event.phone_number))}
${escapeHtml(deliveryModeLabel)} ${escapeHtml(getResultLabel(normalizedResult))}
时间:${escapeHtml(formatText(event.created_at))} · 来源:${escapeHtml(formatText(event.received_at))}
${escapeHtml(formatText(event.content))}
通知数:${escapeHtml(formatText(event.notification_count, '0'))} · 通道:${escapeHtml(channels)}
${errorCategory ? `
错误分类${escapeHtml(errorCategory)}
` : ''}
${escapeHtml(formatText(event.detail, '无补充说明'))}
`; }) .join(''); } async function loadStatus() { const status = await fetchJson('/api/system/status'); renderJson(statusView, status); } async function loadSettings() { const settings = await fetchJson('/api/settings'); settingsEditor.value = JSON.stringify(settings, null, 2); } async function loadDashboard() { const payload = await fetchJson('/api/dashboard'); renderDashboard(payload); renderEvents(payload.latest_events || []); } async function loadEvents() { const payload = await fetchJson('/api/events'); renderEvents(payload.events || []); } function bindEventFilters() { eventSearchInput?.addEventListener('input', () => renderEvents(latestEvents)); eventResultFilter?.addEventListener('change', () => renderEvents(latestEvents)); } async function refreshAll() { await Promise.all([loadDashboard(), loadStatus(), loadSettings()]); } async function saveSettings() { const parsed = JSON.parse(settingsEditor.value || '{}'); const payload = await fetchJson('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(parsed), }); renderResult( { 操作: '保存配置', 状态: '成功', 说明: '配置已通过校验并写入当前运行实例。', }, { title: '配置保存成功' } ); await refreshAll(); } async function sendSms(event) { event.preventDefault(); const formData = new FormData(smsForm); const deliveryMode = formData.get('delivery_mode') || 'dispatch'; const delivery_mode_label = getDeliveryModeLabel(deliveryMode); try { const payload = await fetchJson('/api/sms/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone_number: formData.get('phone_number'), content: formData.get('content'), delivery_mode: deliveryMode === 'real_sms' ? 'real_sms' : 'dispatch', }), }); renderResult( { 操作: '发送测试短信', 状态: '成功', delivery_mode_label, 手机号: payload.phone_number, 来源: payload.received_at, MessagePath: payload.message_path, ModemPath: payload.modem_path, }, { title: `${delivery_mode_label}发送成功` } ); await Promise.all([loadDashboard(), loadEvents(), loadStatus()]); } catch (error) { renderResult( { 操作: '发送测试短信', 状态: '发送失败', delivery_mode_label, 错误分类: error.payload?.error_category || error.payload?.error || 'unknown', 失败原因: error.payload?.detail || error.message, HTTP状态: error.status || '-', }, { status: 'failed', title: `${delivery_mode_label}发送失败` } ); await Promise.all([loadDashboard(), loadEvents(), loadStatus()]); } } async function boot() { try { updateModeHint(); bindEventFilters(); await refreshAll(); } catch (error) { resultView.textContent = error.message; } } refreshDashboardButton.addEventListener('click', async () => { try { await loadDashboard(); } catch (error) { resultView.textContent = error.message; } }); refreshStatusButton.addEventListener('click', async () => { try { await loadStatus(); } catch (error) { resultView.textContent = error.message; } }); refreshEventsButton.addEventListener('click', async () => { try { await loadEvents(); } catch (error) { resultView.textContent = error.message; } }); reloadSettingsButton.addEventListener('click', async () => { try { await loadSettings(); renderResult( { 操作: '重载配置', 状态: '成功', 说明: '编辑器已同步当前运行配置。', }, { title: '配置已重载' } ); } catch (error) { resultView.textContent = error.message; } }); saveSettingsButton.addEventListener('click', async () => { try { await saveSettings(); } catch (error) { resultView.textContent = error.message; } }); if (deliveryModeSelect) { deliveryModeSelect.addEventListener('change', updateModeHint); } smsForm.addEventListener('submit', async (event) => { try { await sendSms(event); } catch (error) { resultView.textContent = error.message; } }); boot();