F: rework fHealth into 3-layer view (summary + issues + grouped services)
This commit is contained in:
@@ -150,21 +150,107 @@ async function doAct(id,act){try{var r=await fetch('/api/agents/'+id+'/'+act,{me
|
||||
function tgL(id){if(L.has(id))L.delete(id);else L.add(id);var lp=qs('lp-'+id);if(lp)lp.className='lp'+(L.has(id)?' open':'');if(L.has(id))fL(id)}
|
||||
async function fL(id){var el=qs('lc-'+id);if(!el||!L.has(id))return;try{var r=await fetch('/api/agents/'+id+'/logs?lines=50'),d=await r.json();el.innerHTML=(d.lines||[]).map(function(l){return'<div>'+esc(l)+'</div>'}).join('')||'<span class="dim">no log data</span>';el.scrollTop=el.scrollHeight}catch(e){el.innerHTML='<span class="red">Failed</span>'}}
|
||||
async function fHealth(){try{
|
||||
// Service metadata: label / layer / impact
|
||||
var M={};
|
||||
M.ejabberd={n:'XMPP服务器',l:'通信层',i:'所有XMPP连接不可用'};
|
||||
M.xmpp_bot={n:'XMPP消息收发',l:'通信层',i:'所有Agent消息收发中断'};
|
||||
M.gateway_mohe={n:'Gateway-莫荷',l:'AI网关',i:'莫荷智能体不可用'};
|
||||
M.gateway_zhiwei={n:'Gateway-知微',l:'AI网关',i:'知微智能体不可用'};
|
||||
M.article_processor={n:'微信全文抓取',l:'辅助服务',i:'微信长文章无法读取'};
|
||||
M.dashboard={n:'管理门户',l:'辅助服务',i:'Dashboard无法访问'};
|
||||
M.watchdog={n:'进程看门狗',l:'辅助服务',i:'异常进程无自动恢复'};
|
||||
M['agents-health-check']={n:'健康检查(5min)',l:'定时任务',i:'5分钟级健康检测未运行'};
|
||||
M['agents-daily-health']={n:'日报健康检查(每日)',l:'定时任务',i:'日报统计未运行'};
|
||||
M['agents-todo-executor']={n:'待办执行器(10min)',l:'定时任务',i:'定时待办任务未执行'};
|
||||
var layerOrder=['通信层','AI网关','辅助服务','定时任务'];
|
||||
|
||||
var h='';
|
||||
try{var r=await fetch('/api/expected'),d=await r.json();var ok=0;for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));if(cmp)ok++}
|
||||
h+='<div class=section><h2>期望矩阵 <span class=tag '+(ok===d.expected.length?'g':'y')+'>'+ok+'/'+d.expected.length+'</span></h2><table><tr><th>服务</th><th>期望</th><th>实际</th><th>状态</th></tr>';
|
||||
for(var i=0;i<d.expected.length;i++){var e=d.expected[i],a=d.actual[e.name]||'',cmp=(e.expected.indexOf('running')===0&&a==='running')||(e.expected.indexOf('scheduled')===0&&(a==='scheduled'||a==='running'));h+='<tr><td>'+esc(e.name)+'</td><td>'+esc(e.expected)+'</td><td>'+esc(a)+'</td><td>'+(cmp?'<span class=tag g>OK</span>':'<span class=tag r>MISMATCH</span>')+'</td></tr>';}
|
||||
h+='</table></div>';}catch(e){}
|
||||
try{var r2=await fetch('/api/services'),d2=await r2.json();var ok2=0;h+='<div class=section><h2>服务健康 <span class=tag g>'+d2.services.length+' services</span></h2><table><tr><th>Service</th><th>Type</th><th>Port</th><th>Health</th><th>WD</th></tr>';
|
||||
for(var i=0;i<d2.services.length;i++){var s=d2.services[i],a=s.health&&s.health.ok;if(a)ok2++;h+='<tr><td>'+esc(s.name)+'</td><td>'+esc(s.type||'-')+'</td><td>'+(s.port||'-')+'</td><td>'+(a?'<span class=tag g>OK</span>':'<span class=tag r>DOWN</span>')+'</td><td>'+(s.watchdog?'<span class=tag g>Y</span>':'<span class=tag r>N</span>')+'</td></tr>';}
|
||||
h+='</table><div style=margin-top:8px;color:var(--dim)>健康: '+ok2+'/'+d2.services.length+' | 看门狗: '+d2.services.filter(function(s){return s.watchdog}).length+'/'+d2.services.length+'</div></div>';}catch(e){}
|
||||
try{var r3=await fetch('/api/monitor'),d3=await r3.json();h+='<div class=section><h2>监控数据</h2>';
|
||||
if(d3.tier1&&d3.tier1.summary)h+='<div class=stat-c><div class=n>'+d3.tier1.summary.ok+'/'+d3.tier1.summary.total+'</div><div class=l>T1 健康</div></div>';
|
||||
if(d3.tier2&&d3.tier2.summary)h+='<div class=stat-c style=margin-top:8px><div class=n>'+d3.tier2.summary.ok+'/'+d3.tier2.summary.total+'</div><div class=l>T2 日报</div></div>';
|
||||
if(d3.tasks&&d3.tasks.length){h+='<div style=margin-top:12px><h2>定时任务</h2>';for(var i=0;i<d3.tasks.length;i++){var t=d3.tasks[i];h+='<div style=background:var(--card);border:1px solid var(--border);border-radius:6px;padding:8px 12px;margin:4px 0><strong>'+esc(t.name)+'</strong> <span class=tag '+(t.status==='ok'||t.status==='timer_ok'?'g':t.status==='not_deployed'?'y':'r')+'>'+esc(t.status)+'</span></div>';}h+='</div>';}
|
||||
h+='</div>';}catch(e){}
|
||||
var all=await Promise.all([
|
||||
fetch('/api/services').then(function(r){return r.json()}).catch(function(){return{}}),
|
||||
fetch('/api/expected').then(function(r){return r.json()}).catch(function(){return{}}),
|
||||
fetch('/api/monitor').then(function(r){return r.json()}).catch(function(){return{}})
|
||||
]);
|
||||
var sD=all[0]||{},eD=all[1]||{},mD=all[2]||{};
|
||||
var svcs=sD.services||[],exp=eD.expected||[],act=eD.actual||{};
|
||||
|
||||
// --- Layer 1: 系统概览 ---
|
||||
var hCount=svcs.filter(function(s){return s.health&&s.health.ok}).length;
|
||||
var cDown=exp.filter(function(e){return e.critical&&e.expected.indexOf('running')===0&&act[e.name]!=='running'}).length;
|
||||
var st=cDown>0?'critical':hCount<svcs.length?'degraded':'good';
|
||||
var stL={good:'正常',degraded:'部分异常',critical:'严重异常'};
|
||||
var stC={good:'var(--green)',degraded:'var(--yellow)',critical:'var(--red)'};
|
||||
h+='<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">';
|
||||
h+='<div style="background:'+stC[st]+';color:#000;border-radius:8px;padding:12px 20px;flex:1;min-width:160px;text-align:center"><div style=font-size:28px;font-weight:700>'+stL[st]+'</div><div style=font-size:17px;margin-top:4px;opacity:.8>'+hCount+'/'+svcs.length+' 服务健康</div></div>';
|
||||
h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px;text-align:center"><div style=font-size:28px;font-weight:700;color:'+stC[st]+'>'+cDown+'</div><div style=font-size:17px;color:var(--dim)>关键异常</div></div>';
|
||||
if(mD.tier1&&mD.tier1.summary){var t1=mD.tier1.summary;var t1c=t1.ok===t1.total?'var(--green)':'var(--red)';
|
||||
h+='<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px 16px;flex:1;min-width:120px;text-align:center"><div style=font-size:28px;font-weight:700;color:'+t1c+'>'+t1.ok+'/'+t1.total+'</div><div style=font-size:17px;color:var(--dim)>T1监控</div></div>';}
|
||||
h+='</div>';
|
||||
|
||||
// --- Layer 2: 异常服务 ---
|
||||
var issues=[];
|
||||
for(var i=0;i<exp.length;i++){var e=exp[i];
|
||||
if(e.critical&&e.expected.indexOf('running')===0&&act[e.name]!=='running'){
|
||||
var m=M[e.name]||{n:e.name,i:'未知影响'};
|
||||
issues.push({name:e.name,label:m.n,impact:m.i,status:act[e.name]||'stopped'});}}
|
||||
for(var i=0;i<svcs.length;i++){var s=svcs[i];
|
||||
if(!s.health||!s.health.ok){
|
||||
if(!issues.some(function(x){return x.name===s.name})){
|
||||
var m=M[s.name]||{n:s.name,i:'服务不可用'};
|
||||
issues.push({name:s.name,label:m.n,impact:m.i,status:'down'});}}}
|
||||
if(issues.length){
|
||||
h+='<div class=section><h2 style="color:var(--red);margin-bottom:8px">异常服务</h2>';
|
||||
for(var i=0;i<issues.length;i++){var iv=issues[i];
|
||||
h+='<div style="background:var(--card);border:1px solid var(--red);border-radius:8px;padding:10px 14px;margin-bottom:6px">';
|
||||
h+='<div style=display:flex;align-items:center;gap:8px><span class="sd r"></span><strong>'+esc(iv.label)+'</strong><span class="tag r" style=font-size:18px>'+esc(iv.status)+'</span></div>';
|
||||
h+='<div style=margin-top:4px;font-size:18px;color:var(--dim)>'+esc(iv.impact)+'</div></div>';}
|
||||
h+='</div>';}
|
||||
|
||||
// --- Layer 3: 全部服务 (merge expected + services) ---
|
||||
var layers={};
|
||||
var added={};
|
||||
for(var i=0;i<exp.length;i++){var e=exp[i];
|
||||
var m=M[e.name]||{n:e.name,l:'辅助服务',i:''};
|
||||
var ly=m.l||'其他';
|
||||
if(!layers[ly])layers[ly]=[];added[e.name]=true;
|
||||
layers[ly].push({name:e.name,label:m.n,port:e.port,status:act[e.name]==='running'?'running':act[e.name],critical:e.critical});}
|
||||
for(var i=0;i<svcs.length;i++){var s=svcs[i];
|
||||
if(!added[s.name]){
|
||||
var rawL=M[s.name]?M[s.name].l:(s.type==='infra'?'通信层':s.type==='gateway'?'AI网关':'辅助服务');
|
||||
var rawN=M[s.name]?M[s.name].n:s.name;
|
||||
if(!layers[rawL])layers[rawL]=[];
|
||||
var hStatus=s.health&&s.health.ok?'running':'down';
|
||||
layers[rawL].push({name:s.name,label:rawN,port:s.port,status:hStatus,critical:false});}}
|
||||
h+='<div class=section><h2 style=margin-bottom:8px>全部服务</h2>';
|
||||
for(var li=0;li<layerOrder.length;li++){var ln=layerOrder[li];
|
||||
var items=layers[ln];
|
||||
if(!items||!items.length)continue;
|
||||
h+='<div style=margin:8px 0 4px><strong style=font-size:19px;color:var(--accent)>'+esc(ln)+'</strong></div>';
|
||||
for(var j=0;j<items.length;j++){var it=items[j];
|
||||
var run=it.status==='running';
|
||||
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;background:var(--bg);border-radius:4px;margin:2px 0">';
|
||||
h+='<span class="sd '+(run?'g':'r')+'"></span>';
|
||||
h+='<span style=flex:1>'+esc(it.label)+'</span>';
|
||||
if(it.port)h+='<span style=color:var(--dim);font-size:18px>:'+it.port+'</span>';
|
||||
h+='<span class="tag '+(run?'g':'r')+'">'+(run?'运行中':esc(it.status))+'</span>';
|
||||
h+='</div>';}
|
||||
}
|
||||
h+='</div>';
|
||||
|
||||
// --- 定时任务状态 ---
|
||||
if(mD.tasks&&mD.tasks.length){
|
||||
h+='<div class=section><h2 style=margin-bottom:8px>定时任务</h2>';
|
||||
for(var i=0;i<mD.tasks.length;i++){var t=mD.tasks[i];
|
||||
var m=M[t.name]||{n:t.name};
|
||||
var tk=t.status==='ok'||t.status==='timer_ok'||t.status==='cron_ok';
|
||||
var tn=t.status==='not_deployed';
|
||||
h+='<div style="display:flex;align-items:center;gap:8px;padding:4px 8px;margin:2px 0">';
|
||||
h+='<span class="sd '+(tk?'g':tn?'y':'r')+'"></span>';
|
||||
h+='<span style=flex:1>'+esc(m.n)+'</span>';
|
||||
h+='<span class="tag '+(tk?'g':tn?'y':'r')+'">'+(tk?'正常':tn?'未部署':'异常')+'</span></div>';}
|
||||
h+='</div>';}
|
||||
|
||||
if(!h)h='<span class=dim>No data</span>';
|
||||
fill('health',h)}catch(e){fill('health','<span class=tag r>Error</span>')}}
|
||||
fill('health',h)}catch(e){fill('health','<span class=tag r>Error: '+esc(e.message)+'</span>')}}
|
||||
async function fK(){try{var r=await fetch('/api/kanban'),d=await r.json();var tasks=d.tasks||[];var s=document.getElementById('kf-status');var a=document.getElementById('kf-assignee');var fs=s?s.value:'';var fa=a?a.value:'';
|
||||
if(tasks.length==0){fill('kanban','<div style=text-align:center;padding:40px;color:var(--dim)>No tasks</div>');return}
|
||||
var html='<div style=display:flex;gap:8px;margin-bottom:16px>';html+='<select id=kf-status style=background:var(--card);border:1px solid var(--border);border-radius:5px;padding:6px 10px;color:var(--text);font-size:18px><option value="">All Status</option><option value=ready>Ready</option><option value=progress>In Progress</option><option value=blocked>Blocked</option><option value=done>Done</option></select>';
|
||||
|
||||
Reference in New Issue
Block a user