Files

391 lines
13 KiB
JavaScript

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('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
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 = `
<div class="result-panel">
<div class="result-title ${escapeHtml(status)}">${escapeHtml(title)}</div>
<div class="result-grid">
${rows
.map(
([label, value]) => `
<div class="result-item">
<span class="result-label">${escapeHtml(label)}</span>
<span class="result-value">${escapeHtml(formatText(value))}</span>
</div>
`
)
.join('')}
</div>
</div>
`;
}
function updateMetric(element, value, fallback = '-') {
element.textContent = value ?? fallback;
}
function renderSummaryItem(label, value) {
return `
<article class="summary-item">
<span class="summary-label">${escapeHtml(label)}</span>
<strong class="summary-value">${escapeHtml(formatText(value))}</strong>
</article>
`;
}
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 `
<article class="event-item ${normalizedResult === 'failed' ? 'event-item-failed' : ''}">
<div class="event-topline">
<strong>${escapeHtml(formatText(event.phone_number))}</strong>
<div class="event-badges">
<span class="event-mode-badge">${escapeHtml(deliveryModeLabel)}</span>
<span class="event-status ${statusClass}">${escapeHtml(getResultLabel(normalizedResult))}</span>
</div>
</div>
<div class="event-meta">时间:${escapeHtml(formatText(event.created_at))} · 来源:${escapeHtml(formatText(event.received_at))}</div>
<div class="event-body">${escapeHtml(formatText(event.content))}</div>
<div class="event-meta">通知数:${escapeHtml(formatText(event.notification_count, '0'))} · 通道:${escapeHtml(channels)}</div>
${errorCategory ? `<div class="event-failure-meta"><span class="event-error-badge">错误分类</span><span>${escapeHtml(errorCategory)}</span></div>` : ''}
<div class="event-detail">${escapeHtml(formatText(event.detail, '无补充说明'))}</div>
</article>
`;
})
.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();