Files
sap-erp/public/audit-logs.html
T
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

197 lines
14 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="en">
<head>
<script src="/auth-guard.js?v=4"></script>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
<title>Audit Trail — SAP B1</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Bebas+Neue&display=swap" rel="stylesheet"/>
<style>
:root{--ink:#080e18;--ink2:#0d1626;--purple:#9f5afd;--blue:#1a6fff;--amber:#f5a623;--green:#0dbb8a;--red:#e53e3e;--teal:#06b6d4;--border:#1c2e48;--border2:#243650;--text:#c8d6e8;--text2:#7a9ab8;--text3:#3d5470;--surface:#0f1d30;--surface2:#142338;--surface3:#0b1520;--radius:10px;--font:'Space Grotesk',sans-serif;--mono:'JetBrains Mono',monospace;--display:'Bebas Neue',sans-serif;--shadow:0 8px 32px rgba(0,0,0,.5)}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--font);background:var(--ink);color:var(--text);min-height:100vh;-webkit-font-smoothing:antialiased}
nav{position:sticky;top:0;z-index:200;background:rgba(8,14,24,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:0 12px;height:50px;display:flex;align-items:center;justify-content:space-between;gap:6px}
.nav-logo{background:var(--blue);color:#fff;font-family:var(--display);font-size:15px;letter-spacing:2px;padding:3px 8px;border-radius:5px}
.nav-links{display:flex;gap:2px;overflow-x:auto;flex:1;justify-content:center}
.nav-link{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:600;color:var(--text2);text-decoration:none;white-space:nowrap}.nav-link.active{color:var(--blue);background:rgba(26,111,255,.1)}
.btn-sm{padding:4px 8px;border-radius:5px;font-size:10px;font-weight:700;border:1px solid var(--border2);color:var(--text3);background:none;cursor:pointer;font-family:var(--font)}.btn-sm:hover{border-color:var(--red);color:var(--red)}
main{padding:12px;max-width:1500px;margin:0 auto}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
.card-hd{background:var(--surface3);padding:10px 14px;font-size:12px;font-weight:700;display:flex;align-items:center;gap:8px;border-bottom:1px solid var(--border)}
.card-hd .right{margin-left:auto;font-size:11px;color:var(--text3)}
.card-bd{padding:12px 14px}
.fl{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;color:var(--text3);margin-bottom:3px;display:block}
.fi{width:100%;padding:7px 9px;border:1.5px solid var(--border2);border-radius:7px;font-size:12.5px;font-family:var(--font);background:var(--surface2);color:var(--text);outline:none;height:34px}
.fi:focus{border-color:var(--blue)}
select.fi{cursor:pointer}
.filters{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
.filters>div{min-width:120px}
.btn{padding:8px 15px;border-radius:8px;border:none;font-family:var(--font);font-size:12px;font-weight:700;cursor:pointer;height:34px}
.b-primary{background:var(--blue);color:#fff}.b-cancel{background:var(--surface2);color:var(--text2);border:1px solid var(--border2)}
.lst{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
.lst th{text-align:left;padding:8px 10px;font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:var(--text3);border-bottom:2px solid var(--border2);white-space:nowrap}
.lst td{padding:8px 10px;border-bottom:1px solid var(--border);vertical-align:top}
.lst tr.row{cursor:pointer}
.lst tr.row:hover td{background:rgba(26,111,255,.05)}
.mono{font-family:var(--mono)}
.pill{font-size:9px;font-weight:800;padding:2px 7px;border-radius:20px;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap}
.p-create{background:rgba(13,187,138,.14);color:#0aa457}
.p-update{background:rgba(26,111,255,.14);color:#5b9bff}
.p-delete{background:rgba(229,62,62,.14);color:#e06666}
.p-approve{background:rgba(159,90,253,.16);color:#b98bff}
.p-login,.p-logout{background:rgba(245,166,35,.14);color:#c78a1a}
.p-other{background:var(--surface3);color:var(--text2)}
.st-ok{color:var(--green);font-weight:700}
.st-fail{color:var(--red);font-weight:700}
.det{background:var(--surface3);border:1px solid var(--border2);border-radius:7px;padding:10px 12px;font-size:11.5px;color:var(--text2);max-height:360px;overflow:auto}
.chg{width:100%;border-collapse:collapse;font-size:11.5px}
.chg th{text-align:left;padding:4px 8px;font-size:9px;text-transform:uppercase;color:var(--text3);border-bottom:1px solid var(--border2)}
.chg td{padding:4px 8px;border-bottom:1px solid var(--border);vertical-align:top}
.chg .fld{font-weight:700;color:var(--text);font-family:var(--mono)}
.chg .old{color:#e06666;font-family:var(--mono);text-decoration:line-through;opacity:.85}
.chg .arw{color:var(--text3);padding:0 6px}
.chg .new{color:#0aa457;font-weight:700;font-family:var(--mono)}
.kv{display:grid;grid-template-columns:auto 1fr;gap:2px 12px;font-family:var(--mono);font-size:11px}
.kv .k{color:var(--text3)}.kv .v{color:var(--text2);word-break:break-word}
.det-meta{margin-top:8px;font-size:10.5px;color:var(--text3)}
.empty-msg{text-align:center;padding:26px;color:var(--text3);font-size:12px}
.pager{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}
.count{font-size:11px;font-weight:700;color:var(--blue);background:rgba(26,111,255,.1);padding:3px 11px;border-radius:20px}
.sp{width:14px;height:14px;border:2px solid rgba(26,111,255,.25);border-top-color:var(--blue);border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
.toast-c{position:fixed;top:56px;right:10px;z-index:9999;display:flex;flex-direction:column;gap:6px}
.toast{padding:9px 13px;border-radius:8px;font-size:12px;font-weight:600;box-shadow:var(--shadow)}
.t-err{background:var(--red);color:#fff}
</style>
<link rel="stylesheet" href="/theme.css?v=3"/>
</head>
<body>
<div class="toast-c" id="toast-c"></div>
<nav>
<span class="nav-logo">SAP</span>
<div class="nav-links"><a href="/" class="nav-link">Home</a><a href="/audit-logs" class="nav-link active">Audit Trail</a></div>
<button class="btn-sm" id="btn-logout">Logout</button>
</nav>
<main id="app"></main>
<script>
'use strict';
let _tok=null;const BE='/api';
try{_tok=sessionStorage.getItem('portal_token');}catch(_e){}
if(!_tok)location.href='/';
document.getElementById('btn-logout').onclick=()=>{sessionStorage.clear();location.href='/';};
const _f={from:'',to:'',username:'',entity:'',action:'',status:'',q:''};
let _page=0,_pageSize=50,_total=0,_rows=[],_facets={entities:[],actions:[],users:[]},_loading=false,_open=new Set();
function esc(v){return(v==null?'':String(v)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
function q(s){return document.querySelector(s);}
function toast(m){const tc=q('#toast-c');const e=document.createElement('div');e.className='toast t-err';e.textContent=m;tc.appendChild(e);setTimeout(()=>e.remove(),4500);}
function fmt(iso){if(!iso)return '—';const d=new Date(iso);if(isNaN(d))return '—';const p=n=>String(n).padStart(2,'0');return `${p(d.getDate())}-${['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()]}-${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;}
function pcls(a){return {create:'p-create',update:'p-update',delete:'p-delete',approve:'p-approve',reject:'p-delete',cancel:'p-delete',login:'p-login',logout:'p-logout'}[a]||'p-other';}
async function api(p){
const r=await fetch(BE+p,{headers:{Authorization:'Bearer '+_tok}});
if(r.status===401){location.href='/';throw new Error('Session expired');}
const d=await r.json();
if(!r.ok||d.success===false)throw new Error(d.message||('HTTP '+r.status));
return d;
}
async function loadFacets(){try{const r=await api('/audit/facets');_facets=r.data||_facets;}catch(_e){}}
async function load(reset){
if(reset)_page=0;
_loading=true;render();
const qs=new URLSearchParams();
Object.entries(_f).forEach(([k,v])=>{if(v)qs.set(k,v);});
qs.set('top',_pageSize);qs.set('skip',_page*_pageSize);
try{const r=await api('/audit?'+qs.toString());_rows=r.data||[];_total=r.total||0;}
catch(e){toast(e.message);_rows=[];_total=0;}
_loading=false;render();
}
function opt(list,cur){return '<option value="">All</option>'+list.map(v=>`<option value="${esc(v)}" ${cur===v?'selected':''}>${esc(v)}</option>`).join('');}
function render(){
const app=document.getElementById('app');
const start=_total?_page*_pageSize+1:0, end=Math.min(_total,(_page+1)*_pageSize);
app.innerHTML=`
<div class="card"><div class="card-hd"><span>🛡 Audit Trail</span><span class="right">${_loading?'':`${_total.toLocaleString()} event${_total===1?'':'s'}`}</span></div>
<div class="card-bd">
<div class="filters">
<div><label class="fl">From</label><input class="fi" type="date" id="f-from" value="${_f.from}"/></div>
<div><label class="fl">To</label><input class="fi" type="date" id="f-to" value="${_f.to}"/></div>
<div><label class="fl">User</label><select class="fi" id="f-user">${opt(_facets.users,_f.username)}</select></div>
<div><label class="fl">Module</label><select class="fi" id="f-entity">${opt(_facets.entities,_f.entity)}</select></div>
<div><label class="fl">Action</label><select class="fi" id="f-action">${opt(_facets.actions,_f.action)}</select></div>
<div><label class="fl">Result</label><select class="fi" id="f-status"><option value="">All</option><option value="ok" ${_f.status==='ok'?'selected':''}>Success</option><option value="fail" ${_f.status==='fail'?'selected':''}>Failed</option></select></div>
<div style="min-width:170px;flex:1"><label class="fl">Search</label><input class="fi" id="f-q" placeholder="summary / id / path…" value="${esc(_f.q)}"/></div>
<button class="btn b-primary" id="f-go">Apply</button>
<button class="btn b-cancel" id="f-clear">Clear</button>
</div>
<div style="overflow-x:auto;margin-top:12px">
${_loading?`<div class="empty-msg"><span class="sp"></span> Loading…</div>`:
!_rows.length?`<div class="empty-msg">No audit events match these filters.</div>`:
`<table class="lst"><thead><tr><th>When</th><th>User</th><th>Action</th><th>Module</th><th>Ref</th><th>Summary</th><th>Result</th></tr></thead>
<tbody>${_rows.map(r=>rowHtml(r)).join('')}</tbody></table>
<div class="pager">
<button class="btn b-cancel" id="p-prev" ${_page===0?'disabled':''}>‹ Prev</button>
<span class="count">${start.toLocaleString()}–${end.toLocaleString()} of ${_total.toLocaleString()}</span>
<button class="btn b-cancel" id="p-next" ${end>=_total?'disabled':''}>Next ›</button>
</div>`}
</div>
</div></div>`;
wire();
}
function fval(v){ if(v==null||v==='')return '<span style="opacity:.5">(empty)</span>'; return esc(typeof v==='object'?JSON.stringify(v):v); }
function renderDetails(r){
const d=r.details;
let inner;
if(d&&Array.isArray(d.changes)&&d.changes.length){
inner=`<table class="chg"><thead><tr><th>Field</th><th>Old value</th><th></th><th>New value</th></tr></thead><tbody>${
d.changes.map(c=>`<tr><td class="fld">${esc(c.field)}</td><td class="old">${c.complex?'(changed)':fval(c.from)}</td><td class="arw">→</td><td class="new">${c.complex?'(changed)':fval(c.to)}</td></tr>`).join('')
}</tbody></table>`;
} else if(d&&d.deleted&&typeof d.deleted==='object'){
inner=`<div style="font-weight:700;color:#e06666;margin-bottom:5px">Deleted record</div><div class="kv">${Object.entries(d.deleted).map(([k,v])=>`<div class="k">${esc(k)}</div><div class="v">${fval(v)}</div>`).join('')}</div>`;
} else if(d&&typeof d==='object'){
const ents=Object.entries(d);
inner=ents.length?`<div class="kv">${ents.map(([k,v])=>`<div class="k">${esc(k)}</div><div class="v">${fval(v)}</div>`).join('')}</div>`:'<span style="opacity:.6">(no fields)</span>';
} else if(d){
inner=`<span class="mono">${esc(String(d))}</span>`;
} else {
inner='<span style="opacity:.6">(no request body captured)</span>';
}
const meta=[r.ip?`IP ${esc(r.ip)}`:'',r.company?`Company ${esc(r.company)}`:'',`${esc(r.method)} ${esc(r.path)}`].filter(Boolean).join(' · ');
return `<div class="det">${inner}<div class="det-meta">${meta}</div></div>`;
}
function rowHtml(r){
const isOpen=_open.has(r.id);
return `<tr class="row" data-id="${r.id}">
<td class="mono" style="white-space:nowrap">${esc(fmt(r.at))}</td>
<td><b>${esc(r.username||'—')}</b>${r.role?`<div style="font-size:9.5px;color:var(--text3)">${esc(r.role)}</div>`:''}</td>
<td><span class="pill ${pcls(r.action)}">${esc(r.action||'—')}</span></td>
<td>${esc(r.entity||'—')}</td>
<td class="mono">${esc(r.entityId||'')}</td>
<td>${esc(r.summary||'')}<div style="font-size:9.5px;color:var(--text3)" class="mono">${esc(r.method)} ${esc(r.path)}</div></td>
<td><span class="${r.ok?'st-ok':'st-fail'}">${r.status||''}${r.ok?' OK':' ✗'}</span></td>
</tr>
${isOpen?`<tr><td colspan="7">${renderDetails(r)}</td></tr>`:''}`;
}
function wire(){
q('#f-go')&&(q('#f-go').onclick=()=>{
_f.from=q('#f-from').value;_f.to=q('#f-to').value;_f.username=q('#f-user').value;_f.entity=q('#f-entity').value;_f.action=q('#f-action').value;_f.status=q('#f-status').value;_f.q=q('#f-q').value.trim();
load(true);
});
q('#f-clear')&&(q('#f-clear').onclick=()=>{Object.keys(_f).forEach(k=>_f[k]='');load(true);});
q('#f-q')&&(q('#f-q').onkeydown=e=>{if(e.key==='Enter')q('#f-go').click();});
q('#p-prev')&&(q('#p-prev').onclick=()=>{if(_page>0){_page--;load();}});
q('#p-next')&&(q('#p-next').onclick=()=>{if((_page+1)*_pageSize<_total){_page++;load();}});
document.querySelectorAll('tr.row').forEach(tr=>tr.onclick=()=>{const id=Number(tr.dataset.id);_open.has(id)?_open.delete(id):_open.add(id);render();});
}
(async function init(){ await loadFacets(); load(); })();
</script>
<script src="/sidebar.js?v=31"></script>
</body>
</html>