<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🧾 审计日志 - Ops-Assistant</title>
<style>
:root{
  --bg:#f5f7fb;
  --text:#222;
  --card:#fff;
  --border:#e5e7eb;
  --muted:#6b7280;
  --accent:#ee5a24;
  --accent-hover:#d63031;
  --header-bg:linear-gradient(135deg,#ff6b6b,#ee5a24);
}
[data-theme="dark"]{
  --bg:#0f172a;
  --text:#e5e7eb;
  --card:#111827;
  --border:#1f2937;
  --muted:#9ca3af;
  --accent:#f97316;
  --accent-hover:#ea580c;
  --header-bg:linear-gradient(135deg,#7c2d12,#ea580c);
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.header{background:var(--header-bg);color:#fff;padding:18px 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,.header button{color:#fff;text-decoration:none;background:rgba(255,255,255,.2);padding:6px 10px;border-radius:8px;font-size:13px;border:none;cursor:pointer}
.header button{display:inline-flex;align-items:center;justify-content:center}
.header button svg{width:16px;height:16px;stroke:#fff;fill:none;stroke-width:2}
.header a:hover{background:rgba(255,255,255,.35)}

.wrap{max-width:760px;margin:0 auto;padding:14px}
.filters{background:var(--card);border-radius:6px;padding:12px;border:1px solid var(--border);box-shadow:none;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 var(--border);border-radius:6px;font-size:13px;background:var(--card);color:var(--text)}
small{color:var(--muted)}

.actions{margin:0 0 10px;display:flex;gap:8px;flex-wrap:wrap}
button{border:none;border-radius:6px;padding:8px 12px;cursor:pointer;color:#fff;background:var(--accent);font-size:13px}
button:hover{background:var(--accent-hover)}
button.secondary{background:#6b7280}
button.secondary:hover{background:#4b5563}

.list{display:flex;flex-direction:column;gap:10px}
.log-card{background:var(--card);border-radius:6px;padding:12px;border:1px solid var(--border);box-shadow:none}
.row{display:flex;justify-content:space-between;gap:8px;align-items:flex-start}
.tag{display:inline-block;padding:2px 8px;border-radius:6px;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:var(--muted);word-break:break-all}
.note{font-size:13px;color:var(--text);margin-top:6px;white-space:pre-wrap;word-break:break-word}
.empty{text-align:center;padding:40px 10px;color:var(--muted)}
.hidden{display:none !important;}

@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}
}
.theme-hidden{display:none !important;}
</style>
</head>
<body>
<div class="header">
  <div>
    <div class="title">🧾 审计日志</div>
    <div class="sub">{{.version}}</div>
  </div>
  <div>
    <button id="themeToggle" class="theme-hidden" title="切换主题" aria-label="切换主题">
      <svg id="themeIcon" viewBox="0 0 24 24" aria-hidden="true">
        <circle cx="12" cy="12" r="5"></circle>
        <line x1="12" y1="2" x2="12" y2="4"></line>
        <line x1="12" y1="20" x2="12" y2="22"></line>
        <line x1="2" y1="12" x2="4" y2="12"></line>
        <line x1="20" y1="12" x2="22" y2="12"></line>
        <line x1="4.2" y1="4.2" x2="5.8" y2="5.8"></line>
        <line x1="18.2" y1="18.2" x2="19.8" y2="19.8"></line>
        <line x1="18.2" y1="5.8" x2="19.8" y2="4.2"></line>
        <line x1="4.2" y1="19.8" x2="5.8" y2="18.2"></line>
      </svg>
    </button>
    <a href="/">返回首页</a>
    <a id="btnChannels" 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>
let me=null;
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
function qs(id){return document.getElementById(id).value.trim();}
async function api(url,opt={}){const r=await fetch(url,opt);const out=await r.json().catch(()=>({}));if(!r.ok) throw new Error(out.message||('HTTP '+r.status));return out?.data||{};}
function can(k){return !!(me&&me.effective_capabilities&&me.effective_capabilities[k]);}

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>';

  try{
    const out=await api('/api/v1/admin/audit?'+p.toString());
    const list=Array.isArray(out.logs)?out.logs:[];
    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('');
  }catch(e){
    listEl.innerHTML='<div class="empty">加载失败：'+esc(e.message||e)+'</div>';
  }
}

async function loadMe(){
  const r=await fetch('/api/v1/me');
  const out=await r.json().catch(()=>({}));
  if(!r.ok) throw new Error(out.message||'读取用户失败');
  me=out?.data||{};
}

function initPermissionUI(){
  document.getElementById('btnChannels').classList.toggle('hidden',!can('can_view_channels'));
}

function applyTheme(theme){
  if(theme==='dark'){
    document.documentElement.setAttribute('data-theme','dark');
  }else{
    document.documentElement.removeAttribute('data-theme');
  }
  localStorage.setItem('theme',theme);
}
function toggleTheme(){
  const cur=localStorage.getItem('theme')||'dark';
  applyTheme(cur==='light'?'dark':'light');
}
function initTheme(){
  const saved=localStorage.getItem('theme')||'dark';
  applyTheme(saved);
  const btn=document.getElementById('themeToggle');
  if(btn){ btn.addEventListener('click',toggleTheme); }
}

(async function(){
  try{ initTheme(); await loadMe(); initPermissionUI(); await loadAudit(); }
  catch(e){ document.getElementById('list').innerHTML='<div class="empty">初始化失败：'+esc(e.message||e)+'</div>'; }
})();
</script>
</body>
</html>
