Files
A-Team-Security-Infra-Agent…/frontend/index.html

361 lines
32 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:#0a0e1a;--bg1:#111827;--bg2:#1a2035;--bg3:#0d1321;--bd:#1e293b;--bf:#f97316;
--t1:#e2e8f0;--t2:#94a3b8;--tm:#64748b;--ac:#f97316;--ah:#ea580c;
--gn:#22c55e;--rd:#ef4444;--bl:#3b82f6;--yl:#eab308;--pp:#8b5cf6;
--r:8px;--rl:12px;--sh:0 4px 24px rgba(0,0,0,.3);--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.25rem;border-bottom:1px solid var(--bd)}
.sb-h h1{font-size:.95rem;font-weight:700;color:var(--ac);font-family:var(--fm);letter-spacing:-.02em}
.sb-h .st{font-size:.65rem;color:var(--tm);margin-top:2px;font-family:var(--fm)}
.nav{padding:.75rem .6rem;flex:1;overflow-y:auto}
.nl{font-size:.6rem;color:var(--tm);text-transform:uppercase;letter-spacing:.1em;padding:.5rem .6rem;font-weight:600}
.ni{display:flex;align-items:center;gap:.65rem;padding:.55rem .6rem;border-radius:var(--r);cursor:pointer;color:var(--t2);font-size:.82rem;font-weight:500;transition:all .15s;margin-bottom:1px}
.ni:hover{background:rgba(249,115,22,.08);color:var(--t1)}.ni.on{background:rgba(249,115,22,.15);color:var(--ac)}
.ni .ic{width:16px;text-align:center;font-size:.95rem}
.sb-f{padding:.75rem 1rem;border-top:1px solid var(--bd)}
.ui{display:flex;align-items:center;gap:.6rem}
.ua{width:30px;height:30px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.75rem;color:#fff}
.un{font-weight:600;color:var(--t1);font-size:.8rem}.ur{color:var(--tm);font-size:.65rem;font-family:var(--fm)}
.mc{flex:1;margin-left:250px;min-height:100vh}
.tb{height:52px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.25rem;position:sticky;top:0;z-index:50}
.tb-t{font-weight:600;font-size:.9rem}.tb-a{display:flex;align-items:center;gap:.6rem}
.pc{padding:1.25rem}
.cd{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl);padding:1.25rem;margin-bottom:.75rem}
.ct{font-weight:600;font-size:.9rem;margin-bottom:.75rem;display:flex;align-items:center;gap:.4rem}
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.45rem .85rem;border-radius:var(--r);font-size:.78rem;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:transparent;color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)}
.bd{background:var(--rd);color:#fff}.bd:hover{background:#dc2626}
.bsm{padding:.3rem .6rem;font-size:.72rem}
.btn:disabled{opacity:.5;cursor:not-allowed}
.ig{margin-bottom:.75rem}.ig label{display:block;font-size:.78rem;font-weight:600;color:var(--t2);margin-bottom:.25rem}
.ig .ht{font-size:.67rem;color:var(--tm);margin-bottom:.2rem}
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.5rem .65rem;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.8rem;font-family:var(--fm);transition:border .15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac)}
input[type=file]{font-family:var(--fm);font-size:.75rem;color:var(--t2)}
.badge{padding:.18rem .55rem;border-radius:12px;font-size:.68rem;font-weight:600;font-family:var(--fm)}
.b-pass{background:rgba(34,197,94,.15);color:var(--gn)}.b-fail{background:rgba(239,68,68,.15);color:var(--rd)}
.b-pend{background:rgba(234,179,8,.15);color:var(--yl)}.b-rev{background:rgba(59,130,246,.15);color:var(--bl)}
.b-admin{background:rgba(139,92,246,.15);color:var(--pp)}.b-user{background:rgba(59,130,246,.15);color:var(--bl)}
.b-viewer{background:rgba(148,163,184,.15);color:var(--tm)}
table{width:100%;border-collapse:collapse;font-size:.78rem}
th{text-align:left;padding:.55rem .65rem;font-size:.67rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:1px solid var(--bd);font-weight:600}
td{padding:.55rem .65rem;border-bottom:1px solid rgba(30,41,59,.4);color:var(--t2)}
tr:hover td{background:rgba(249,115,22,.03)}
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.75rem}
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 52px - 2.5rem)}
.ch-m{flex:1;overflow-y:auto;padding:.75rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0}
.cm{margin-bottom:.75rem;display:flex;gap:.6rem}
.cm-u{justify-content:flex-end}
.cm-u .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cb{max-width:75%;padding:.6rem .85rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.82rem;line-height:1.5;border:1px solid var(--bd);white-space:pre-wrap}
.cb code{font-family:var(--fm);font-size:.75rem;background:rgba(0,0,0,.3);padding:.1rem .25rem;border-radius:3px}
.cb strong{color:var(--ac)}
.ch-i{display:flex;gap:.4rem;padding:.6rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)}
.ch-i input{flex:1;background:var(--bg3)}
.rif{width:100%;height:calc(100vh - 52px - 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:var(--bg0);background-image:radial-gradient(ellipse at 20% 50%,rgba(249,115,22,.08) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(139,92,246,.06) 0%,transparent 50%)}
.lc{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl);padding:2.25rem;width:100%;max-width:380px;box-shadow:var(--sh)}
.lc h2{font-size:1.15rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.78rem;margin-bottom:1.25rem}
.al{padding:.6rem .85rem;border-radius:var(--r);font-size:.78rem;margin-bottom:.75rem}
.al-e{background:rgba(239,68,68,.1);color:var(--rd);border:1px solid rgba(239,68,68,.2)}
.al-s{background:rgba(34,197,94,.1);color:var(--gn);border:1px solid rgba(34,197,94,.2)}
.al-i{background:rgba(59,130,246,.1);color:var(--bl);border:1px solid rgba(59,130,246,.2)}
.tag{display:inline-block;padding:.12rem .4rem;background:rgba(249,115,22,.1);color:var(--ac);border-radius:4px;font-size:.67rem;font-family:var(--fm)}
.emp{text-align:center;padding:2.5rem;color:var(--tm)}.emp .eic{font-size:2.2rem;margin-bottom:.5rem}
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .25s ease}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.82rem;text-align:center;padding:1.5rem}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}::-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 S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],vdbCfg:[],dls:[],users:[],auditLogs:[]};
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]=await Promise.all([$api('/oci/configs'),$api('/reports')]);
if(S.user.role==='admin'){S.users=await $api('/users')}
try{S.vdbCfg=await $api('/vectordb/configs')}catch(e){S.vdbCfg=[]}
}catch(e){console.error(e)}
}
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind()}
function switchTab(t){S.tab=t;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.25rem"><div style="font-size:1.8rem;margin-bottom:.4rem">🛡️</div>
<h2>OCI CIS Agent</h2><div class="st">Infrastructure & Security Engineer</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:1.1rem;letter-spacing:.3em"></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:.75rem;font-size:.67rem;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'],['report','📊','Report'],['downloads','📁','Downloads'],['oci-config','☁️','Config OCI'],['vectordb','🔗','Vector DB']];
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>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security</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('')}
${S.user?.role==='admin'?`<div class="nl" style="margin-top:.75rem">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','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ Configuração OCI','vectordb':'🔗 Vector Database','users':'👥 Gerenciar Usuários','mfa':'🔐 Autenticação MFA','audit':'📋 Audit Log'};
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">session: ${(S.sid||'---').slice(0,8)}</span></div></div>`}
function rPg(){switch(S.tab){case'chat':return rChat();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'vectordb':return rVdb();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></div>'
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
return`<div class="ch-c">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">
<span style="font-size:.78rem;color:var(--tm)">Pronto.</span>
<button class="btn bd bsm" onclick="clrChat()">Limpar conversa</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 d=await $api('/chat',{method:'POST',body:{message:m,session_id:S.sid}});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()}
// ── Report ───────────────────────────────────────────────────────────────
function rReport(){
const comp=S.reports.filter(r=>r.status==='completed');
if(!comp.length)return`<div class="cd"><div class="ct">📊 Report (HTML)</div>
<div class="emp"><div class="eic">📄</div><p>Nenhum relatório disponível.</p>
<p style="font-size:.72rem;margin-top:.4rem">Configure uma conexão OCI e execute um relatório.</p></div></div>${rRunRpt()}`;
const lt=comp[0];
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem">
<span style="font-size:.78rem;color:var(--tm)">Report (HTML) — ${lt.tenancy_name}</span>
<div style="display:flex;gap:.4rem"><select id="rsel" onchange="ldRpt(this.value)" style="max-width:280px">
${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select>
<button class="btn bs bsm" onclick="ldRpt(document.getElementById('rsel').value)">Recarregar</button></div></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:.75rem"><div class="ct">🚀 Executar Novo Relatório</div>
<div class="g2"><div class="ig"><label>Configuração 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>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 uma configuração OCI');
const rg=document.getElementById('rr').value.split(',').map(r=>r.trim()).filter(Boolean);
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,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:.5rem">
<span style="font-size:.78rem;color:var(--tm)">${S.reports.length} relatório(s)</span>
<div style="display:flex;gap:.4rem"><input type="text" id="dlf" placeholder="Filtrar por tenancy name ou nome do arquivo..." style="max-width:320px" oninput="fDl()">
<button class="btn bp bsm" onclick="refreshDl()">🔄 Atualizar lista</button></div></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 id="dlt">${S.reports.map(r=>`<tr data-n="${r.tenancy_name.toLowerCase()}">
<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 encontrado.</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()}
function fDl(){const q=document.getElementById('dlf').value.toLowerCase();document.querySelectorAll('#dlt tr').forEach(tr=>{tr.style.display=tr.dataset.n?.includes(q)?'':'none'})}
// ── OCI Config ───────────────────────────────────────────────────────────
function rOci(){return`
<div class="cd"><div class="ct">☁️ Configurações OCI Existentes</div>
<table><thead><tr><th>Tenancy</th><th>Region</th><th>OCID</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:.67rem">${(c.tenancy_ocid||'').slice(0,28)}...</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:.75rem"><p>Nenhuma configuração.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Configuração OCI</div>
<p style="font-size:.78rem;color:var(--tm);margin-bottom:.75rem">Configure as credenciais OCI</p><div id="om"></div>
<div class="g2">
<div class="ig"><label>Tenancy Name</label><div class="ht">Nome identificador do tenancy (usado para nomear os arquivos)</div><input type="text" id="otn" placeholder="minha-empresa"></div>
<div class="ig"><label>OCID User</label><div class="ht">Ex: ocid1.user.oc1..aaaaaaaaxxxxxxxxxx</div><input type="text" id="ouo" placeholder="ocid1.user.oc1..aaaaaa..."></div>
<div class="ig"><label>Fingerprint</label><div class="ht">Ex: aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99</div><input type="text" id="ofp" placeholder="aa:bb:cc:dd:..."></div>
<div class="ig"><label>OCID Tenancy</label><div class="ht">Ex: ocid1.tenancy.oc1..aaaaaaaaxxxxxxxxxx</div><input type="text" id="oto" placeholder="ocid1.tenancy.oc1..aaaaaa..."></div>
<div class="ig"><label>Region</label><div class="ht">Ex: us-ashburn-1, sa-saopaulo-1, etc.</div><input type="text" id="org" placeholder="sa-saopaulo-1"></div></div>
<div class="g2"><div class="ig"><label>Public Key (.pem)</label><div class="ht">Selecione o arquivo da chave pública</div><input type="file" id="opk" accept=".pem"></div>
<div class="ig"><label>Private Key (.pem)</label><div class="ht">Selecione o arquivo da chave privada</div><input type="file" id="osk" accept=".pem"></div></div>
<button class="btn bp" onclick="sOci()">🚀 Salvar Configuração</button></div>
${S.user?.role==='admin'?`<div class="cd"><div class="ct">🔧 OCI CLI</div>
<button class="btn bs" onclick="iCli()">Instalar OCI CLI no Container</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);
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','✅ Configuração salva!','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('Erro: '+e.message)}}
async function dOci(id){if(!confirm('Excluir?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
async function iCli(){if(!confirm('Instalar OCI CLI? Pode levar alguns minutos.'))return;try{const d=await $api('/oci/install-cli',{method:'POST'});alert(d.status==='success'?'✅ Instalado!':'❌ '+d.message)}catch(e){alert(e.message)}}
// ── Vector DB ────────────────────────────────────────────────────────────
function rVdb(){return`
<div class="cd"><div class="ct">🔗 Conexões Vector Database</div>
<table><thead><tr><th>Tipo</th><th>Host</th><th>Porta</th><th>Database</th><th>Collection</th><th>Ações</th></tr></thead><tbody>
${S.vdbCfg.map(c=>`<tr><td><span class="tag">${c.db_type}</span></td><td>${c.host}</td><td>${c.port}</td>
<td>${c.database_name||'—'}</td><td>${c.collection_name||'—'}</td>
<td><button class="btn bs bsm" onclick="tVdb('${c.id}')">Testar</button>
<button class="btn bd bsm" onclick="dVdb('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.vdbCfg.length?'<div class="emp" style="padding:.75rem"><p>Nenhuma conexão configurada.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Conexão Vector DB</div><div id="vm"></div>
<div class="g2">
<div class="ig"><label>Tipo de Banco</label><select id="vt"><option value="chromadb">ChromaDB</option><option value="pinecone">Pinecone</option><option value="weaviate">Weaviate</option><option value="pgvector">PGVector (PostgreSQL)</option><option value="qdrant">Qdrant</option><option value="milvus">Milvus</option><option value="oracle23ai">Oracle 23ai Vector</option></select></div>
<div class="ig"><label>Host</label><input type="text" id="vh" placeholder="localhost ou URL"></div>
<div class="ig"><label>Porta</label><input type="number" id="vp" placeholder="8000"></div>
<div class="ig"><label>Database Name</label><input type="text" id="vd" placeholder="(opcional)"></div>
<div class="ig"><label>Collection / Index</label><input type="text" id="vc" placeholder="cis_findings"></div>
<div class="ig"><label>Usuário</label><input type="text" id="vu" placeholder="(opcional)"></div>
<div class="ig"><label>Senha</label><input type="password" id="vw" placeholder="(opcional)"></div>
<div class="ig"><label>API Key</label><input type="password" id="va" placeholder="(opcional)"></div></div>
<button class="btn bp" onclick="sVdb()">💾 Salvar Conexão</button></div>`}
async function sVdb(){try{await $api('/vectordb/config',{method:'POST',body:{
db_type:document.getElementById('vt').value,host:document.getElementById('vh').value,
port:parseInt(document.getElementById('vp').value),
database_name:document.getElementById('vd').value||null,collection_name:document.getElementById('vc').value||null,
username:document.getElementById('vu').value||null,password:document.getElementById('vw').value||null,
api_key:document.getElementById('va').value||null}});S.vdbCfg=await $api('/vectordb/configs');sm('vm','✅ Salvo!','s');R()}catch(e){sm('vm',e.message,'e')}}
async function tVdb(id){try{const d=await $api('/vectordb/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message:'❌ '+d.message)}catch(e){alert(e.message)}}
async function dVdb(id){if(!confirm('Excluir?'))return;await $api('/vectordb/configs/'+id,{method:'DELETE'});S.vdbCfg=await $api('/vectordb/configs');R()}
// ── Users ────────────────────────────────────────────────────────────────
function rUsers(){return`
<div class="cd"><div class="ct">👥 Usuários do Sistema</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:.72rem">${u.last_login||'Nunca'}</td>
<td><select onchange="uRole('${u.id}',this.value)" style="max-width:90px;padding:.2rem">
<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:180px"><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 Usuário</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','✅ Usuário 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">🔐 Configurar MFA (TOTP)</div>
<p style="font-size:.82rem;color:var(--t2);margin-bottom:.75rem">A autenticação multi-fator adiciona uma camada extra de segurança. Use Google Authenticator ou Authy.</p>
<div id="mm"></div>
${S.user?.mfa_enabled?'<div class="al al-s">✅ MFA está ativado para sua conta.</div>'
:`<button class="btn bp" onclick="sMfa()">Gerar Secret</button><div id="msa" style="margin-top:.75rem"></div>`}</div>
<div class="cd"><div class="ct"> Sobre Roles</div>
<table><thead><tr><th>Role</th><th>Permissões</th></tr></thead><tbody>
<tr><td><span class="badge b-admin">admin</span></td><td>Acesso total: gerenciar usuários, configs, relatórios, MFA, audit log</td></tr>
<tr><td><span class="badge b-user">user</span></td><td>Criar/gerenciar configs OCI e Vector DB, executar relatórios, chat</td></tr>
<tr><td><span class="badge b-viewer">viewer</span></td><td>Visualizar relatórios e downloads, usar chat (somente leitura)</td></tr></tbody></table></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><br>
<strong>URI:</strong> <code style="font-size:.67rem;word-break:break-all">${d.uri}</code></div>
<p style="font-size:.78rem;margin:.6rem 0">Adicione o secret no app autenticador e insira o código:</p>
<div style="display:flex;gap:.4rem;max-width:280px"><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">📋 Log de Auditoria</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:.67rem">${l.created_at}</td><td>${l.username||'—'}</td><td><span class="tag">${l.action}</span></td>
<td style="font-size:.67rem">${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">Erro: '+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(){/* re-bind any handlers after render if needed */}
// ── Init ─────────────────────────────────────────────────────────────────
(async()=>{await checkAuth();R()})();
</script>
</body>
</html>