183 lines
12 KiB
HTML
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{'&':'&','<':'<','>':'>','"':'"'}[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 %}
|