// 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 ? '编辑规则' : '新建规则', `
`, 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; }
}, '清理');
}