Files
ops-assistant/templates/audit.html

136 lines
6.3 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧾 审计日志 - 虾记</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f0f2f5;color:#333;min-height:100vh}
.header{background:linear-gradient(135deg,#ff6b6b,#ee5a24);color:#fff;padding:20px;position:sticky;top:0;z-index:100;box-shadow:0 2px 10px rgba(0,0,0,.15);display:flex;justify-content:space-between;align-items:center;gap:10px}
.header .title{font-weight:700}
.header .sub{font-size:12px;opacity:.9}
.header a{color:#fff;text-decoration:none;background:rgba(255,255,255,.2);padding:6px 10px;border-radius:8px;font-size:13px}
.header a:hover{background:rgba(255,255,255,.35)}
.wrap{max-width:600px;margin:0 auto;padding:15px}
.filters{background:#fff;border-radius:12px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,.05);margin-bottom:10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.filters input,.filters select{width:100%;padding:8px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:#fff}
small{color:#6b7280}
.actions{margin:0 0 10px;display:flex;gap:8px;flex-wrap:wrap}
button{border:none;border-radius:8px;padding:8px 12px;cursor:pointer;color:#fff;background:#ee5a24;font-size:13px}
button:hover{background:#d63031}
button.secondary{background:#6b7280}
button.secondary:hover{background:#4b5563}
.list{display:flex;flex-direction:column;gap:10px}
.log-card{background:#fff;border-radius:12px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,.05)}
.row{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}
.tag{display:inline-block;padding:2px 8px;border-radius:999px;font-size:12px}
.tag.success{background:#dcfce7;color:#166534}
.tag.failed{background:#fee2e2;color:#991b1b}
.tag.denied{background:#fef3c7;color:#92400e}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:#4b5563;word-break:break-all}
.note{font-size:13px;color:#374151;margin-top:6px;white-space:pre-wrap;word-break:break-word}
.empty{text-align:center;padding:40px 10px;color:#999}
@media(max-width:640px){
.header{padding:14px 12px 10px;align-items:flex-start;flex-direction:column}
.header>div:last-child{display:flex;gap:8px}
.wrap{padding:12px}
.filters{grid-template-columns:1fr}
}
</style>
</head>
<body>
<div class="header">
<div>
<div class="title">🧾 审计日志</div>
<div class="sub">{{.version}}</div>
</div>
<div>
<a href="/">返回首页</a>
<a href="/channels">渠道配置</a>
<a href="/logout">退出</a>
</div>
</div>
<div class="wrap">
<div class="filters">
<div><small>操作类型</small><input id="fAction" placeholder="如record.delete.self"></div>
<div><small>目标类型</small><input id="fTarget" placeholder="如transaction"></div>
<div><small>结果</small><select id="fResult"><option value="">全部</option><option value="success">成功</option><option value="denied">拒绝</option><option value="failed">失败</option></select></div>
<div><small>操作人ID</small><input id="fActor" placeholder="如1"></div>
<div><small>开始时间RFC3339</small><input id="fFrom" placeholder="2026-03-09T00:00:00+08:00"></div>
<div><small>结束时间RFC3339</small><input id="fTo" placeholder="2026-03-10T00:00:00+08:00"></div>
</div>
<div class="actions">
<button onclick="loadAudit()">查询</button>
<button class="secondary" onclick="resetFilters()">重置</button>
</div>
<div id="list" class="list"></div>
</div>
<script>
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function qs(id){return document.getElementById(id).value.trim();}
const actionMap={
'auth.login.success':'登录成功','auth.login.failed':'登录失败','auth.logout':'退出登录',
'record.delete.self':'删除本人记录','record.delete.all':'删除全员记录','record.export':'导出记录',
'flag.update':'修改高级开关',
'channel_update_draft':'更新渠道草稿','channel_publish':'发布渠道草稿','channel_reload':'热加载渠道',
'channel_disable_all':'一键关闭全部渠道','channel_enable':'启用渠道','channel_disable':'停用渠道','channel_test':'测试渠道连接'
};
const targetMap={
'transaction':'记账记录','feature_flag':'高级开关','channel':'渠道','user':'用户','system':'系统'
};
function actionLabel(v){return actionMap[v]||v||'-';}
function targetLabel(v){return targetMap[v]||v||'-';}
function parseResult(note){
const m=String(note||'').match(/result=(success|failed|denied)/);
return m?m[1]:'';
}
function resultLabel(r){
if(r==='success') return '成功';
if(r==='failed') return '失败';
if(r==='denied') return '拒绝';
return '未标注';
}
function resetFilters(){['fAction','fTarget','fResult','fActor','fFrom','fTo'].forEach(id=>document.getElementById(id).value='');loadAudit();}
async function loadAudit(){
const p=new URLSearchParams();
const m={action:qs('fAction'),target_type:qs('fTarget'),result:qs('fResult'),actor_id:qs('fActor'),from:qs('fFrom'),to:qs('fTo')};
Object.entries(m).forEach(([k,v])=>{if(v)p.set(k,v)});
p.set('limit','200');
const listEl=document.getElementById('list');
listEl.innerHTML='<div class="empty">加载中...</div>';
const r=await fetch('/api/v1/admin/audit?'+p.toString());
const data=await r.json().catch(()=>[]);
const list=Array.isArray(data)?data:[];
if(!list.length){listEl.innerHTML='<div class="empty">暂无审计记录</div>';return;}
listEl.innerHTML=list.map(it=>{
const result=parseResult(it.note);
const resultClass=result||'success';
const note=String(it.note||'').replace(/\s*\|\s*result=(success|failed|denied)\s*$/,'').trim();
return `<div class="log-card">
<div class="row"><strong>${actionLabel(it.action)}</strong><span class="tag ${resultClass}">${resultLabel(result)}</span></div>
<div class="mono" style="margin-top:4px;">#${it.id} · ${esc(it.created_at||'')}</div>
<div class="mono" style="margin-top:2px;">操作人: ${it.actor_id} · 目标: ${targetLabel(it.target_type)} (${esc(it.target_id||'')})</div>
<div class="note">${esc(note||'(无备注)')}</div>
</div>`;
}).join('');
}
loadAudit();
</script>
</body>
</html>