alphax/static/system_logs.html
2026-05-16 15:45:59 +08:00

182 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}系统日志 · AlphaX Agent Crypto{% 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 的内部错误、上下文和堆栈信息。</div>
</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 %}