alphax/static/logs.html
2026-06-07 20:58:35 +08:00

159 lines
20 KiB
HTML

{% extends "base.html" %}
{% block title %}诊断中心 · AlphaX Agent{% endblock %}
{% block extra_head_css %}
<style>
main{max-width:1380px;margin:0 auto;width:100%;padding:24px;display:flex;flex-direction:column;gap:14px}
.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;flex-wrap:wrap}
.page-title{font-size:28px;font-weight:950;color:var(--ink);letter-spacing:-.7px}
.page-sub{margin-top:5px;font-size:13px;color:var(--stone);line-height:1.55;max-width:860px}
.ops-strip{display:grid;grid-template-columns:1.25fr repeat(3,minmax(0,1fr));gap:10px}
.ops-card{position:relative;overflow:hidden;border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);background:linear-gradient(145deg,#fff 0%,#f7f8fa 100%);padding:15px;min-height:92px}
.ops-card::after{content:"";position:absolute;right:-32px;top:-48px;width:120px;height:120px;border-radius:50%;background:rgba(255,208,47,.22)}
.ops-card span{display:block;color:var(--stone);font-size:11px;font-weight:950;letter-spacing:.04em}
.ops-card b{display:block;margin-top:8px;color:var(--ink);font-size:24px;line-height:1;font-weight:950;letter-spacing:-.5px}
.ops-card small{display:block;margin-top:8px;color:var(--slate);font-size:12px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tabs{display:flex;gap:8px;padding:6px;border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);background:var(--canvas);width:max-content;max-width:100%;overflow:auto}
.tab-btn{height:38px;border:0;background:transparent;border-radius:10px;padding:0 14px;font-size:13px;font-weight:950;color:var(--stone);cursor:pointer;white-space:nowrap}
.tab-btn.active{background:var(--primary);color:var(--on-primary)}
.panel{display:none;border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-lg);overflow:hidden}
.panel.active{display:block}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;padding:14px;border-bottom:1px solid var(--hairline-soft);background:linear-gradient(180deg,#fff,#fafbfc)}
.toolbar input,.toolbar select{height:38px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);padding:0 12px;font-size:13px;color:var(--ink);outline:none}
.toolbar input{min-width:220px;flex:1}
.toolbar button{height:38px;border:0;border-radius:var(--radius-md);padding:0 14px;background:var(--primary);color:var(--on-primary);font-size:13px;font-weight:900;cursor:pointer}
.table-wrap{overflow:auto}
table{width:100%;border-collapse:collapse;min-width:980px}
th{padding:10px 12px;border-bottom:1px solid var(--hairline-soft);background:var(--surface);color:var(--stone);font-size:11px;font-weight:950;text-align:left;letter-spacing:.04em}
td{padding:11px 12px;border-bottom:1px solid var(--hairline-soft);font-size:12px;color:var(--ink);vertical-align:top}
tr:hover td{background:var(--surface)}
.badge{display:inline-flex;align-items:center;height:23px;border-radius:999px;padding:0 8px;border:1px solid var(--hairline-soft);background:var(--surface);color:var(--stone);font-size:11px;font-weight:950;white-space:nowrap}
.badge.ok{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}
.badge.err{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}
.badge.warn{background:rgba(255,208,47,.16);border-color:rgba(255,208,47,.28);color:var(--yellow-dark)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:850}
.msg{max-width:460px;word-break:break-word;line-height:1.45;color:var(--slate)}
.empty,.loading{text-align:center;padding:34px 14px;color:var(--stone);font-size:13px}
.layout{display:grid;grid-template-columns:minmax(0,1fr) 420px;gap:14px}
.detail{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-lg);padding:15px;min-height:220px;position:sticky;top:18px;max-height:calc(100vh - 40px);overflow:auto}
.detail h3{font-size:15px;margin-bottom:10px;color:var(--ink)}
.meta{display:grid;grid-template-columns:86px minmax(0,1fr);gap:7px 10px;font-size:12px;color:var(--stone);margin-bottom:12px}
.meta span:nth-child(2n){color:var(--ink);overflow:hidden;text-overflow:ellipsis}
.codebox{white-space:pre-wrap;word-break:break-word;background:#15171d;color:#eef0f5;border-radius:var(--radius-md);padding:12px;font-size:12px;line-height:1.55;max-height:520px;overflow:auto}
.mini-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;padding:14px;border-bottom:1px solid var(--hairline-soft)}
.mini{border:1px solid var(--hairline-soft);border-radius:var(--radius-md);background:var(--surface);padding:12px}
.mini span{display:block;color:var(--stone);font-size:11px;font-weight:900}.mini b{display:block;margin-top:5px;font-size:20px;color:var(--ink);font-weight:950}
.pagination{display:flex;justify-content:center;align-items:center;gap:12px;padding:14px;color:var(--stone);font-size:12px}
.pagination button{height:34px;border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-md);padding:0 12px;color:var(--ink);cursor:pointer}.pagination button:disabled{opacity:.45;cursor:default}
@media(max-width:1050px){.ops-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.layout{grid-template-columns:1fr}.detail{position:static;max-height:none}.mini-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:620px){main{padding:18px}.ops-strip{grid-template-columns:1fr}.page-title{font-size:23px}.tabs{width:100%}.tab-btn{flex:1}.mini-grid{grid-template-columns:1fr}}
</style>
{% endblock %}
{% block content %}
<main>
<div class="page-head">
<div>
<div class="page-title">诊断中心</div>
<div class="page-sub">管理员查看系统是否正常运转、哪里卡住、哪些页面需要继续排查。这里保留工程细节,但不会出现在普通用户菜单里。</div>
</div>
<div class="tabs" id="tabs">
<button class="tab-btn active" data-tab="system" onclick="switchTab('system')">系统错误</button>
<button class="tab-btn" data-tab="cron" onclick="switchTab('cron')">调度运行</button>
<button class="tab-btn" data-tab="pipeline" onclick="switchTab('pipeline')">链路批次</button>
<button class="tab-btn" data-tab="chat" onclick="switchTab('chat')">问答日志</button>
</div>
</div>
<div class="toolbar" style="border:1px solid var(--hairline-soft);border-radius:var(--radius-lg);background:var(--canvas)">
<a class="badge" href="/operations" target="_blank" rel="noopener">运行大屏</a>
<a class="badge" href="/pipeline">链路批次详情</a>
<a class="badge" href="/llm-insights">AI 调用记录</a>
<a class="badge" href="/strategy">策略归因</a>
<a class="badge" href="/iteration">策略迭代</a>
<a class="badge" href="/data-export">数据导出</a>
</div>
<div class="ops-strip" id="opsStrip"><div class="ops-card"><span>状态</span><b>加载中</b><small>正在读取运行日志</small></div></div>
<section class="panel active" id="panel-system">
<div class="layout">
<div class="panel active">
<div class="toolbar">
<input id="sysSearch" placeholder="搜索错误、路径、用户..." onkeydown="if(event.key==='Enter')loadSystem(0)">
<select id="sysLevel" onchange="loadSystem(0)"><option value="all">全部级别</option><option value="error">Error</option><option value="warning">Warning</option></select>
<select id="sysSource" onchange="loadSystem(0)"><option value="all">全部来源</option><option value="web">Web</option><option value="scheduler">Scheduler</option><option value="paper_trading">策略交易</option><option value="price_streamer">Price Streamer</option></select>
<select class="hoursSel" id="sysHours" onchange="loadSystem(0)"><option value="24">近 24h</option><option value="168" selected>近 7 天</option><option value="720">近 30 天</option><option value="0">全部</option></select>
<button onclick="loadSystem(0)">查询</button>
</div>
<div class="table-wrap"><table><thead><tr><th>时间</th><th>来源</th><th>类型</th><th>消息</th><th>路径 / 用户</th><th>状态</th></tr></thead><tbody id="sysRows"><tr><td colspan="6" class="loading">加载中...</td></tr></tbody></table></div>
<div class="pagination" id="sysPager"></div>
</div>
<aside class="detail" id="sysDetail"><div class="empty">选择一条系统错误查看堆栈。</div></aside>
</div>
</section>
<section class="panel" id="panel-cron">
<div class="mini-grid" id="cronMini"><div class="mini"><span>调度</span><b>--</b></div></div>
<div class="toolbar">
<select id="cronJob" onchange="loadCron()"><option value="">全部任务</option><option value="粗筛">粗筛</option><option value="爆发确认">爆发确认</option><option value="price-streamer">Price Streamer</option><option value="策略交易">策略交易</option></select>
<select id="cronHours" onchange="loadCron()"><option value="24">近 24h</option><option value="168">近 7 天</option></select>
<button onclick="loadCron()">刷新</button>
<a class="badge" href="/cron">打开调度中心</a>
</div>
<div class="table-wrap"><table><thead><tr><th>时间</th><th>任务</th><th>运行</th><th>结果</th><th>耗时</th><th>摘要</th><th>错误</th></tr></thead><tbody id="cronRows"><tr><td colspan="7" class="loading">加载中...</td></tr></tbody></table></div>
</section>
<section class="panel" id="panel-pipeline">
<div class="mini-grid" id="pipeMini"><div class="mini"><span>链路</span><b>--</b></div></div>
<div class="toolbar">
<select id="pipeHours" onchange="loadPipeline(0)"><option value="24">近 24h</option><option value="72">近 3 天</option><option value="168">近 7 天</option></select>
<button onclick="loadPipeline(0)">刷新</button>
<a class="badge" href="/pipeline">打开原链路页</a>
</div>
<div class="table-wrap"><table><thead><tr><th>批次</th><th>时间</th><th>漏斗</th><th>推荐</th><th>转化</th><th>复盘</th><th>状态</th></tr></thead><tbody id="pipeRows"><tr><td colspan="7" class="loading">加载中...</td></tr></tbody></table></div>
<div class="pagination" id="pipePager"></div>
</section>
<section class="panel" id="panel-chat">
<div class="mini-grid" id="chatMini"><div class="mini"><span>问答</span><b>--</b></div></div>
<div class="toolbar">
<input id="chatSearch" placeholder="搜索问题、回答..." onkeydown="if(event.key==='Enter')loadChat(0)">
<select id="chatIntent" onchange="loadChat(0)"><option value="all">全部意图</option><option value="coin_analysis">币种分析</option><option value="sentiment">舆情</option><option value="review">复盘</option></select>
<select id="chatHours" onchange="loadChat(0)"><option value="24">近 24h</option><option value="168">近 7 天</option></select>
<button onclick="loadChat(0)">查询</button>
<a class="badge" href="/chat-logs">打开原问答日志</a>
</div>
<div class="table-wrap"><table><thead><tr><th>时间</th><th>用户</th><th>意图</th><th>问题</th><th>回答摘要</th></tr></thead><tbody id="chatRows"><tr><td colspan="5" class="loading">加载中...</td></tr></tbody></table></div>
<div class="pagination" id="chatPager"></div>
</section>
</main>
{% endblock %}
{% block password_modal %}{% endblock %}
{% block extra_script %}
<script>
var API='',PAGE=50,state={tab:'system',sysOffset:0,sysTotal:0,pipeOffset:0,pipeTotal:0,chatOffset:0,chatTotal:0};
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function short(s,n){s=String(s||'');return s.length>n?s.slice(0,n)+'…':s}
function time(ts){if(!ts)return'--';var d=new Date(ts);if(isNaN(d.getTime()))return String(ts).slice(0,19).replace('T',' ');return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0')}
function dur(ms){ms=Number(ms||0);if(ms>=1000)return (ms/1000).toFixed(1)+'s';return ms+'ms'}
function badge(v){var s=String(v||'');var cls=s==='success'||s==='processed'||s==='ok'?'ok':(s==='error'||s.indexOf('fail')>=0?'err':(s==='warning'?'warn':''));return '<span class="badge '+cls+'">'+esc(s||'--')+'</span>'}
function summary(x){try{if(typeof x==='string')x=JSON.parse(x||'{}')}catch(e){};return Object.keys(x||{}).slice(0,6).map(function(k){return k+': '+x[k]}).join(' · ')||'--'}
function switchTab(tab){state.tab=tab;document.querySelectorAll('.tab-btn').forEach(function(b){b.classList.toggle('active',b.dataset.tab===tab)});document.querySelectorAll('section.panel').forEach(function(p){p.classList.toggle('active',p.id==='panel-'+tab)});if(tab==='system')loadSystem(state.sysOffset||0);if(tab==='cron')loadCron();if(tab==='pipeline')loadPipeline(state.pipeOffset||0);if(tab==='chat')loadChat(state.chatOffset||0)}
async function ensureAdmin(){try{var r=await fetch('/api/admin/check');var d=await r.json();if(!d.is_admin)location.href='/subscription'}catch(e){location.href='/subscription'}}
function renderOps(data){document.getElementById('opsStrip').innerHTML=data.map(function(x){return '<div class="ops-card"><span>'+esc(x[0])+'</span><b>'+esc(x[1])+'</b><small>'+esc(x[2]||'')+'</small></div>'}).join('')}
async function loadOps(){try{var s=await(await fetch('/api/admin/system-errors/stats?hours=24')).json();var c=await(await fetch('/api/admin/cron-runs/summary?hours=24')).json();renderOps([['系统错误',s.total||0,'近 24 小时'],['调度成功率',(c.overall||{}).success_rate+'%','运行 '+((c.overall||{}).total_runs||0)+' 次'],['调度失败',(c.overall||{}).error_runs||0,'需要排查的任务'],['平均耗时',dur((c.overall||{}).avg_duration_ms),'近 24 小时']])}catch(e){renderOps([['状态','加载失败','请检查接口权限']])}}
async function loadSystem(offset){state.sysOffset=offset;loadOps();var q=sysSearch.value.trim(),level=sysLevel.value,source=sysSource.value,h=sysHours.value;sysRows.innerHTML='<tr><td colspan="6" class="loading">加载中...</td></tr>';try{var d=await(await fetch('/api/admin/system-errors?search='+encodeURIComponent(q)+'&offset='+offset+'&limit='+PAGE+'&level='+level+'&source='+source+'&hours='+h)).json();state.sysTotal=d.total||0;sysRows.innerHTML=(d.items||[]).length?(d.items||[]).map(function(x){return '<tr onclick="loadSystemDetail('+x.id+')" style="cursor:pointer"><td>'+time(x.created_at)+'</td><td>'+badge(x.source||'app')+'</td><td>'+esc(x.error_type||'Error')+'</td><td class="msg">'+esc(short(x.message,140))+'</td><td class="msg">'+esc(short((x.request_path||'--')+(x.user_email?' · '+x.user_email:''),80))+'</td><td>'+badge(x.level||x.status_code)+'</td></tr>'}).join(''):'<tr><td colspan="6" class="empty">暂无系统错误</td></tr>';pager('sysPager',offset,state.sysTotal,'loadSystem')}catch(e){sysRows.innerHTML='<tr><td colspan="6" class="empty">加载失败</td></tr>'}}
async function loadSystemDetail(id){sysDetail.innerHTML='<div class="loading">加载详情...</div>';try{var d=await(await fetch('/api/admin/system-errors/'+id)).json();sysDetail.innerHTML='<h3>#'+esc(d.id)+' · '+esc(d.error_type||'Error')+'</h3><div class="meta"><span>时间</span><span>'+time(d.created_at)+'</span><span>来源</span><span>'+esc(d.source||'app')+' · PID '+esc(d.pid||0)+'</span><span>路径</span><span>'+esc((d.request_method||'')+' '+(d.request_path||'--'))+'</span><span>用户</span><span>'+esc(d.user_email||'--')+'</span><span>指纹</span><span>'+esc(d.fingerprint||'--')+'</span><span>消息</span><span>'+esc(d.message||'--')+'</span></div><div class="codebox">'+esc(d.stack_trace||'无堆栈信息')+'</div>'}catch(e){sysDetail.innerHTML='<div class="empty">详情加载失败</div>'}}
async function loadCron(){loadOps();cronRows.innerHTML='<tr><td colspan="7" class="loading">加载中...</td></tr>';try{var s=await(await fetch('/api/admin/cron-runs/summary?hours='+cronHours.value)).json();cronMini.innerHTML=[['总运行',(s.overall||{}).total_runs||0,'近 '+cronHours.value+'h'],['成功率',(s.overall||{}).success_rate+'%','调度稳定性'],['失败',(s.overall||{}).error_runs||0,'异常任务'],['平均耗时',dur((s.overall||{}).avg_duration_ms),'单次任务'],['任务数',(s.job_stats||[]).length,'已配置任务']].map(mini).join('');var d=await(await fetch('/api/admin/cron-runs?job_name='+encodeURIComponent(cronJob.value)+'&limit=100')).json();renderCronRows('cronRows',d.items||[])}catch(e){cronRows.innerHTML='<tr><td colspan="7" class="empty">加载失败</td></tr>'}}
function renderCronRows(id,items){var el=document.getElementById(id);el.innerHTML=items.length?items.map(function(x){return '<tr><td>'+time(x.started_at)+'</td><td>'+esc(x.job_name||'--')+'</td><td>'+badge(x.run_status)+'</td><td>'+badge(x.result_status)+'</td><td>'+dur(x.duration_ms)+'</td><td class="msg">'+esc(summary(x.summary_json))+'</td><td class="msg">'+esc(short(x.error_message||'',180))+'</td></tr>'}).join(''):'<tr><td colspan="7" class="empty">暂无运行日志</td></tr>'}
async function loadPipeline(offset){state.pipeOffset=offset;loadOps();pipeRows.innerHTML='<tr><td colspan="7" class="loading">加载中...</td></tr>';try{var d=await(await fetch('/api/admin/pipeline-runs?hours='+pipeHours.value+'&limit=30&offset='+offset)).json();var k=d.kpi||{};pipeMini.innerHTML=[['批次数',k.run_count||0,'粗筛批次'],['宇宙过滤',k.universe_gate_count||0,'候选入口'],['质量通过',k.quality_pass_count||0,'过滤后样本'],['交易确认',k.trade_confirm_count||0,'确认机会'],['推荐转化',(k.recommendation_rate||0)+'%','推荐/合格']].map(mini).join('');var p=d.pagination||{};state.pipeTotal=p.total_count||0;pipeRows.innerHTML=(d.runs||[]).length?(d.runs||[]).map(function(x){return '<tr><td class="mono">#'+esc(x.run_id||x.id)+'</td><td>'+time(x.started_at)+'</td><td>'+esc((x.universe_gate_count||0)+' / '+(x.discovery_count||0)+' / '+(x.quality_pass_count||0))+'</td><td>'+esc(x.recommendations||0)+'</td><td>'+esc((x.recommendation_rate||0)+'%')+'</td><td>'+esc((x.perf_success||0)+' / '+(x.perf_failed||0)+' / '+(x.missed_count||0))+'</td><td>'+badge(x.result_status||x.run_status)+'</td></tr>'}).join(''):'<tr><td colspan="7" class="empty">暂无链路批次</td></tr>';pager('pipePager',offset,state.pipeTotal,'loadPipeline',30)}catch(e){pipeRows.innerHTML='<tr><td colspan="7" class="empty">加载失败</td></tr>'}}
async function loadChat(offset){state.chatOffset=offset;loadOps();chatRows.innerHTML='<tr><td colspan="5" class="loading">加载中...</td></tr>';try{var ov=await(await fetch('/api/admin/chat-logs/overview?hours='+chatHours.value)).json();chatMini.innerHTML=[['提问数',ov.total_questions||0,'近 '+chatHours.value+'h'],['会话数',ov.total_sessions||0,'涉及 '+(ov.total_users||0)+' 位用户'],['消息数',ov.total_messages||0,'用户与助手消息'],['热门意图',((ov.top_intents||[])[0]||{}).intent||'--','当前最常见']].map(mini).join('');var d=await(await fetch('/api/admin/chat-logs?search='+encodeURIComponent(chatSearch.value.trim())+'&intent='+encodeURIComponent(chatIntent.value)+'&hours='+chatHours.value+'&offset='+offset+'&limit='+PAGE)).json();state.chatTotal=d.total||0;chatRows.innerHTML=(d.items||[]).length?(d.items||[]).map(function(x){return '<tr><td>'+time(x.created_at)+'</td><td>'+esc(x.user_email||'--')+'</td><td>'+badge(x.intent||'--')+'</td><td class="msg">'+esc(short(x.content_text||'',170))+'</td><td class="msg">'+esc(short((x.symbol?x.symbol+' · ':'')+(x.session_title||('会话 #'+x.session_id)),120))+'</td></tr>'}).join(''):'<tr><td colspan="5" class="empty">暂无问答日志</td></tr>';pager('chatPager',offset,state.chatTotal,'loadChat')}catch(e){chatRows.innerHTML='<tr><td colspan="5" class="empty">加载失败</td></tr>'}}
function mini(x){return '<div class="mini"><span>'+esc(x[0])+'</span><b>'+esc(x[1])+'</b><span>'+esc(x[2]||'')+'</span></div>'}
function pager(id,offset,total,fn,size){size=size||PAGE;var cur=Math.floor(offset/size)+1,totalPages=Math.max(1,Math.ceil((total||0)/size));document.getElementById(id).innerHTML='<button '+(offset<=0?'disabled':'')+' onclick="'+fn+'('+(offset-size)+')">上一页</button><span>第 '+cur+' / '+totalPages+' 页 · 共 '+(total||0)+' 条</span><button '+(offset+size>=total?'disabled':'')+' onclick="'+fn+'('+(offset+size)+')">下一页</button>'}
(async function(){await ensureAdmin();loadOps();switchTab('system')})();
</script>
{% endblock %}