Files
A-Team-Security-Infra-Agent…/frontend/index.html
2026-02-27 20:20:22 -03:00

418 lines
44 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="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OCI CIS AI Agent</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg0:#f7f8fa;--bg1:#ffffff;--bg2:#f0f2f5;--bg3:#e8eaed;--bd:#d4d8de;--bda:#c74634;
--t1:#1a1a2e;--t2:#4a5568;--tm:#718096;--ac:#c74634;--ah:#a53727;--acl:#fef2f0;
--gn:#0d8a41;--gnl:#e6f5ec;--rd:#c74634;--rdl:#fef2f0;--bl:#0b72b9;--bll:#e6f1fa;
--yl:#b45309;--yll:#fef3cd;--pp:#6b21a8;--ppl:#f3e8ff;
--r:8px;--rl:10px;--sh:0 1px 3px rgba(0,0,0,.08);--fm:'JetBrains Mono',monospace;--fs:'DM Sans',sans-serif}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh}
.app{display:flex;min-height:100vh}
.sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100}
.sb-h{padding:1.15rem 1rem;border-bottom:1px solid var(--bd);background:linear-gradient(135deg,#c74634 0%,#a53727 100%)}
.sb-h h1{font-size:.88rem;font-weight:700;color:#fff;font-family:var(--fm);letter-spacing:-.02em;display:flex;align-items:center;gap:.4rem}
.sb-h .st{font-size:.63rem;color:rgba(255,255,255,.75);margin-top:2px;font-family:var(--fm)}
.nav{padding:.65rem .5rem;flex:1;overflow-y:auto}
.nl{font-size:.58rem;color:var(--tm);text-transform:uppercase;letter-spacing:.1em;padding:.6rem .6rem .25rem;font-weight:700}
.ni{display:flex;align-items:center;gap:.6rem;padding:.5rem .6rem;border-radius:var(--r);cursor:pointer;color:var(--t2);font-size:.8rem;font-weight:500;transition:all .15s;margin-bottom:1px}
.ni:hover{background:var(--acl);color:var(--ac)}.ni.on{background:var(--acl);color:var(--ac);font-weight:600;border-left:3px solid var(--ac)}
.ni .ic{width:16px;text-align:center;font-size:.9rem}
.sb-f{padding:.65rem .85rem;border-top:1px solid var(--bd);background:var(--bg2)}
.ui{display:flex;align-items:center;gap:.5rem}
.ua{width:28px;height:28px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;color:#fff}
.un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--tm);font-size:.63rem;font-family:var(--fm)}
.mc{flex:1;margin-left:250px;min-height:100vh}
.tb{height:50px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.15rem;position:sticky;top:0;z-index:50}
.tb-t{font-weight:600;font-size:.88rem;color:var(--t1)}.tb-a{display:flex;align-items:center;gap:.5rem}
.pc{padding:1.15rem}
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.15rem;margin-bottom:.65rem;box-shadow:var(--sh)}
.ct{font-weight:600;font-size:.88rem;margin-bottom:.65rem;display:flex;align-items:center;gap:.35rem;color:var(--t1)}
.btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .8rem;border-radius:var(--r);font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)}
.bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)}
.bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)}
.bd{background:var(--rdl);color:var(--rd);border-color:rgba(199,70,52,.2)}.bd:hover{background:var(--rd);color:#fff}
.bsm{padding:.28rem .55rem;font-size:.7rem}
.btn:disabled{opacity:.5;cursor:not-allowed}
.ig{margin-bottom:.65rem}.ig label{display:block;font-size:.76rem;font-weight:600;color:var(--t2);margin-bottom:.2rem}
.ig .ht{font-size:.65rem;color:var(--tm);margin-bottom:.15rem}
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.45rem .6rem;background:var(--bg1);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.78rem;font-family:var(--fm);transition:border .15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 2px rgba(199,70,52,.12)}
input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t2)}
.badge{padding:.15rem .5rem;border-radius:12px;font-size:.66rem;font-weight:600;font-family:var(--fm)}
.b-pass{background:var(--gnl);color:var(--gn)}.b-fail{background:var(--rdl);color:var(--rd)}
.b-pend{background:var(--yll);color:var(--yl)}.b-rev{background:var(--bll);color:var(--bl)}
.b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--tm)}
.b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--tm)}
table{width:100%;border-collapse:collapse;font-size:.76rem}
th{text-align:left;padding:.5rem .6rem;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:2px solid var(--bd);font-weight:700;background:var(--bg2)}
td{padding:.5rem .6rem;border-bottom:1px solid var(--bg3);color:var(--t2)}
tr:hover td{background:var(--acl)}
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.65rem}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.65rem}
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 50px - 2.3rem)}
.ch-m{flex:1;overflow-y:auto;padding:.65rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0}
.cm{margin-bottom:.65rem;display:flex;gap:.5rem}.cm-user{justify-content:flex-end}
.cm-user .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cb{max-width:75%;padding:.55rem .75rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.8rem;line-height:1.55;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh)}
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.1rem .2rem;border-radius:3px}
.cb strong{color:var(--ac)}
.ch-i{display:flex;gap:.35rem;padding:.55rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)}
.ch-i input{flex:1}
.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem;flex-wrap:wrap}
.ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem}
.ch-t label{font-weight:600;color:var(--tm);font-size:.68rem}
.rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff}
.lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#fef2f0 0%,#f0f2f5 50%,#e6f1fa 100%)}
.lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:2rem;width:100%;max-width:370px;box-shadow:0 4px 24px rgba(0,0,0,.08)}
.lc h2{font-size:1.1rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.76rem;margin-bottom:1.15rem}
.al{padding:.55rem .75rem;border-radius:var(--r);font-size:.76rem;margin-bottom:.65rem}
.al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(199,70,52,.2)}
.al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(13,138,65,.2)}
.al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(11,114,185,.2)}
.tag{display:inline-block;padding:.1rem .35rem;background:var(--acl);color:var(--ac);border-radius:4px;font-size:.65rem;font-family:var(--fm);font-weight:500}
.emp{text-align:center;padding:2rem;color:var(--tm)}.emp .eic{font-size:2rem;margin-bottom:.4rem}
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.5rem;margin-top:.5rem}
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.65rem;font-size:.76rem}
.exp-c strong{color:var(--t1);font-size:.8rem}.exp-c .em{font-family:var(--fm);font-size:.63rem;color:var(--tm);word-break:break-all}
@keyframes fi{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .2s ease}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.8rem;text-align:center;padding:1.2rem}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg0)}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
@media(max-width:768px){.sb{transform:translateX(-100%)}.sb.open{transform:translateX(0)}.mc{margin-left:0}.g2,.g3{grid-template-columns:1fr}}
</style>
</head>
<body>
<div id="app"></div>
<script>
const V='1.1';
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selGenai:'',expData:null};
const API='/api';
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)}
const r=await fetch(API+p,{...o,headers:h});if(r.status===401){S.user=null;S.token=null;localStorage.removeItem('t');R();throw new Error('Unauthorized')}
const d=await r.json();if(!r.ok)throw new Error(d.detail||d.message||'Erro');return d;}
async function doLogin(u,pw,totp){const b={username:u,password:pw};if(totp)b.totp_code=totp;
const d=await $api('/auth/login',{method:'POST',body:b});if(d.mfa_required)return d;
S.token=d.token;S.user=d.user;localStorage.setItem('t',d.token);await loadData();R();return d;}
async function doLogout(){try{await $api('/auth/logout',{method:'POST'})}catch(e){}S.user=null;S.token=null;S.msgs=[];localStorage.removeItem('t');R()}
async function checkAuth(){const t=localStorage.getItem('t');if(!t)return;S.token=t;try{S.user=await $api('/users/me');await loadData()}catch(e){S.token=null;localStorage.removeItem('t')}}
async function loadData(){try{
[S.ociCfg,S.reports,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/reports'),$api('/genai/configs'),$api('/mcp/servers')]);
try{const m=await $api('/genai/models');S.models=m.models;S.regions=m.regions}catch(e){}
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
}catch(e){console.error(e)}}
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind()}
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
function rLogin(){return`<div class="lp"><div class="lc fi">
<div style="text-align:center;margin-bottom:1.15rem"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="48" height="48"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.1)" stroke="#C74634" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.3" stroke="#C74634" stroke-width="0.8"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/></svg>
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security · v${V}</div></div>
<div id="le"></div>
<div id="mfa-s" style="display:none"><div class="al al-i">MFA obrigatório. Insira o código do autenticador.</div>
<div class="ig"><label>Código MFA</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1rem;letter-spacing:.25em"></div>
<button class="btn bp" style="width:100%" onclick="hMfa()">Verificar</button></div>
<div id="lf"><div class="ig"><label>Usuário</label><input type="text" id="lu" placeholder="admin" autofocus></div>
<div class="ig"><label>Senha</label><input type="password" id="lp" placeholder="••••••••" onkeydown="if(event.key==='Enter')hLogin()"></div>
<button class="btn bp" style="width:100%" onclick="hLogin()">Entrar</button>
<div style="text-align:center;margin-top:.65rem;font-size:.65rem;color:var(--tm)">Padrão: admin / admin123</div></div></div></div>`}
function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`}
function rSb(){
const tabs=[['chat','💬','Chat Agent'],['explorer','🔍','OCI Explorer'],['report','📊','Report'],['downloads','📁','Downloads']];
const ctabs=[['oci-config','☁️','OCI Credentials'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector']];
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
const i=(S.user?.username||'?')[0].toUpperCase();
return`<div class="sb"><div class="sb-h"><h1><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="28" height="28" style="vertical-align:middle;flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.15)" stroke="#fff" stroke-width="2"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" opacity="0.95"/><circle cx="15" cy="17" r="2" fill="#C74634"/><circle cx="21" cy="17" r="2" fill="#C74634"/><circle cx="15" cy="16.7" r="0.7" fill="#fff"/><circle cx="21" cy="16.7" r="0.7" fill="#fff"/><rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/><line x1="18" y1="11" x2="18" y2="6" stroke="#fff" stroke-width="1.2" stroke-linecap="round"/><circle cx="18" cy="5.5" r="1.5" fill="#fff" opacity="0.9"/><line x1="11" y1="18" x2="6" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><line x1="25" y1="18" x2="30" y2="18" stroke="#fff" stroke-width="1" stroke-linecap="round" opacity="0.7"/><circle cx="5.5" cy="18" r="1" fill="#fff" opacity="0.7"/><circle cx="30.5" cy="18" r="1" fill="#fff" opacity="0.7"/></svg> OCI CIS Agent</h1><div class="st">Infrastructure & Security · v${V}</div></div>
<div class="nav"><div class="nl">Principal</div>
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
<div class="nl" style="margin-top:.5rem">Configuração</div>
${ctabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
${S.user?.role==='admin'?`<div class="nl" style="margin-top:.5rem">Administração</div>
${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}`:''}</div>
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<button class="btn bs bsm" style="margin-left:auto" onclick="doLogout()" title="Sair">⏻</button></div></div></div>`}
function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ OCI Credentials','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','users':'👥 Usuários','mfa':'🔐 MFA','audit':'📋 Audit Log'};
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">v${V}</span></div></div>`}
function rPg(){switch(S.tab){case'chat':return rChat();case'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
// ── Chat ──
function rChat(){
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p><p style="font-size:.72rem;margin-top:.3rem">Selecione um modelo GenAI acima ou use o agente local.</p></div>'
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${g.name||mi?.name||g.model_id} (${g.genai_region})</option>`}).join('');
return`<div class="ch-c">
<div class="ch-t"><label>🧠 Modelo:</label><select id="gsel" onchange="S.selGenai=this.value"><option value="">Agente Local (sem IA)</option>${gcOpts}</select>
<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
<div class="ch-m" id="chm">${ms}</div>
<div class="ch-i"><input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
<button class="btn bp" onclick="sChat()">Enviar</button></div></div>`}
function fm(t){return t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>')}
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();if(!m)return;el.value='';
S.msgs.push({r:'user',c:m});R();scCh();
try{const body={message:m,session_id:S.sid};if(S.selGenai)body.genai_config_id=S.selGenai;
const d=await $api('/chat',{method:'POST',body});S.sid=d.session_id;S.msgs.push({r:'assistant',c:d.response});R();scCh()}
catch(e){S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}}
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
// ── OCI Explorer ──
function rExplorer(){
const cfgSel=S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('');
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">☁️</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.72rem;margin-top:.3rem">Vá em <strong>OCI Credentials</strong> para adicionar.</p></div></div>`;
return`<div class="cd"><div class="ct">🔍 Explorar Conta OCI</div>
<div class="g3"><div class="ig"><label>Conexão OCI</label><select id="exCfg">${cfgSel}</select></div>
<div class="ig"><label>Recurso</label><select id="exRes"><option value="compartments">Compartments</option><option value="regions">Regions</option><option value="instances">Compute Instances</option><option value="vcns">VCNs</option><option value="databases">Autonomous DBs</option><option value="buckets">Object Storage Buckets</option></select></div>
<div class="ig"><label>&nbsp;</label><button class="btn bp" onclick="doExplore()">🔍 Explorar</button></div></div></div>
<div class="cd"><div class="ct" id="exTitle">Resultados</div><div id="exOut">${S.expData?renderExpData(S.expData):'<div class="emp"><p>Selecione uma conexão e recurso acima.</p></div>'}</div></div>`}
async function doExplore(){const cid=document.getElementById('exCfg').value;const res=document.getElementById('exRes').value;
document.getElementById('exOut').innerHTML='<div class="ld">Consultando OCI...</div>';
try{const d=await $api('/oci/explore/'+cid+'/'+res);S.expData=d;
document.getElementById('exTitle').textContent='Resultados: '+res;
document.getElementById('exOut').innerHTML=renderExpData(d)}catch(e){document.getElementById('exOut').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
function renderExpData(d){if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum resultado.</p></div>';
return`<div style="font-size:.72rem;color:var(--tm);margin-bottom:.4rem">${d.length} item(ns)</div><div class="exp-r">${d.map(i=>{
const keys=Object.keys(i).filter(k=>k!=='id');
const name=i.display_name||i.name||i.db_name||'—';
return`<div class="exp-c"><strong>${name}</strong>${i.lifecycle_state?` <span class="badge ${i.lifecycle_state==='RUNNING'||i.lifecycle_state==='ACTIVE'||i.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${i.lifecycle_state}</span>`:''}<br>
${keys.filter(k=>!['display_name','name','db_name','lifecycle_state'].includes(k)).map(k=>`<span style="color:var(--tm);font-size:.65rem">${k}:</span> <span class="em">${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}</span>`).join('<br>')}</div>`}).join('')}</div>`}
// ── Report ──
function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
if(!comp.length)return`<div class="cd"><div class="ct">📊 Report</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório.</p></div></div>${rRunRpt()}`;
const lt=comp[0];
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"><span style="font-size:.76rem;color:var(--tm)">Report HTML — ${lt.tenancy_name}</span>
<select id="rsel" onchange="ldRpt(this.value)" style="max-width:260px">${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select></div>
<iframe class="rif" id="rfr" src="${API}/reports/${lt.id}/html"></iframe>${rRunRpt()}`}
function rRunRpt(){if(S.user?.role==='viewer')return'';
return`<div class="cd" style="margin-top:.65rem"><div class="ct">🚀 Executar Relatório</div>
<div class="g3"><div class="ig"><label>Config OCI</label><select id="rc"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
<div class="ig"><label>MCP Server (opcional)</label><select id="rmcp"><option value="">Padrão (stub)</option>
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}${m.linked_adb_id?' 🗄️':''}</option>`).join('')}</select></div>
<div class="ig"><label>Regiões (opcional)</label><input type="text" id="rr" placeholder="sa-saopaulo-1, us-ashburn-1"></div></div>
<button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`}
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
async function runRpt(){const c=document.getElementById('rc').value;if(!c)return alert('Selecione config OCI');
const rg=document.getElementById('rr').value.split(',').map(r=>r.trim()).filter(Boolean);
const mcp=document.getElementById('rmcp').value||null;
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,mcp_server_id:mcp,regions:rg.length?rg:null}});
alert('Relatório iniciado! ID: '+d.report_id);S.reports=await $api('/reports');R()}catch(e){alert('Erro: '+e.message)}}
// ── Downloads ──
function rDl(){return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">
<span style="font-size:.76rem;color:var(--tm)">${S.reports.length} relatório(s)</span>
<button class="btn bp bsm" onclick="refreshDl()">🔄</button></div>
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead>
<tbody>${S.reports.map(r=>`<tr><td><strong>${r.tenancy_name}</strong></td>
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'?'b-fail':'b-pend'}">${r.status}</span></td>
<td>${r.created_at||'—'}</td><td>${r.completed_at||'—'}</td>
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:'—'}</td></tr>`).join('')}</tbody></table>
${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
async function refreshDl(){S.reports=await $api('/reports');R()}
// ── OCI Config ──
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais OCI</div>
<table><thead><tr><th>Tenancy</th><th>Region</th><th>Compartment</th><th>Criado</th><th>Ações</th></tr></thead><tbody>
${S.ociCfg.map(c=>`<tr><td><strong>${c.tenancy_name}</strong></td><td><span class="tag">${c.region}</span></td>
<td style="font-family:var(--fm);font-size:.65rem">${c.compartment_id?(c.compartment_id.slice(0,20)+'...'):'—'}</td><td>${c.created_at}</td>
<td><button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.ociCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhuma credencial.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Credencial OCI</div><div id="om"></div>
<div class="g2">
<div class="ig"><label>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa"></div>
<div class="ig"><label>OCID User</label><input type="text" id="ouo" placeholder="ocid1.user.oc1.."></div>
<div class="ig"><label>Fingerprint</label><input type="text" id="ofp" placeholder="aa:bb:cc:dd:..."></div>
<div class="ig"><label>OCID Tenancy</label><input type="text" id="oto" placeholder="ocid1.tenancy.oc1.."></div>
<div class="ig"><label>Region</label><input type="text" id="org" placeholder="sa-saopaulo-1"></div>
<div class="ig"><label>Compartment OCID</label><input type="text" id="ocp" placeholder="ocid1.compartment.oc1.."></div></div>
<div class="g2"><div class="ig"><label>Private Key (.pem)</label><input type="file" id="osk" accept=".pem"></div>
<div class="ig"><label>Public Key (.pem)</label><input type="file" id="opk" accept=".pem"></div></div>
<button class="btn bp" onclick="sOci()">Salvar Credencial</button></div>`}
async function sOci(){const fd=new FormData();fd.append('tenancy_name',document.getElementById('otn').value);fd.append('tenancy_ocid',document.getElementById('oto').value);
fd.append('user_ocid',document.getElementById('ouo').value);fd.append('fingerprint',document.getElementById('ofp').value);
fd.append('region',document.getElementById('org').value);fd.append('compartment_id',document.getElementById('ocp').value);
const sk=document.getElementById('osk').files[0];if(!sk)return sm('om','Selecione a chave privada','e');fd.append('private_key',sk);
const pk=document.getElementById('opk').files[0];if(pk)fd.append('public_key',pk);
try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Salvo!','s');R()}catch(e){sm('om',e.message,'e')}}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ Conexão OK!':'❌ '+d.message)}catch(e){alert(e.message)}}
async function dOci(id){if(!confirm('Excluir?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
// ── GenAI Config ──
function rGenAI(){const mOpts=Object.entries(S.models).map(([k,v])=>`<option value="${k}">${v.name} (${v.provider})</option>`).join('');
const rOpts=S.regions.map(r=>`<option value="${r}" ${r==='us-ashburn-1'?'selected':''}>${r}</option>`).join('');
return`<div class="cd"><div class="ct">🧠 Configurações GenAI</div>
<table><thead><tr><th>Nome</th><th>Modelo</th><th>Endpoint</th><th>Temp</th><th>Max Tokens</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${g.name||'—'}</strong>${g.is_default?' ':''}<br><span class="tag">${mi.provider||'?'}</span></td>
<td>${mi.name||g.model_id}<br><span style="font-size:.6rem;color:var(--tm)">${g.model_ocid||g.model_id}</span></td>
<td style="font-size:.63rem;font-family:var(--fm)">${g.endpoint||'—'}</td>
<td>${g.temperature}</td><td>${g.max_tokens}</td>
<td><button class="btn bs bsm" onclick="tGenai('${g.id}')">Testar</button> <button class="btn bd bsm" onclick="dGenai('${g.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
${!S.genaiCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
<div class="cd"><div class="ct"> Novo Modelo GenAI</div>
<p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Configura conexão via <strong>OCI SDK</strong> com <code>GenerativeAiInferenceClient</code>. Endpoint é gerado automaticamente pela região.</p>
<div id="gm"></div>
<div class="g3"><div class="ig"><label>Nome da Config</label><input type="text" id="gname" placeholder="Llama Produção"></div>
<div class="ig"><label>Credencial OCI</label><select id="goci"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')}</select></div>
<div class="ig"><label>Modelo (catálogo)</label><select id="gmod">${mOpts}</select></div></div>
<div class="g3"><div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div>
<div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
<div class="ig"><label>Model OCID (opcional)</label><div class="ht">Se diferente do catálogo (ex: modelo dedicado)</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..."></div></div>
<div style="font-size:.72rem;font-weight:600;color:var(--t2);margin:.5rem 0 .3rem">Parâmetros do Chat</div>
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="1" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="6000" min="1" max="128000"></div>
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.95" step="0.05" min="0" max="1"></div></div>
<div class="g3"><div class="ig"><label>Top K</label><input type="number" id="gtk" value="1" min="-1" max="500"></div>
<div class="ig"><label>Frequency Penalty</label><input type="number" id="gfp" value="0" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Presence Penalty</label><input type="number" id="gpp" value="0" step="0.1" min="0" max="2"></div></div>
<button class="btn bp" onclick="sGenai()">Salvar Modelo</button></div>`}
async function sGenai(){try{await $api('/genai/config',{method:'POST',body:{
name:document.getElementById('gname').value||'default',
oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
model_ocid:document.getElementById('gocid').value||null,
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
endpoint:null,
serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
top_p:parseFloat(document.getElementById('gtp').value),top_k:parseInt(document.getElementById('gtk').value),
frequency_penalty:parseFloat(document.getElementById('gfp').value),presence_penalty:parseFloat(document.getElementById('gpp').value),
is_default:S.genaiCfg.length===0}});
S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo salvo!','s');R()}catch(e){sm('gm',e.message,'e')}}
async function tGenai(id){try{const d=await $api('/genai/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message+'\n'+d.response:'❌ '+d.message)}catch(e){alert(e.message)}}
async function dGenai(id){if(!confirm('Excluir?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
// ── MCP Servers ──
function rMCP(){const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name} (${a.dsn})</option>`).join('');
return`<div class="cd"><div class="ct">🔌 MCP Servers</div>
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Cmd/URL</th><th>ADB Link</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
return`<tr><td><strong>${m.name}</strong><br><span style="font-size:.63rem;color:var(--tm)">${m.description||''}</span></td>
<td><span class="tag">${m.server_type}</span></td>
<td style="font-family:var(--fm);font-size:.65rem">${m.command||m.url||m.module_path||'—'}</td>
<td>${adb?`<span class="badge b-pass">${adb.config_name}</span>`:'<span style="color:var(--tm);font-size:.65rem">—</span>'}</td>
<td><span class="badge ${m.is_active?'b-on':'b-off'}">${m.is_active?'Ativo':'Inativo'}</span></td>
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'Off':'On'}</button>
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Del</button></td></tr>`}).join('')}</tbody></table>
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server.</p></div>':''}</div>
<div class="cd"><div class="ct"> Registrar MCP Server</div><div id="mcm"></div>
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server"></div>
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0"></div></div>
<div class="g3"><div class="ig"><label>Tipo</label><select id="mt"><option value="stdio">stdio</option><option value="sse">SSE (HTTP)</option><option value="module">Python Module</option></select></div>
<div class="ig"><label>Comando (stdio)</label><input type="text" id="mcmd" placeholder="python3 server.py"></div>
<div class="ig"><label>URL (SSE)</label><input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div>
<div class="g2"><div class="ig"><label>🗄️ Vincular ADB Vector (tool)</label><div class="ht">MCP server usará este banco como ferramenta</div><select id="madb"><option value="">Nenhum</option>${adbOpts}</select></div>
<div class="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div></div>
<button class="btn bp" onclick="sMcp()">Registrar Server</button></div>
<div class="cd"><div class="ct">📤 Upload Script / 🔗 Link ADB</div>
<div class="g3"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
<div class="ig"><label>Arquivo .py</label><input type="file" id="muf" accept=".py"></div>
<div class="ig"><label>Vincular ADB</label><select id="mlnk"><option value="">Nenhum</option>${adbOpts}</select>
<button class="btn bs bsm" style="margin-top:.3rem" onclick="lnkMcp()">Vincular</button></div></div>
<button class="btn bs" onclick="uMcp()">Upload Script</button></div>`}
async function sMcp(){try{let args=null;const a=document.getElementById('margs').value;if(a)try{args=JSON.parse(a)}catch(e){}
await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value,
server_type:document.getElementById('mt').value,command:document.getElementById('mcmd').value||null,url:document.getElementById('murl').value||null,args,
linked_adb_id:document.getElementById('madb').value||null}});
S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ Registrado!','s');R()}catch(e){sm('mcm',e.message,'e')}}
async function tgMcp(id){await $api('/mcp/servers/'+id+'/toggle',{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');R()}
async function dMcp(id){if(!confirm('Excluir?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()}
async function uMcp(){const fd=new FormData();const f=document.getElementById('muf').files[0];if(!f)return alert('Selecione um arquivo');
fd.append('file',f);const id=document.getElementById('mup').value;if(!id)return alert('Selecione um server');
try{await $api('/mcp/servers/'+id+'/upload',{method:'POST',body:fd,headers:{}});alert('Upload OK!')}catch(e){alert(e.message)}}
async function lnkMcp(){const mid=document.getElementById('mup').value;const aid=document.getElementById('mlnk').value;if(!mid)return alert('Selecione MCP');
try{await $api('/mcp/servers/'+mid+'/link-adb?adb_id='+(aid||''),{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');alert('Vinculado!');R()}catch(e){alert(e.message)}}
// ── ADB Vector ──
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões Autonomous Database (Vector)</div>
<table><thead><tr><th>Nome</th><th>DSN</th><th>User</th><th>Tabela</th><th>mTLS</th><th>Wallet</th><th>Ações</th></tr></thead><tbody>
${S.adbCfg.map(c=>`<tr><td><strong>${c.config_name}</strong></td><td style="font-family:var(--fm);font-size:.65rem">${c.dsn}</td>
<td>${c.username}</td><td><span class="tag">${c.table_name}</span></td>
<td>${c.use_mtls?'✅':'—'}</td><td>${c.wallet_dir?'✅':'❌'}</td>
<td><button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.adbCfg.length?'<div class="emp" style="padding:.65rem"><p>Nenhuma conexão ADB.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Conexão Autonomous DB</div>
<p style="font-size:.72rem;color:var(--tm);margin-bottom:.65rem">Conexão via <strong>python-oracledb</strong> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</p><div id="am"></div>
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB"></div>
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium</div><input type="text" id="adsn" placeholder="myatp_high"></div>
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN"></div>
<div class="ig"><label>Password</label><input type="password" id="apw"></div>
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div></div>
<button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div>
<div class="cd"><div class="ct">📤 Upload Wallet (.zip)</div>
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="awsel">${S.adbCfg.map(c=>`<option value="${c.id}">${c.config_name}</option>`).join('')}</select></div>
<div class="ig"><label>Wallet ZIP</label><input type="file" id="awf" accept=".zip"></div></div>
<button class="btn bs" onclick="uWallet()">Upload Wallet</button></div>`}
async function sAdb(){try{await $api('/adb/config',{method:'POST',body:{config_name:document.getElementById('an').value,dsn:document.getElementById('adsn').value,
username:document.getElementById('auser').value,password:document.getElementById('apw').value,
wallet_password:document.getElementById('awpw').value||null,table_name:document.getElementById('atbl').value}});
S.adbCfg=await $api('/adb/configs');sm('am','✅ Salvo!','s');R()}catch(e){sm('am',e.message,'e')}}
async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message:'❌ '+d.message)}catch(e){alert(e.message)}}
async function dAdb(id){if(!confirm('Excluir?'))return;await $api('/adb/configs/'+id,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');R()}
async function uWallet(){const fd=new FormData();const f=document.getElementById('awf').files[0];if(!f)return alert('Selecione wallet ZIP');
fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return alert('Selecione conexão');
try{const d=await $api('/adb/'+id+'/upload-wallet',{method:'POST',body:fd,headers:{}});alert('Wallet OK! Arquivos: '+d.files.join(', '))}catch(e){alert(e.message)}}
// ── Users ──
function rUsers(){return`<div class="cd"><div class="ct">👥 Usuários</div>
<table><thead><tr><th>Usuário</th><th>Email</th><th>Role</th><th>MFA</th><th>Status</th><th>Último Login</th><th>Ações</th></tr></thead><tbody>
${S.users.map(u=>`<tr><td><strong>${u.username}</strong></td><td>${u.email||'—'}</td>
<td><span class="badge b-${u.role}">${u.role}</span></td><td>${u.mfa_enabled?'✅':'❌'}</td>
<td><span class="badge ${u.is_active?'b-pass':'b-fail'}">${u.is_active?'Ativo':'Inativo'}</span></td>
<td style="font-size:.7rem">${u.last_login||'Nunca'}</td>
<td><select onchange="uRole('${u.id}',this.value)" style="max-width:85px;padding:.18rem"><option value="admin"${u.role==='admin'?' selected':''}>Admin</option>
<option value="user"${u.role==='user'?' selected':''}>User</option><option value="viewer"${u.role==='viewer'?' selected':''}>Viewer</option></select>
${u.is_active?` <button class="btn bd bsm" onclick="deact('${u.id}')">Desativar</button>`:''}</td></tr>`).join('')}</tbody></table></div>
<div class="cd"><div class="ct"> Criar Usuário</div><div id="um"></div>
<div class="g3"><div class="ig"><label>Usuário</label><input type="text" id="nu"></div>
<div class="ig"><label>Email</label><input type="email" id="ne"></div>
<div class="ig"><label>Senha</label><input type="password" id="np"></div></div>
<div class="ig" style="max-width:150px"><label>Role</label><select id="nr"><option value="viewer">Viewer</option><option value="user">User</option><option value="admin">Admin</option></select></div>
<button class="btn bp" onclick="cUser()">Criar</button></div>`}
async function cUser(){try{await $api('/auth/register',{method:'POST',body:{username:document.getElementById('nu').value,email:document.getElementById('ne').value,
password:document.getElementById('np').value,role:document.getElementById('nr').value}});S.users=await $api('/users');sm('um','✅ Criado!','s');R()}catch(e){sm('um',e.message,'e')}}
async function uRole(id,r){try{await $api('/users/'+id,{method:'PUT',body:{role:r}});S.users=await $api('/users')}catch(e){alert(e.message)}}
async function deact(id){if(!confirm('Desativar?'))return;await $api('/users/'+id,{method:'DELETE'});S.users=await $api('/users');R()}
// ── MFA ──
function rMfa(){return`<div class="cd"><div class="ct">🔐 MFA (TOTP)</div>
<p style="font-size:.8rem;color:var(--t2);margin-bottom:.65rem">Use Google Authenticator ou Authy.</p><div id="mm"></div>
${S.user?.mfa_enabled?'<div class="al al-s">✅ MFA ativado.</div>'
:`<button class="btn bp" onclick="sMfa()">Gerar Secret</button><div id="msa" style="margin-top:.65rem"></div>`}</div>`}
async function sMfa(){try{const d=await $api('/mfa/setup',{method:'POST'});
document.getElementById('msa').innerHTML=`<div class="al al-i"><strong>Secret:</strong> <code>${d.secret}</code></div>
<div style="display:flex;gap:.35rem;max-width:260px;margin-top:.5rem"><input type="text" id="mvc" placeholder="000000" maxlength="6">
<button class="btn bp" onclick="vMfa()">Ativar</button></div>`}catch(e){sm('mm',e.message,'e')}}
async function vMfa(){try{await $api('/mfa/verify',{method:'POST',body:{totp_code:document.getElementById('mvc').value}});
S.user.mfa_enabled=true;sm('mm','✅ MFA ativado!','s');R()}catch(e){sm('mm',e.message,'e')}}
// ── Audit ──
function rAudit(){return`<div class="cd"><div class="ct">📋 Audit Log</div><div id="ac"><div class="ld">Carregando...</div></div></div>`}
async function loadAudit(){try{const logs=await $api('/audit-log?limit=50');
document.getElementById('ac').innerHTML=`<table><thead><tr><th>Data</th><th>Usuário</th><th>Ação</th><th>Recurso</th><th>IP</th></tr></thead><tbody>
${logs.map(l=>`<tr><td style="font-size:.65rem">${l.created_at}</td><td>${l.username||'—'}</td><td><span class="tag">${l.action}</span></td>
<td style="font-size:.65rem">${l.resource?(l.resource.slice(0,12)+'...'):'—'}</td><td>${l.ip||'—'}</td></tr>`).join('')}</tbody></table>`}catch(e){document.getElementById('ac').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
// ── Helpers ──
function sm(id,msg,t){const el=document.getElementById(id);if(el)el.innerHTML=`<div class="al al-${t==='s'?'s':t==='i'?'i':'e'}">${msg}</div>`}
let _lu='',_lp='';
async function hLogin(){const u=document.getElementById('lu').value,p=document.getElementById('lp').value;_lu=u;_lp=p;
try{const d=await doLogin(u,p);if(d&&d.mfa_required){document.getElementById('lf').style.display='none';document.getElementById('mfa-s').style.display='block'}}
catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
async function hMfa(){try{await doLogin(_lu,_lp,document.getElementById('mfa-c').value)}catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
function bind(){}
(async()=>{await checkAuth();R()})();
</script>
</body>
</html>