alphax/static/system_logs.html
2026-05-21 16:44:48 +08:00

183 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}系统日志 · AlphaX Agent{% endblock %}
{% block extra_head_css %}
<style>
main { max-width: 1320px; margin: 0 auto; width: 100%; padding: 24px; display: flex; flex-direction: column; gap: 16px; }
.page-head { display:flex; align-items:flex-end; justify-content:space-between; gap:14px; flex-wrap:wrap; }
.page-title { font-size: 24px; font-weight: 900; color: var(--ink); letter-spacing: -.4px; }
.page-sub { margin-top:4px; font-size:13px; color:var(--stone); }
.log-summary { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; }
.log-chip { padding:14px 15px; border:1px solid var(--hairline-soft); border-radius:var(--radius-md); background:var(--canvas); min-width:0; }
.log-chip span { display:block; color:var(--stone); font-size:11px; font-weight:900; }
.log-chip b { display:block; margin-top:6px; color:var(--ink); font-size:24px; line-height:1; font-weight:900; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.log-layout { display: grid; grid-template-columns: minmax(0, 1fr) 430px; gap: 14px; align-items:start; }
.card { background: var(--canvas); border: 1px solid var(--hairline-soft); border-radius: var(--radius-md); overflow: hidden; }
.log-toolbar { display:flex; gap:8px; flex-wrap:wrap; padding:14px; border-bottom:1px solid var(--hairline-soft); }
.log-toolbar input,.log-toolbar select { min-height:38px; padding:8px 12px; background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius-md); color:var(--ink); font-size:13px; outline:none; }
.log-toolbar input { flex:1; min-width:220px; }
.log-toolbar button { padding:8px 16px; border:none; border-radius:var(--radius-md); background:var(--primary); color:var(--on-primary); font-size:13px; font-weight:800; cursor:pointer; }
.table-wrap { overflow-x:auto; }
table { width:100%; border-collapse:collapse; min-width:840px; font-size:13px; }
th { text-align:left; padding:10px 12px; color:var(--stone); font-weight:900; border-bottom:1px solid var(--hairline-soft); font-size:11px; text-transform:uppercase; letter-spacing:.4px; background:var(--surface); }
td { padding:11px 12px; border-bottom:1px solid var(--hairline-soft); color:var(--ink); vertical-align:top; }
tr:hover td { background:var(--surface); }
.badge { display:inline-flex; align-items:center; height:22px; padding:0 8px; border-radius:var(--radius-full); font-size:11px; font-weight:900; border:1px solid var(--hairline-soft); background:var(--surface); color:var(--stone); white-space:nowrap; }
.badge-red { background:rgba(229,62,62,.10); color:var(--red); border-color:rgba(229,62,62,.18); }
.badge-yellow { background:rgba(255,208,47,.14); color:var(--yellow-dark); border-color:rgba(255,208,47,.25); }
.msg-cell { max-width:390px; line-height:1.45; word-break:break-word; }
.pagination { display:flex; justify-content:center; align-items:center; gap:12px; padding:14px; font-size:13px; color:var(--stone); }
.pagination button { padding:6px 14px; background:var(--surface); border:1px solid var(--hairline); border-radius:var(--radius-md); color:var(--ink); font-size:13px; cursor:pointer; }
.pagination button:disabled { opacity:.4; cursor:default; }
.log-detail { position:sticky; top:18px; max-height:calc(100vh - 40px); overflow:auto; padding:16px; }
.log-title { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:12px; }
.log-title b { font-size:15px; color:var(--ink); }
.log-meta { display:grid; grid-template-columns:86px minmax(0,1fr); gap:7px 10px; font-size:12px; color:var(--stone); margin-bottom:12px; }
.log-meta span:nth-child(2n) { color:var(--ink); overflow:hidden; text-overflow:ellipsis; }
.stack-box { 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; }
.empty { text-align:center; padding:34px 14px; color:var(--stone); font-size:13px; }
@media(max-width:960px){main{padding:18px}.log-layout{grid-template-columns:1fr}.log-detail{position:static;max-height:none}.log-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:560px){.log-summary{grid-template-columns:1fr}.page-title{font-size:21px}}
</style>
{% endblock %}
{% block content %}
<main>
<div class="page-head">
<div>
<div class="page-title">系统日志</div>
<div class="page-sub">集中查看 Web、CLI、Scheduler 的内部错误、上下文和堆栈信息。新版入口见 <a href="/logs">日志中心</a></div>
<a class="active admin-link" href="/system-logs" style="display:none">系统日志兼容入口</a>
</div>
</div>
<div class="log-summary" id="logSummary">
<div class="log-chip"><span>统计</span><b>加载中</b></div>
</div>
<div class="log-layout">
<div class="card">
<div class="log-toolbar">
<input type="text" id="logSearch" placeholder="搜索错误、路径、用户..." onkeydown="if(event.key==='Enter')loadLogs(0)">
<select id="logLevel" onchange="loadLogs(0)">
<option value="all">全部级别</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
</select>
<select id="logSource" onchange="loadLogs(0)">
<option value="all">全部来源</option>
<option value="web">Web</option>
<option value="cli">CLI</option>
<option value="scheduler">Scheduler</option>
</select>
<select id="logHours" onchange="loadLogs(0)">
<option value="24">近 24h</option>
<option value="168" selected>近 7 天</option>
<option value="720">近 30 天</option>
<option value="0">全部</option>
</select>
<button onclick="loadLogs(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="logTable"><tr><td colspan="6" class="empty">加载中...</td></tr></tbody>
</table>
</div>
<div class="pagination" id="logPagination"></div>
</div>
<div class="card log-detail" id="logDetail">
<div style="color:var(--stone);font-size:13px">选择一条日志查看堆栈。</div>
</div>
</div>
</main>
{% endblock %}
{% block password_modal %}{% endblock %}
{% block extra_script %}
<script>
var API = '';
var LOG_PAGE_SIZE=50,logOffset=0,logTotal=0;
async function init(){
try{var chk=await fetch(API+'/api/admin/check');if(!chk.ok){window.location.href='/subscription';return}
var info=await chk.json();if(!info.is_admin){window.location.href='/subscription';return}}catch(e){window.location.href='/subscription';return}
loadLogs(0);
}
async function loadLogSummary(){
try{
var h=document.getElementById('logHours').value||24;
var r=await fetch(API+'/api/admin/system-errors/stats?hours='+encodeURIComponent(h));
if(!r.ok)throw new Error(r.status);
var d=await r.json(), groups=d.groups||[];
var cards=[
'<div class="log-chip"><span>窗口</span><b>'+esc(d.hours)+'h</b></div>',
'<div class="log-chip"><span>总错误</span><b>'+esc(d.total||0)+'</b></div>'
];
groups.slice(0,6).forEach(function(g){cards.push('<div class="log-chip"><span>'+esc(g.source)+' / '+esc(g.level)+'</span><b>'+esc(g.n)+'</b></div>')});
document.getElementById('logSummary').innerHTML=cards.join('');
}catch(e){document.getElementById('logSummary').innerHTML='<div class="log-chip"><span>统计</span><b>加载失败</b></div>'}
}
async function loadLogs(offset){
logOffset=offset;loadLogSummary();
var q=document.getElementById('logSearch').value.trim();
var level=document.getElementById('logLevel').value;
var source=document.getElementById('logSource').value;
var hours=document.getElementById('logHours').value;
document.getElementById('logTable').innerHTML='<tr><td colspan="6" class="empty">加载中...</td></tr>';
try{
var url=API+'/api/admin/system-errors?search='+encodeURIComponent(q)+'&offset='+offset+'&limit='+LOG_PAGE_SIZE+'&level='+encodeURIComponent(level)+'&source='+encodeURIComponent(source)+'&hours='+encodeURIComponent(hours);
var r=await fetch(url);if(!r.ok)throw new Error(r.status);
var d=await r.json();logTotal=d.total||0;renderLogs(d.items||[]);renderLogPagination();
}catch(e){
document.getElementById('logTable').innerHTML='<tr><td colspan="6" class="empty" style="color:var(--red)">加载失败</td></tr>';
}
}
function renderLogs(items){
var tb=document.getElementById('logTable');
if(!items.length){tb.innerHTML='<tr><td colspan="6" class="empty">暂无系统错误</td></tr>';return}
tb.innerHTML=items.map(function(x){
var badge=x.level==='error'?'badge-red':x.level==='warning'?'badge-yellow':'';
return '<tr onclick="loadLogDetail('+esc(x.id)+')" style="cursor:pointer">'+
'<td style="color:var(--stone);font-size:12px">'+fmtDateTime(x.created_at)+'</td>'+
'<td><span class="badge">'+esc(x.source||'app')+'</span></td>'+
'<td>'+esc(x.error_type||'Error')+'</td>'+
'<td class="msg-cell">'+esc(shortText(x.message||'--',120))+'</td>'+
'<td style="color:var(--stone);font-size:12px">'+esc(shortText((x.request_path||'--')+(x.user_email?' · '+x.user_email:''),80))+'</td>'+
'<td><span class="badge '+badge+'">'+esc(x.status_code||0)+'</span></td>'+
'</tr>';
}).join('');
}
function renderLogPagination(){
var pg=document.getElementById('logPagination'),totalPages=Math.ceil(logTotal/LOG_PAGE_SIZE),cur=Math.floor(logOffset/LOG_PAGE_SIZE)+1;
pg.innerHTML='<button '+(logOffset===0?'disabled':'')+' onclick="loadLogs('+(logOffset-LOG_PAGE_SIZE)+')">上一页</button>'+
'<span>第 '+cur+' / '+Math.max(1,totalPages)+' 页 · 共 '+logTotal+' 条</span>'+
'<button '+((logOffset+LOG_PAGE_SIZE>=logTotal)?'disabled':'')+' onclick="loadLogs('+(logOffset+LOG_PAGE_SIZE)+')">下一页</button>';
}
async function loadLogDetail(id){
document.getElementById('logDetail').innerHTML='<div style="color:var(--stone);font-size:13px">加载详情...</div>';
try{
var r=await fetch(API+'/api/admin/system-errors/'+id);if(!r.ok)throw new Error(r.status);
var d=await r.json();
document.getElementById('logDetail').innerHTML=
'<div class="log-title"><b>#'+esc(d.id)+' · '+esc(d.error_type||'Error')+'</b><span class="badge '+(d.level==='error'?'badge-red':'')+'">'+esc(d.level)+'</span></div>'+
'<div class="log-meta">'+
'<span>时间</span><span>'+fmtDateTime(d.created_at)+'</span>'+
'<span>来源</span><span>'+esc(d.source||'app')+' · '+esc(d.host||'')+' · 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="stack-box">'+esc(d.stack_trace||'无堆栈信息')+'</div>';
}catch(e){document.getElementById('logDetail').innerHTML='<div style="color:var(--red);font-size:13px">详情加载失败</div>'}
}
function esc(s){return String(s||'').replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]})}
function shortText(s,n){s=String(s||'');return s.length>n?s.slice(0,n)+'…':s}
function fmtDateTime(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()+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2)}
init();
</script>
{% endblock %}