feat: XMPP bot journal monitoring — detect inbound/outbound/errors, smooth health refresh
This commit is contained in:
+98
-14
@@ -154,9 +154,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (name !== 'health' && healthRefreshInterval) {
|
||||
clearInterval(healthRefreshInterval);
|
||||
healthRefreshInterval = null;
|
||||
const he = document.getElementById('tab-health');
|
||||
if (he) delete he.dataset.rendered;
|
||||
}
|
||||
if (name === 'health' && !healthRefreshInterval) {
|
||||
healthRefreshInterval = setInterval(renderHealth, 10000);
|
||||
healthRefreshInterval = setInterval(refreshHealth, 10000);
|
||||
}
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
||||
@@ -174,10 +176,12 @@ function switchTab(name) {
|
||||
if (name !== 'health' && healthRefreshInterval) {
|
||||
clearInterval(healthRefreshInterval);
|
||||
healthRefreshInterval = null;
|
||||
const he = document.getElementById('tab-health');
|
||||
if (he) delete he.dataset.rendered;
|
||||
}
|
||||
// 进入 health 时确保定时器运行
|
||||
if (name === 'health' && !healthRefreshInterval) {
|
||||
healthRefreshInterval = setInterval(renderHealth, 10000);
|
||||
healthRefreshInterval = setInterval(refreshHealth, 10000);
|
||||
}
|
||||
const btn = document.querySelector(`[data-tab="${name}"]`);
|
||||
if (btn) {
|
||||
@@ -1465,13 +1469,13 @@ async function renderHealth() {
|
||||
|
||||
// Summary cards — 包含 XMPP 状态
|
||||
let html = '<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-4">';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#3fb950] font-mono">' + (summary.ok || 0) + '</div><div class="text-xs text-slate-500">正常服务</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-white font-mono">' + (summary.total || 0) + '</div><div class="text-xs text-slate-500">总服务数</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#3fb950] font-mono" id="hSvcOk">' + (summary.ok || 0) + '</div><div class="text-xs text-slate-500">正常服务</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-white font-mono" id="hSvcTotal">' + (summary.total || 0) + '</div><div class="text-xs text-slate-500">总服务数</div></div>';
|
||||
const xmppStatus = xmpp ? xmpp.status : 'unknown';
|
||||
const xmppColor = xmppStatus === 'ok' ? '#3fb950' : xmppStatus === 'degraded' ? '#d29922' : '#f85149';
|
||||
const xmppLabel = xmppStatus === 'ok' ? 'XMPP 正常' : xmppStatus === 'degraded' ? 'XMPP 降级' : xmppStatus === 'critical' ? 'XMPP 断联' : 'XMPP 无数据';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold font-mono" style="color:' + xmppColor + '">' + (xmpp ? (xmpp.last_message_age_sec > 0 ? Math.round(xmpp.last_message_age_sec/60) + 'm' : '--') : '--') + '</div><div class="text-xs text-slate-500">' + xmppLabel + '</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#d29922] font-mono">' + (xmpp ? xmpp.error_rate_1h + '%' : '--') + '</div><div class="text-xs text-slate-500">XMPP 错误率(1h)</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold font-mono" id="hXmppAge" style="color:' + xmppColor + '">' + (xmpp ? (xmpp.last_message_age_sec > 0 ? Math.round(xmpp.last_message_age_sec/60) + 'm' : '--') : '--') + '</div><div class="text-xs text-slate-500" id="hXmppLabel">' + xmppLabel + '</div></div>';
|
||||
html += '<div class="card p-3 text-center"><div class="text-lg font-bold text-[#d29922] font-mono" id="hXmppErr">' + (xmpp ? xmpp.error_rate_1h + '%' : '--') + '</div><div class="text-xs text-slate-500">XMPP 错误率(1h)</div></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Services by layer
|
||||
@@ -1491,9 +1495,9 @@ async function renderHealth() {
|
||||
const label = s.label || s.name || s.type || '';
|
||||
const port = s.port || '-';
|
||||
if (i === 0) {
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3 font-semibold text-slate-300" rowspan="' + layers[layer].length + '">' + layer + '</td><td class="p-3">' + s.name + ' <span class="text-slate-500">' + label + '</span></td><td class="p-3 font-mono">' + port + '</td><td class="p-3 ' + statusColor + '">' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + '</td></tr>';
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3 font-semibold text-slate-300" rowspan="' + layers[layer].length + '">' + layer + '</td><td class="p-3">' + s.name + ' <span class="text-slate-500">' + label + '</span></td><td class="p-3 font-mono">' + port + '</td><td class="p-3 ' + statusColor + '" id="hSvc_' + s.name + '">' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + '</td></tr>';
|
||||
} else {
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3">' + s.name + ' <span class="text-slate-500">' + label + '</span></td><td class="p-3 font-mono">' + port + '</td><td class="p-3 ' + statusColor + '">' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + '</td></tr>';
|
||||
html += '<tr class="border-b border-slate-800/30"><td class="p-3">' + s.name + ' <span class="text-slate-500">' + label + '</span></td><td class="p-3 font-mono">' + port + '</td><td class="p-3 ' + statusColor + '" id="hSvc_' + s.name + '">' + statusIcon + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知')) + '</td></tr>';
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1510,24 +1514,104 @@ async function renderHealth() {
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
html += '<div class="text-xs text-slate-600 mt-2">更新于 ' + (monitor.generated_at || new Date().toLocaleTimeString()) + ' · 每10秒刷新</div>';
|
||||
html += '<div class="text-xs text-slate-600 mt-2" id="hUpdated">更新于 ' + (monitor.generated_at || new Date().toLocaleTimeString()) + '</div>';
|
||||
|
||||
// XMPP 通道详情
|
||||
if (xmpp) {
|
||||
html += '<div class="card p-4 mt-4"><h3 class="text-sm font-semibold text-[#58a6ff] mb-2">📡 XMPP 通道</h3>';
|
||||
html += '<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">';
|
||||
html += '<div><span class="text-slate-500">最后消息</span><br><span class="font-mono ' + (xmpp.last_message_age_sec > 600 ? 'text-[#f85149]' : 'text-[#3fb950]') + '">' + (xmpp.last_message_age_sec > 0 ? Math.round(xmpp.last_message_age_sec) + '秒前' : '无数据') + '</span></div>';
|
||||
html += '<div><span class="text-slate-500">1h错误率</span><br><span class="font-mono ' + (xmpp.error_rate_1h > 50 ? 'text-[#f85149]' : xmpp.error_rate_1h > 10 ? 'text-[#d29922]' : 'text-[#3fb950]') + '">' + xmpp.error_rate_1h + '%</span></div>';
|
||||
html += '<div><span class="text-slate-500">ejabberd</span><br><span class="font-mono ' + ((xmpp.ejabberd || '').includes('Up') ? 'text-[#3fb950]' : 'text-[#f85149]') + '">' + (xmpp.ejabberd || '?') + '</span></div>';
|
||||
html += '<div><span class="text-slate-500">状态</span><br><span class="font-mono ' + (xmpp.status === 'ok' ? 'text-[#3fb950]' : xmpp.status === 'degraded' ? 'text-[#d29922]' : 'text-[#f85149]') + '">' + xmpp.status + '</span></div>';
|
||||
html += '</div></div>';
|
||||
html += '<div><span class="text-slate-500">最后消息</span><br><span class="font-mono" id="hXmppDetail_age">' + (xmpp.last_message_age_sec > 0 ? Math.round(xmpp.last_message_age_sec) + '秒前' : '无数据') + '</span></div>';
|
||||
html += '<div><span class="text-slate-500">1h错误率</span><br><span class="font-mono" id="hXmppDetail_err">' + xmpp.error_rate_1h + '%</span></div>';
|
||||
html += '<div><span class="text-slate-500">ejabberd</span><br><span class="font-mono" id="hXmppDetail_eja">' + (xmpp.ejabberd || '?') + '</span></div>';
|
||||
html += '<div><span class="text-slate-500">状态</span><br><span class="font-mono" id="hXmppDetail_st">' + xmpp.status + '</span></div>';
|
||||
html += '</div>';
|
||||
|
||||
// Bot 活动(知微)
|
||||
const ba = xmpp.bot_activity || {};
|
||||
html += '<div class="grid grid-cols-3 gap-2 text-xs mt-3 pt-3 border-t border-slate-800/50">';
|
||||
html += '<div><span class="text-slate-500">📩 入站</span><br><span class="font-mono text-[#58a6ff]" id="hBot_in">' + (ba.inbound || 0) + '</span></div>';
|
||||
html += '<div><span class="text-slate-500">📤 出站</span><br><span class="font-mono ' + (ba.outbound ? 'text-[#3fb950]' : (ba.inbound ? 'text-[#f85149]' : 'text-slate-500')) + '" id="hBot_out">' + (ba.outbound || 0) + '</span></div>';
|
||||
html += '<div><span class="text-slate-500">⚠️ 错误</span><br><span class="font-mono ' + (ba.errors ? 'text-[#f85149]' : 'text-slate-500') + '" id="hBot_err">' + (ba.errors || 0) + '</span></div>';
|
||||
html += '</div>';
|
||||
if (ba.last_error) {
|
||||
html += '<div class="text-xs text-[#f85149] mt-2 p-2 bg-red-900/20 rounded" id="hBot_lastErr">' + ba.last_error + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
el.innerHTML = html;
|
||||
el.dataset.rendered = '1'; // 标记已首渲
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="text-red-400">加载健康状态失败: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 增量刷新健康数据(不重建 DOM,无闪烁)
|
||||
async function refreshHealth() {
|
||||
const el = document.getElementById('tab-health');
|
||||
if (!el || el.classList.contains('hidden') || !el.dataset.rendered) return;
|
||||
try {
|
||||
const [services, xmpp] = await Promise.all([
|
||||
fetchJSON('/api/services'),
|
||||
fetchJSON('/api/xmpp/health')
|
||||
]);
|
||||
const svcs = (services.services || []);
|
||||
const sum = services.summary || {};
|
||||
|
||||
// 更新卡片数字
|
||||
const okEl = document.getElementById('hSvcOk');
|
||||
const totalEl = document.getElementById('hSvcTotal');
|
||||
const ageEl = document.getElementById('hXmppAge');
|
||||
const errEl = document.getElementById('hXmppErr');
|
||||
const labelEl = document.getElementById('hXmppLabel');
|
||||
if (okEl) okEl.textContent = sum.ok || 0;
|
||||
if (totalEl) totalEl.textContent = sum.total || 0;
|
||||
if (ageEl && xmpp) ageEl.textContent = xmpp.last_message_age_sec > 0 ? Math.round(xmpp.last_message_age_sec/60) + 'm' : '--';
|
||||
if (errEl && xmpp) errEl.textContent = xmpp.error_rate_1h + '%';
|
||||
|
||||
// 更新服务行状态
|
||||
svcs.forEach(s => {
|
||||
const cell = document.getElementById('hSvc_' + s.name);
|
||||
if (cell) {
|
||||
const ok = s.health?.ok;
|
||||
cell.textContent = (ok ? '🟢' : (ok === false ? '🔴' : '🟡')) + (ok ? ' 正常' : (ok === false ? ' 异常' : ' 未知'));
|
||||
cell.className = 'p-3 ' + (ok ? 'text-[#3fb950]' : (ok === false ? 'text-[#f85149]' : 'text-[#d29922]'));
|
||||
}
|
||||
});
|
||||
|
||||
// 更新 XMPP 详情
|
||||
if (xmpp) {
|
||||
const d_age = document.getElementById('hXmppDetail_age');
|
||||
const d_err = document.getElementById('hXmppDetail_err');
|
||||
const d_eja = document.getElementById('hXmppDetail_eja');
|
||||
const d_st = document.getElementById('hXmppDetail_st');
|
||||
if (d_age) d_age.textContent = xmpp.last_message_age_sec > 0 ? Math.round(xmpp.last_message_age_sec) + '秒前' : '无数据';
|
||||
if (d_err) d_err.textContent = xmpp.error_rate_1h + '%';
|
||||
if (d_eja) d_eja.textContent = xmpp.ejabberd || '?';
|
||||
if (d_st) d_st.textContent = xmpp.status;
|
||||
// 更新卡片颜色
|
||||
if (ageEl && xmpp) ageEl.style.color = xmpp.last_message_age_sec > 600 ? '#f85149' : '#3fb950';
|
||||
if (labelEl && xmpp) {
|
||||
const labels = {ok:'XMPP 正常', degraded:'XMPP 降级', critical:'XMPP 断联'};
|
||||
labelEl.textContent = labels[xmpp.status] || 'XMPP 无数据';
|
||||
}
|
||||
// 更新 Bot 活动
|
||||
const ba = xmpp.bot_activity || {};
|
||||
const bi = document.getElementById('hBot_in');
|
||||
const bo = document.getElementById('hBot_out');
|
||||
const be = document.getElementById('hBot_err');
|
||||
const bl = document.getElementById('hBot_lastErr');
|
||||
if (bi) bi.textContent = ba.inbound || 0;
|
||||
if (bo) { bo.textContent = ba.outbound || 0; bo.className = 'font-mono ' + (ba.outbound ? 'text-[#3fb950]' : (ba.inbound ? 'text-[#f85149]' : 'text-slate-500')); }
|
||||
if (be) { be.textContent = ba.errors || 0; be.className = 'font-mono ' + (ba.errors ? 'text-[#f85149]' : 'text-slate-500'); }
|
||||
if (bl && ba.last_error) bl.textContent = ba.last_error;
|
||||
}
|
||||
|
||||
// 更新时间戳
|
||||
const upd = document.getElementById('hUpdated');
|
||||
if (upd) upd.textContent = '更新于 ' + new Date().toLocaleTimeString();
|
||||
} catch(e) { /* 静默失败,保留旧数据 */ }
|
||||
}
|
||||
|
||||
// ── 盯盘 ──
|
||||
let watchTimer = null;
|
||||
async function renderWatch() {
|
||||
|
||||
Reference in New Issue
Block a user