Files
A-Team-Security-Infra-Agent…/frontend/index.html
nogueiraguh 2c3fb724bf feat: add RAG pipeline with OCI GenAI embeddings and ADB vector search
Integrate Retrieval-Augmented Generation into the chat flow using OCI
GenAI embed_text API and Oracle Autonomous Database vector storage.
The chat automatically queries ADB for relevant context when an active
ADB config with a linked GenAI config exists.

- Add EMBEDDING_MODELS catalog (Cohere Embed v3.0/light)
- Add _embed_text() using OCI GenAI SDK embed endpoint
- Add _vector_search() with VECTOR_DISTANCE cosine similarity
- Add _get_adb_connection(), _ensure_embeddings_table(), _build_rag_context()
- Add document ingestion endpoint (POST /api/adb/{vid}/ingest)
- Add table creation endpoint (POST /api/adb/{vid}/ensure-table)
- Modify chat endpoint with automatic RAG augmentation (non-fatal)
- Add GenAI config and embedding model selectors to ADB UI
- Add RAG status indicator in chat toolbar
- Add document ingestion section in ADB tab
2026-03-02 19:48:53 -03:00

602 lines
54 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 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};
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()}
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
/* ── 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']];
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','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'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 GenAI ou use o agente local.</p></div>'
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
const gcOpts=S.genaiCfg.map(g=>{const mi=S.models[g.model_id];return`<option value="${g.id}" ${S.selGenai===g.id?'selected':''}>${g.name||mi?.name||g.model_id} · ${g.genai_region}</option>`}).join('');
return`<div class="ch-c">
<div class="ch-t"><label>🧠 Modelo:</label><select id="gsel" onchange="S.selGenai=this.value"><option value="">Agente Local (sem IA)</option>${gcOpts}</select>
${S.adbCfg.some(c=>c.genai_config_id&&c.is_active)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem;margin-left:.5rem">RAG Ativo</span>':''}
<div style="flex:1"></div><button class="btn bd bsm" onclick="clrChat()">Limpar</button></div>
<div class="ch-m" id="chm">${ms}</div>
<div class="ch-i"><input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div>`}
function fm(t){return t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>')}
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();if(!m)return;el.value='';
S.msgs.push({r:'user',c:m});R();scCh();
try{const body={message:m,session_id:S.sid};if(S.selGenai)body.genai_config_id=S.selGenai;
const d=await $api('/chat',{method:'POST',body});S.sid=d.session_id;S.msgs.push({r:'assistant',c:d.response});R();scCh()}
catch(e){S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}}
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
/* ── OCI Explorer ── */
function rExplorer(){
const cfgSel=S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('');
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">☁️</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.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()}
/* ── OCI Config ── */
function rOci(){return`<div class="cd"><div class="ct">☁️ Credenciais Registradas</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:.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="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"> Nova Credencial</div>
<div class="cdesc">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"></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','✅ Credencial salva com sucesso!','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 credencial?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
/* ── GenAI Config ── */
function rGenAI(){const mOpts=Object.entries(S.models).map(([k,v])=>`<option value="${k}">${v.name} (${v.provider})</option>`).join('');
const rOpts=S.regions.map(r=>`<option value="${r}" ${r==='us-ashburn-1'?'selected':''}>${r}</option>`).join('');
return`<div class="cd"><div class="ct">🧠 Modelos Configurados</div>
<table><thead><tr><th>Config</th><th>Modelo</th><th>Região</th><th>Temp</th><th>Tokens</th><th>Ações</th></tr></thead><tbody>
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return`<tr><td><strong>${g.name||'—'}</strong>${g.is_default?' ':''}</td>
<td>${mi.name||g.model_id}<br><span class="tag">${mi.provider||'?'}</span></td>
<td style="font-family:var(--fm);font-size:.7rem">${g.genai_region}</td>
<td>${g.temperature}</td><td>${g.max_tokens}</td>
<td><button class="btn bs bsm" onclick="tGenai('${g.id}')">Testar</button> <button class="btn bd bsm" onclick="dGenai('${g.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
${!S.genaiCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhum modelo configurado.</p></div>':''}</div>
<div class="cd"><div class="ct"> Novo Modelo GenAI</div>
<div class="cdesc">Configura conexão via OCI SDK (<code style="font-size:.72rem">GenerativeAiInferenceClient</code>). O endpoint é gerado automaticamente pela região selecionada.</div><div id="gm"></div>
<div class="g3"><div class="ig"><label>Nome da Config</label><input type="text" id="gname" placeholder="Llama Produção"></div>
<div class="ig"><label>Credencial OCI</label><select id="goci"><option value="">Selecione...</option>
${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name}</option>`).join('')}</select></div>
<div class="ig"><label>Modelo</label><select id="gmod">${mOpts}</select></div></div>
<div class="g3"><div class="ig"><label>Região GenAI</label><select id="greg">${rOpts}</select></div>
<div class="ig"><label>Compartment OCID</label><input type="text" id="gcmp" placeholder="ocid1.compartment.oc1.."></div>
<div class="ig"><label>Model OCID (opcional)</label><div class="ht">Somente se diferente do catálogo</div><input type="text" id="gocid" placeholder="ocid1.generativeaimodel.oc1..."></div></div>
<div style="font-size:.76rem;font-weight:700;color:var(--t2);margin:.65rem 0 .4rem;letter-spacing:-.01em">Parâmetros</div>
<div class="g3"><div class="ig"><label>Temperature</label><input type="number" id="gtemp" value="1" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Max Tokens</label><input type="number" id="gmt" value="6000" min="1" max="128000"></div>
<div class="ig"><label>Top P</label><input type="number" id="gtp" value="0.95" step="0.05" min="0" max="1"></div></div>
<div class="g3"><div class="ig"><label>Top K</label><input type="number" id="gtk" value="1" min="-1" max="500"></div>
<div class="ig"><label>Frequency Penalty</label><input type="number" id="gfp" value="0" step="0.1" min="0" max="2"></div>
<div class="ig"><label>Presence Penalty</label><input type="number" id="gpp" value="0" step="0.1" min="0" max="2"></div></div>
<button class="btn bp" onclick="sGenai()">Salvar Modelo</button></div>`}
async function sGenai(){try{await $api('/genai/config',{method:'POST',body:{
name:document.getElementById('gname').value||'default',
oci_config_id:document.getElementById('goci').value,model_id:document.getElementById('gmod').value,
model_ocid:document.getElementById('gocid').value||null,
compartment_id:document.getElementById('gcmp').value,genai_region:document.getElementById('greg').value,
endpoint:null,serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
temperature:parseFloat(document.getElementById('gtemp').value),max_tokens:parseInt(document.getElementById('gmt').value),
top_p:parseFloat(document.getElementById('gtp').value),top_k:parseInt(document.getElementById('gtk').value),
frequency_penalty:parseFloat(document.getElementById('gfp').value),presence_penalty:parseFloat(document.getElementById('gpp').value),
is_default:S.genaiCfg.length===0}});
S.genaiCfg=await $api('/genai/configs');sm('gm','✅ Modelo salvo com sucesso!','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 modelo?'))return;await $api('/genai/configs/'+id,{method:'DELETE'});S.genaiCfg=await $api('/genai/configs');R()}
/* ── MCP Servers ── */
function rMCP(){const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name} (${a.dsn})</option>`).join('');
return`<div class="cd"><div class="ct">🔌 Servidores Registrados</div>
<table><thead><tr><th>Nome</th><th>Tipo</th><th>Cmd / URL</th><th>ADB Link</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${S.mcpSvr.map(m=>{const adb=m.linked_adb_id?S.adbCfg.find(a=>a.id===m.linked_adb_id):null;
return`<tr><td><strong>${m.name}</strong><br><span style="font-size:.66rem;color:var(--t4)">${m.description||''}</span></td>
<td><span class="tag">${m.server_type}</span></td>
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4)">${m.command||m.url||m.module_path||'—'}</td>
<td>${adb?`<span class="badge b-pass">${adb.config_name}</span>`:'<span style="color:var(--t4)">—</span>'}</td>
<td><span class="badge ${m.is_active?'b-on':'b-off'}">${m.is_active?'Ativo':'Inativo'}</span></td>
<td><button class="btn bs bsm" onclick="tgMcp('${m.id}')">${m.is_active?'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:.85rem"><p>Nenhum MCP server registrado.</p></div>':''}</div>
<div class="cd"><div class="ct"> Registrar MCP Server</div>
<div class="cdesc">Registre servidores MCP para execução de tarefas e integração com ferramentas.</div><div id="mcm"></div>
<div class="g2"><div class="ig"><label>Nome</label><input type="text" id="mn" placeholder="CIS Benchmark Server"></div>
<div class="ig"><label>Descrição</label><input type="text" id="md" placeholder="Executa checks CIS 3.0"></div></div>
<div class="g3"><div class="ig"><label>Tipo</label><select id="mt"><option value="stdio">stdio</option><option value="sse">SSE (HTTP)</option><option value="module">Python Module</option></select></div>
<div class="ig"><label>Comando (stdio)</label><input type="text" id="mcmd" placeholder="python3 server.py"></div>
<div class="ig"><label>URL (SSE)</label><input type="text" id="murl" placeholder="http://localhost:8001/sse"></div></div>
<div class="g2"><div class="ig"><label>🗄️ Vincular ADB Vector</label><div class="ht">MCP server usará este banco como ferramenta</div><select id="madb"><option value="">Nenhum</option>${adbOpts}</select></div>
<div class="ig"><label>Args (JSON)</label><input type="text" id="margs" placeholder='["--config", "/path"]'></div></div>
<button class="btn bp" onclick="sMcp()">Registrar Server</button></div>
<div class="cd"><div class="ct">📤 Upload & Vínculo</div>
<div class="g3"><div class="ig"><label>MCP Server</label><select id="mup">${S.mcpSvr.map(m=>`<option value="${m.id}">${m.name}</option>`).join('')}</select></div>
<div class="ig"><label>Script .py</label><input type="file" id="muf" accept=".py">
<button class="btn bs bsm" style="margin-top:.35rem" onclick="uMcp()">Upload</button></div>
<div class="ig"><label>Vincular ADB</label><select id="mlnk"><option value="">Nenhum</option>${adbOpts}</select>
<button class="btn bs bsm" style="margin-top:.35rem" onclick="lnkMcp()">Vincular</button></div></div></div>`}
async function sMcp(){try{let args=null;const a=document.getElementById('margs').value;if(a)try{args=JSON.parse(a)}catch(e){}
await $api('/mcp/servers',{method:'POST',body:{name:document.getElementById('mn').value,description:document.getElementById('md').value,
server_type:document.getElementById('mt').value,command:document.getElementById('mcmd').value||null,url:document.getElementById('murl').value||null,args,
linked_adb_id:document.getElementById('madb').value||null}});
S.mcpSvr=await $api('/mcp/servers');sm('mcm','✅ Registrado!','s');R()}catch(e){sm('mcm',e.message,'e')}}
async function tgMcp(id){await $api('/mcp/servers/'+id+'/toggle',{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');R()}
async function dMcp(id){if(!confirm('Excluir server?'))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 realizado!')}catch(e){alert(e.message)}}
async function lnkMcp(){const mid=document.getElementById('mup').value;const aid=document.getElementById('mlnk').value;if(!mid)return alert('Selecione MCP');
try{await $api('/mcp/servers/'+mid+'/link-adb?adb_id='+(aid||''),{method:'PUT'});S.mcpSvr=await $api('/mcp/servers');alert('Vinculado!');R()}catch(e){alert(e.message)}}
/* ── ADB Vector ── */
function rADB(){return`<div class="cd"><div class="ct">🗄️ Conexões 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><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="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"> Nova Conexão</div>
<div class="cdesc">Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database.</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"></div>
<div class="ig"><label>DSN (TNS Name)</label><div class="ht">Ex: myatp_high, myatp_medium</div><input type="text" id="adsn" placeholder="myatp_high"></div>
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN"></div>
<div class="ig"><label>Password</label><input type="password" id="apw"></div>
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></div>
<div class="ig"><label>Tabela de Embeddings</label><input type="text" id="atbl" value="CIS_EMBEDDINGS"></div>
<div 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+'">'+(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+'">'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
<button class="btn bp" onclick="sAdb()">Salvar Conexão</button></div>
<div class="cd"><div class="ct">📤 Upload Wallet</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>
<div class="cd"><div class="ct">📄 Ingerir Documentos</div>
<div class="cdesc">Gere embeddings e insira documentos na tabela ADB para uso com RAG.</div>
<div class="g2"><div class="ig"><label>Conexão ADB</label>
<select id="aisel">${S.adbCfg.filter(c=>c.genai_config_id).map(c=>'<option value="'+c.id+'">'+c.config_name+'</option>').join('')}</select></div>
<div class="ig"><label>Documentos (JSON)</label>
<div class="ht">Array de objetos: [{"content":"texto","source":"fonte","metadata":"info"}]</div>
<textarea id="adocs" rows="6" style="font-family:var(--fm);font-size:.72rem;width:100%;padding:.5rem;border:1px solid var(--bd);border-radius:var(--r)" placeholder='[{"content":"...","source":"doc.pdf","metadata":"page 1"}]'></textarea></div></div>
<button class="btn bp" onclick="ingestDocs()">Ingerir Documentos</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,
genai_config_id:document.getElementById('agcfg').value||null,
embedding_model_id:document.getElementById('aemb').value||'cohere.embed-multilingual-v3.0'}});
S.adbCfg=await $api('/adb/configs');sm('am','✅ Conexão salva!','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 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('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 enviada! Arquivos: '+d.files.join(', '))}catch(e){alert(e.message)}}
async function ensureTable(id){try{const d=await $api('/adb/'+id+'/ensure-table',{method:'POST'});alert('Tabela criada/verificada: '+d.table)}catch(e){alert('Erro: '+e.message)}}
async function ingestDocs(){const id=document.getElementById('aisel').value;
if(!id)return alert('Selecione conexão ADB com GenAI configurado');
let docs;try{docs=JSON.parse(document.getElementById('adocs').value)}catch(e){return alert('JSON inválido')}
if(!Array.isArray(docs)||!docs.length)return alert('Forneça array de documentos');
try{const d=await $api('/adb/'+id+'/ingest',{method:'POST',body:{adb_config_id:id,documents:docs}});alert(d.message)}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>'}}
/* ── 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>