// P12 —— 告警中心前端(渠道 / 规则 / 事件历史 三个标签页) let ALERT_EVENT_TYPES = []; // [{event_type, name, default_params}] let ALERT_CHANNELS = []; // [{id, name, kind}] let evPage = 1; function initAlerts(){ switchTab('channels'); loadEventTypes(); } function switchTab(t){ document.querySelectorAll('#tab-channels,#tab-rules,#tab-events').forEach(d => d.style.display = 'none'); document.getElementById('tab-'+t).style.display = ''; document.querySelectorAll('.tabs button').forEach(b => b.classList.toggle('active', b.dataset.tab === t)); if (t === 'channels') loadChannels(); if (t === 'rules') loadRules(); if (t === 'events') loadEvents(); } async function loadEventTypes(){ try { const r = await api('GET', '/api/alerts/rules'); ALERT_EVENT_TYPES = r.data.event_types || []; const sel = document.getElementById('ev-type'); sel.innerHTML = '' + ALERT_EVENT_TYPES.map(e => ``).join(''); } catch(e){} } // ============ 渠道 ============ function channelFormHtml(kind, cfg){ cfg = cfg || {}; if (kind === 'telegram') return `
`; if (kind === 'webhook') return `
`; if (kind === 'email') return `
`; return ''; } function collectChannelCfg(kind){ const v = id => document.getElementById(id).value.trim(); if (kind === 'telegram') return { bot_token: v('f-bot_token'), chat_id: v('f-chat_id'), proxy: v('f-proxy') }; if (kind === 'webhook') { let headers = {}; try { if (v('f-headers')) headers = JSON.parse(v('f-headers')); } catch(e){ toast('请求头 JSON 格式错误', true); return null; } return { url: v('f-url'), secret: v('f-secret'), headers }; } if (kind === 'email') return { host: v('f-host'), port: parseInt(v('f-port'))||587, tls: document.getElementById('f-tls').value, username: v('f-username'), password: v('f-password'), from: v('f-from'), to: v('f-to').split('\n').map(s=>s.trim()).filter(Boolean), }; return null; } async function loadChannels(){ try { const r = await api('GET', '/api/alerts/channels'); const items = r.data || []; ALERT_CHANNELS = items; document.getElementById('ch-rows').innerHTML = items.map(c => { let cfg = ''; const cf = c.config || {}; if (c.kind === 'telegram') cfg = `chat_id=${esc(cf.chat_id||'-')}`; else if (c.kind === 'webhook') cfg = esc(cf.url||'-'); else if (c.kind === 'email') cfg = `${esc(cf.host||'-')}:${esc(cf.port||'')} → ${esc((cf.to||[]).join(', '))}`; return `${esc(c.name)}${esc(c.kind)}${cfg} ${c.enabled ? '启用' : '停用'} ${IS_VIEWER ? '' : ` `} `; }).join('') || '暂无渠道,点击"新建渠道"添加'; } catch(e){ toast('加载渠道失败:'+e.message, true); } } async function editChannel(id){ const ch = id ? ALERT_CHANNELS.find(c => c.id === id) : null; const kind = ch ? ch.kind : 'telegram'; const kinds = ['telegram','webhook','email']; openModal(id ? '编辑渠道' : '新建渠道', `
${channelFormHtml(kind, ch ? ch.config : {})}
`, async () => { const name = document.getElementById('f-name').value.trim(); if (!name) { toast('名称不能为空', true); return false; } const cfg = collectChannelCfg(document.getElementById('f-kind').value); if (!cfg) return false; try { if (id) await api('PUT', '/api/alerts/channels/'+id, { name, kind: ch.kind, config: cfg, enabled: parseInt(document.getElementById('f-enabled').value) }); else await api('POST', '/api/alerts/channels', { name, kind: document.getElementById('f-kind').value, config: cfg, enabled: parseInt(document.getElementById('f-enabled').value) }); toast(id ? '已更新' : '已创建'); loadChannels(); } catch(e){ toast('保存失败:'+e.message, true); return false; } }); document.getElementById('f-kind').onchange = e => { document.getElementById('ch-form').innerHTML = channelFormHtml(e.target.value, {}); }; } async function testChannel(id){ try { await api('POST', `/api/alerts/channels/${id}/test`); toast('测试消息已发送'); } catch(e){ toast('测试发送失败:'+e.message, true); } } function delChannel(id){ confirmDo('删除该渠道?已绑定到规则的会被自动移除。', async () => { try { await api('DELETE', '/api/alerts/channels/'+id); toast('已删除'); loadChannels(); } catch(e){ toast('删除失败:'+e.message, true); } }, '删除'); } // ============ 规则 ============ async function loadRules(){ try { const r = await api('GET', '/api/alerts/rules'); const items = r.data.items || []; const nameOf = id => { const c = ALERT_CHANNELS.find(x => x.id === id); return c ? esc(c.name) : '#'+id; }; document.getElementById('ru-rows').innerHTML = items.map(ru => { const et = ALERT_EVENT_TYPES.find(e => e.event_type === ru.event_type); const params = JSON.stringify(ru.params); const chs = (ru.channel_ids||[]).map(nameOf).join('、') || '未绑定渠道'; return `${esc(ru.name)} ${esc(et?et.name:ru.event_type)} ${esc(ru.event_type)} ${esc(params)} ${chs} ${ru.cooldown_secs}s ${ru.enabled ? '启用' : '停用'} ${IS_VIEWER ? '' : ` `} `; }).join('') || '暂无规则'; } catch(e){ toast('加载规则失败:'+e.message, true); } } let RULE_CACHE = []; async function editRule(id){ const r = id ? (await api('GET', '/api/alerts/rules')).data.items.find(x => x.id === id) : null; if (id && !r) return; const et = ALERT_EVENT_TYPES.find(e => e.event_type === (r ? r.event_type : 'node_offline')); openModal(id ? '编辑规则' : '新建规则', `
${ALERT_CHANNELS.map(c=>``).join('') || '暂无渠道'}
`, async () => { const name = document.getElementById('f-name').value.trim(); if (!name) { toast('名称不能为空', true); return false; } let params = {}; try { params = JSON.parse(document.getElementById('f-params').value || '{}'); } catch(e){ toast('参数 JSON 格式错误', true); return false; } const channel_ids = [...document.querySelectorAll('.f-ch:checked')].map(el => parseInt(el.value)); const payload = { name, event_type: document.getElementById('f-event_type').value, params, channel_ids, cooldown_secs: parseInt(document.getElementById('f-cooldown').value)||0, enabled: parseInt(document.getElementById('f-enabled').value) }; try { if (id) await api('PUT', '/api/alerts/rules/'+id, payload); else await api('POST', '/api/alerts/rules', payload); toast(id ? '已更新' : '已创建'); loadRules(); } catch(e){ toast('保存失败:'+e.message, true); return false; } }); document.getElementById('f-event_type').onchange = e => { const t = ALERT_EVENT_TYPES.find(x => x.event_type === e.target.value); if (t) document.getElementById('f-params').value = t.default_params; }; } async function toggleRule(id){ try { await api('POST', `/api/alerts/rules/${id}/toggle`); loadRules(); } catch(e){ toast('操作失败:'+e.message, true); } } function delRule(id){ confirmDo('删除该规则?', async () => { try { await api('DELETE', '/api/alerts/rules/'+id); toast('已删除'); loadRules(); } catch(e){ toast('删除失败:'+e.message, true); } }, '删除'); } // ============ 事件历史 ============ const SEV = { info: 'ℹ info', warning: '⚠ warning', critical: '🔴 critical' }; const EST = { pending: '待发送', sent: '已发送', failed: '失败' }; async function loadEvents(){ try { const qs = `page=${evPage}&page_size=20&status=${document.getElementById('ev-status').value}&event_type=${document.getElementById('ev-type').value}`; const r = await api('GET', '/api/alerts/events?'+qs); const d = r.data; document.getElementById('ev-rows').innerHTML = d.items.map(e => `${esc(e.created_at)}${esc(e.event_type)} ${esc(e.title)}
${esc(e.body).slice(0,120)}
${SEV[e.severity]||esc(e.severity)}${EST[e.status]||esc(e.status)} ${e.attempts}${esc(e.last_error||'-')} ${(e.status==='failed' && !IS_VIEWER) ? `` : ''}` ).join('') || '暂无事件'; const pages = Math.max(1, Math.ceil(d.total / d.page_size)); document.getElementById('ev-pager').innerHTML = `共 ${d.total} 条 ${evPage}/${pages} `; } catch(e){ toast('加载事件失败:'+e.message, true); } } async function retryEvent(id){ try { await api('POST', `/api/alerts/events/${id}/retry`); toast('已重新投递'); loadEvents(); } catch(e){ toast('重试失败:'+e.message, true); } } function cleanupEvents(){ openModal('清理历史事件', `
`, async () => { try { const r = await api('POST', '/api/alerts/events/cleanup', { days: parseInt(document.getElementById('f-days').value)||30 }); toast(`已清理 ${r.data.deleted} 条`); loadEvents(); } catch(e){ toast('清理失败:'+e.message, true); return false; } }, '清理'); }