feat: v2 - OCI light theme, GenAI integration, MCP servers, ADB vector

This commit is contained in:
nogueiraguh
2026-02-27 18:54:38 -03:00
parent faf54ec611
commit c759d0f06d
11 changed files with 765 additions and 591 deletions

View File

@@ -6,354 +6,378 @@
<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}
: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}
/* ── Sidebar ── */
.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)}
.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)}
/* ── Main ── */
.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)}
.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)}
/* ── Buttons ── */
.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: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}
.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:.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}
/* ── Form ── */
.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)}
/* ── Badges ── */
.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 ── */
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}
/* ── Chat ── */
.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-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{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:.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}
.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}
.ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem}
.ch-t label{font-weight:600;color:var(--tm);font-size:.68rem}
/* ── Report iframe ── */
.rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff}
/* ── Login ── */
.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}
/* ── Alerts ── */
.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}
@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 S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],vdbCfg:[],dls:[],users:[],auditLogs:[]};
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],selModel:'',selGenai:''};
const API='/api';
async function $api(p,o={}){
const h={...(o.headers||{})};
if(S.token)h['Authorization']='Bearer '+S.token;
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;
}
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')]);
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'){S.users=await $api('/users')}
try{S.vdbCfg=await $api('/vectordb/configs')}catch(e){S.vdbCfg=[]}
}catch(e){console.error(e)}
}
}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>
function rLogin(){return`<div class="lp"><div class="lc fi">
<div style="text-align:center;margin-bottom:1.15rem"><div style="font-size:1.6rem;margin-bottom:.3rem">🛡️</div>
<h2>OCI CIS Agent</h2><div class="st">Oracle Cloud Infrastructure · Security</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>
<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:.75rem;font-size:.67rem;color:var(--tm)">Padrão: admin / admin123</div></div></div></div>`}
<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'],['report','📊','Report'],['downloads','📁','Downloads'],['oci-config','☁️','Config OCI'],['vectordb','🔗','Vector DB']];
const tabs=[['chat','💬','Chat Agent'],['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>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security</div></div>
return`<div class="sb"><div class="sb-h"><h1>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security Engineer</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>
<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','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 rTb(){const t={'chat':'💬 AI Agent Chat','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">v2.0</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''}}
function rPg(){switch(S.tab){case'chat':return rChat();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 ─────────────────────────────────────────────────────────────────
// ── Chat ──
function rChat(){
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p></div>'
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':''}>${mi?mi.name:g.model_id} (${g.genai_region})</option>`}).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-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 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()}
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()}
// ── 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()}`;
// ── 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:.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>
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>
<div style="display:flex;gap:.35rem"><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></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>
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}</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');
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);
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,regions:rg.length?rg:null}});
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:.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>
// ── 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>
<div style="display:flex;gap:.35rem"><input type="text" id="dlf" placeholder="Filtrar..." style="max-width:240px" oninput="fDl()">
<button class="btn bp bsm" onclick="refreshDl()">🔄</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>
<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>`}
<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()}
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>
// ── 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:.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>
<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><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);
<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','✅ 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)}}
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()}
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)}}
// ── 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}</option>`).join('');
return`<div class="cd"><div class="ct">🧠 Configurações GenAI</div>
<table><thead><tr><th>Modelo</th><th>Provider</th><th>Região</th><th>Serving</th><th>Default</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${mi.name||g.model_id}</strong><br><span style="font-size:.63rem;color:var(--tm)">${g.model_id}</span></td>
<td><span class="tag">${mi.provider||'?'}</span></td><td>${g.genai_region}</td><td>${g.serving_type}</td>
<td>${g.is_default?'⭐':'—'}</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. Adicione abaixo.</p></div>':''}</div>
<div class="cd"><div class="ct"> Novo Modelo GenAI</div><div id="gm"></div>
<div class="g3"><div class="ig"><label>Config OCI (credenciais)</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</label><select id="gmod">${mOpts}</select></div>
<div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div></div>
<div class="g3"><div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
<div class="ig"><label>Serving Type</label><select id="gsrv"><option value="ON_DEMAND">On-Demand</option><option value="DEDICATED">Dedicated</option></select></div>
<div class="ig"><label>Endpoint OCID (dedicado)</label><input type="text" id="gep" placeholder="(opcional)"></div></div>
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="0.7" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="2048" min="1" max="128000"></div>
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.9" step="0.05" min="0" max="1"></div></div>
<button class="btn bp" onclick="sGenai()">Salvar Modelo</button></div>`}
async function sGenai(){try{await $api('/genai/config',{method:'POST',body:{
oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
serving_type:document.getElementById('gsrv').value,endpoint_id:document.getElementById('gep').value||null,
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
top_p:parseFloat(document.getElementById('gtp').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()}
// ── 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>
// ── MCP Servers ──
function rMCP(){return`<div class="cd"><div class="ct">🔌 MCP Servers Registrados</div>
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Comando/URL</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${S.mcpSvr.map(m=>`<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><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?'Desativar':'Ativar'}</button>
<button class="btn bd bsm" onclick="dMcp('${m.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.mcpSvr.length?'<div class="emp" style="padding:.65rem"><p>Nenhum MCP server. Registre abaixo para usar em relatórios.</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="ig"><label>Args (JSON array)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div>
<button class="btn bp" onclick="sMcp()">Registrar Server</button></div>
<div class="cd"><div class="ct">📤 Upload Script MCP</div>
<div class="g2"><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>
<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}});
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)}}
<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>`}
// ── 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:.76rem;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 (do tnsnames.ora)</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" placeholder="••••••••"></div>
<div class="ig"><label>Wallet Password</label><div class="ht">Senha do ewallet.pem (definida ao baixar wallet)</div><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)}}
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>
// ── 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:.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>
<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: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')}}
<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">🔐 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>`}
// ── 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><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">
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">📋 Log de Auditoria</div><div id="ac"><div class="ld">Carregando...</div></div></div>`}
// ── 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:.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>'}}
${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 ──────────────────────────────────────────────────────────────
// ── 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'}}
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 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>