Files
A-Team-Security-Infra-Agent…/frontend/index.html
nogueiraguh 4de5852430 fix: filter non-chat models (Codex, Image, Audio, GPT-oss) from Chat dropdown
These models use the completions endpoint, not chat/completions,
and return 400 errors when used via the chat API.
2026-03-03 23:43:29 -03:00

769 lines
76 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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg:#f4f2ef;--bg1:#ffffff;--bg2:#faf9f7;--bg3:#efeee9;--bg4:#e5e3dd;
--bd:#ddd9d2;--bdf:#c5c0b8;
--t1:#1c1917;--t2:#44403c;--t3:#78716c;--t4:#a8a29e;
--ac:#c74634;--ach:#b13d2e;--acd:#9a3427;--acl:rgba(199,70,52,.06);--acl2:rgba(199,70,52,.1);
--gn:#16a34a;--gnl:rgba(22,163,74,.08);--rd:#dc2626;--rdl:rgba(220,38,38,.06);
--bl:#2563eb;--bll:rgba(37,99,235,.06);--yl:#ca8a04;--yll:rgba(202,138,4,.06);
--pp:#7c3aed;--ppl:rgba(124,58,237,.06);
--r:12px;--rl:16px;--rr:20px;
--sh1:0 1px 2px rgba(28,25,23,.04),0 1px 3px rgba(28,25,23,.06);
--sh2:0 4px 6px -1px rgba(28,25,23,.05),0 2px 4px -2px rgba(28,25,23,.04);
--sh3:0 10px 15px -3px rgba(28,25,23,.06),0 4px 6px -4px rgba(28,25,23,.04);
--shh:0 20px 25px -5px rgba(28,25,23,.08),0 8px 10px -6px rgba(28,25,23,.04);
--fm:'JetBrains Mono',monospace;
--fs:'Plus Jakarta Sans',-apple-system,system-ui,sans-serif;
--trans:cubic-bezier(.22,1,.36,1)
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--fs);background:var(--bg);color:var(--t1);min-height:100vh;-webkit-font-smoothing:antialiased}
::selection{background:rgba(199,70,52,.15);color:var(--t1)}
/* ── Sidebar ── */
.app{display:flex;min-height:100vh}
.sb{width:260px;background:var(--bg1);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100;
border-right:1px solid var(--bd);transition:transform .4s var(--trans)}
.sb-h{padding:1.25rem 1.15rem;background:linear-gradient(145deg,#c74634 0%,#a83428 50%,#8b2a20 100%);position:relative;overflow:hidden}
.sb-h::after{content:'';position:absolute;top:-30%;right:-20%;width:120px;height:120px;background:radial-gradient(circle,rgba(255,255,255,.08) 0%,transparent 70%);border-radius:50%}
.sb-h h1{font-size:.82rem;font-weight:700;color:#fff;font-family:var(--fs);letter-spacing:-.02em;display:flex;align-items:center;gap:.55rem;position:relative;z-index:1}
.sb-h .st{font-size:.62rem;color:rgba(255,255,255,.6);margin-top:3px;font-family:var(--fm);font-weight:400;letter-spacing:.02em;position:relative;z-index:1}
.nav{padding:.75rem .6rem;flex:1;overflow-y:auto}
.nl{font-size:.6rem;color:var(--t4);text-transform:uppercase;letter-spacing:.12em;padding:.85rem .65rem .35rem;font-weight:700}
.ni{display:flex;align-items:center;gap:.65rem;padding:.55rem .7rem;border-radius:var(--r);cursor:pointer;color:var(--t3);
font-size:.82rem;font-weight:500;transition:all .25s var(--trans);margin-bottom:2px;position:relative}
.ni:hover{background:var(--bg2);color:var(--t2);transform:translateX(2px)}
.ni.on{background:var(--acl2);color:var(--ac);font-weight:600}
.ni.on::before{content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:60%;background:var(--ac);border-radius:0 3px 3px 0}
.ni .ic{width:18px;text-align:center;font-size:.95rem;transition:transform .2s var(--trans)}
.ni:hover .ic{transform:scale(1.12)}
.sb-f{padding:.85rem 1rem;border-top:1px solid var(--bd);background:var(--bg2)}
.ui{display:flex;align-items:center;gap:.6rem}
.ua{width:32px;height:32px;background:linear-gradient(135deg,var(--ac),var(--acd));border-radius:10px;display:flex;align-items:center;justify-content:center;
font-weight:700;font-size:.72rem;color:#fff;box-shadow:0 2px 8px rgba(199,70,52,.2)}
.un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--t4);font-size:.62rem;font-family:var(--fm)}
.lo-btn{margin-left:auto;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;
color:var(--t4);border:1px solid var(--bd);background:var(--bg1);font-size:.82rem;transition:all .2s}
.lo-btn:hover{border-color:var(--rd);color:var(--rd);background:var(--rdl)}
/* ── Main Content ── */
.mc{flex:1;margin-left:260px;min-height:100vh}
.tb{height:56px;background:rgba(255,255,255,.8);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);
border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.5rem;position:sticky;top:0;z-index:50}
.tb-t{font-weight:700;font-size:.92rem;color:var(--t1);letter-spacing:-.01em}
.tb-a{display:flex;align-items:center;gap:.6rem}
.pc{padding:1.5rem;max-width:1400px}
/* ── Cards ── */
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.35rem;margin-bottom:.85rem;
box-shadow:var(--sh1);transition:box-shadow .3s var(--trans)}
.cd:hover{box-shadow:var(--sh2)}
.ct{font-weight:700;font-size:.92rem;margin-bottom:.85rem;display:flex;align-items:center;gap:.45rem;color:var(--t1);letter-spacing:-.01em}
.cdesc{font-size:.78rem;color:var(--t3);margin-top:-.45rem;margin-bottom:.85rem;line-height:1.55}
/* ── Buttons ── */
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.5rem .95rem;border-radius:10px;font-size:.78rem;font-weight:600;
cursor:pointer;border:1px solid transparent;transition:all .2s var(--trans);font-family:var(--fs);letter-spacing:-.01em}
.bp{background:linear-gradient(145deg,var(--ac),var(--ach));color:#fff;border-color:transparent;box-shadow:0 2px 8px rgba(199,70,52,.2)}
.bp:hover{background:linear-gradient(145deg,var(--ach),var(--acd));box-shadow:0 4px 12px rgba(199,70,52,.3);transform:translateY(-1px)}
.bp:active{transform:translateY(0);box-shadow:0 1px 4px rgba(199,70,52,.2)}
.bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--bdf);color:var(--t1);background:var(--bg2)}
.bd{background:var(--rdl);color:var(--rd);border-color:rgba(220,38,38,.12)}.bd:hover{background:var(--rd);color:#fff;box-shadow:0 2px 8px rgba(220,38,38,.2)}
.bsm{padding:.32rem .6rem;font-size:.72rem;border-radius:8px}
.btn:disabled{opacity:.45;cursor:not-allowed;transform:none!important;box-shadow:none!important}
/* ── Form ── */
.ig{margin-bottom:.75rem}
.ig label{display:block;font-size:.74rem;font-weight:600;color:var(--t2);margin-bottom:.3rem;letter-spacing:-.01em}
.ig .ht{font-size:.66rem;color:var(--t4);margin-bottom:.2rem;line-height:1.45}
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{
width:100%;padding:.55rem .75rem;background:var(--bg2);border:1.5px solid var(--bd);border-radius:10px;color:var(--t1);
font-size:.8rem;font-family:var(--fm);transition:all .2s var(--trans);font-weight:500}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);background:var(--bg1);box-shadow:0 0 0 3px rgba(199,70,52,.08)}
input::placeholder{color:var(--t4);font-weight:400}
input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t3);padding:.4rem}
select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%2378716c'%3E%3Cpath d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat:no-repeat;background-position:right .7rem center}
/* ── Badges ── */
.badge{padding:.2rem .6rem;border-radius:20px;font-size:.66rem;font-weight:600;font-family:var(--fm);letter-spacing:.01em}
.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(--t3)}
.b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--t4)}
/* ── Tables ── */
table{width:100%;border-collapse:separate;border-spacing:0;font-size:.78rem}
th{text-align:left;padding:.65rem .75rem;font-size:.66rem;text-transform:uppercase;letter-spacing:.08em;color:var(--t4);
border-bottom:1.5px solid var(--bd);font-weight:700;background:transparent}
td{padding:.7rem .75rem;border-bottom:1px solid var(--bg3);color:var(--t2);transition:background .15s}
tr{transition:background .2s}tr:hover td{background:var(--acl)}
tbody tr:last-child td{border-bottom:none}
/* ── Grid ── */
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.75rem}
/* ── Chat ── */
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 56px - 3rem);overflow:hidden}
.ch-t{display:flex;gap:.5rem;align-items:center;padding:.6rem .85rem;background:var(--bg1);border:1px solid var(--bd);
border-radius:var(--rl) var(--rl) 0 0;font-size:.74rem;flex-wrap:wrap;box-shadow:var(--sh1)}
.ch-t select{max-width:260px;font-size:.74rem;padding:.35rem .5rem}.ch-t select.cmod-sel{max-width:320px}
.ch-params{padding:.55rem .85rem;background:var(--bg1);border-left:1px solid var(--bd);border-right:1px solid var(--bd);border-bottom:1px solid var(--bg3);font-size:.72rem}
.ch-params .g3{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.3rem}
.ch-params label{font-weight:600;color:var(--t3);font-size:.66rem;margin-bottom:2px;display:block}
.ch-params input{width:100%;padding:.25rem .4rem;font-size:.72rem;border:1px solid var(--bd);border-radius:6px;background:var(--bg2)}
.gear-btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;border:1px solid var(--bd);background:var(--bg1);font-size:.78rem;transition:all .2s;flex-shrink:0}
.gear-btn:hover{border-color:var(--ac);color:var(--ac)}
.ch-t label{font-weight:600;color:var(--t3);font-size:.72rem}
.ch-m{flex:1;overflow-y:auto;padding:1rem;background:var(--bg2);border-left:1px solid var(--bd);border-right:1px solid var(--bd)}
.cm{margin-bottom:.85rem;display:flex;gap:.6rem;animation:msgIn .3s var(--trans)}.cm-user{justify-content:flex-end}
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.cm-user .cb{background:linear-gradient(145deg,var(--ac),var(--ach));color:#fff;border-radius:var(--rl) var(--rl) 6px var(--rl);border-color:transparent;box-shadow:0 2px 8px rgba(199,70,52,.15)}
.cm-user .cb code{background:rgba(255,255,255,.2);color:#fff}
.cm-user .cb strong{color:#ffe0db}
.cb{max-width:72%;padding:.7rem .95rem;border-radius:var(--rl) var(--rl) var(--rl) 6px;background:var(--bg1);font-size:.82rem;
line-height:1.6;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh1)}
.cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.12rem .3rem;border-radius:5px}
.cb strong{color:var(--ac)}
.ch-i{display:flex;gap:.45rem;padding:.7rem .85rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;
border-radius:0 0 var(--rl) var(--rl);box-shadow:0 -2px 8px rgba(0,0,0,.02)}
.ch-i input{flex:1;background:var(--bg2);border-radius:10px}
.ch-i .btn{border-radius:10px}
/* ── Report ── */
.rif{width:100%;height:calc(100vh - 56px - 6rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff;box-shadow:var(--sh2)}
/* ── Login ── */
.lp{min-height:100vh;display:flex;align-items:center;justify-content:center;
background:var(--bg);position:relative;overflow:hidden}
.lp::before{content:'';position:absolute;top:-30%;right:-15%;width:600px;height:600px;
background:radial-gradient(circle,rgba(199,70,52,.06) 0%,transparent 70%);border-radius:50%;pointer-events:none}
.lp::after{content:'';position:absolute;bottom:-20%;left:-10%;width:500px;height:500px;
background:radial-gradient(circle,rgba(199,70,52,.04) 0%,transparent 70%);border-radius:50%;pointer-events:none}
.lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rr);padding:2.5rem;width:100%;max-width:380px;
box-shadow:var(--sh3);position:relative;z-index:1;animation:lcIn .5s var(--trans)}
@keyframes lcIn{from{opacity:0;transform:translateY(16px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
.lc h2{font-size:1.2rem;font-family:var(--fs);color:var(--t1);font-weight:800;letter-spacing:-.02em}
.lc .st{color:var(--t4);font-size:.76rem;margin-top:.15rem;margin-bottom:1.35rem;font-weight:400}
/* ── Alerts ── */
.al{padding:.65rem .85rem;border-radius:10px;font-size:.78rem;margin-bottom:.75rem;font-weight:500}
.al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(220,38,38,.1)}
.al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(22,163,74,.1)}
.al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(37,99,235,.1)}
/* ── Tags ── */
.tag{display:inline-block;padding:.15rem .45rem;background:var(--acl2);color:var(--ac);border-radius:6px;font-size:.66rem;font-family:var(--fm);font-weight:600}
/* ── Empty States ── */
.emp{text-align:center;padding:2.5rem 1.5rem;color:var(--t4)}
.emp .eic{font-size:2.2rem;margin-bottom:.55rem;filter:grayscale(.3)}
.emp p{line-height:1.55}
/* ── Explorer ── */
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:.65rem;margin-top:.65rem}
.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.85rem;font-size:.78rem;transition:all .2s var(--trans)}
.exp-c:hover{border-color:var(--bdf);box-shadow:var(--sh1);transform:translateY(-1px)}
.exp-c strong{color:var(--t1);font-size:.84rem;font-weight:600}
.exp-c .em{font-family:var(--fm);font-size:.65rem;color:var(--t4);word-break:break-all}
/* ── Animations ── */
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.fi{animation:fi .35s var(--trans)}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.4}}
.ld{animation:pu 1.2s ease infinite;color:var(--t4);font-size:.82rem;text-align:center;padding:1.5rem}
@keyframes spin{to{transform:rotate(360deg)}}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--bd);border-top-color:var(--ac);border-radius:50%;animation:spin .6s linear infinite}
/* ── Scrollbar ── */
::-webkit-scrollbar{width:6px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--bdf)}
/* ── Responsive ── */
@media(max-width:768px){.sb{transform:translateX(-100%)}.sb.open{transform:translateX(0)}.mc{margin-left:0}.g2,.g3{grid-template-columns:1fr}.pc{padding:1rem}}
</style>
</head>
<body>
<div id="app"></div>
<script>
const V='1.1';
const LOGO_W=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="26" height="26" style="flex-shrink:0"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(255,255,255,0.12)" 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>`;
const LOGO_R=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="52" height="52"><rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="1.8"/><rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.4"/><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.25" stroke="#C74634" stroke-width="0.7"/><line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.4"/><circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.35"/><circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.35"/></svg>`;
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],genaiCfg:[],adbCfg:[],mcpSvr:[],users:[],auditLogs:[],models:{},regions:[],embModels:{},selGenai:'',expData:null,editing:null,
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatParamsOpen:false};
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;S.embModels=m.embedding_models||{}}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();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
const tm={'oci-config':'oci','genai':'genai','adb':'adb','mcp':'mcp'};if(tm[t])setTimeout(()=>refreshCLogs(tm[t]),100)}
/* ── Login ── */
function rLogin(){return`<div class="lp"><div class="lc fi">
<div style="text-align:center;margin-bottom:1.5rem">${LOGO_R}
<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">Código MFA obrigatório</div>
<div class="ig"><label>Código TOTP</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1.1rem;letter-spacing:.3em;font-weight:600"></div>
<button class="btn bp" style="width:100%;justify-content:center;padding:.65rem" 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%;justify-content:center;padding:.65rem" onclick="hLogin()">Entrar</button>
<div style="text-align:center;margin-top:.85rem;font-size:.66rem;color:var(--t4)">Padrão: admin / admin123</div></div></div></div>`}
/* ── App Shell ── */
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','📊','Reports'],['downloads','📁','Downloads']];
const ctabs=[['oci-config','☁️','Credenciais OCI'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector'],['embeddings','🧬','Embeddings']];
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
const i=(S.user?.first_name||S.user?.username||'?')[0].toUpperCase();
return`<div class="sb">
<div class="sb-h"><h1>${LOGO_W} 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">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">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?.first_name?S.user.first_name+' '+S.user.last_name:S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
function rTb(){const t={'chat':'💬 AI Agent Chat','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Reports','downloads':'📁 Downloads','oci-config':'☁️ Credenciais OCI','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','embeddings':'🧬 Embeddings','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">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'embeddings':return rEmbeddings();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>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo 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('');
// Build model select with optgroups
const presetOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="cfg:${g.id}" ${S.chatModel==='cfg:'+g.id?'selected':''}>${g.name||mi?.name||g.model_id} (${g.genai_region})</option>`}).join('');
const skip=new Set(['openai.gpt-image-1','openai.gpt-image-1.5','openai.gpt-audio','meta.llama-guard-4-12b','protectai.deberta-v3-base-prompt-injection-v2',
'openai.gpt-5.3-codex','openai.gpt-5.2-codex','openai.gpt-5.1-codex','openai.gpt-5.1-codex-max','openai.gpt-5.1-codex-mini','openai.gpt-5-codex',
'openai.gpt-oss-120b','openai.gpt-oss-20b']);
const provs={};for(const[mid,info]of Object.entries(S.models)){if(skip.has(mid))continue;const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
const provOrder=['openai','google','meta','cohere','xai'];
const catalogOpts=provOrder.filter(p=>provs[p]).map(p=>{const label=p.charAt(0).toUpperCase()+p.slice(1);const opts=provs[p].map(m=>`<option value="${m.id}" ${S.chatModel===m.id?'selected':''}>${m.name}</option>`).join('');return`<optgroup label="${label}">${opts}</optgroup>`}).join('');
const isDirect=S.chatModel&&!S.chatModel.startsWith('cfg:');
const hasModel=!!S.chatModel;
// OCI selector for direct mode
const ociSel=isDirect?`<select id="coci" onchange="chatOciChanged()" style="max-width:200px"><option value="">Credencial OCI...</option>${S.ociCfg.map(c=>`<option value="${c.id}" ${S.chatOci===c.id?'selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
const gearBtn=hasModel?`<div class="gear-btn" onclick="toggleChatParams()" title="Parâmetros">${S.chatParamsOpen?'✕':'⚙️'}</div>`:'';
const ragBadge=S.adbCfg.some(c=>c.genai_config_id&&c.is_active)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem">RAG</span>':'';
// Params panel - available for any model (direct or preset)
const pp=S.chatParams;
const paramsPanel=hasModel&&S.chatParamsOpen?`<div class="ch-params"><div class="g3">
<div><label>Temperature</label><input type="number" value="${pp.temperature}" step="0.1" min="0" max="2" onchange="S.chatParams.temperature=+this.value"></div>
<div><label>Max Tokens</label><input type="number" value="${pp.max_tokens}" min="1" max="128000" onchange="S.chatParams.max_tokens=+this.value"></div>
<div><label>Top P</label><input type="number" value="${pp.top_p}" step="0.05" min="0" max="1" onchange="S.chatParams.top_p=+this.value"></div>
</div><div class="g3">
<div><label>Top K</label><input type="number" value="${pp.top_k}" min="-1" max="500" onchange="S.chatParams.top_k=+this.value"></div>
<div><label>Freq Penalty</label><input type="number" value="${pp.frequency_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.frequency_penalty=+this.value"></div>
<div><label>Pres Penalty</label><input type="number" value="${pp.presence_penalty}" step="0.1" min="0" max="2" onchange="S.chatParams.presence_penalty=+this.value"></div>
</div></div>`:'';
return`<div class="ch-c">
<div class="ch-t"><label>🧠 Modelo:</label><select id="cmod" class="cmod-sel" onchange="chatModelChanged()"><option value="">Agente Local (sem IA)</option>${presetOpts?'<optgroup label="Configs Salvas">'+presetOpts+'</optgroup>':''}${catalogOpts}</select>
${ociSel}${gearBtn}${ragBadge}<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
${paramsPanel}<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 chatModelChanged(){S.chatModel=document.getElementById('cmod').value;
if(S.chatModel&&!S.chatModel.startsWith('cfg:')&&!S.chatOci&&S.ociCfg.length){S.chatOci=S.ociCfg[0].id;const c=S.ociCfg[0];S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}
R()}
function chatOciChanged(){S.chatOci=document.getElementById('coci').value;const c=S.ociCfg.find(x=>x.id===S.chatOci);if(c){S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}}
function toggleChatParams(){S.chatParamsOpen=!S.chatParamsOpen;R()}
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.chatModel.startsWith('cfg:')){
body.genai_config_id=S.chatModel.slice(4);
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
else if(S.chatModel){
if(!S.chatOci){S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI para usar o modelo direto.'});R();return}
body.model_id=S.chatModel;body.oci_config_id=S.chatOci;body.genai_region=S.chatRegion;body.compartment_id=S.chatCompartment;
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
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:.74rem;margin-top:.35rem">Configure em <strong>Credenciais OCI</strong>.</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.</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"><span class="spinner"></span> 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 encontrado.</p></div>';
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} item(ns) encontrado(s)</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(--t4);font-size:.66rem">${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">📊 Compliance Reports</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório gerado ainda.</p></div></div>${rRunRpt()}`;
const lt=comp[0];
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem"><span style="font-size:.78rem;color:var(--t3)">Report HTML — ${lt.tenancy_name}</span>
<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></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:.85rem"><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:.55rem">
<span style="font-size:.78rem;color:var(--t3)">${S.reports.length} relatório(s)</span>
<button class="btn bs bsm" onclick="refreshDl()">🔄 Atualizar</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 style="font-size:.72rem">${r.created_at||'—'}</td><td style="font-size:.72rem">${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>`:'<span style="color:var(--t4)">—</span>'}</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()}
/* ── Edit helpers ── */
function editCfg(type,id){S.editing={type,id};R();
const form=document.querySelector('.cd:has(#obtn), .cd:has(#gbtn), .cd:has(#abtn), .cd:has(#mcbtn)');if(form)form.scrollIntoView({behavior:'smooth',block:'start'})}
function cancelEdit(){S.editing=null;R()}
/* ── OCI Config ── */
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
return`<div class="cd"><div class="ct">☁️ Credenciais Registradas</div><div id="ocm"></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${eo&&eo.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.tenancy_name}</strong></td><td><span class="tag">${c.region}</span></td>
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4)">${c.compartment_id?(c.compartment_id.slice(0,22)+'…'):'—'}</td><td style="font-size:.72rem">${c.created_at}</td>
<td><button class="btn bs bsm" onclick="editCfg('oci','${c.id}')">Editar</button> <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:.85rem"><p>Nenhuma credencial registrada.</p></div>':''}</div>
<div class="cd"><div class="ct">${eo?'✏️ Editar Credencial':' Nova Credencial'}</div>
<div class="cdesc">${eo?'Editando: <strong>'+eo.tenancy_name+'</strong>':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}</div><div id="om"></div>
<div class="g2">
<div class="ig"><label>Tenancy Name</label><input type="text" id="otn" placeholder="minha-empresa" value="${eo?eo.tenancy_name:''}"></div>
<div class="ig"><label>OCID User</label><input type="text" id="ouo" placeholder="ocid1.user.oc1.." value="${eo?eo.user_ocid||'':''}"></div>
<div class="ig"><label>Fingerprint</label><input type="text" id="ofp" placeholder="aa:bb:cc:dd:..." value="${eo?eo.fingerprint||'':''}"></div>
<div class="ig"><label>OCID Tenancy</label><input type="text" id="oto" placeholder="ocid1.tenancy.oc1.." value="${eo?eo.tenancy_ocid:''}"></div>
<div class="ig"><label>Region</label><input type="text" id="org" placeholder="sa-saopaulo-1" value="${eo?eo.region:''}"></div>
<div class="ig"><label>Compartment OCID</label><input type="text" id="ocp" placeholder="ocid1.compartment.oc1.." value="${eo?eo.compartment_id||'':''}"></div></div>
<div class="g2"><div class="ig"><label>Private Key (.pem)</label>${eo?'<div class="ht">Deixe vazio para manter a chave atual</div>':''}<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>
<div class="g2"><div class="ig"><label>Key Passphrase</label><div class="ht">Apenas se a chave privada for protegida por senha</div><input type="password" id="okp" placeholder="(opcional)"></div>
<div class="ig"></div></div>
<div style="display:flex;gap:8px"><button class="btn bp" id="obtn" onclick="sOci()">${eo?'Salvar Alterações':'Salvar Credencial'}</button>${eo?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
`+rConfigLogs('oci')}
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);
fd.append('key_passphrase',document.getElementById('okp').value||'');
const sk=document.getElementById('osk').files[0];
const isEdit=S.editing?.type==='oci';
if(!isEdit&&!sk)return sm('om','Selecione a chave privada','e');
if(sk)fd.append('private_key',sk);
const pk=document.getElementById('opk').files[0];if(pk)fd.append('public_key',pk);
const btn=document.getElementById('obtn');btn.disabled=true;btn.textContent='Salvando...';sm('om','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando credencial...','i');
try{const url=isEdit?'/oci/configs/'+S.editing.id:'/oci/config';const method=isEdit?'PUT':'POST';
await $api(url,{method,body:fd,headers:{}});S.editing=null;S.ociCfg=await $api('/oci/configs');sm('om','✅ Credencial '+(isEdit?'atualizada':'salva')+' com sucesso!','s');R()}catch(e){sm('om',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Credencial'}}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});sm('ocm',d.status==='success'?'✅ Conexão OK!':'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('oci')}catch(e){sm('ocm',e.message,'e')}}
async function dOci(id){if(!confirm('Excluir credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
/* ── GenAI Config ── */
function rGenAI(){const eg=S.editing?.type==='genai'?S.genaiCfg.find(g=>g.id===S.editing.id):null;
const mOpts='<option value="_custom"'+(eg&&eg.model_ocid?' selected':'')+'>🔗 Personalizado (usar OCID)</option>'+Object.entries(S.models).map(([k,v])=>`<option value="${k}"${eg&&eg.model_id===k&&!eg.model_ocid?' selected':''}>${v.name} (${v.provider})</option>`).join('');
const rOpts=S.regions.map(r=>`<option value="${r}" ${eg?(r===eg.genai_region?'selected':''):(r==='us-ashburn-1'?'selected':'')}>${r}</option>`).join('');
return`<div class="cd"><div class="ct">🧠 Modelos Configurados</div>
<table><thead><tr><th>Config</th><th>Modelo</th><th>Região</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr${eg&&eg.id===g.id?' style="background:var(--bg3)"':''}><td><strong>${g.name||'—'}</strong>${g.is_default?' ':''}</td>
<td>${mi.name||(g.model_ocid?'OCID Personalizado':g.model_id)}<br><span class="tag">${mi.provider||(g.model_ocid?'custom':'?')}</span></td>
<td style="font-family:var(--fm);font-size:.7rem">${g.genai_region}</td>
<td><button class="btn bs bsm" onclick="editCfg('genai','${g.id}')">Editar</button> <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:.85rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
<div class="cd"><div class="ct">${eg?'✏️ Editar Modelo GenAI':' Novo Modelo GenAI'}</div>
<div class="cdesc">${eg?'Editando: <strong>'+eg.name+'</strong>':'Adicione modelos personalizados que não estão no catálogo do Chat Agent, ou crie presets com credenciais específicas.'}</div><div id="gm"></div>
<div class="g3"><div class="ig"><label>Nome da Config</label><input type="text" id="gname" placeholder="Meu Modelo Custom" value="${eg?eg.name:''}"></div>
<div class="ig"><label>Credencial OCI</label><select id="goci" onchange="fillGenaiFromOci()"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}"${eg&&eg.oci_config_id===c.id?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
<div class="ig"><label>Modelo</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.." value="${eg?eg.compartment_id:''}"></div>
<div class="ig"><label>Model OCID</label><div class="ht">Cole o OCID do modelo do portal OCI. Se preenchido, será usado no lugar do modelo selecionado acima.</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1.iad.amaaaaaa..." value="${eg&&eg.model_ocid?eg.model_ocid:''}" style="${eg&&eg.model_ocid?'border-color:var(--p)':''}"></div></div>
<div style="display:flex;gap:8px"><button class="btn bp" id="gbtn" onclick="sGenai()">${eg?'Salvar Alterações':'Salvar Modelo'}</button>${eg?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
`+rConfigLogs('genai')}
async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=true;btn.textContent='Salvando...';sm('gm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando modelo...','i');
const isEdit=S.editing?.type==='genai';
const selModel=document.getElementById('gmod').value;const ocid=document.getElementById('gocid').value||null;
if(selModel==='_custom'&&!ocid){sm('gm','Informe o Model OCID para usar modelo personalizado.','e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Modelo';return}
const body={
name:document.getElementById('gname').value||'default',
oci_config_id:document.getElementById('goci').value,model_id:selModel==='_custom'?'custom':selModel,
model_ocid:ocid,
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
endpoint:null,serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
is_default:isEdit?(S.genaiCfg.find(g=>g.id===S.editing.id)?.is_default||false):S.genaiCfg.length===0};
try{const url=isEdit?'/genai/configs/'+S.editing.id:'/genai/config';const method=isEdit?'PUT':'POST';
await $api(url,{method,body});S.editing=null;S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo '+(isEdit?'atualizado':'salvo')+' com sucesso!','s');R()}catch(e){sm('gm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Modelo'}}
function fillGenaiFromOci(){const sel=document.getElementById('goci');const cfg=S.ociCfg.find(c=>c.id===sel.value);if(!cfg)return;
const greg=document.getElementById('greg');if(greg){for(let i=0;i<greg.options.length;i++){if(greg.options[i].value===cfg.region){greg.selectedIndex=i;break}}}
if(cfg.compartment_id)document.getElementById('gcmp').value=cfg.compartment_id;
sm('gm','Região e Compartment preenchidos da credencial <strong>'+cfg.tenancy_name+'</strong>','i')}
async function tGenai(id){try{const d=await $api('/genai/test/'+id,{method:'POST'});sm('gm',d.status==='success'?'✅ '+d.message+(d.response?' — '+d.response:''):'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('genai')}catch(e){sm('gm',e.message,'e')}}
async function dGenai(id){if(!confirm('Excluir modelo?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
/* ── MCP Servers ── */
function mcpTypeFields(){const t=document.querySelector('input[name="mt"]:checked')?.value;
document.querySelectorAll('.mcp-f').forEach(el=>{el.style.display='none'});
if(t==='stdio'){document.getElementById('mf-cmd').style.display='';document.getElementById('mf-args').style.display=''}
if(t==='sse')document.getElementById('mf-url').style.display='';
if(t==='module'){document.getElementById('mf-cmd').style.display='';document.getElementById('mf-args').style.display=''}}
function rMCP(){const em=S.editing?.type==='mcp'?S.mcpSvr.find(m=>m.id===S.editing.id):null;
const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name}</option>`).join('');
return`<div class="cd"><div class="ct">🔌 Servidores Registrados</div>
<div class="cdesc">MCP servers disponíveis para integração com ferramentas e execução de tarefas.</div>
${S.mcpSvr.length?S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
return`<div style="border:1px solid ${em&&em.id===m.id?'var(--p)':'var(--bd)'};border-radius:var(--r);padding:1rem;margin-bottom:.75rem;background:var(--bg2)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.65rem">
<div><strong style="font-size:.88rem">${m.name}</strong> <span class="tag" style="margin-left:.4rem">${m.server_type}</span>
<span class="badge ${m.is_active?'b-on':'b-off'}" style="margin-left:.35rem">${m.is_active?'Ativo':'Inativo'}</span></div>
<div style="display:flex;gap:.35rem"><button class="btn bs bsm" onclick="editCfg('mcp','${m.id}')">Editar</button> <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></div></div>
${m.description?`<div style="font-size:.72rem;color:var(--t3);margin-bottom:.5rem">${m.description}</div>`:''}
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;font-size:.72rem;color:var(--t4)">
${m.command?`<div><span style="color:var(--t3);font-weight:600">Comando:</span> <code style="font-family:var(--fm);font-size:.66rem">${m.command}</code></div>`:''}
${m.url?`<div><span style="color:var(--t3);font-weight:600">URL:</span> <code style="font-family:var(--fm);font-size:.66rem">${m.url}</code></div>`:''}
${m.module_path?`<div><span style="color:var(--t3);font-weight:600">Módulo:</span> <code style="font-family:var(--fm);font-size:.66rem">${m.module_path}</code></div>`:''}
<div><span style="color:var(--t3);font-weight:600">ADB:</span> ${adb?`<span class="badge b-pass">${adb.config_name}</span>`:'<span>Nenhum</span>'}</div></div>
<div id="mcpm_${m.id}"></div>
<div style="display:flex;gap:.5rem;margin-top:.65rem;align-items:center;flex-wrap:wrap">
<input type="file" id="muf_${m.id}" accept=".py" style="font-size:.68rem;max-width:200px">
<button class="btn bs bsm" id="mub_${m.id}" onclick="uMcp('${m.id}')">Upload .py</button>
<select id="mlnk_${m.id}" style="font-size:.72rem;padding:.28rem .5rem;border-radius:6px;border:1px solid var(--bd)">
<option value="">Sem ADB</option>${adbOpts}</select>
<button class="btn bs bsm" id="mlb_${m.id}" onclick="lnkMcp('${m.id}')">Vincular ADB</button></div></div>`}).join('')
:'<div class="emp" style="padding:.85rem"><p>Nenhum MCP server registrado.</p></div>'}</div>
<div class="cd"><div class="ct">${em?'✏️ Editar Server':' Registrar Novo Server'}</div>
<div class="cdesc">${em?'Editando: <strong>'+em.name+'</strong>':'Configure um novo servidor MCP. Os campos se ajustam ao tipo selecionado.'}</div><div id="mcm"></div>
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server" value="${em?em.name:''}"></div>
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0" value="${em&&em.description?em.description:''}"></div></div>
<div class="ig" style="margin-bottom:.75rem"><label>Tipo de Servidor</label>
<div style="display:flex;gap:.5rem;margin-top:.3rem">
<label style="display:flex;align-items:center;gap:.3rem;padding:.4rem .75rem;border:1px solid var(--bd);border-radius:8px;cursor:pointer;font-size:.78rem;transition:all .2s">
<input type="radio" name="mt" value="stdio" ${!em||em.server_type==='stdio'?'checked':''} onchange="mcpTypeFields()"> <span>⌨️ stdio</span></label>
<label style="display:flex;align-items:center;gap:.3rem;padding:.4rem .75rem;border:1px solid var(--bd);border-radius:8px;cursor:pointer;font-size:.78rem;transition:all .2s">
<input type="radio" name="mt" value="sse" ${em&&em.server_type==='sse'?'checked':''} onchange="mcpTypeFields()"> <span>🌐 SSE (HTTP)</span></label>
<label style="display:flex;align-items:center;gap:.3rem;padding:.4rem .75rem;border:1px solid var(--bd);border-radius:8px;cursor:pointer;font-size:.78rem;transition:all .2s">
<input type="radio" name="mt" value="module" ${em&&em.server_type==='module'?'checked':''} onchange="mcpTypeFields()"> <span>🐍 Python Module</span></label></div></div>
<div id="mf-cmd" class="mcp-f"><div class="ig"><label>Comando</label><div class="ht">Comando para iniciar o servidor</div>
<input type="text" id="mcmd" placeholder="python3 server.py" value="${em&&em.command?em.command:''}"></div></div>
<div id="mf-url" class="mcp-f" style="display:none"><div class="ig"><label>URL do Servidor</label><div class="ht">Endpoint SSE do servidor MCP</div>
<input type="text" id="murl" placeholder="http://localhost:8001/sse" value="${em&&em.url?em.url:''}"></div></div>
<div id="mf-args" class="mcp-f"><div class="ig"><label>Argumentos (JSON)</label><div class="ht">Array de argumentos passados ao comando</div>
<input type="text" id="margs" placeholder='["--config", "/path/to/config"]' value="${em&&em.args?JSON.stringify(em.args).replace(/"/g,'&quot;'):''}"></div></div>
<div class="ig" style="margin-top:.5rem"><label>🗄️ Vincular ADB Vector</label><div class="ht">Opcional — o MCP server poderá usar este banco como ferramenta.</div>
<select id="madb"><option value="">Nenhum</option>${S.adbCfg.map(a=>'<option value="'+a.id+'"'+(em&&em.linked_adb_id===a.id?' selected':'')+'>'+a.config_name+'</option>').join('')}</select></div>
<div style="display:flex;gap:8px;margin-top:.75rem"><button class="btn bp" id="mcbtn" onclick="sMcp()">${em?'Salvar Alterações':'Registrar Server'}</button>${em?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
`+rConfigLogs('mcp')}
async function sMcp(){const btn=document.getElementById('mcbtn');const isEdit=S.editing?.type==='mcp';
btn.disabled=true;btn.textContent='Salvando...';sm('mcm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> '+(isEdit?'Atualizando':'Registrando')+' servidor...','i');
try{let args=null;const a=document.getElementById('margs')?.value;if(a)try{args=JSON.parse(a)}catch(e){}
const mt=document.querySelector('input[name="mt"]:checked')?.value||'stdio';
const body={name:document.getElementById('mn').value,description:document.getElementById('md').value,
server_type:mt,command:document.getElementById('mcmd')?.value||null,url:document.getElementById('murl')?.value||null,args,
linked_adb_id:document.getElementById('madb').value||null};
const url=isEdit?'/mcp/servers/'+S.editing.id:'/mcp/servers';const method=isEdit?'PUT':'POST';
await $api(url,{method,body});S.editing=null;
S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ '+(isEdit?'Atualizado':'Registrado')+'!','s');R()}catch(e){sm('mcm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Registrar Server'}}
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 server?'))return;await $api('/mcp/servers/'+id,{method:'DELETE'});S.mcpSvr=await $api('/mcp/servers');R()}
async function uMcp(id){const fd=new FormData();const f=document.getElementById('muf_'+id)?.files[0];if(!f)return sm('mcpm_'+id,'Selecione um arquivo .py','e');
fd.append('file',f);const btn=document.getElementById('mub_'+id);btn.disabled=true;btn.textContent='Enviando...';sm('mcpm_'+id,'<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Enviando arquivo...','i');
try{await $api('/mcp/servers/'+id+'/upload',{method:'POST',body:fd,headers:{}});S.mcpSvr=await $api('/mcp/servers');sm('mcpm_'+id,'✅ Upload realizado!','s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Upload .py'}}
async function lnkMcp(id){const aid=document.getElementById('mlnk_'+id)?.value||'';
const btn=document.getElementById('mlb_'+id);btn.disabled=true;btn.textContent='Vinculando...';
try{await $api('/mcp/servers/'+id+'/link-adb?adb_id='+aid,{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');sm('mcpm_'+id,'✅ '+(aid?'Vinculado ao ADB!':'ADB desvinculado!'),'s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Vincular ADB'}}
/* ── ADB Vector ── */
function rADB(){const ea=S.editing?.type==='adb'?S.adbCfg.find(c=>c.id===S.editing.id):null;
return`<div class="cd"><div class="ct">🗄️ Conexões Registradas</div>
<table><thead><tr><th>Nome</th><th>DSN</th><th>User</th><th>Tabela</th><th>mTLS</th><th>Wallet</th><th>Embed</th><th>Ações</th></tr></thead><tbody>
${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];return`<tr${ea&&ea.id===c.id?' style="background:var(--bg3)"':''}><td><strong>${c.config_name}</strong></td><td style="font-family:var(--fm);font-size:.68rem;color:var(--t4)">${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>${c.genai_config_id?'<span class="tag" style="background:var(--gnl);color:var(--gn)">'+(em?em.name:c.embedding_model_id)+'</span>':'<span style="color:var(--t4)">—</span>'}</td>
<td><button class="btn bs bsm" onclick="editCfg('adb','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tAdb('${c.id}')">Testar</button> <button class="btn bs bsm" onclick="ensureTable('${c.id}')">Criar Tabela</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:.85rem"><p>Nenhuma conexão ADB registrada.</p></div>':''}</div>
<div class="cd"><div class="ct">${ea?'✏️ Editar Conexão':' Nova Conexão'}</div>
<div class="cdesc">${ea?'Editando: <strong>'+ea.config_name+'</strong>':'Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.'}</div><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" value="${ea?ea.config_name:''}"></div>
<div class="ig"><label>Wallet ZIP</label><div class="ht">${ea?'Envie novo wallet ou deixe vazio para manter o atual':'Envie o wallet para extrair os DSNs do tnsnames.ora'}</div>
<div style="display:flex;gap:6px;align-items:center"><input type="file" id="awf" accept=".zip" style="flex:1"><button class="btn bs bsm" id="wpbtn" type="button" onclick="parseWallet()">Analisar</button></div></div>
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Selecione ou digite o DSN. Ex: myatp_high</div>
<div id="adsn-wrap"><input type="text" id="adsn" placeholder="myatp_high" value="${ea?ea.dsn:''}"></div></div>
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value="${ea?ea.username:''}"></div>
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="${ea?'(manter atual)':''}"></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="${ea?ea.table_name:'CIS_EMBEDDINGS'}"></div>
<div class="ig"><label>Config GenAI (Embeddings)</label><div class="ht">Credenciais OCI usadas para gerar embeddings via GenAI.</div>
<select id="agcfg"><option value="">Nenhum (sem RAG)</option>${S.genaiCfg.map(g=>'<option value="'+g.id+'"'+(ea&&ea.genai_config_id===g.id?' selected':'')+'>'+(g.name||g.model_id)+' · '+g.genai_region+'</option>').join('')}</select></div>
<div class="ig"><label>Modelo de Embedding</label><div class="ht">Modelo OCI GenAI para gerar vetores.</div>
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'"'+(ea&&ea.embedding_model_id===k?' selected':'')+'>'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
<div style="display:flex;gap:8px"><button class="btn bp" id="abtn" onclick="sAdb()">${ea?'Salvar Alterações':'Salvar Conexão'}</button>${ea?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
${S.adbCfg.length?`<div class="cd"><div class="ct">📤 Atualizar Wallet</div><div id="wm"></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="awf2" accept=".zip"></div></div>
<button class="btn bs" id="wbtn" onclick="uWallet()">Upload Wallet</button></div>`:''}
`+rConfigLogs('adb')}
async function parseWallet(){const f=document.getElementById('awf').files[0];if(!f)return sm('am','Selecione um arquivo wallet ZIP.','e');
const btn=document.getElementById('wpbtn');btn.disabled=true;btn.textContent='...';
try{const fd=new FormData();fd.append('wallet',f);const d=await $api('/adb/parse-wallet',{method:'POST',body:fd,headers:{}});
const wrap=document.getElementById('adsn-wrap');
wrap.innerHTML='<select id="adsn">'+d.dsn_names.map(n=>'<option value="'+n+'">'+n+'</option>').join('')+'</select>';
sm('am','✅ Wallet analisado! '+d.dsn_names.length+' DSN(s) encontrado(s): <strong>'+d.dsn_names.join(', ')+'</strong>. Arquivos: '+d.files.join(', '),'s');
}catch(e){sm('am',e.message,'e')}finally{btn.disabled=false;btn.textContent='Analisar'}}
async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=true;btn.textContent='Salvando...';sm('am','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Salvando conexão...','i');
const isEdit=S.editing?.type==='adb';
try{const fd=new FormData();fd.append('config_name',document.getElementById('an').value);
fd.append('dsn',document.getElementById('adsn').value);fd.append('username',document.getElementById('auser').value);
fd.append('password',document.getElementById('apw').value);fd.append('wallet_password',document.getElementById('awpw').value||'');
fd.append('table_name',document.getElementById('atbl').value);fd.append('use_mtls','true');
fd.append('genai_config_id',document.getElementById('agcfg').value||'');
fd.append('embedding_model_id',document.getElementById('aemb').value||'cohere.embed-multilingual-v3.0');
const wf=document.getElementById('awf').files[0];if(wf)fd.append('wallet',wf);
const url=isEdit?'/adb/configs/'+S.editing.id:'/adb/config';const method=isEdit?'PUT':'POST';
await $api(url,{method,body:fd,headers:{}});S.editing=null;
S.adbCfg=await $api('/adb/configs');sm('am','✅ Conexão '+(isEdit?'atualizada':'salva')+'!'+(wf?' Wallet incluído.':''),'s');R()}catch(e){sm('am',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Conexão'}}
async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});sm('am',d.status==='success'?'✅ '+d.message:'❌ '+d.message,d.status==='success'?'s':'e');refreshCLogs('adb')}catch(e){sm('am',e.message,'e')}}
async function dAdb(id){if(!confirm('Excluir conexão?'))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('awf2').files[0];if(!f)return sm('wm','Selecione um arquivo wallet ZIP.','e');
fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return sm('wm','Selecione uma conexão ADB.','e');
const btn=document.getElementById('wbtn');btn.disabled=true;btn.textContent='Enviando...';sm('wm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Enviando wallet...','i');
try{const d=await $api('/adb/'+id+'/upload-wallet',{method:'POST',body:fd,headers:{}});S.adbCfg=await $api('/adb/configs');sm('wm','✅ Wallet enviada! Arquivos: '+d.files.join(', '),'s');R()}catch(e){sm('wm',e.message,'e');btn.disabled=false;btn.textContent='Upload Wallet'}}
async function ensureTable(id){try{const d=await $api('/adb/'+id+'/ensure-table',{method:'POST'});sm('am','✅ Tabela criada/verificada: '+d.table,'s')}catch(e){sm('am','Erro: '+e.message,'e')}}
/* ── Embeddings ── */
function rEmbeddings(){
const adbOpts=S.adbCfg.filter(c=>c.genai_config_id).map(c=>'<option value="'+c.id+'">'+c.config_name+'</option>').join('');
const rptOpts=S.reports.filter(r=>r.status==='completed').map(r=>'<option value="'+r.id+'">'+r.tenancy_name+' ('+r.created_at?.substring(0,10)+')</option>').join('');
if(!S.adbCfg.filter(c=>c.genai_config_id).length)return`<div class="cd"><div class="emp"><div class="eic">🧬</div><p>Nenhuma conexão ADB com GenAI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>ADB Vector</strong> vinculando uma Config GenAI.</p></div></div>`;
return`<div class="cd"><div class="ct">📋 Embeddings Existentes</div>
<div class="cdesc">Consulte os documentos armazenados na tabela de embeddings do ADB.</div>
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="elsel">${adbOpts}</select></div>
<div class="ig" style="align-self:end"><button class="btn bs" onclick="loadEmbs()">Carregar</button></div></div>
<div id="emb-list"></div></div>
<div class="cd"><div class="ct">📊 Embed Relatório CIS</div>
<div class="cdesc">Gere embeddings a partir de relatórios CIS. Cada seção (IAM, Networking, etc.) gera um embedding separado.</div><div id="erm"></div>
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="ersel">${adbOpts}</select></div>
<div class="ig"><label>Relatório</label><select id="errpt">${rptOpts||'<option value="">Nenhum relatório disponível</option>'}</select></div></div>
<button class="btn bp" id="erbtn" onclick="embedReport()">Gerar Embeddings do Relatório</button></div>
<div class="cd"><div class="ct">📄 Upload de Arquivo</div>
<div class="cdesc">Faça upload de um arquivo .txt para gerar embeddings automaticamente (chunking por parágrafos).</div><div id="efm"></div>
<div class="g2"><div class="ig"><label>Conexão ADB</label><select id="efsel">${adbOpts}</select></div>
<div class="ig"><label>Arquivo (.txt)</label><input type="file" id="eff" accept=".txt"></div></div>
<button class="btn bp" id="efbtn" onclick="embedFile()">Upload e Gerar Embeddings</button></div>`}
async function loadEmbs(){const vid=document.getElementById('elsel').value;if(!vid)return;
try{const d=await $api('/embeddings/'+vid+'/list');
if(!d.documents.length){document.getElementById('emb-list').innerHTML='<div class="emp" style="padding:.65rem"><p>Nenhum embedding encontrado.</p></div>';return}
document.getElementById('emb-list').innerHTML='<table style="margin-top:.65rem"><thead><tr><th>ID</th><th>Source</th><th>Metadata</th><th>Data</th><th>Ações</th></tr></thead><tbody>'+
d.documents.map(e=>'<tr><td style="font-family:var(--fm);font-size:.62rem">'+e.id.substring(0,8)+'...</td><td>'+
(e.source||'—')+'</td><td style="font-size:.72rem;color:var(--t3)">'+(e.metadata||'—')+'</td><td style="font-size:.72rem;color:var(--t4)">'+
(e.created_at||'—')+'</td><td><button class="btn bd bsm" onclick="delEmb(\''+vid+'\',\''+e.id+'\')">Excluir</button></td></tr>').join('')+
'</tbody></table><div style="padding:.5rem;font-size:.72rem;color:var(--t4)">Total: '+d.total+' documentos</div>'}
catch(e){document.getElementById('emb-list').innerHTML='<div style="color:var(--rd);padding:.5rem">Erro: '+e.message+'</div>'}}
async function embedReport(){const vid=document.getElementById('ersel').value;const rid=document.getElementById('errpt').value;
if(!vid||!rid)return sm('erm','Selecione conexão ADB e relatório.','e');
const btn=document.getElementById('erbtn');btn.disabled=true;btn.textContent='Gerando...';sm('erm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Gerando embeddings do relatório...','i');
try{const d=await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid}});sm('erm','✅ '+d.message,'s');btn.disabled=false;btn.textContent='Gerar Embeddings do Relatório'}catch(e){sm('erm',e.message,'e');btn.disabled=false;btn.textContent='Gerar Embeddings do Relatório'}}
async function embedFile(){const vid=document.getElementById('efsel').value;const f=document.getElementById('eff').files[0];
if(!vid||!f)return sm('efm','Selecione conexão ADB e arquivo.','e');
const fd=new FormData();fd.append('file',f);fd.append('adb_config_id',vid);
const btn=document.getElementById('efbtn');btn.disabled=true;btn.textContent='Enviando...';sm('efm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Enviando e gerando embeddings...','i');
try{const d=await $api('/embeddings/upload',{method:'POST',body:fd,headers:{}});sm('efm','✅ '+d.message,'s');btn.disabled=false;btn.textContent='Upload e Gerar Embeddings'}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Upload e Gerar Embeddings'}}
async function delEmb(vid,docId){if(!confirm('Excluir este embedding?'))return;
try{await $api('/embeddings/'+vid+'/'+docId,{method:'DELETE'});loadEmbs()}catch(e){alert('Erro: '+e.message)}}
/* ── Users ── */
function rUsers(){return`<div class="cd"><div class="ct">👥 Gerenciamento de Usuários</div><div id="um"></div>
<table style="margin-top:.65rem"><thead><tr><th>Nome</th><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.first_name||''} ${u.last_name||''}</strong></td>
<td style="font-family:var(--fm);font-size:.7rem">${u.username}</td>
<td style="font-size:.72rem">${u.email||'—'}</td>
<td><span class="badge b-${u.role}">${u.role}</span></td>
<td>${u.mfa_enabled?'<span class="tag" style="background:var(--gnl);color:var(--gn)">✓</span>':'—'}</td>
<td>${u.is_active?'<span class="tag" style="background:var(--gnl);color:var(--gn)">Ativo</span>':'<span class="tag">Inativo</span>'}</td>
<td style="font-size:.72rem;color:var(--t4)">${u.last_login||'Nunca'}</td>
<td><select onchange="uRole('${u.id}',this.value)" style="max-width:90px;padding:.22rem .35rem;font-size:.72rem" ${u.username==='admin'?'disabled':''}>
<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.username!=='admin'&&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 Novo Usuário</div><div id="um2"></div>
<div class="g3">
<div class="ig"><label>Nome</label><input type="text" id="nfn" placeholder="João"></div>
<div class="ig"><label>Sobrenome</label><input type="text" id="nln" placeholder="Silva"></div>
<div class="ig"><label>Usuário (login)</label><input type="text" id="nu" placeholder="joao.silva"></div></div>
<div class="g3"><div class="ig"><label>Email</label><input type="email" id="ne" placeholder="joao@empresa.com"></div>
<div class="ig"><label>Senha</label><input type="password" id="np"></div>
<div class="ig"><label>Role</label><select id="nr"><option value="viewer">Viewer</option><option value="user">User</option><option value="admin">Admin</option></select></div></div>
<button class="btn bp" onclick="cUser()">Criar Usuário</button></div>`}
async function cUser(){
const firstName = document.getElementById('nfn').value.trim();
const lastName = document.getElementById('nln').value.trim();
const username = document.getElementById('nu').value.trim();
const email = document.getElementById('ne').value.trim();
const password = document.getElementById('np').value;
const role = document.getElementById('nr').value;
if (!firstName || !lastName) {
sm('um2', '❌ Nome e sobrenome são obrigatórios', 'e');
return;
}
if (!username) {
sm('um2', '❌ Usuário (login) é obrigatório', 'e');
return;
}
if (!password) {
sm('um2', '❌ Senha é obrigatória', 'e');
return;
}
try {
await $api('/auth/register', {
method: 'POST',
body: {
first_name: firstName,
last_name: lastName,
username: username,
email: email,
password: password,
role: role
}
});
S.users = await $api('/users');
sm('um2', '✅ Usuário criado com sucesso!', 's');
document.getElementById('nfn').value = '';
document.getElementById('nln').value = '';
document.getElementById('nu').value = '';
document.getElementById('ne').value = '';
document.getElementById('np').value = '';
R();
} catch(e) {
sm('um2', 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 usuário?'))return;await $api('/users/'+id,{method:'DELETE'});S.users=await $api('/users');R()}
/* ── MFA ── */
function rMfa(){return`<div class="cd"><div class="ct">🔐 Autenticação MFA (TOTP)</div>
<div class="cdesc">Proteja sua conta com autenticação em dois fatores via Google Authenticator ou Authy.</div><div id="mm"></div>
${S.user?.mfa_enabled?'<div class="al al-s">✅ MFA está ativado na sua conta.</div>'
:`<button class="btn bp" onclick="sMfa()">Gerar Secret</button><div id="msa" style="margin-top:.75rem"></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 style="font-size:.78rem">${d.secret}</code></div>
<div style="display:flex;gap:.45rem;max-width:280px;margin-top:.55rem"><input type="text" id="mvc" placeholder="000000" maxlength="6">
<button class="btn bp" onclick="vMfa()">Ativar MFA</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 com sucesso!','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"><span class="spinner"></span> Carregando logs...</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:.68rem;color:var(--t4)">${l.created_at}</td><td>${l.username||'—'}</td><td><span class="tag">${l.action}</span></td>
<td style="font-size:.68rem;font-family:var(--fm);color:var(--t4)">${l.resource?(l.resource.slice(0,14)+'…'):'—'}</td><td style="font-size:.72rem">${l.ip||'—'}</td></tr>`).join('')}</tbody></table>`}catch(e){document.getElementById('ac').innerHTML='<div class="al al-e">'+e.message+'</div>'}}
/* ── Config Logs ── */
function rConfigLogs(type){return`<div class="cd"><div class="ct" style="display:flex;justify-content:space-between;align-items:center">
<span>📜 Log de Atividades</span>
<div style="display:flex;gap:.4rem;align-items:center">
<select id="cl-sev-${type}" onchange="refreshCLogs('${type}')" style="font-size:.7rem;padding:.22rem .4rem;border-radius:6px;border:1px solid var(--bd)">
<option value="">Todos</option><option value="error">Erros</option><option value="success">Sucesso</option><option value="info">Info</option></select>
<button class="btn bs bsm" onclick="refreshCLogs('${type}')">Atualizar</button></div></div>
<div id="cl-${type}"><div style="padding:.75rem;font-size:.74rem;color:var(--t4)">Carregando logs...</div></div></div>`}
async function refreshCLogs(type){
const sev=document.getElementById('cl-sev-'+type)?.value||'';
const qs='config_type='+type+'&limit=30'+(sev?'&severity='+sev:'');
try{const logs=await $api('/config-logs?'+qs);
const el=document.getElementById('cl-'+type);if(!el)return;
if(!logs.length){el.innerHTML='<div class="emp" style="padding:.65rem"><p>Nenhum log registrado.</p></div>';return}
el.innerHTML='<table style="font-size:.74rem"><thead><tr><th>Data</th><th>Config</th><th>Ação</th><th>Status</th><th>Mensagem</th></tr></thead><tbody>'+
logs.map(l=>{const sc=l.severity==='error'?'rd':l.severity==='success'?'gn':'bl';
return'<tr><td style="font-size:.62rem;color:var(--t4);white-space:nowrap">'+l.created_at+'</td>'+
'<td style="font-size:.72rem">'+(l.config_name||l.config_id?.slice(0,8)||'—')+'</td>'+
'<td><span class="tag">'+l.action+'</span></td>'+
'<td><span class="badge" style="background:var(--'+sc+'l);color:var(--'+sc+')">'+l.severity+'</span></td>'+
'<td style="font-size:.72rem;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+(l.message||'').replace(/"/g,'&quot;')+'">'+(l.message?.slice(0,150)||'—')+'</td></tr>'}).join('')+
'</tbody></table>'}
catch(e){const el=document.getElementById('cl-'+type);if(el)el.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>