Files
A-Team-Security-Infra-Agent…/frontend/index.html
nogueiraguh e4ba2cfaab feat: region safety guard, rollback button, SSH key support, split-panel layout
- Block terraform plan if model omits variable "region" (prevents wrong-region provisioning)
- Rollback button on apply failure with ROLLBACK confirmation modal
- SSH public key field in OCI configs, auto-injected into terraform.tfvars
- Split-panel layout: terminal right (40%), files/plan/resources below chat (60%)
- reasoning_effort uppercase fix for OCI SDK
- Plan button persists after destroy status
2026-03-12 00:26:13 -03:00

3058 lines
275 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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,html.light {
--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);
--ok:#16a34a;--w:#ca8a04;--err:#dc2626;
--tx2:#78716c;--p:#1c1917;--pl:rgba(199,70,52,.06);
--b2:#ddd9d2;--b3:#c5c0b8;
--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);
--tb-bg:rgba(255,255,255,.8);
--fm:'JetBrains Mono',monospace;
--fs:'Plus Jakarta Sans',-apple-system,system-ui,sans-serif;
--trans:cubic-bezier(.22,1,.36,1)
}
html.dark {
--bg:#0d0f14;--bg1:#151820;--bg2:#1a1e28;--bg3:#222733;--bg4:#2a3040;
--bd:#2a3040;--bdf:#3a4258;
--t1:#f0f0f4;--t2:#c8cad0;--t3:#8b8fa3;--t4:#5a5e6e;
--ac:#c74634;--ach:#d9523f;--acd:#a83428;--acl:rgba(199,70,52,.1);--acl2:rgba(199,70,52,.16);
--gn:#34c759;--gnl:rgba(52,199,89,.1);--rd:#ff453a;--rdl:rgba(255,69,58,.1);
--bl:#0a84ff;--bll:rgba(10,132,255,.1);--yl:#ffd60a;--yll:rgba(255,214,10,.1);
--pp:#bf5af2;--ppl:rgba(191,90,242,.1);
--ok:#34c759;--w:#ffd60a;--err:#ff453a;
--tx2:#8b8fa3;--p:#f0f0f4;--pl:rgba(199,70,52,.1);
--b2:#2a3040;--b3:#3a4258;
--sh1:0 1px 3px rgba(0,0,0,.3),0 1px 2px rgba(0,0,0,.2);
--sh2:0 4px 8px rgba(0,0,0,.3),0 2px 4px rgba(0,0,0,.2);
--sh3:0 10px 20px rgba(0,0,0,.35),0 4px 8px rgba(0,0,0,.2);
--shh:0 20px 30px rgba(0,0,0,.4),0 8px 12px rgba(0,0,0,.25);
--tb-bg:rgba(21,24,32,.85);
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--fs);background:var(--bg);color:var(--t1);height:100vh;overflow:hidden;-webkit-font-smoothing:antialiased}
::selection{background:rgba(199,70,52,.2);color:var(--t1)}
/* ── Sidebar ── */
.app{display:flex;height:100vh;overflow:hidden}
.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;height:100vh;overflow:hidden;display:flex;flex-direction:column}
.tb{height:56px;background:var(--tb-bg);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;flex:1;overflow-y:auto;min-height:0}
/* ── Cards ── */
.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.35rem;margin-bottom:.85rem;box-shadow:var(--sh1)}
.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)}
/* ── KPI Cards ── */
.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-bottom:.85rem}
.kpi{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1rem 1.15rem;box-shadow:var(--sh1);transition:all .3s var(--trans);position:relative;overflow:hidden}
.kpi:hover{box-shadow:var(--sh2);transform:translateY(-2px)}
.kpi::after{content:'';position:absolute;bottom:0;left:0;right:0;height:3px;border-radius:0 0 var(--rl) var(--rl)}
.kpi.kpi-score::after{background:var(--ac)}.kpi.kpi-pass::after{background:var(--gn)}.kpi.kpi-fail::after{background:var(--rd)}.kpi.kpi-warn::after{background:var(--yl)}.kpi.kpi-info::after{background:var(--bl)}
.kpi-label{font-size:.68rem;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:.35rem;margin-bottom:.45rem}
.kpi-val{font-size:1.65rem;font-weight:800;letter-spacing:-.03em;line-height:1;color:var(--t1);font-variant-numeric:tabular-nums}
.kpi-val.v-ok{color:var(--gn)}.kpi-val.v-err{color:var(--rd)}.kpi-val.v-warn{color:var(--yl)}.kpi-val.v-info{color:var(--bl)}.kpi-val.v-ac{color:var(--ac)}
.kpi-sub{font-size:.66rem;color:var(--t4);margin-top:.3rem;font-family:var(--fm);font-variant-numeric:tabular-nums}
.kpi-bar{height:6px;background:var(--bg3);border-radius:3px;margin-top:.55rem;overflow:hidden}
.kpi-bar-fill{height:100%;border-radius:3px;transition:width .6s ease}
.kpi-sections{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:.5rem;margin-top:.55rem}
.kpi-sec{background:var(--bg2);border:1px solid var(--bd);border-radius:10px;padding:.6rem .75rem;transition:all .2s}
.kpi-sec:hover{border-color:var(--bdf);box-shadow:var(--sh1)}
.kpi-sec-name{font-size:.68rem;font-weight:700;color:var(--t2);margin-bottom:.3rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.kpi-sec-row{display:flex;gap:.5rem;font-size:.64rem;font-family:var(--fm)}
.kpi-sec-row .s-ok{color:var(--gn)}.kpi-sec-row .s-fail{color:var(--rd)}.kpi-sec-row .s-rev{color:var(--yl)}
/* ── 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 History Panel (inside chat/terraform) ── */
.ch-hp{width:0;overflow:hidden;transition:width .25s ease;background:var(--bg1);border-right:0 solid var(--bd);flex-shrink:0;order:0}
.ch-hp.open{width:260px;border-right-width:1px}
.ch-hp-inner{width:260px;display:flex;flex-direction:column;height:100%;box-sizing:border-box}
.ch-hp-hdr{display:flex;align-items:center;justify-content:space-between;padding:.55rem .7rem;border-bottom:1px solid var(--bd);font-size:.74rem;font-weight:700;color:var(--t2)}
.ch-hp-new{font-size:.72rem;font-weight:600;color:var(--ac);cursor:pointer;padding:.35rem .7rem;border:1px dashed var(--ac);border-radius:6px;transition:background .15s}
.ch-hp-new:hover{background:var(--acl)}
.ch-hp-list{overflow-y:auto;padding:.3rem .45rem;flex:1}
.ch-hp-grp{font-size:.58rem;font-weight:700;color:var(--t4);text-transform:uppercase;letter-spacing:.06em;padding:.5rem .25rem .15rem}
.ch-hp-item{display:flex;align-items:center;gap:.3rem;padding:.4rem .5rem;border-radius:8px;cursor:pointer;font-size:.72rem;color:var(--t2);transition:background .15s;margin-bottom:1px}
.ch-hp-item:hover{background:var(--bg3)}
.ch-hp-item.active{background:var(--acl2);color:var(--ac);font-weight:600}
.ch-hp-ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ch-hp-del{opacity:0;transition:opacity .15s;cursor:pointer;color:var(--t4);flex-shrink:0;display:flex;align-items:center;padding:2px}
.ch-hp-del:hover{color:var(--rd)}
.ch-hp-time{font-size:.58rem;color:var(--t4);flex-shrink:0;font-variant-numeric:tabular-nums}
.ch-hp-item:hover .ch-hp-del{opacity:1}
/* ── Chat ── */
.ch-c{display:flex;height:calc(100vh - 56px - 3rem);overflow:hidden;border:1px solid var(--bd);border-radius:var(--rl);box-shadow:var(--sh2)}
.ch-side{width:0;overflow:hidden;transition:width .3s cubic-bezier(.4,0,.2,1);background:var(--bg1);border-left:0 solid var(--bd);flex-shrink:0;order:2}
.ch-side.open{width:300px;border-left-width:1px}
.ch-side-inner{width:300px;padding:.85rem;overflow-y:auto;height:100%;box-sizing:border-box}
.ch-side-title{font-weight:700;font-size:.82rem;padding-bottom:.5rem;border-bottom:1px solid var(--bd);margin-bottom:.65rem;display:flex;align-items:center;gap:.4rem}
.ch-side-section{margin-bottom:.85rem}
.ch-side-section label{font-weight:600;color:var(--t3);font-size:.68rem;display:block;margin-bottom:3px}
.ch-side-section input[type="number"],.ch-side-section select{width:100%;padding:.32rem .5rem;font-size:.74rem;border:1px solid var(--bd);border-radius:6px;background:var(--bg2);box-sizing:border-box}
.ch-side .g2{display:grid;grid-template-columns:1fr 1fr;gap:.5rem}
.ch-main{flex:1;display:flex;flex-direction:column;min-width:0;order:1}
.ch-t{display:flex;gap:.5rem;align-items:center;padding:.55rem .85rem;background:var(--bg1);
border-bottom:1px solid var(--bd);font-size:.74rem;flex-wrap:wrap}
.ch-t select{max-width:260px;font-size:.74rem;padding:.35rem .5rem}
.ch-icon-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;border:1px solid var(--bd);background:var(--bg1);font-size:.82rem;transition:all .2s;flex-shrink:0}
.ch-icon-btn:hover{border-color:var(--ac);color:var(--ac)}
.ch-icon-btn.active{background:var(--pl);border-color:var(--ac);color:var(--ac)}
.mdrop{position:relative;display:inline-block}
.mdrop-btn{background:var(--bg2);border:1px solid var(--bd);border-radius:8px;padding:.35rem .6rem;font-size:.74rem;cursor:pointer;max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:center;gap:.3rem;min-width:180px}
.mdrop-btn:hover{border-color:var(--ac)}
.mdrop-btn .arr{font-size:.55rem;margin-left:auto;opacity:.5}
.mdrop-dd{position:absolute;top:100%;left:0;z-index:100;background:var(--bg1);border:1px solid var(--bd);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);margin-top:4px;min-width:320px;max-width:400px;display:none}
.mdrop-dd.open{display:block}
.mdrop-dd input{width:100%;padding:.45rem .6rem;border:none;border-bottom:1px solid var(--bd);font-size:.74rem;background:var(--bg2);border-radius:10px 10px 0 0;outline:none;box-sizing:border-box}
.mdrop-list{max-height:220px;overflow-y:auto;padding:.25rem 0}
.mdrop-list .grp{font-size:.64rem;font-weight:700;color:var(--t3);padding:.4rem .7rem .15rem;text-transform:uppercase;letter-spacing:.04em}
.mdrop-list .itm{padding:.3rem .7rem;font-size:.74rem;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mdrop-list .itm:hover{background:var(--bg3)}
.mdrop-list .itm.sel{background:var(--pl);color:var(--p);font-weight:600}
.ch-t label{font-weight:600;color:var(--t3);font-size:.72rem}
.ch-m{flex:1;overflow-y:auto;padding:1rem;background:var(--bg2)}
.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)}
.cm-ts{font-size:.65rem;color:var(--t4);margin-top:3px;opacity:.7}
.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-top:1px solid var(--bd);box-shadow:0 -2px 8px rgba(0,0,0,.02)}
.ch-i textarea,.ch-i input{flex:1;background:var(--bg2);border-radius:10px;font-family:inherit;font-size:inherit;line-height:1.4;padding:.45rem .65rem;border:1.5px solid var(--bd);min-height:36px;max-height:200px}
.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:var(--bg1);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,.12) 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,.08) 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-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 .eic svg{width:48px;height:48px}
.emp p{line-height:1.55}
/* ── Explorer ── */
.exp-wrap{display:flex;height:calc(100vh - 56px - 3rem);border:1px solid var(--bd);border-radius:var(--rl);overflow:hidden;box-shadow:var(--sh2)}
.exp-tree{width:240px;flex-shrink:0;background:var(--bg1);border-right:none;overflow-y:auto;padding:.5rem 0;min-width:140px;max-width:50%}
.exp-resize{width:5px;flex-shrink:0;cursor:col-resize;background:var(--bd);transition:background .15s;position:relative}
.exp-resize:hover,.exp-resize.dragging{background:var(--ac);opacity:.6}
.exp-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:3px;height:28px;border-left:1px solid var(--t4);border-right:1px solid var(--t4);opacity:.4}
.exp-resize:hover::after,.exp-resize.dragging::after{border-color:var(--ac);opacity:.8}
.exp-main{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
.exp-toolbar{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg);flex-wrap:wrap}
.exp-toolbar select{font-size:.74rem;padding:.3rem .5rem;border-radius:8px;border:1px solid var(--bd);background:var(--bg2);color:var(--t1)}
.exp-nav{display:flex;border-bottom:1px solid var(--bd);background:var(--bg);flex-shrink:0}
.exp-cats{display:flex;overflow-x:auto;gap:0;flex:1}
.exp-cat{padding:.45rem .7rem;font-size:.66rem;font-weight:600;cursor:pointer;color:var(--t4);white-space:nowrap;border-bottom:2px solid transparent;transition:all .15s;text-transform:uppercase;letter-spacing:.3px}
.exp-cat:hover{color:var(--t2);background:var(--bg2)}
.exp-cat.active{color:var(--ac);border-bottom-color:var(--ac);background:var(--acl)}
.exp-tabs{display:flex;gap:3px;padding:.3rem .75rem;border-bottom:1px solid var(--bd);background:var(--bg2);overflow-x:auto;flex-shrink:0}
.exp-tab{padding:.3rem .6rem;font-size:.7rem;border-radius:8px;cursor:pointer;color:var(--t3);white-space:nowrap;transition:all .15s;border:1px solid transparent}
.exp-tab:hover{background:var(--bg1);color:var(--t1);border-color:var(--bd)}
.exp-tab.active{background:var(--bg1);color:var(--ac);font-weight:600;border-color:var(--ac);box-shadow:0 1px 3px rgba(199,70,52,.1)}
.exp-content{flex:1;overflow-y:auto;padding:.75rem}
.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:.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);overflow:hidden;word-wrap:break-word;overflow-wrap:break-word;min-width:0;box-sizing:border-box}
.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;display:block;overflow:hidden;text-overflow:ellipsis}
.exp-c .em{font-family:var(--fm);font-size:.65rem;color:var(--t4);word-break:break-all;overflow-wrap:break-word;display:inline-block;max-width:100%}
.exp-node{padding:.15rem 0;font-size:.74rem;color:var(--t2)}
.exp-node-row{display:flex;align-items:center;gap:.25rem;padding:.25rem .5rem;border-radius:6px;cursor:pointer;transition:all .15s}
.exp-node-row:hover{background:var(--bg2)}
.exp-node-row.selected{background:var(--acl);color:var(--ac);font-weight:600;border-left:2px solid var(--ac)}
.exp-node-toggle{width:16px;text-align:center;font-size:.55rem;color:var(--t4);flex-shrink:0;cursor:pointer;transition:transform .15s}
.exp-node-children{padding-left:.85rem;border-left:1px solid var(--bg4);margin-left:.65rem}
/* ── Animations ── */
@keyframes fi{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
@keyframes fiScale{from{opacity:0;transform:scale(.96) translateY(6px)}to{opacity:1;transform:scale(1) translateY(0)}}
@keyframes slideIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
@keyframes alertIn{from{opacity:0;transform:translateY(-6px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.4}}
@keyframes spin{to{transform:rotate(360deg)}}
@keyframes pulseGlow{0%,100%{box-shadow:0 0 0 0 rgba(199,70,52,.15)}50%{box-shadow:0 0 0 6px rgba(199,70,52,0)}}
.fi{animation:fi .35s var(--trans) both}
.ld{animation:pu 1.2s ease infinite;color:var(--t4);font-size:.82rem;text-align:center;padding:1.5rem}
.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}
/* Page content fade-in — only on tab switch (class .tab-enter added by switchTab, removed after animation) */
.tab-enter>.cd,.tab-enter>.kpi-row,.tab-enter>.exp-wrap,.tab-enter>.ch-c,.tab-enter>div{animation:fi .4s var(--trans) both}
.tab-enter>.cd:nth-child(2),.tab-enter>div:nth-child(2){animation-delay:.05s}
.tab-enter>.cd:nth-child(3),.tab-enter>div:nth-child(3){animation-delay:.1s}
.tab-enter>.cd:nth-child(4),.tab-enter>div:nth-child(4){animation-delay:.15s}
/* Alert slide-in */
.al{padding:.65rem .85rem;border-radius:10px;font-size:.78rem;margin-bottom:.75rem;font-weight:500;animation:alertIn .3s var(--trans) both}
/* KPI card stagger — only on tab switch */
.tab-enter .kpi{animation:fiScale .4s var(--trans) both}
.tab-enter .kpi:nth-child(2){animation-delay:.06s}
.tab-enter .kpi:nth-child(3){animation-delay:.12s}
.tab-enter .kpi:nth-child(4){animation-delay:.18s}
/* Enhanced card hover */
.cd{transition:box-shadow .3s var(--trans),transform .3s var(--trans),border-color .3s var(--trans)}
.cd:hover{box-shadow:var(--sh2);transform:translateY(-1px);border-color:var(--bdf)}
/* Button press effect */
.btn{transition:all .2s var(--trans);position:relative;overflow:hidden}
.btn:active:not(:disabled){transform:scale(.97)}
.bp:active:not(:disabled){transform:scale(.97);box-shadow:0 1px 4px rgba(199,70,52,.2)}
/* Skeleton loading */
.skel{background:linear-gradient(90deg,var(--bg3) 25%,var(--bg4) 50%,var(--bg3) 75%);background-size:200% 100%;animation:shimmer 1.5s ease infinite;border-radius:8px}
/* Badge hover */
.badge{transition:transform .15s var(--trans),box-shadow .15s var(--trans)}
.badge:hover{transform:scale(1.05)}
/* Table row slide-in — only on tab switch */
.tab-enter tbody tr{animation:fi .3s var(--trans) both}
.tab-enter tbody tr:nth-child(2){animation-delay:.03s}
.tab-enter tbody tr:nth-child(3){animation-delay:.06s}
.tab-enter tbody tr:nth-child(4){animation-delay:.09s}
.tab-enter tbody tr:nth-child(5){animation-delay:.12s}
/* Explorer card stagger — only on tab switch */
.tab-enter .exp-c{animation:fiScale .3s var(--trans) both}
.tab-enter .exp-c:nth-child(2){animation-delay:.04s}
.tab-enter .exp-c:nth-child(3){animation-delay:.08s}
.tab-enter .exp-c:nth-child(4){animation-delay:.12s}
.tab-enter .exp-c:nth-child(5){animation-delay:.16s}
.tab-enter .exp-c:nth-child(6){animation-delay:.2s}
/* Nav item hover glow */
.ni:hover{background:var(--bg2);color:var(--t2);transform:translateX(2px)}
.ni.on{animation:slideIn .3s var(--trans) both}
/* Smooth theme transition */
html{transition:background .4s ease,color .3s ease}
html *{transition-property:background-color,border-color,color,box-shadow,opacity;transition-duration:.25s;transition-timing-function:var(--trans)}
html input,html select,html textarea,html button,html .btn,html .cd,html .kpi,html .exp-c,html .ni,html .badge,html .lo-btn,html .ch-hp,html .ch-side{transition-duration:.25s}
html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important}
/* Chat bubble animation */
.ch-msg{animation:fi .3s var(--trans) both}
/* Dropdown animation */
.mdrop-dd,.mdrop-list{animation:fiScale .2s var(--trans) both}
/* Empty state subtle bounce */
.emp .eic{animation:fi .5s var(--trans) both}
/* Active tab glow */
.exp-cat.active,.exp-tab.active{transition:all .2s var(--trans)}
/* Theme toggle spin */
.lo-btn svg{transition:transform .4s var(--trans)}
.lo-btn:hover svg{transform:rotate(25deg)}
/* ── 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}
.exp-wrap{flex-direction:column;height:auto;min-height:calc(100vh - 56px - 3rem)}
.exp-tree{width:100%;max-height:180px;border-right:none;border-bottom:1px solid var(--bd)}
.exp-r{grid-template-columns:1fr}
.exp-cats{flex-wrap:wrap}
.exp-cat{padding:.3rem .5rem;font-size:.6rem}
}
/* ── Terraform Agent ── */
.tf-wrap{display:flex;flex-direction:row;height:calc(100vh - 56px - 3rem);overflow:hidden}
.tf-left{flex:6;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.tf-chat{flex:7;display:flex;flex-direction:column;overflow:hidden;min-height:0}
.tf-output-panel{flex:4;display:flex;flex-direction:column;overflow:hidden;border-left:1px solid var(--bd);min-width:0}
.tf-toolbar{display:flex;align-items:center;gap:.5rem;padding:.45rem .7rem;border-bottom:1px solid var(--bd);flex-wrap:wrap;background:var(--bg)}
.tf-msgs{flex:1;overflow-y:auto;padding:.7rem}
.tfp-acts{display:flex;gap:.35rem;margin-top:.5rem;padding-top:.45rem;border-top:1px solid var(--bd)}
.tfp-act{display:inline-flex;align-items:center;gap:.2rem;padding:.2rem .45rem;font-size:.62rem;border-radius:5px;border:1px solid var(--bd);background:var(--bg2);color:var(--t2);cursor:pointer;transition:all .15s;font-family:inherit;line-height:1.3}
.tfp-act svg{width:11px;height:11px}
.tfp-act:hover{background:var(--bg3);border-color:var(--t4)}
.tfp-act-tf{background:#7b42bc18;border-color:#7b42bc55;color:#7b42bc}
.tfp-act-tf:hover{background:#7b42bc30;border-color:#7b42bc}
.tf-input{display:flex;gap:.5rem;padding:.5rem .7rem;border-top:1px solid var(--bd)}
.tf-input textarea,.tf-input input{flex:1;font-family:inherit;font-size:inherit;line-height:1.4;padding:.45rem .65rem;border:1.5px solid var(--bd);border-radius:10px;background:var(--bg2);min-height:36px;max-height:200px}
.tf-resize{height:5px;cursor:row-resize;background:var(--bd);transition:background .15s;position:relative;flex-shrink:0}
.tf-resize:hover,.tf-resize.dragging{background:var(--pp);opacity:.7}
.tf-resize::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:28px;height:3px;border-top:1px solid var(--t4);border-bottom:1px solid var(--t4);opacity:.4}
.tf-resize:hover::after{border-color:var(--pp);opacity:.8}
.tf-bottom{flex:3;display:flex;flex-direction:column;overflow:hidden;border-top:1px solid var(--bd);background:var(--bg1);min-height:0}
.tf-bottom-tabs{display:flex;align-items:center;gap:0;border-bottom:1px solid var(--bd);background:var(--bg);padding:0 .5rem}
.tf-btab{padding:.4rem .75rem;font-size:.72rem;font-weight:600;cursor:pointer;border-bottom:2px solid transparent;color:var(--t3);display:flex;align-items:center;gap:.3rem;transition:all .15s}
.tf-btab:hover{color:var(--t1)}
.tf-btab.active{color:var(--pp);border-bottom-color:var(--pp)}
.tf-btab svg{width:14px;height:14px}
.tf-bottom-actions{margin-left:auto;display:flex;align-items:center;gap:.4rem;padding-right:.3rem}
.tf-bottom-content{flex:1;overflow:auto;padding:.5rem .7rem}
.tf-output-panel .tf-term-hdr{padding:.3rem .6rem;font-size:.68rem;font-weight:700;color:var(--t2);background:var(--bg);border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:.4rem}
.tf-output-panel .tf-term-hdr svg{width:12px;height:12px;fill:var(--t2)}
.tf-output-panel .tf-term-body{flex:1;overflow:auto;background:var(--bg);padding:.5rem;font-family:var(--fm);font-size:.68rem;line-height:1.5;color:var(--t2);white-space:pre-wrap;word-break:break-word}
.tf-file{display:flex;align-items:center;gap:.5rem;padding:.45rem .6rem;border-radius:8px;border:1px solid var(--bd);margin-bottom:.4rem;background:var(--bg);transition:border-color .15s;cursor:pointer}
.tf-file:hover{border-color:var(--pp)}
.tf-file.editing{border-color:var(--pp);background:rgba(191,90,242,.06)}
.tf-editor{border:1px solid var(--bd);border-radius:var(--r);overflow:hidden;margin-bottom:.5rem;background:var(--bg2)}
.tf-editor-hdr{display:flex;align-items:center;gap:.4rem;padding:.35rem .6rem;background:var(--bg3);border-bottom:1px solid var(--bd);font-size:.7rem;font-weight:600;color:var(--pp)}
.tf-editor-hdr .tf-ed-name{flex:1;font-family:var(--fm)}
.tf-editor-hdr .btn{font-size:.62rem;padding:2px 8px}
.tf-editor-ta{width:100%;min-height:120px;max-height:400px;border:none;outline:none;resize:vertical;font-family:var(--fm);font-size:.72rem;line-height:1.6;color:var(--t1);background:var(--bg2);padding:.6rem .7rem;box-sizing:border-box;tab-size:2}
.tf-file-icon{width:32px;height:32px;border-radius:6px;background:linear-gradient(135deg,rgba(191,90,242,.15),rgba(191,90,242,.08));display:flex;align-items:center;justify-content:center;flex-shrink:0}
.tf-file-icon svg{width:16px;height:16px;fill:var(--pp)}
.tf-file-info{flex:1;min-width:0}
.tf-file-name{font-size:.76rem;font-weight:600;font-family:var(--fm);color:var(--t1)}
.tf-file-meta{font-size:.64rem;color:var(--t3)}
.tf-file-actions{display:flex;gap:.3rem}
.tf-file-btn{width:26px;height:26px;border-radius:6px;border:1px solid var(--bd);background:var(--bg);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:.7rem;transition:all .15s}
.tf-file-btn:hover{border-color:var(--pp);background:rgba(191,90,242,.07)}
.tf-code-block{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);overflow:hidden;margin:.6rem 0}
.tf-code-header{display:flex;align-items:center;gap:.4rem;padding:.4rem .7rem;background:var(--bg3);font-size:.72rem;color:var(--pp);border-bottom:1px solid var(--bd)}
.tf-code-header span{flex:1;font-weight:600}
.tf-code-header .btn{color:var(--pp);border-color:var(--bd);background:transparent;font-size:.66rem;padding:2px 8px}
.tf-code-header .btn:hover{background:var(--bg4)}
.tf-pre{padding:.85rem;overflow-x:auto;font-size:.74rem;line-height:1.6;color:var(--t2);font-family:var(--fm);margin:0;white-space:pre}
.tf-plan-block{background:var(--gnl);border:1px solid rgba(22,163,74,.15);border-radius:var(--r);margin:.6rem 0;overflow:hidden}
.tf-plan-header{padding:.45rem .7rem;font-weight:700;font-size:.76rem;color:var(--gn);border-bottom:1px solid rgba(22,163,74,.12)}
.tf-plan-list{padding:.4rem .7rem}
.tf-plan-item{display:flex;align-items:baseline;gap:.5rem;padding:.25rem 0;font-size:.74rem}
.tf-plan-add{color:var(--gn);font-weight:700;font-family:var(--fm)}
.tf-plan-type{font-family:var(--fm);font-weight:600;color:var(--t1)}
.tf-plan-desc{color:var(--t3);font-size:.68rem}
.tf-badge{font-size:.58rem;padding:1px 6px;border-radius:10px;font-weight:700;display:inline-flex;align-items:center;gap:3px}
.tf-badge-draft{background:rgba(191,90,242,.15);color:var(--pp)}
.tf-badge-ok{background:var(--gnl);color:var(--gn)}
.tf-badge-run{background:var(--bll);color:var(--bl)}
.tf-badge-err{background:var(--rdl);color:var(--rd)}
.tf-badge-dest{background:var(--t4);color:var(--bg)}
.tf-menu{position:absolute;top:100%;right:0;background:var(--bg);border:1px solid var(--bd);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12);min-width:220px;z-index:50;padding:.3rem 0;margin-top:4px}
.tf-menu-item{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;font-size:.72rem;color:var(--t1);cursor:pointer;transition:background .12s}
.tf-menu-item:hover{background:var(--bg2)}
.tf-menu-item svg{flex-shrink:0;color:var(--t3)}
.tf-menu-sub{font-size:.6rem;color:var(--t3);margin-left:auto;white-space:nowrap}
.tf-menu-item.loading{opacity:.6;pointer-events:none}
.tf-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:100}
.tf-modal{background:var(--bg);border-radius:var(--r);padding:1.5rem;max-width:420px;width:90%;box-shadow:0 8px 30px rgba(0,0,0,.25)}
.tf-modal.wide{max-width:720px}
.tf-prompt-ta{width:100%;min-height:320px;max-height:60vh;background:var(--bg2);color:var(--t1);border:1px solid var(--bd);border-radius:6px;padding:.6rem;font-family:monospace;font-size:.72rem;line-height:1.5;resize:vertical;box-sizing:border-box}
.tf-prompt-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.6rem}
.tf-prompt-header h3{margin:0;color:var(--ac);font-size:.82rem}
.tf-prompt-saved{font-size:.62rem;color:var(--t3);transition:opacity .3s}
.tf-prompt-name{background:var(--bg2);color:var(--t1);border:1px solid var(--bd);border-radius:4px;padding:.25rem .4rem;font-size:.72rem;width:100%;margin-bottom:.5rem;box-sizing:border-box}
.tf-modal h3{margin:0 0 .5rem;color:var(--rd)}
.tf-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--t3);font-size:.76rem;gap:.3rem}
.tf-empty svg{width:40px;height:40px;fill:#7b42bc;opacity:.3}
@media(max-width:768px){
.tf-wrap{flex-direction:column}
.tf-left{flex:none;height:60%}
.tf-output-panel{flex:none;height:40%;border-left:none;border-top:1px solid var(--bd)}
.tf-bottom{min-height:0}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const V='1.1';
// Theme: restore from localStorage, default to light
(function(){const t=localStorage.getItem('theme')||'light';document.documentElement.className=t})();
function toggleTheme(){const h=document.documentElement;const isDark=h.classList.contains('dark');h.className=isDark?'light':'dark';localStorage.setItem('theme',isDark?'light':'dark');R()}
// SVG Icon system (Lucide-based, 16x16, currentColor)
const _i=(d,s=16)=>`<svg viewBox="0 0 24 24" width="${s}" height="${s}" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-3px;flex-shrink:0">${d}</svg>`;
const IC={
chat:_i('<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/>'),
search:_i('<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>'),
chart:_i('<path d="M18 20V10M12 20V4M6 20v-6"/>'),
folder:_i('<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>'),
cloud:_i('<path d="M18 10h-1.26A8 8 0 109 20h9a5 5 0 000-10z"/>'),
brain:_i('<path d="M12 5a3 3 0 10-5.997.125 4 4 0 00-2.526 5.77 4 4 0 00.556 6.588A4 4 0 1012 18Z"/><path d="M12 5a3 3 0 115.997.125 4 4 0 012.526 5.77 4 4 0 01-.556 6.588A4 4 0 1112 18Z"/><path d="M12 5v13M9.5 8.5L12 12l2.5-3.5M8 14.5l4-2.5 4 2.5"/>'),
plug:_i('<path d="M12 22v-5M9 7V2M15 7V2M5 7h14v4a7 7 0 01-14 0V7z"/>'),
db:_i('<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>'),
dna:_i('<path d="M2 15c6.667-6 13.333-6 20 0M2 9c6.667 6 13.333 6 20 0"/><path d="M7 21V3M17 21V3"/>'),
users:_i('<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/>'),
lock:_i('<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>'),
log:_i('<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>'),
ok:_i('<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>'),
err:_i('<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>'),
warn:_i('<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>'),
wait:_i('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>'),
timeout:_i('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/><path d="M3 12h1M20 12h1"/>'),
settings:_i('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/>'),
edit:_i('<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>'),
plus:_i('<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>'),
upload:_i('<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>'),
download:_i('<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>'),
bot:_i('<rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4M2 13h2M20 13h2"/><line x1="9" y1="15" x2="9" y2="15.01"/><line x1="15" y1="15" x2="15" y2="15.01"/>'),
wrench:_i('<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>'),
clock:_i('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>'),
history:_i('<path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 106 5.3L3 8"/>'),
file:_i('<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/>'),
spark:_i('<path d="M12 2L9 12l8-4-14 2 5 10 3-10-8 4 14-2z"/>'),
clip:_i('<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.49"/>'),
key:_i('<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.78 7.78 5.5 5.5 0 017.78-7.78zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>'),
building:_i('<rect x="4" y="2" width="16" height="20" rx="2" ry="2"/><path d="M9 22v-4h6v4M8 6h.01M16 6h.01M12 6h.01M12 10h.01M12 14h.01M16 10h.01M16 14h.01M8 10h.01M8 14h.01"/>'),
user:_i('<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/>'),
pkg:_i('<path d="M16.5 9.4l-9-5.19M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>'),
globe:_i('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>'),
python:_i('<path d="M12 2C6.5 2 6 4.5 6 4.5V7h6v1H4.5S2 7.5 2 12s2 5 2 5h2v-3s0-2 2-2h6s2 0 2-2V5s0-3-4-3zm-2 2a1 1 0 110 2 1 1 0 010-2z"/><path d="M12 22c5.5 0 6-2.5 6-2.5V17h-6v-1h7.5s2.5.5 2.5-4-2-5-2-5h-2v3s0 2-2 2h-6s-2 0-2 2v5s0 3 4 3zm2-2a1 1 0 110-2 1 1 0 010 2z"/>'),
terminal:_i('<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
rocket:_i('<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 012-3.95A12.88 12.88 0 0122 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 01-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>'),
fix:_i('<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>'),
refresh:_i('<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>'),
shield:_i('<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>'),
eye:_i('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'),
scroll:_i('<path d="M8 21h12a2 2 0 002-2v-2H10v2a2 2 0 01-2 2zm0 0a2 2 0 01-2-2V5a2 2 0 012-2h12v14"/>'),
link:_i('<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/>'),
sun:_i('<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>'),
moon:_i('<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>'),
power:_i('<path d="M18.36 6.64a9 9 0 11-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/>'),
zap:_i('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'),
barChart:_i('<line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/>'),
computer:_i('<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>'),
hdd:_i('<line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/>'),
container:_i('<path d="M22 7L12 2 2 7v10l10 5 10-5V7z"/><path d="M2 7l10 5 10-5M12 22V12"/>'),
activity:_i('<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>'),
}
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:[],models:{},regions:[],embModels:{},expData:null,expCfg:'',expSelRegions:[],expRegDdOpen:false,expTree:[],expSelComp:'',expCat:'Compute',expResType:'instances',expTreeOpen:{},expLoading:false,expRegions:[],expCounts:{},expTreeW:null,editing:null,
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',chatHistOpen:false,chatHistory:[],tfHistOpen:false,tfHistory:[],
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0,reasoning_effort:'medium'},chatPanel:'',chatUseTools:true,
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',dlSectionFilter:'',
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',rptKpi:null,rptKpiLoading:false,chatFiles:[],
tfMsgs:[],tfSid:null,tfModel:'',tfOci:'',tfRegion:'',tfCompartment:'',tfPlan:[],tfCode:'',tfPanel:'',
tfWs:null,tfWsList:[],tfPlanOut:'',tfApplyOut:'',tfDestroyOut:'',tfStatus:'draft',tfRunning:false,tfHadApply:false,tfConfirmDest:false,tfConfirmRollback:false,tfMdOpen:false,
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null,
tfpMsgs:[],tfpLoading:false,tfpCopied:false,tfpModel:'',tfpMdOpen:false,tfpOci:'',tfpRegion:'',tfpCompartment:'',
tfpHistOpen:false,tfpHistory:[],tfpSid: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 ct=r.headers.get('content-type')||'';
if(!ct.includes('application/json')){const txt=await r.text();throw new Error(r.ok?'Resposta inesperada do servidor':`HTTP ${r.status}: ${txt.slice(0,120).replace(/<[^>]*>/g,'').trim()||'Erro do servidor'}`)}
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();
const _pg=document.getElementById('pg');if(_pg){const _pc=_pg.querySelector('.pc');if(_pc){_pc.classList.add('tab-enter');setTimeout(()=>_pc.classList.remove('tab-enter'),500)}}
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||{};S.ociRegions=m.oci_regions||{}}catch(e){}
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
try{S.chatPrompts=await $api('/prompts/chat')}catch(e){S.chatPrompts=[]}
if(!S.chatModel){const dc=S.genaiCfg.find(g=>g.is_default);if(dc)S.chatModel='cfg:'+dc.id;else if(S.models['openai.gpt-4.1'])S.chatModel='openai.gpt-4.1'}
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
try{S.cisVer=await $api('/cis-engine/version')}catch(e){}
loadHistory('chat');
}catch(e){console.error(e)}}
function R(){try{
// Preserve inputs
const chiEl=document.getElementById('chi');if(chiEl)S._chiVal=chiEl.value;
const tfiEl=document.getElementById('tfi');if(tfiEl)S._tfiVal=tfiEl.value;
const edEl=document.getElementById('tfEdTa');if(edEl)S._tfEdVal=edEl.value;
// Preserve scroll positions
const scrollIds=['chm','tfchm','tfBottom','tfTermOut','pg'];
const scrollMap={};
scrollIds.forEach(id=>{const el=document.getElementById(id);if(el)scrollMap[id]=el.scrollTop});
const bcLeft=document.querySelector('.tf-bottom-content');if(bcLeft)scrollMap._bcContent=bcLeft.scrollTop;
const appEl=document.getElementById('app');
if(!S.user){appEl.innerHTML=rLogin();return}
// Partial update: only update page content + sidebar
const pgEl=document.getElementById('pg');
if(pgEl){
pgEl.innerHTML=rPg();
const sbEl=document.querySelector('.nav');if(sbEl){const tmp=document.createElement('div');tmp.innerHTML=rSb();const nn=tmp.querySelector('.nav');if(nn)sbEl.innerHTML=nn.innerHTML}
}else{
appEl.innerHTML=rApp();
}
// Restore inputs
const chi2=document.getElementById('chi');if(chi2&&S._chiVal){chi2.value=S._chiVal;autoGrow(chi2)}
const tfi2=document.getElementById('tfi');if(tfi2&&S._tfiVal){tfi2.value=S._tfiVal;autoGrow(tfi2)}
const ed2=document.getElementById('tfEdTa');if(ed2&&S._tfEdVal)ed2.value=S._tfEdVal;
// Restore scroll positions
requestAnimationFrame(()=>{
Object.entries(scrollMap).forEach(([id,top])=>{if(id==='_bcContent'){const el=document.querySelector('.tf-bottom-content');if(el)el.scrollTop=top}else{const el=document.getElementById(id);if(el)el.scrollTop=top}});
if(S.tfBottomH){const bel=document.getElementById('tfBottom');if(bel){bel.style.height=S.tfBottomH+'px';bel.style.flex='none'}}
if(S.expTreeW){const tel=document.getElementById('exp-tree');if(tel)tel.style.width=S.expTreeW+'px'}
});
if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)
}catch(e){console.error('Render error:',e);document.getElementById('app').innerHTML='<div style="padding:2rem;color:red"><h3>Render Error</h3><pre>'+e.message+'</pre></div>'}}
function switchTab(t){S.tab=t;R();
const pg=document.getElementById('pg');if(pg){const pc=pg.querySelector('.pc');if(pc){pc.classList.add('tab-enter');setTimeout(()=>pc.classList.remove('tab-enter'),500)}}
if(t==='chat')loadHistory('chat');if(t==='terraform')loadHistory('terraform');if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();if(t==='explorer'&&S.ociCfg.length&&!S.expTree.length){if(!S.expCfg)S.expCfg=S.ociCfg[0].id;expLoadTree();expLoadRegions()}
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" id="pg">${rPg()}</div></div></div>`}
const TF_IC='<svg viewBox="0 0 16 16" width="14" height="14" style="vertical-align:-2px;margin-right:2px"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>';
function rSb(){
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['tf-prompt',IC.edit,'Prompt Generator','sub'],['explorer',IC.search,'OCI Explorer'],['report',IC.chart,'Reports'],['downloads',IC.folder,'Downloads']];
const ctabs=[['oci-config',IC.cloud,'Credenciais OCI'],['genai',IC.brain,'GenAI Config'],['mcp',IC.plug,'MCP Servers'],['adb',IC.db,'ADB Vector'],['embeddings',IC.dna,'Embeddings'],['emb-consult',IC.search,'Consultar Embeddings','sub']];
const atabs=[['users',IC.users,'Usuários'],['mfa',IC.lock,'MFA'],['audit',IC.log,'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]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><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]}')" ${t[3]==='sub'?'style="padding-left:2.2rem;font-size:.78rem;opacity:.9"':''}><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="toggleTheme()" title="Alternar tema">${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}</div>
<div class="lo-btn" onclick="doLogout()" title="Sair">⏻</div></div></div></div>`}
function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','tf-prompt':IC.edit+' Prompt Generator','explorer':IC.search+' OCI Account Explorer','report':IC.chart+' Compliance Reports','downloads':IC.folder+' Downloads','oci-config':IC.cloud+' Credenciais OCI','genai':IC.brain+' OCI Generative AI','mcp':IC.plug+' MCP Servers','adb':IC.db+' Autonomous DB Vector','embeddings':IC.dna+' Embeddings','emb-consult':IC.search+' Consultar Embeddings','users':IC.users+' Gerenciar Usuários','mfa':IC.lock+' Autenticação MFA','audit':IC.log+' 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'terraform':return rTerraform();case'tf-prompt':return rTfPrompt();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'emb-consult':return rEmbConsultPage();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">${IC.bot}</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>`
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
const provs={};for(const[mid,info]of Object.entries(S.models)){const p=info.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:info.name})}
const provOrder=['openai','google','meta','xai'];
let ddItems='';
if(S.genaiCfg.length){ddItems+=`<div class="grp">Configs Salvas</div>`;S.genaiCfg.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.chatModel==='cfg:'+g.id?' sel':''}" onclick="pickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id} (${g.genai_region})</div>`})}
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p.charAt(0).toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.chatModel===m.id?' sel':''}" onclick="pickModel('${m.id}')">${m.name}</div>`})});
let curLabel='Selecione um modelo...';
if(S.chatModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.chatModel.slice(4));const mi=g?S.models[g.model_id]:null;curLabel=g?(g.name||mi?.name||g.model_id)+' ('+g.genai_region+')':'Config...'}
else if(S.chatModel){const mi=S.models[S.chatModel];curLabel=mi?mi.name:S.chatModel}
const isDirect=S.chatModel&&!S.chatModel.startsWith('cfg:');
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 ragBadge=S.adbCfg.some(c=>c.is_active)&&(S.genaiCfg.length||S.ociCfg.length)?'<span class="tag" style="background:var(--gnl);color:var(--gn);font-size:.62rem">RAG</span>':'';
const toolCount=S.mcpSvr.filter(m=>m.is_active&&Array.isArray(m.tools)&&m.tools.length).reduce((a,m)=>a+m.tools.length,0);
let sideContent='';
if(S.chatPanel==='config')sideContent=rChatConfig(isDirect,toolCount);
else if(S.chatPanel==='logs')sideContent=rChatLogs();
return`<div class="ch-c">
<div class="ch-hp${S.chatHistOpen?' open':''}"><div class="ch-hp-inner">
<div class="ch-hp-hdr"><span>${IC.history} Histórico</span><div class="ch-hp-new" onclick="newChat()">+ Novo Chat</div></div>
<div class="ch-hp-list">${rHistList('chat',S.chatHistory,S.sid)}</div>
</div></div>
<div class="ch-side${S.chatPanel?' open':''}"><div class="ch-side-inner">${sideContent}</div></div>
<div class="ch-main">
<div class="ch-t">
<div class="ch-icon-btn${S.chatHistOpen?' active':''}" onclick="S.chatHistOpen=!S.chatHistOpen;if(S.chatHistOpen)loadHistory('chat');R()" title="Histórico">☰</div>
<label>${IC.brain} Modelo:</label><div class="mdrop"><div class="mdrop-btn" onclick="toggleModelDrop()">${curLabel} <span class="arr">▼</span></div><div class="mdrop-dd" id="mdd"><input type="text" id="mds" placeholder="Buscar modelo..." oninput="filterModels(this.value)"><div class="mdrop-list" id="mdl">${ddItems}</div></div></div>
${ociSel}${ragBadge}<div style="flex:1"></div>
<div class="ch-icon-btn${S.chatPanel==='config'?' active':''}" onclick="togglePanel('config')" title="Configurações">${IC.settings}</div>
<div class="ch-icon-btn${S.chatPanel==='logs'?' active':''}" onclick="togglePanel('logs')" title="Chat Logs">${IC.log}</div>
<div class="ch-icon-btn" onclick="newChat()" title="Nova conversa">${IC.spark}</div></div>
<div class="ch-m" id="chm">${ms}</div>
${S.chatFiles.length?`<div style="display:flex;gap:.4rem;flex-wrap:wrap;padding:.4rem .85rem;background:var(--bg1);border-top:1px solid var(--bd)">${S.chatFiles.map((f,i)=>f.type==='image'?`<div style="position:relative;width:48px;height:48px"><img src="${f.preview}" style="width:48px;height:48px;object-fit:cover;border-radius:6px;border:1px solid var(--bd)"><span onclick="rmChatFile(${i})" style="position:absolute;top:-4px;right:-4px;background:var(--err);color:#fff;width:16px;height:16px;border-radius:50%;font-size:.6rem;display:flex;align-items:center;justify-content:center;cursor:pointer">&times;</span></div>`:`<div style="position:relative;display:flex;align-items:center;gap:.25rem;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;padding:.25rem .5rem;font-size:.68rem"><span>${IC.file} ${f.name}</span><span onclick="rmChatFile(${i})" style="color:var(--err);cursor:pointer;font-weight:700">&times;</span></div>`).join('')}</div>`:''}
<div class="ch-i"><input type="file" id="chf" multiple accept="image/*,.pdf,.txt,.csv,.json,.log,.xml,.yaml,.yml,.md,.py,.js,.html,.css,.sql" style="display:none" onchange="addChatFiles(this)">
<button class="btn bs" onclick="document.getElementById('chf').click()" title="Anexar arquivo" style="font-size:1rem;padding:.3rem .5rem">${IC.clip}</button>
<textarea id="chi" placeholder="Digite sua mensagem... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sChat()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
<button class="btn bp" onclick="sChat()">Enviar →</button></div></div></div>`}
function _getModelInfo(){
let mid='';
if(S.chatModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.chatModel.slice(4));if(g)mid=g.model_id}
else mid=S.chatModel;
return S.models[mid]||{}}
function _isReasoningModel(){return _getModelInfo().reasoning||false}
function rChatConfig(isDirect,toolCount){
const pp=S.chatParams;const mi=_getModelInfo();const pv=mi.provider||'';const isR=mi.reasoning||false;
const maxLim=mi.max_tokens||128000;
const hasTopK=pv==='meta'||pv==='google'||pv==='';
const hasPenalties=mi.penalties===true;
return`<div class="ch-side-title">${IC.settings} Configurações</div>
<div class="ch-side-section"><div class="g2">
${isR?`<div><label>Max Tokens <span style="font-size:.6rem;color:var(--t4)">(max ${maxLim.toLocaleString()})</span></label><input type="number" value="${pp.max_tokens}" min="1" max="${maxLim}" onchange="S.chatParams.max_tokens=+this.value"></div>
<div><label>Reasoning Effort</label><select onchange="S.chatParams.reasoning_effort=this.value"><option value="low"${pp.reasoning_effort==='low'?' selected':''}>Low</option><option value="medium"${pp.reasoning_effort==='medium'?' selected':''}>Medium</option><option value="high"${pp.reasoning_effort==='high'?' selected':''}>High</option></select></div>`
:`<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 <span style="font-size:.6rem;color:var(--t4)">(max ${maxLim.toLocaleString()})</span></label><input type="number" value="${pp.max_tokens}" min="1" max="${maxLim}" 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>
${hasTopK?`<div><label>Top K</label><input type="number" value="${pp.top_k}" min="-1" max="500" onchange="S.chatParams.top_k=+this.value"></div>`:''}
${hasPenalties?`<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>
<div class="ch-side-section" style="border-top:1px solid var(--bd);padding-top:.65rem">
<label style="display:flex;align-items:center;gap:.35rem;cursor:pointer;font-size:.76rem"><input type="checkbox" ${S.chatUseTools?'checked':''} onchange="S.chatUseTools=this.checked"> ${IC.plug} MCP Tools</label>
${toolCount?`<span style="font-size:.66rem;color:var(--t4);margin-top:.2rem;display:block">${toolCount} tools disponíveis</span>`:''}
</div>
${isDirect?`<div class="ch-side-section" style="border-top:1px solid var(--bd);padding-top:.65rem">
<label>Credencial OCI</label>
<select onchange="S.chatOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}" style="margin-top:3px">
<option value="">Selecionar...</option>${S.ociCfg.map(c=>`<option value="${c.id}" ${S.chatOci===c.id?'selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>`:''}`}
function rChatLogs(){
return`<div class="ch-side-title">${IC.log} Chat Logs</div>
<div style="display:flex;gap:.4rem;margin-bottom:.5rem">
<select id="cl-sev-chat" onchange="loadChatLogs()" style="flex:1;font-size:.7rem;padding:.25rem .4rem;border-radius:6px;border:1px solid var(--bd)">
<option value="">Todos</option><option value="error">Erros</option><option value="warning">Avisos</option><option value="info">Info</option></select>
<button class="btn bs bsm" onclick="loadChatLogs()" style="font-size:.68rem">↻</button></div>
<div id="chat-logs-body"><div style="padding:.5rem;font-size:.74rem;color:var(--t4)">Carregando...</div></div>`}
function toggleModelDrop(){const dd=document.getElementById('mdd');dd.classList.toggle('open');if(dd.classList.contains('open')){const inp=document.getElementById('mds');inp.value='';inp.focus();filterModels('')}}
function pickModel(v){S.chatModel=v;document.getElementById('mdd').classList.remove('open');
if(v&&!v.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 filterModels(q){const list=document.getElementById('mdl');const items=list.querySelectorAll('.itm,.grp');const ql=q.toLowerCase();
let lastGrp=null,grpHasVisible=false;
items.forEach(el=>{if(el.classList.contains('grp')){if(lastGrp)lastGrp.style.display=grpHasVisible?'':'none';lastGrp=el;grpHasVisible=false}
else{const show=!q||el.textContent.toLowerCase().includes(ql);el.style.display=show?'':'none';if(show)grpHasVisible=true}});
if(lastGrp)lastGrp.style.display=grpHasVisible?'':'none'}
document.addEventListener('click',e=>{const dd=document.getElementById('mdd');if(dd&&dd.classList.contains('open')&&!e.target.closest('.mdrop'))dd.classList.remove('open');
if(!e.target.closest('.dd')){let changed=false;if(S.rptOciOpen){S.rptOciOpen=false;changed=true}if(S.rptRselOpen){S.rptRselOpen=false;changed=true}if(S.ociFormRegOpen){S.ociFormRegOpen=false;changed=true}if(S.rptRegionsOpen){S.rptRegionsOpen=false;changed=true}if(S.expRegDdOpen){S.expRegDdOpen=false;changed=true}if(changed)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 togglePanel(p){S.chatPanel=S.chatPanel===p?'':p;R();if(S.chatPanel==='logs')loadChatLogs()}
function editPrompt(id){S.editingPrompt=id;R()}
async function savePrompt(){const name=document.getElementById('spname').value;const content=document.getElementById('gsysp').value;
if(!name||!content)return sm('spm','Preencha nome e conteúdo do prompt.','e');
try{if(S.editingPrompt){await $api('/prompts/'+S.editingPrompt,{method:'PUT',body:{name,content}})}
else{await $api('/prompts',{method:'POST',body:{agent:'chat',name,content,is_active:!S.chatPrompts.length}})}
S.editingPrompt=null;S.chatPrompts=await $api('/prompts/chat');sm('spm',IC.ok+' Prompt salvo!','s');R()}catch(e){sm('spm',e.message,'e')}}
async function activatePrompt(id){try{await $api('/prompts/'+id,{method:'PUT',body:{is_active:true}});S.chatPrompts=await $api('/prompts/chat');R()}catch(e){sm('spm',e.message,'e')}}
async function delPrompt(id){if(!confirm('Excluir prompt?'))return;try{await $api('/prompts/'+id,{method:'DELETE'});S.editingPrompt=null;S.chatPrompts=await $api('/prompts/chat');R()}catch(e){sm('spm',e.message,'e')}}
function fm(t){return t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>')}
function autoGrow(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,200)+'px'}
function addChatFiles(input){for(const f of input.files){if(S.chatFiles.length>=5){alert('Máximo 5 arquivos');break}
const entry={file:f,name:f.name,type:f.type.startsWith('image/')?'image':'file',preview:null};
if(entry.type==='image'){const r=new FileReader();r.onload=e=>{entry.preview=e.target.result;R()};r.readAsDataURL(f)}
S.chatFiles.push(entry)}input.value='';R()}
function rmChatFile(i){S.chatFiles.splice(i,1);R()}
function tstamp(){const d=new Date();return d.toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'})}
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();el.style.height='auto';S._chiVal='';
if(!m&&!S.chatFiles.length)return;
if(!S.chatModel){S.msgs.push({r:'assistant',c:IC.warn+' Selecione um modelo antes de enviar uma mensagem.'});R();return}
el.value='';
const hasFiles=S.chatFiles.length>0;
const fileNames=S.chatFiles.map(f=>f.name);
let userDisplay=m||'';
if(hasFiles)userDisplay+=(userDisplay?'\n':'')+IC.clip+' '+fileNames.join(', ');
S.msgs.push({r:'user',c:userDisplay,t:tstamp()});R();scCh();
const btns=document.querySelectorAll('.ch-i .btn');
el.disabled=true;btns.forEach(b=>{b.disabled=true});
const sendBtn=btns[btns.length-1];if(sendBtn){sendBtn.dataset.origText=sendBtn.textContent;sendBtn.innerHTML='<span class="spinner" style="width:14px;height:14px;border-width:2px;margin-right:6px"></span>Pensando...'}
S.msgs.push({r:'assistant',c:IC.wait+' _Pensando..._',thinking:true});R();scCh();
try{let d;
if(hasFiles){
const fd=new FormData();
fd.append('message',m||'Analise os arquivos anexados.');
if(S.sid)fd.append('session_id',S.sid);
fd.append('use_tools',S.chatUseTools);
if(S.chatModel.startsWith('cfg:')){fd.append('genai_config_id',S.chatModel.slice(4))}
else if(S.chatModel){
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:IC.warn+' Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;return}
fd.append('model_id',S.chatModel);fd.append('oci_config_id',S.chatOci);fd.append('genai_region',S.chatRegion);fd.append('compartment_id',S.chatCompartment)}
fd.append('temperature',S.chatParams.temperature);fd.append('max_tokens',S.chatParams.max_tokens);
fd.append('top_p',S.chatParams.top_p);fd.append('top_k',S.chatParams.top_k);
fd.append('frequency_penalty',S.chatParams.frequency_penalty);fd.append('presence_penalty',S.chatParams.presence_penalty);
if(_isReasoningModel()&&S.chatParams.reasoning_effort)fd.append('reasoning_effort',S.chatParams.reasoning_effort);
S.chatFiles.forEach(f=>fd.append('files',f.file));
d=await $api('/chat/upload',{method:'POST',body:fd});S.chatFiles=[]
}else{
const body={message:m,session_id:S.sid,use_tools:S.chatUseTools};
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;
if(_isReasoningModel()&&S.chatParams.reasoning_effort)body.reasoning_effort=S.chatParams.reasoning_effort}
else if(S.chatModel){
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:IC.warn+' Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;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;
if(_isReasoningModel()&&S.chatParams.reasoning_effort)body.reasoning_effort=S.chatParams.reasoning_effort}
d=await $api('/chat',{method:'POST',body})}
S.sid=d.session_id;
if(S.chatHistOpen)loadHistory('chat');
if(d.status==='processing'&&d.message_id){
await pollChatResult(d.message_id)
}else{
S.msgs=S.msgs.filter(x=>!x.thinking);
let resp=d.response;
if(d.tools_used&&d.tools_used.length){resp+='\n\n'+IC.wrench+' **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
S.msgs.push({r:'assistant',c:resp,t:tstamp()});R();scCh()}}
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp()});R()}
finally{el.disabled=false;el.focus();btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText||'Enviar →'}}
async function pollChatResult(mid){
for(let i=0;i<1200;i++){
await new Promise(r=>setTimeout(r,i<10?1000:3000));
try{const r=await $api('/chat/'+mid+'/status');
if(r.status==='done'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:r.content,t:tstamp()});R();scCh();return}
if(r.status==='failed'){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp()});R();scCh();return}
}catch(e){}}
S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:IC.timeout+' Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});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()}
function newChat(){S.msgs=[];S.sid=null;S.chatFiles=[];R()}
function newTfChat(){S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfHadApply=false;S.tfConfirmDest=false;S.tfConfirmRollback=false;S.tfEditIdx=-1;R()}
function tfToggleMenu(){
const m=document.getElementById('tfMenu');
if(!m)return;
const show=m.style.display==='none';
m.style.display=show?'block':'none';
if(show){const h=e=>{if(!m.contains(e.target)&&!e.target.closest('[onclick*="tfToggleMenu"]')){m.style.display='none';document.removeEventListener('click',h)}};setTimeout(()=>document.addEventListener('click',h),0)}
}
let _tfPromptSaveTimer=null;
async function tfOpenPrompt(){
document.getElementById('tfMenu').style.display='none';
try{
const prompts=await $api('/prompts/terraform');
const active=prompts.find(p=>p.is_active)||prompts[0];
if(!active){alert('Nenhum prompt encontrado.');return}
S.tfPrompt={id:active.id,name:active.name,content:active.content};
S.tfPromptOpen=true;R();
}catch(e){alert('Erro ao carregar prompt: '+e.message)}
}
function tfPromptChanged(field,value){
if(!S.tfPrompt)return;
S.tfPrompt[field]=value;
const el=document.getElementById('tfPromptSaved');
if(el){el.textContent='Salvando...';el.style.opacity='1'}
clearTimeout(_tfPromptSaveTimer);
_tfPromptSaveTimer=setTimeout(async()=>{
try{
await $api('/prompts/'+S.tfPrompt.id,{method:'PUT',body:{name:S.tfPrompt.name,content:S.tfPrompt.content,is_active:true}});
const el=document.getElementById('tfPromptSaved');
if(el){el.textContent='Salvo ✓';el.style.opacity='1';setTimeout(()=>{if(el)el.style.opacity='.4'},2000)}
}catch(e){const el=document.getElementById('tfPromptSaved');if(el){el.textContent='Erro ao salvar';el.style.opacity='1'}}
},800);
}
function tfClosePrompt(){S.tfPromptOpen=false;R()}
async function tfRefreshRef(){
const item=document.querySelector('.tf-menu-item');
const sub=document.getElementById('tfRefStatus');
if(item)item.classList.add('loading');
if(sub)sub.textContent='Atualizando...';
try{
const r=await $api('/terraform/refresh-reference',{method:'POST'});
if(r.ok){
S.tfRefStatus=r.lines+' linhas — '+r.updated_at;
if(sub)sub.textContent=S.tfRefStatus;
alert('Referência Terraform atualizada com sucesso!\n'+r.lines+' linhas de recursos OCI.');
}else{
alert('Erro ao atualizar: '+(r.error||'Erro desconhecido'));
if(sub)sub.textContent='Erro';
}
}catch(e){alert('Erro: '+e.message);if(sub)sub.textContent='Erro'}
finally{if(item)item.classList.remove('loading');const m=document.getElementById('tfMenu');if(m)m.style.display='none'}
}
async function loadHistory(type){
try{const h=await $api('/chat/sessions?agent_type='+type+'&limit=50');
if(type==='chat')S.chatHistory=h;else if(type==='tf-prompt')S.tfpHistory=h;else S.tfHistory=h}
catch(e){if(type==='chat')S.chatHistory=[];else if(type==='tf-prompt')S.tfpHistory=[];else S.tfHistory=[]}
R();
}
async function loadSession(sid,type){
try{const d=await $api('/chat/sessions/'+sid+'/messages');
if(type==='chat'){
S.sid=sid;S.msgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''}));R();scCh();
}else if(type==='tf-prompt'){
S.tfpSid=sid;S.tfpMsgs=d.messages.map(m=>({r:m.role,c:m.content,_raw:m.role==='assistant'?m.content:undefined,t:m.created_at?.slice(11,16)||''}));R();
}else{
S.tfSid=sid;S.tfMsgs=d.messages.map(m=>({r:m.role,c:m.content,t:m.created_at?.slice(11,16)||''}));
S.tfFiles=[];S.tfCode='';S.tfPlan=[];S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';
try{const wsList=await $api('/terraform/workspaces');const ws=wsList.find(w=>w.session_id===sid);
if(ws){
S.tfWs=ws.id;S.tfStatus=ws.status;
const detail=await $api('/terraform/workspaces/'+ws.id);
S.tfPlanOut=detail.plan_output||'';S.tfApplyOut=detail.apply_output||'';S.tfDestroyOut=detail.destroy_output||'';
if(S.tfApplyOut)S.tfHadApply=true;
if(detail.tf_code){
S.tfCode=detail.tf_code;
const parts=detail.tf_code.split(/^\/\/\s*filename:\s*/m).filter(Boolean);
let md='';
if(parts.length>1){
md=parts.map(p=>{const nl=p.indexOf('\n');const fn=p.substring(0,nl).trim();const code=p.substring(nl+1).trim();return '```hcl\n// filename: '+fn+'\n'+code+'\n```'}).join('\n\n');
}else{md='```hcl\n'+detail.tf_code+'\n```'}
const lastMsg=[...d.messages].reverse().find(m=>m.role==='assistant'&&m.content&&(m.content.includes('```plan')||/Resource Plan/.test(m.content)));
if(lastMsg){
const pm=lastMsg.content.match(/```plan\s*\n([\s\S]*?)```/);
if(pm){md+='\n\n```plan\n'+pm[1]+'```'}
else{
const fm=lastMsg.content.match(/(\*{0,2}Resource Plan\*{0,2})\s*\n((?:[+~][\s\S]*?)(?=\n\n|\n##|\n\*\*|$))/);
if(fm)md+='\n\n'+fm[0];
}
}
fmTf(md);
}
if(detail.oci_config_id){
S.tfOci=detail.oci_config_id;
const oc=S.ociCfg.find(x=>x.id===detail.oci_config_id);
if(oc)S.tfRegion=oc.region;
}
if(detail.compartment_id)S.tfCompartment=detail.compartment_id;
if(S.tfPlanOut)S.tfBtab='plan';
else if(S.tfFiles.length)S.tfBtab='files';
if(S.tfOci){tfLoadComps();if(S.tfCompartment)tfLoadResources()}
}else{
S.tfWs=null;S.tfStatus='draft';
const last=[...d.messages].reverse().find(m=>m.role==='assistant'&&m.content&&m.content.includes('```'));
if(last)fmTf(last.content);
}
}catch(e){S.tfWs=null;S.tfStatus='draft'}
R();
}
}catch(e){alert('Erro ao carregar sessão: '+e.message)}
}
async function delSession(sid,type){
if(!confirm('Excluir esta conversa?'))return;
try{await $api('/chat/'+sid,{method:'DELETE'})}catch(e){}
if(type==='chat'){if(S.sid===sid)newChat();S.chatHistory=S.chatHistory.filter(s=>s.id!==sid)}
else if(type==='tf-prompt'){if(S.tfpSid===sid){S.tfpMsgs=[];S.tfpSid=null}S.tfpHistory=S.tfpHistory.filter(s=>s.id!==sid)}
else{if(S.tfSid===sid)newTfChat();S.tfHistory=S.tfHistory.filter(s=>s.id!==sid)}
R();
}
async function renameSession(sid,type){
const cur=(type==='chat'?S.chatHistory:S.tfHistory).find(s=>s.id===sid);
const t=prompt('Renomear conversa:',cur?.title||'');
if(t===null)return;
try{await $api('/chat/sessions/'+sid+'/title',{method:'PUT',body:{title:t}});
if(cur)cur.title=t;R()}catch(e){}
}
function _histDateGroup(dateStr){
if(!dateStr)return'Outras';
const d=new Date(dateStr+'Z'),now=new Date(),diff=(now-d)/86400000;
if(diff<1)return'Hoje';if(diff<2)return'Ontem';if(diff<7)return'Últimos 7 dias';if(diff<30)return'Últimos 30 dias';return'Anteriores';
}
function rHistList(type,hist,curSid){
if(!hist.length)return'<div style="text-align:center;padding:1rem .5rem;font-size:.7rem;color:var(--t4)">Nenhuma conversa</div>';
let html='';let lastGroup='';
hist.forEach(s=>{
const g=_histDateGroup(s.updated_at||s.created_at);
if(g!==lastGroup){lastGroup=g;html+=`<div class="ch-hp-grp">${g}</div>`}
const active=s.id===curSid?' active':'';
const title=s.title||'Nova conversa';
const ts=s.updated_at?new Date(s.updated_at+'Z').toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'}):'';
html+=`<div class="ch-hp-item${active}" onclick="loadSession('${s.id}','${type}')">
<span class="ch-hp-ttl" title="${title.replace(/"/g,'&quot;')}">${escHtml(title)}</span>
${ts?`<span class="ch-hp-time">${ts}</span>`:''}
<span class="ch-hp-del" onclick="event.stopPropagation();delSession('${s.id}','${type}')" title="Excluir">
<svg viewBox="0 0 16 16" width="11" height="11" fill="currentColor"><path d="M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 01-1 1H13v9a2 2 0 01-2 2H5a2 2 0 01-2-2V4h-.5a1 1 0 010-2H6a1 1 0 011-1h2a1 1 0 011 1h3.5a1 1 0 011 1zM4.118 4L4 4.059V13a1 1 0 001 1h6a1 1 0 001-1V4.059L11.882 4H4.118z"/></svg>
</span>
</div>`;
});
return html;
}
async function loadChatLogs(){
const sev=document.getElementById('cl-sev-chat')?.value||'';
const qs='limit=50'+(sev?'&severity='+sev:'')+(S.sid?'&session_id='+S.sid:'');
const el=document.getElementById('chat-logs-body');if(!el)return;
try{const logs=await $api('/chat-logs?'+qs);
if(!logs.length){el.innerHTML='<div class="emp" style="padding:.5rem"><p>Nenhum log registrado.</p></div>';return}
el.innerHTML='<table style="font-size:.74rem"><thead><tr><th>Data</th><th>Fonte</th><th>Ação</th><th>Status</th><th>Mensagem</th></tr></thead><tbody>'+
logs.map(l=>{const sc=l.severity==='error'?'err':l.severity==='warning'?'w':'ok';
return'<tr><td style="font-size:.62rem;color:var(--t4);white-space:nowrap">'+l.created_at+'</td>'+
'<td><span class="tag">'+l.source+'</span></td>'+
'<td style="font-size:.72rem">'+l.action+'</td>'+
'<td><span style="color:var(--'+sc+');font-weight:600;font-size:.7rem">'+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){el.innerHTML='<div class="al al-e">'+e.message+'</div>'}}
/* ── Terraform Agent ── */
const TF_ICON=`<svg viewBox="0 0 16 16" width="16" height="16"><path d="M5.6 1v4.2l3.6 2.1V3.1L5.6 1zm4.4 2.1v4.2l3.6-2.1V1L10 3.1zM1.4 3.5v4.2l3.6 2.1V5.6L1.4 3.5zM5.6 8.1v4.2L9.2 14.4V10.2L5.6 8.1z" fill="#7b42bc"/></svg>`;
function escHtml(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
function _fixSingleLineBlocks(content){
// Expand ANY single-line HCL block with multiple args into multi-line format.
// Matches both top-level (variable, resource) AND nested blocks (ingress_security_rules, route_rules, etc.)
// Does NOT match map assignments (tags = { ... }) because of the "=" before "{"
// Pattern: identifier ["quoted"]* { key=val, key=val } (no "=" between identifier and "{")
return content.replace(/^(\s*[a-z]\w*(?:\s+"[^"]*")*)\s*\{\s*(.+?)\s*\}\s*$/gm, (match, header, body) => {
// Skip if body doesn't have multiple key=value pairs
let args = body.split(/,\s*/);
if(args.length < 2){
args = [];
const re = /(\w+)\s*=\s*(?:"[^"]*"|\S+)/g;
let m2;
while((m2 = re.exec(body)) !== null) args.push(m2[0]);
}
if(args.length < 2) return match;
const indent = header.match(/^(\s*)/)[1];
return header.trimEnd() + ' {\n' + args.map(a => indent + ' ' + a.trim()).join('\n') + '\n' + indent + '}';
});
}
function _splitTfMonolith(content){
// Split a monolithic HCL file into multiple logical files by parsing top-level blocks.
// Returns array of {name, content} or null if splitting not needed.
content = _fixSingleLineBlocks(content);
const lines=content.split('\n');
const topBlocks=[];
let preamble=[]; // comments/blanks before next block
let accum=[];
let inBlock=false;
let bDepth=0;
let curHeader='';
for(let i=0;i<lines.length;i++){
const line=lines[i];
// Count braces outside of quoted strings (simple heuristic: ignore braces inside "...")
const stripped=line.replace(/"[^"]*"/g,'');
const open=(stripped.match(/\{/g)||[]).length;
const close=(stripped.match(/\}/g)||[]).length;
if(!inBlock){
// Match top-level block declarations only (not attribute assignments like "provider = oci.x")
const hdr=line.match(/^\s*(variable|output|resource|data|provider|module)\s+"/) || line.match(/^\s*(locals|terraform)\s*\{/);
if(hdr){
// Start of a new block — attach preamble (comments above)
inBlock=true;
curHeader=line;
accum=preamble.concat([line]);
preamble=[];
bDepth=open-close;
if(bDepth<=0) bDepth=0;
// Single-line block (opens and closes on same line)
if(bDepth===0 && open>0){
topBlocks.push({header:curHeader,body:accum.join('\n')});
inBlock=false; accum=[];
}
} else {
// Comment, blank, or stray line between blocks — save as preamble for next block
preamble.push(line);
}
} else {
accum.push(line);
bDepth+=open-close;
if(bDepth<=0){
bDepth=0;
topBlocks.push({header:curHeader,body:accum.join('\n')});
inBlock=false; accum=[]; curHeader='';
}
}
}
// Flush remaining
if(accum.length) topBlocks.push({header:curHeader||'',body:accum.join('\n')});
if(topBlocks.length<3) return null;
// Categorize each block by its header
const cats={variables:[],outputs:[],networking:[],compute:[],database:[],
firewall:[],loadbalancer:[],storage:[],iam:[],drg:[],data:[],providers:[],other:[]};
for(const b of topBlocks){
const h=b.header.trim();
if(h.startsWith('variable ')) cats.variables.push(b.body);
else if(h.startsWith('output ')) cats.outputs.push(b.body);
else if(h.startsWith('provider ')) cats.providers.push(b.body);
else if(/resource\s+"oci_core_(drg|remote_peering|drg_route|drg_attachment)/.test(h)) cats.drg.push(b.body);
else if(/resource\s+"oci_core_(vcn|subnet|internet_gateway|nat_gateway|service_gateway|route_table|security_list|network_security_group|dhcp_options|local_peering_gateway|virtual_circuit)/.test(h)) cats.networking.push(b.body);
else if(/resource\s+"oci_core_instance/.test(h)) cats.compute.push(b.body);
else if(/resource\s+"oci_(database|nosql|mysql|psql)/.test(h) || /resource\s+"oci_core_autonomous/.test(h)) cats.database.push(b.body);
else if(/resource\s+"oci_network_firewall/.test(h)) cats.firewall.push(b.body);
else if(/resource\s+"oci_(load_balancer|network_load_balancer)/.test(h)) cats.loadbalancer.push(b.body);
else if(/resource\s+"oci_(objectstorage|file_storage)/.test(h)) cats.storage.push(b.body);
else if(/resource\s+"oci_identity/.test(h)) cats.iam.push(b.body);
else if(h.startsWith('data ')) cats.data.push(b.body);
else cats.other.push(b.body);
}
// Build result files in logical order
const result=[];
const add=(name,arr)=>{if(arr.length) result.push({name,content:arr.join('\n\n')})};
add('variables.tf', cats.variables);
add('providers.tf', cats.providers);
add('networking.tf', cats.networking);
add('drg.tf', cats.drg);
add('compute.tf', cats.compute);
add('database.tf', cats.database);
add('firewall.tf', cats.firewall);
add('loadbalancer.tf', cats.loadbalancer);
add('storage.tf', cats.storage);
add('iam.tf', cats.iam);
add('data.tf', cats.data);
add('main.tf', cats.other);
add('outputs.tf', cats.outputs);
return result.length>=3 ? result : null;
}
function fmTf(t){
// Extract ALL hcl blocks, parse // filename: comments, build separate files
const codeBlocks=[];
let r=t.replace(/```(?:hcl|terraform)\s*\n([\s\S]*?)```/g,(m,code)=>{
const trimmed=_fixSingleLineBlocks(code.trim());
// Check for // filename: comment on first line
const fnMatch=trimmed.match(/^\/\/\s*filename:\s*(\S+)/);
const fileName=fnMatch?fnMatch[1]:'main.tf';
// Content without the filename comment line
const content=fnMatch?trimmed.replace(/^\/\/\s*filename:\s*\S+\s*\n?/,'').trim():trimmed;
codeBlocks.push({name:fileName,content:content});
S.tfCode=content;
return '<div class="tf-code-block"><div class="tf-code-header"><span>'+TF_ICON+' '+escHtml(fileName)+'</span>'+
'<button class="btn" onclick="tfCopyBlock('+( codeBlocks.length-1)+')">Copy</button></div>'+
'<pre class="tf-pre"><code>'+escHtml(content)+'</code></pre></div>';
});
r=r.replace(/```plan\s*\n([\s\S]*?)```/g,(m,plan)=>{
const lines=plan.trim().split('\n');
S.tfPlan=lines.map(l=>{const m2=l.match(/^[+~]\s+(\S+)\s+\((.+)\)$/);return m2?{type:m2[1],desc:m2[2]}:{type:l.replace(/^[+~]\s*/,'').trim(),desc:''}}).filter(x=>x.type);
return _renderTfPlanBlock();
});
// Also match free-form "Resource Plan" / "**Resource Plan**" followed by +/~ lines (model often skips ```plan block)
if(!S.tfPlan.length){
r=r.replace(/(\*{0,2}Resource Plan\*{0,2})\s*\n((?:[+~][\s\S]*?)(?=\n\n|\n##|\n\*\*|$))/g,(m,hdr,plan)=>{
const lines=plan.trim().split('\n').filter(l=>/^[+~]/.test(l.trim()));
S.tfPlan=lines.map(l=>{const clean=l.trim().replace(/^[+~]\s*/,'').replace(/\s{2,}/g,' ').trim();return{type:clean,desc:''}}).filter(x=>x.type);
return _renderTfPlanBlock();
});
}
function _renderTfPlanBlock(){
return '<div class="tf-plan-block"><div class="tf-plan-header">'+TF_ICON+' Resource Plan — '+S.tfPlan.length+' resource(s)</div><div class="tf-plan-list">'+
S.tfPlan.map(x=>'<div class="tf-plan-item"><span class="tf-plan-add">+</span><span class="tf-plan-type">'+escHtml(x.type)+'</span>'+(x.desc?'<span class="tf-plan-desc">'+escHtml(x.desc)+'</span>':'')+'</div>').join('')+'</div></div>';
}
// Auto-split: if model returned 1-2 large blocks, split into multiple files
// Only auto-split on FIRST generation (no existing files) — during corrections, trust the model's named files
const hasExistingFiles=S.tfFiles.length>0;
const allNamed=codeBlocks.every(b=>b.name!=='main.tf'||codeBlocks.length===1);
let didAutoSplit=false;
if(codeBlocks.length<=2 && !hasExistingFiles){
const totalContent=codeBlocks.map(b=>b.content).join('\n\n');
if(totalContent.length>2000){
const split=_splitTfMonolith(totalContent);
if(split){
codeBlocks.length=0;
split.forEach(f=>codeBlocks.push(f));
didAutoSplit=true;
// Rebuild HTML with split files
const splitHtml=split.map((f,i)=>
'<div class="tf-code-block"><div class="tf-code-header"><span>'+TF_ICON+' '+escHtml(f.name)+'</span>'+
'<button class="btn" onclick="tfCopyBlock('+i+')">Copy</button></div>'+
'<pre class="tf-pre"><code>'+escHtml(f.content)+'</code></pre></div>'
).join('');
// Replace the original single code block HTML
r=r.replace(/<div class="tf-code-block">[\s\S]*?<\/pre><\/div>/g,'');
r=splitHtml+r;
console.log('[TF] Auto-split monolithic file into '+split.length+' files');
}
}
}
// Build files list
if(codeBlocks.length){
const newMap={};
codeBlocks.forEach(b=>{
if(newMap[b.name])newMap[b.name]+='\n\n'+b.content;
else newMap[b.name]=b.content;
});
// If auto-split happened, REPLACE all files (monolithic = full regeneration, not partial correction)
// If model returned multiple named files (no auto-split), use merge logic for corrections
if(didAutoSplit || !S.tfFiles.length){
S.tfFiles=Object.entries(newMap).map(([name,content])=>({name,content,size:content.length,type:'hcl'}));
}else{
const merged=[];
const usedNew=new Set();
for(const f of S.tfFiles){
if(newMap[f.name]!=null){merged.push({name:f.name,content:newMap[f.name],size:newMap[f.name].length,type:'hcl'});usedNew.add(f.name)}
else{merged.push(f)}
}
for(const[name,content] of Object.entries(newMap)){
if(!usedNew.has(name))merged.push({name,content,size:content.length,type:'hcl'})
}
S.tfFiles=merged;
}
S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
}
r=r.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
return r;
}
function tfCopyBlock(i){const b=S.tfFiles[i];if(b)navigator.clipboard.writeText(b.content).then(()=>alert('Copiado!'))}
/* ── Terraform Prompt Generator ── */
const TFP_EXAMPLES=[
'VCN com subnets pública e privada, internet gateway, NAT gateway e 2 compute instances Ubuntu',
'Ambiente multi-region MAD1 + MAD3 com DRG, RPC e VCNs espelhadas',
'Cluster OKE com 3 node pools, load balancer e container registry',
'Banco Autonomous Database com vault, encryption key e bastion para acesso',
'Infraestrutura completa: VCN, compute, block storage, object storage, IAM policies e monitoring',
];
function rTfPrompt(){
const msgs=S.tfpMsgs;
const msHtml=msgs.length===0
?`<div class="tf-empty">${IC.edit}<p>Descreva a infraestrutura OCI que você precisa</p><p style="font-size:.68rem;opacity:.6">O agente vai gerar um prompt estruturado e otimizado para o Terraform Agent.</p>
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.75rem;justify-content:center">${TFP_EXAMPLES.map(e=>`<button class="btn bs bsm" onclick="document.getElementById('tfpi').value='${e.replace(/'/g,"\\'")}';document.getElementById('tfpi').focus()" style="font-size:.66rem;white-space:normal;text-align:left;max-width:280px;line-height:1.35">${e}</button>`).join('')}</div></div>`
:msgs.map((m,i)=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}${m._raw?`
<div class="tfp-acts"><button class="tfp-act" onclick="tfpCopy(${i})">${m._copied?IC.ok+' Copiado':IC.clip+' Copiar'}</button>
<button class="tfp-act tfp-act-tf" onclick="tfpSendToTf(${i})">${TF_IC} Terraform</button></div>`:''}</div></div>`).join('');
// Model dropdown — only recommended models, grouped by provider
const tfpAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
const tfpProvs={};tfpAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!tfpProvs[p])tfpProvs[p]=[];tfpProvs[p].push({id:mid,name:mi.name})}});
const tfpProvOrder=['openai','google'];
let ddItems='';
if(S.genaiCfg.length){const cfgFiltered=S.genaiCfg.filter(g=>tfpAllowed.includes(g.model_id));if(cfgFiltered.length){ddItems+='<div class="grp">Configs Salvas</div>';cfgFiltered.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.tfpModel==='cfg:'+g.id?' sel':''}" onclick="tfpPickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id}</div>`})}}
tfpProvOrder.filter(p=>tfpProvs[p]).forEach(p=>{ddItems+=`<div class="grp">${p[0].toUpperCase()+p.slice(1)}</div>`;tfpProvs[p].forEach(m=>{ddItems+=`<div class="itm${S.tfpModel===m.id?' sel':''}" onclick="tfpPickModel('${m.id}')">${m.name}</div>`})});
let curLabel='Selecione modelo...';
if(S.tfpModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfpModel.slice(4));curLabel=g?(g.name||g.model_id):'Config...'}
else if(S.tfpModel){const mi=S.models[S.tfpModel];curLabel=mi?mi.name:S.tfpModel}
const isDirect=S.tfpModel&&!S.tfpModel.startsWith('cfg:');
const ociSel=isDirect?`<select onchange="S.tfpOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}" style="max-width:180px"><option value="">OCI Config...</option>${S.ociCfg.map(c=>`<option value="${c.id}"${S.tfpOci===c.id?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
return`<div class="tf-wrap">
<div style="display:flex;flex:1;min-height:0;overflow:hidden">
<div class="ch-hp${S.tfpHistOpen?' open':''}"><div class="ch-hp-inner">
<div class="ch-hp-hdr"><span>${IC.history} Histórico</span><div class="ch-hp-new" onclick="S.tfpMsgs=[];S.tfpSid=null;R()">+ Nova Conversa</div></div>
<div class="ch-hp-list">${rHistList('tf-prompt',S.tfpHistory,S.tfpSid)}</div>
</div></div>
<div class="tf-chat">
<div class="tf-toolbar">
<div class="ch-icon-btn${S.tfpHistOpen?' active':''}" onclick="S.tfpHistOpen=!S.tfpHistOpen;if(S.tfpHistOpen)loadHistory('tf-prompt');R()" title="Histórico" style="width:24px;height:24px;font-size:.85rem">☰</div>
${IC.edit}
<div class="mdrop"><div class="mdrop-btn" onclick="tfpToggleModelDrop()">${curLabel} <span class="arr">▼</span></div>
<div class="mdrop-dd" id="tfpmdd"><input type="text" id="tfpmds" placeholder="Buscar..." oninput="tfpFilterModels(this.value)"><div class="mdrop-list" id="tfpmdl">${ddItems}</div></div>
</div>
${ociSel}
<div style="flex:1"></div>
</div>
<div class="tf-msgs" id="tfpchm">${msHtml}${S.tfpLoading?'<div class="cm cm-assistant"><div class="cb"><span class="spinner" style="width:16px;height:16px;border-width:2px;vertical-align:middle"></span> Gerando prompt...</div></div>':''}</div>
<div class="tf-input">
<textarea id="tfpi" placeholder="Descreva a infraestrutura desejada... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();tfpSend()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
<button class="btn bp" onclick="tfpSend()" style="background:#7b42bc;border-color:#7b42bc">Enviar</button>
</div>
</div>
</div></div>`}
function tfpToggleModelDrop(){const dd=document.getElementById('tfpmdd');if(!dd)return;dd.classList.toggle('open');if(dd.classList.contains('open'))document.getElementById('tfpmds')?.focus()}
function tfpFilterModels(q){const l=document.getElementById('tfpmdl');if(!l)return;l.querySelectorAll('.itm').forEach(el=>{el.style.display=el.textContent.toLowerCase().includes(q.toLowerCase())?'':'none'})}
async function tfpSend(){
const el=document.getElementById('tfpi');const m=(el?.value||'').trim();if(!m||S.tfpLoading)return;
el.value='';el.style.height='auto';
S.tfpMsgs.push({r:'user',c:m});S.tfpLoading=true;R();
const mc=document.getElementById('tfpchm');if(mc)mc.scrollTop=mc.scrollHeight;
try{
const gc=await _tfpResolveGenai();
const hist=S.tfpMsgs.slice(0,-1).filter(x=>x.r==='user'||x.r==='assistant').map(x=>({role:x.r==='user'?'USER':'CHATBOT',content:x._raw||x.c}));
const body={message:m,genai_config:gc,history:hist.length?hist:null};
if(S.tfpSid)body.session_id=S.tfpSid;
const d=await $api('/terraform/generate-prompt',{method:'POST',body});
if(d.session_id)S.tfpSid=d.session_id;
S.tfpMsgs.push({r:'assistant',c:d.prompt,_raw:d.prompt});
}catch(e){S.tfpMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message})}
S.tfpLoading=false;R();
const mc2=document.getElementById('tfpchm');if(mc2)mc2.scrollTop=mc2.scrollHeight}
function tfpPickModel(v){S.tfpModel=v;S.tfpMdOpen=false;
if(v&&!v.startsWith('cfg:')&&!S.tfpOci&&S.ociCfg.length){S.tfpOci=S.ociCfg[0].id;S.tfpRegion=S.ociCfg[0].region;S.tfpCompartment=S.ociCfg[0].compartment_id||''}
else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfpOci){S.tfpOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfpRegion=c.region;S.tfpCompartment=c.compartment_id||''}}}
R()}
async function _tfpResolveGenai(){
if(S.tfpModel.startsWith('cfg:'))return{genai_config_id:S.tfpModel.slice(4)};
if(S.tfpModel&&S.tfpOci)return{oci_config_id:S.tfpOci,model_id:S.tfpModel,genai_region:S.tfpRegion,compartment_id:S.tfpCompartment};
const def=S.genaiCfg.find(g=>g.is_default)||S.genaiCfg[0];
if(def)return{genai_config_id:def.id};
if(!S.ociCfg.length)return{};
const oc=S.ociCfg[0];
return{oci_config_id:oc.id,model_id:'openai.gpt-4.1',genai_region:oc.region,compartment_id:oc.compartment_id}}
function tfpCopy(idx){
const m=S.tfpMsgs[idx];if(!m||!m._raw)return;
navigator.clipboard.writeText(m._raw).then(()=>{
// Show toast instead of re-rendering
const toast=document.createElement('div');
toast.textContent='Prompt copiado!';
toast.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#2ecc71;color:#fff;padding:6px 16px;border-radius:6px;font-size:.75rem;z-index:9999;opacity:0;transition:opacity .2s';
document.body.appendChild(toast);
requestAnimationFrame(()=>toast.style.opacity='1');
setTimeout(()=>{toast.style.opacity='0';setTimeout(()=>toast.remove(),200)},1500);
})}
function tfpSendToTf(idx){
const m=S.tfpMsgs[idx];if(!m||!m._raw)return;
S.tab='terraform';
setTimeout(()=>{const el=document.getElementById('tfi');if(el){el.value=m._raw;el.style.height='auto';el.style.height=el.scrollHeight+'px'}R()},100)}
function rTerraform(){
const ms=S.tfMsgs.length===0
?`<div class="tf-empty">${TF_ICON}<p>Descreva a infraestrutura OCI desejada</p><p style="font-size:.68rem;opacity:.6">Ex: "Crie uma VCN com subnets pública e privada, internet gateway e um compute instance"</p></div>`
:S.tfMsgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='assistant'?fmTf(m.c):fm(m.c)}</div>${m.t?`<div class="cm-ts">${m.t}</div>`:''}</div>`).join('');
// Model dropdown — same filtered models as Prompt Generator, grouped by provider
const tfAllowed=['openai.gpt-4.1','openai.o3','openai.o4-mini','openai.gpt-5.1','openai.gpt-5.2','google.gemini-2.5-pro','google.gemini-2.5-flash'];
const provs={};tfAllowed.forEach(mid=>{const mi=S.models[mid];if(mi){const p=mi.provider||'other';if(!provs[p])provs[p]=[];provs[p].push({id:mid,name:mi.name})}});
const provOrder=['openai','google'];
let ddItems='';
if(S.genaiCfg.length){const cfgF=S.genaiCfg.filter(g=>tfAllowed.includes(g.model_id));if(cfgF.length){ddItems+='<div class="grp">Configs Salvas</div>';cfgF.forEach(g=>{const mi=S.models[g.model_id];ddItems+=`<div class="itm${S.tfModel==='cfg:'+g.id?' sel':''}" onclick="tfPickModel('cfg:${g.id}')">${g.name||mi?.name||g.model_id}</div>`})}}
provOrder.filter(p=>provs[p]).forEach(p=>{ddItems+=`<div class="grp">${p[0].toUpperCase()+p.slice(1)}</div>`;provs[p].forEach(m=>{ddItems+=`<div class="itm${S.tfModel===m.id?' sel':''}" onclick="tfPickModel('${m.id}')">${m.name}</div>`})});
let curLabel='Selecione modelo...';
if(S.tfModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfModel.slice(4));curLabel=g?(g.name||g.model_id):'Config...'}
else if(S.tfModel){const mi=S.models[S.tfModel];curLabel=mi?mi.name:S.tfModel}
const isDirect=S.tfModel&&!S.tfModel.startsWith('cfg:');
const ociSel=isDirect?`<select id="tfoci" onchange="S.tfOci=this.value;const c=S.ociCfg.find(x=>x.id===this.value);if(c){S.tfRegion=c.region;S.tfCompartment=c.compartment_id||''}tfLoadComps()" style="max-width:180px"><option value="">OCI Config...</option>${S.ociCfg.map(c=>`<option value="${c.id}"${S.tfOci===c.id?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('')}</select>`:'';
const compSel=(S.tfComps.length||S.tfCompLoading||S.tfOci)?`<select onchange="S.tfCompartment=this.value;tfLoadResources()" style="max-width:220px">${S.tfCompLoading?'<option>Carregando...</option>':S.tfComps.length?'<option value="">Compartment...</option>'+tfFlatComps(S.tfComps,0).map(c=>`<option value="${c.id}"${S.tfCompartment===c.id?' selected':''}>${c.name}</option>`).join(''):'<option value="">Selecione Compartment...</option>'}</select>`:'';
// Status badge
const stBadge=_tfBadge(S.tfStatus);
return `<div class="tf-wrap">
<div class="tf-left">
<div style="display:flex;flex:7;min-height:0;overflow:hidden">
<div class="ch-hp${S.tfHistOpen?' open':''}"><div class="ch-hp-inner">
<div class="ch-hp-hdr"><span>${IC.history} Histórico</span><div class="ch-hp-new" onclick="newTfChat()">+ Novo Chat</div></div>
<div class="ch-hp-list">${rHistList('terraform',S.tfHistory,S.tfSid)}</div>
</div></div>
<div class="tf-chat">
<div class="tf-toolbar">
<div class="ch-icon-btn${S.tfHistOpen?' active':''}" onclick="S.tfHistOpen=!S.tfHistOpen;if(S.tfHistOpen)loadHistory('terraform');R()" title="Histórico" style="width:24px;height:24px;font-size:.85rem">☰</div>
${TF_ICON}
<div class="mdrop"><div class="mdrop-btn" onclick="tfToggleModelDrop()">${curLabel} <span class="arr">▼</span></div>
<div class="mdrop-dd" id="tfmdd"><input type="text" id="tfmds" placeholder="Buscar..." oninput="tfFilterModels(this.value)"><div class="mdrop-list" id="tfmdl">${ddItems}</div></div>
</div>
${ociSel}${compSel}
${stBadge}
<div style="flex:1"></div>
<div style="position:relative">
<div class="tf-file-btn" onclick="tfToggleMenu()" title="Menu" style="width:24px;height:24px;cursor:pointer"><svg viewBox="0 0 16 16" width="14" height="14" fill="var(--t3)"><path d="M8 4.5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm0 5a1.5 1.5 0 110-3 1.5 1.5 0 010 3zm0 5a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/></svg></div>
<div class="tf-menu" id="tfMenu" style="display:none">
<div class="tf-menu-item" onclick="tfOpenPrompt()">
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor"><path d="M13.5 2.5l-1-1L3 11l-1 3 3-1L14.5 3.5l-1-1zM2.5 13.5l.7-2.1 1.4 1.4-2.1.7z"/></svg>
<span>Editar Prompt</span>
</div>
<div class="tf-menu-item" onclick="tfRefreshRef()">
<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor"><path d="M8 1a7 7 0 106.33 4h-2.1A5 5 0 108 13a5 5 0 004.24-2.36l1.52 1.02A7 7 0 018 1z"/><path d="M11 1v4h4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
<span>Atualizar Referência TF</span>
<span class="tf-menu-sub" id="tfRefStatus">${S.tfRefStatus||''}</span>
</div>
</div>
</div>
</div>
<div class="tf-msgs" id="tfchm">${ms}</div>
<div class="tf-input">
<textarea id="tfi" placeholder="Descreva a infraestrutura desejada... (Shift+Enter para nova linha)" rows="1" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();tfSend()}" oninput="autoGrow(this)" onpaste="setTimeout(()=>autoGrow(this),0)" style="resize:none;overflow-y:auto"></textarea>
<button class="btn bp" onclick="tfSend()" style="background:#7b42bc;border-color:#7b42bc">Enviar</button>
</div>
</div>
</div>
<div class="tf-resize" onmousedown="tfStartResize(event)"></div>
<div class="tf-bottom" id="tfBottom">
<div class="tf-bottom-tabs">
<div class="tf-btab${S.tfBtab==='files'?' active':''}" onclick="tfSwitchBtab('files')">
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M1 2.5A1.5 1.5 0 012.5 1h3.379a1.5 1.5 0 011.06.44l1.122 1.12A1.5 1.5 0 009.121 3H13.5A1.5 1.5 0 0115 4.5v8a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 011 12.5z"/></svg>
Files${S.tfFiles.length?' ('+S.tfFiles.length+')':''}
</div>
<div class="tf-btab${S.tfBtab==='plan'?' active':''}" onclick="tfSwitchBtab('plan')">
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M2 2h12v2H2zm0 4h8v2H2zm0 4h10v2H2z"/></svg>
Plan${S.tfPlan.length?' ('+S.tfPlan.length+')':''}
</div>
<div class="tf-btab${S.tfBtab==='resources'?' active':''}" onclick="tfSwitchBtab('resources')">
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg>
Resources${S.tfResources?(' ('+Object.values(S.tfResources).reduce((a,b)=>a+(Array.isArray(b)?b.length:0),0)+')'):''}
</div>
<div class="tf-bottom-actions">
${S.tfFiles.length?'<button class="btn bs bsm" onclick="tfDlAll()" style="font-size:.66rem;padding:2px 8px">Download All</button>':''}
${rTfActions()}
</div>
</div>
<div class="tf-bottom-content">${rTfBottomContent()}</div>
</div>
</div>
<div class="tf-output-panel">
${rTfTerminal()}
</div>
${S.tfConfirmDest?rTfConfirmDestroy():''}
${S.tfConfirmRollback?rTfConfirmRollback():''}
${S.tfPromptOpen?rTfPromptModal():''}
</div>`;
}
function rTfPromptModal(){
const p=S.tfPrompt||{};
return`<div class="tf-modal-overlay" onclick="tfClosePrompt()"><div class="tf-modal wide" onclick="event.stopPropagation()">
<div class="tf-prompt-header"><h3>Editar Prompt — Terraform Agent</h3><span id="tfPromptSaved" class="tf-prompt-saved" style="opacity:.4">Salvo ✓</span></div>
<input class="tf-prompt-name" value="${(p.name||'').replace(/"/g,'&quot;')}" placeholder="Nome do prompt" oninput="tfPromptChanged('name',this.value)">
<textarea class="tf-prompt-ta" oninput="tfPromptChanged('content',this.value)">${p.content||''}</textarea>
<div style="text-align:right;margin-top:.6rem"><button class="btn bs" onclick="tfClosePrompt()">Fechar</button></div>
</div></div>`;
}
function _tfBadge(st){
const m={draft:['draft','Draft'],planning:['run','Planning...'],planned:['ok','Plan OK'],applying:['run','Applying...'],applied:['ok','Applied'],destroying:['run','Destroying...'],destroyed:['dest','Destroyed'],failed:['err','Failed']};
const[cls,label]=m[st]||['draft',st];
return`<span class="tf-badge tf-badge-${cls}">${label}</span>`;
}
function rTfActions(){
if(S.tfRunning)return'<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="tfCancel()">Cancel</button>';
const st=S.tfStatus;let btns='';
if(S.tfCode&&(st==='draft'||st==='failed'||st==='destroyed'))btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Plan</button>`;
if(st==='failed'&&S.tfPlanOut&&S.tfFiles.length&&S.tfModel)btns+=`<button class="btn bsm" style="background:#e67e22;color:#fff;border-color:#e67e22;font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfAutoFix()">${IC.fix} Auto-corrigir</button>`;
if(st==='failed'&&(S.tfApplyOut||S.tfHadApply))btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="S.tfConfirmRollback=true;R()">↩ Rollback</button>';
if(st==='planned'||st==='failed')btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
if(st==='planned')btns+='<button class="btn bsm" style="background:var(--gn);color:#fff;border-color:var(--gn);font-size:.72rem;padding:4px 12px" onclick="tfApply()">✓ Apply</button>';
if(st==='applied')btns+=`<button class="btn bsm" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.72rem;padding:4px 12px;gap:4px;display:inline-flex;align-items:center" onclick="tfSaveAndPlan()">${TF_ICON} Re-plan</button>`;
if(st==='applied'||st==='planned')btns+='<button class="btn bsm" style="background:var(--rd);color:#fff;border-color:var(--rd);font-size:.72rem;padding:4px 12px" onclick="S.tfConfirmDest=true;R()">✕ Destroy</button>';
return btns;
}
function rTfBottomContent(){
if(S.tfBtab==='files')return rTfFiles();
if(S.tfBtab==='plan')return rTfPlanPanel();
if(S.tfBtab==='resources')return rTfResourcesPanel();
return'';
}
function rTfTerminal(){
const out=S.tfPlanOut||S.tfApplyOut||S.tfDestroyOut;
let label='Terminal',color='#cdd6f4',content='';
if(out){
if(S.tfPlanOut)content=S.tfPlanOut;
if(S.tfApplyOut)content=S.tfApplyOut;
if(S.tfDestroyOut)content=S.tfDestroyOut;
label=S.tfDestroyOut?'Destroy Output':S.tfApplyOut?'Apply Output':'Plan Output';
color=S.tfDestroyOut?'#f38ba8':S.tfApplyOut?'#a6e3a1':'#cba6f7';
}
return`<div class="tf-term-hdr"><svg viewBox="0 0 16 16"><path d="M2 1h12a1 1 0 011 1v12a1 1 0 01-1 1H2a1 1 0 01-1-1V2a1 1 0 011-1zm1 3v9h10V4zm2 1l3 2.5L5 10z"/></svg><span style="color:${color}">${label}</span>${S.tfRunning?'<span style="margin-left:auto;font-size:.6rem;color:#f9e2af">● Executando...</span>':''}</div>
<div class="tf-term-body" id="tfTermOut">${out?escHtml(content):'<span style="opacity:.4">Aguardando execução...</span>'}</div>`;
}
function rTfFiles(){
if(!S.tfFiles.length)return`<div class="tf-empty">${TF_ICON}<p>Nenhum arquivo gerado ainda</p><p style="font-size:.66rem;opacity:.5">Descreva a infraestrutura desejada no chat acima</p></div>`;
const files=[...S.tfFiles];
if(S.tfWs||S.tfOci){
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
if(oc){
const comp=S.tfCompartment||oc.compartment_id||'';
if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,size:0,type:'tfvars',auto:true});
}
}
let html='';
for(let i=0;i<files.length;i++){
const f=files[i];const isEdit=S.tfEditIdx===i;
const sz=f.content?f.content.length:f.size;
const sizeStr=sz>1024?(sz/1024).toFixed(1)+' KB':sz+' B';
const iconFill=f.auto?'#888':'#7b42bc';
html+=`<div class="tf-file${isEdit?' editing':''}" onclick="tfOpenFile(${i})">
<div class="tf-file-icon"><svg viewBox="0 0 16 16" fill="${iconFill}"><path d="M4 1a1 1 0 00-1 1v12a1 1 0 001 1h8a1 1 0 001-1V4.5L9.5 1H4zm5 0v3.5h3.5"/></svg></div>
<div class="tf-file-info"><div class="tf-file-name">${f.name}</div><div class="tf-file-meta">${sizeStr}${f.auto?' · auto-generated':''}</div></div>
<div class="tf-file-actions">
${!f.auto?`<div class="tf-file-btn" onclick="event.stopPropagation();tfOpenFile(${isEdit?-1:i})" title="${isEdit?'Fechar':'Editar'}"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M12.146.854a.5.5 0 01.708 0l2.292 2.292a.5.5 0 010 .708L5.854 13.146a.5.5 0 01-.22.128l-3.5 1.166a.5.5 0 01-.632-.632l1.166-3.5a.5.5 0 01.128-.22z"/></svg></div>`:''}
<div class="tf-file-btn" onclick="event.stopPropagation();tfCopyFile(${i})" title="Copiar"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M4 2a2 2 0 012-2h4.586L14 3.414V10a2 2 0 01-2 2H6a2 2 0 01-2-2zm6-1.414L13.414 4H10zM2 6a2 2 0 00-2 2v6a2 2 0 002 2h4a2 2 0 002-2v-1H6a3 3 0 01-3-3V6z"/></svg></div>
<div class="tf-file-btn" onclick="event.stopPropagation();tfDlFile(${i})" title="Download"><svg viewBox="0 0 16 16" width="12" height="12" fill="var(--t3)"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="var(--t3)" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></div>
</div>
</div>`;
if(isEdit){
html+=`<div class="tf-editor">
<div class="tf-editor-hdr"><svg viewBox="0 0 16 16" width="12" height="12" fill="#7b42bc"><path d="M12.146.854a.5.5 0 01.708 0l2.292 2.292a.5.5 0 010 .708L5.854 13.146a.5.5 0 01-.22.128l-3.5 1.166a.5.5 0 01-.632-.632l1.166-3.5a.5.5 0 01.128-.22z"/></svg><span class="tf-ed-name">${f.name}</span>${f.auto?'<span style="font-size:.58rem;color:var(--t3);font-weight:400">somente leitura</span>':'<button class="btn bs" onclick="tfSaveFile(${i})" style="background:var(--pp);color:#fff;border-color:var(--pp);font-size:.6rem;padding:1px 8px">Salvar</button>'}<button class="btn bs" onclick="S.tfEditIdx=-1;R()" style="font-size:.6rem;padding:1px 8px">Fechar</button></div>
<textarea class="tf-editor-ta" id="tfEdTa" ${f.auto?'readonly':''}onkeydown="if(event.key==='Tab'){event.preventDefault();const s=this.selectionStart,e=this.selectionEnd;this.value=this.value.substring(0,s)+' '+this.value.substring(e);this.selectionStart=this.selectionEnd=s+2}">${escHtml(f.content||'')}</textarea>
</div>`;
}
}
return html;
}
function rTfPlanPanel(){
if(!S.tfPlan.length)return`<div class="tf-empty">${TF_ICON}<p>Nenhum recurso no plano</p></div>`;
return S.tfPlan.map(r=>`<div style="display:flex;align-items:center;gap:.5rem;padding:.4rem .5rem;border-radius:8px;background:var(--gnl);margin-bottom:.3rem">
<span style="color:var(--gn);font-weight:700;font-size:.82rem;font-family:var(--fm)">+</span>
<div><div style="font-size:.74rem;font-weight:600;font-family:var(--fm);color:var(--t1)">${escHtml(r.type)}</div>
<div style="font-size:.64rem;color:var(--t3)">${escHtml(r.desc)}</div></div></div>`).join('');
}
function rTfConfirmDestroy(){
return`<div class="tf-modal-overlay" onclick="S.tfConfirmDest=false;R()"><div class="tf-modal" onclick="event.stopPropagation()">
<h3 style="display:flex;align-items:center;gap:.4rem"><svg viewBox="0 0 16 16" width="18" height="18" fill="var(--rd)"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2.5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 3.5zM8 10a1 1 0 100 2 1 1 0 000-2z"/></svg> Confirmar Destroy</h3>
<p style="font-size:.8rem;margin:.5rem 0">Isso irá <strong>destruir todos os recursos</strong> provisionados. Esta ação é irreversível.</p>
<p style="font-size:.74rem;color:var(--t3);margin-bottom:.8rem">Digite <strong>DESTROY</strong> para confirmar:</p>
<input type="text" id="tfDestInput" placeholder="DESTROY" style="margin-bottom:.6rem">
<div style="display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn bs" onclick="S.tfConfirmDest=false;R()">Cancelar</button>
<button class="btn bd" onclick="tfConfirmDestroy()">Destruir</button>
</div>
</div></div>`;
}
function rTfConfirmRollback(){
return`<div class="tf-modal-overlay" onclick="S.tfConfirmRollback=false;R()"><div class="tf-modal" onclick="event.stopPropagation()">
<h3 style="display:flex;align-items:center;gap:.4rem"><svg viewBox="0 0 16 16" width="18" height="18" fill="#e67e22"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2.5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 3.5zM8 10a1 1 0 100 2 1 1 0 000-2z"/></svg> Confirmar Rollback</h3>
<p style="font-size:.8rem;margin:.5rem 0">O apply falhou parcialmente. O rollback irá <strong>destruir todos os recursos</strong> que foram criados antes do erro.</p>
<p style="font-size:.74rem;color:var(--t3);margin-bottom:.8rem">Digite <strong>ROLLBACK</strong> para confirmar:</p>
<input type="text" id="tfRollbackInput" placeholder="ROLLBACK" style="margin-bottom:.6rem">
<div style="display:flex;gap:.5rem;justify-content:flex-end">
<button class="btn bs" onclick="S.tfConfirmRollback=false;R()">Cancelar</button>
<button class="btn bd" onclick="tfDoRollback()">Rollback</button>
</div>
</div></div>`;
}
async function tfDoRollback(){
const inp=document.getElementById('tfRollbackInput');
if(!inp||inp.value!=='ROLLBACK'){alert('Digite ROLLBACK para confirmar.');return}
S.tfConfirmRollback=false;R();
if(!S.tfWs)return;
try{
await $api('/terraform/workspaces/'+S.tfWs+'/destroy',{method:'POST',body:{confirm:'DESTROY'}});
S.tfStatus='destroying';S.tfRunning=true;S.tfDestroyOut='';S.tfBtab='plan';R();
await tfPollExec();
}catch(e){alert('Erro ao iniciar rollback: '+e.message)}
}
function tfStartResize(e){
e.preventDefault();const el=document.getElementById('tfBottom');if(!el)return;
const startY=e.clientY;const startH=el.offsetHeight;
const bar=e.target;bar.classList.add('dragging');
function onMove(ev){const dh=startY-ev.clientY;const h=Math.max(100,Math.min(startH+dh,window.innerHeight-200));el.style.height=h+'px';el.style.flex='none';S.tfBottomH=h}
function onUp(){bar.classList.remove('dragging');document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp)}
document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);
}
function tfOpenFile(i){S.tfEditIdx=S.tfEditIdx===i?-1:i;R();if(S.tfEditIdx>=0)setTimeout(()=>{const ta=document.getElementById('tfEdTa');if(ta){ta.style.height='auto';ta.style.height=Math.min(Math.max(ta.scrollHeight,120),400)+'px'}},20)}
function tfSaveFile(i){const ta=document.getElementById('tfEdTa');if(!ta||i<0||i>=S.tfFiles.length)return;S.tfFiles[i].content=ta.value;S.tfFiles[i].size=ta.value.length;S.tfCode=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');S.tfStatus='draft';R()}
function tfCopyFile(i){
const files=[...S.tfFiles];
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
if(oc){const comp=S.tfCompartment||oc.compartment_id||'';if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,auto:true})}
if(files[i])navigator.clipboard.writeText(files[i].content).then(()=>alert('Copiado!'));
}
function tfDlFile(i){
const files=[...S.tfFiles];
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
if(oc){const comp=S.tfCompartment||oc.compartment_id||'';if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,auto:true})}
const f=files[i];if(!f)return;
const b=new Blob([f.content],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=f.name;a.click();URL.revokeObjectURL(a.href);
}
function tfDlAll(){
// Download each file
const files=[...S.tfFiles];
const oc=S.ociCfg.find(x=>x.id===S.tfOci);
if(oc){const comp=S.tfCompartment||oc.compartment_id||'';if(!files.find(f=>f.name==='terraform.tfvars'))files.push({name:'terraform.tfvars',content:`compartment_id = "${comp}"\nregion = "${oc.region}"`,auto:true})}
files.forEach((f,i)=>{setTimeout(()=>{const b=new Blob([f.content],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=f.name;a.click();URL.revokeObjectURL(a.href)},i*200)});
}
function tfPickModel(v){S.tfModel=v;S.tfMdOpen=false;if(v&&!v.startsWith('cfg:')&&!S.tfOci&&S.ociCfg.length){S.tfOci=S.ociCfg[0].id;S.tfRegion=S.ociCfg[0].region;S.tfCompartment=S.ociCfg[0].compartment_id||'';tfLoadComps()}else if(v&&v.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===v.slice(4));if(g&&g.oci_config_id&&!S.tfOci){S.tfOci=g.oci_config_id;const c=S.ociCfg.find(x=>x.id===g.oci_config_id);if(c){S.tfRegion=c.region;S.tfCompartment=c.compartment_id||''}tfLoadComps()}}R()}
async function tfLoadComps(){
const oid=S.tfOci||(S.tfModel.startsWith('cfg:')?((S.genaiCfg.find(x=>x.id===S.tfModel.slice(4))||{}).oci_config_id||''):'');
if(!oid){S.tfComps=[];R();return}
S.tfCompLoading=true;R();
try{const d=await $api('/oci/explore/'+oid+'/compartment-tree');S.tfComps=d&&!d.error?d:[];
if(S.tfComps.length&&!S.tfCompartment){S.tfCompartment=S.tfComps[0].id;tfLoadResources()}
}catch(e){S.tfComps=[]}
S.tfCompLoading=false;R();
}
function tfFlatComps(comps,depth){
let r=[];for(const c of comps){r.push({id:c.id,name:' '.repeat(depth)+c.name,depth});if(c.children&&c.children.length)r=r.concat(tfFlatComps(c.children,depth+1))}return r;
}
async function tfLoadResources(){
const oid=S.tfOci||(S.tfModel.startsWith('cfg:')?((S.genaiCfg.find(x=>x.id===S.tfModel.slice(4))||{}).oci_config_id||''):'');
const comp=S.tfCompartment;
if(!oid||!comp){S.tfResources=null;R();return}
S.tfResLoading=true;R();
try{const d=await $api('/terraform/resources?oci_config_id='+encodeURIComponent(oid)+'&compartment_id='+encodeURIComponent(comp)+(S.tfRegion?'&region='+encodeURIComponent(S.tfRegion):''));
S.tfResources=d&&!d.error?d:null;
}catch(e){S.tfResources=null}
S.tfResLoading=false;R();
}
function rTfResourcesPanel(){
if(S.tfResLoading)return`<div class="tf-empty"><div class="spinner"></div><p>Carregando recursos...</p></div>`;
if(!S.tfCompartment)return`<div class="tf-empty"><svg viewBox="0 0 16 16" width="36" height="36" fill="#7b42bc" opacity=".2"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg><p>Selecione um compartment para ver os recursos existentes</p></div>`;
if(!S.tfResources)return`<div class="tf-empty"><svg viewBox="0 0 16 16" width="36" height="36" fill="#7b42bc" opacity=".2"><path d="M8 1L1 5l7 4 7-4zm0 6L1 11l7 4 7-4z"/></svg><p>Nenhum recurso carregado</p><button class="btn bs bsm" onclick="tfLoadResources()" style="margin-top:.5rem">Carregar Recursos</button></div>`;
const res=S.tfResources;
const cats=[
{key:'vcns',label:'VCNs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M1 3h14v2H1zm2 4h10v2H3zm3 4h4v2H6z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${(r.cidr_blocks||[]).join(', ')})</span>`},
{key:'subnets',label:'Subnets',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h5v5H2zm7 0h5v5H9zm-7 7h5v5H2zm7 0h5v5H9z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.cidr_block}, ${r.public?'pública':'privada'})</span>`},
{key:'instances',label:'Compute',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 2h12v12H2z"/></svg>',fmt:r=>{const stopped=r.state==='STOPPED';const running=r.state==='RUNNING';const stC=stopped?'var(--rd)':running?'var(--gn)':'var(--t3)';return`${r.display_name} <span style="opacity:.6">(${r.shape})</span> <span style="color:${stC};font-weight:600;font-size:.62rem">${r.state}</span>`}},
{key:'internet_gateways',label:'Internet GWs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><circle cx="8" cy="8" r="6" fill="none" stroke="#7b42bc" stroke-width="1.5"/><path d="M1 8h14M8 2c-2 2-2 10 0 12m0-12c2 2 2 10 0 12"/></svg>',fmt:r=>r.display_name},
{key:'nat_gateways',label:'NAT GWs',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M3 3l5 5-5 5"/><path d="M8 8h6" stroke="#7b42bc" stroke-width="1.5"/></svg>',fmt:r=>r.display_name},
{key:'route_tables',label:'Route Tables',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M2 3h12v2H2zm0 4h8v2H2zm0 4h10v2H2z"/></svg>',fmt:r=>r.display_name},
{key:'security_lists',label:'Security Lists',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M8 1l6 3v4c0 3.5-2.5 6.5-6 7.5C4.5 14.5 2 11.5 2 8V4z"/></svg>',fmt:r=>r.display_name},
{key:'autonomous_databases',label:'Databases',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><ellipse cx="8" cy="4" rx="6" ry="2.5"/><path d="M2 4v8c0 1.4 2.7 2.5 6 2.5s6-1.1 6-2.5V4"/></svg>',fmt:r=>{const stopped=r.state==='STOPPED';const avail=r.state==='AVAILABLE';const stC=stopped?'var(--rd)':avail?'var(--gn)':'var(--t3)';return`${r.display_name} <span style="opacity:.6">(${r.db_name}${r.is_free_tier?' Free':''})</span> <span style="color:${stC};font-weight:600;font-size:.62rem">${r.state}</span>`}},
{key:'block_volumes',label:'Block Volumes',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M3 2h10v12H3z"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.size_in_gbs||'?'} GB)</span>`},
{key:'load_balancers',label:'Load Balancers',icon:'<svg viewBox="0 0 16 16" width="14" height="14" fill="#7b42bc"><path d="M8 2v12M2 8h12M4 4l8 8M12 4l-8 8"/></svg>',fmt:r=>`${r.display_name} <span style="opacity:.6">(${r.shape_name||'flexible'})</span>`}
];
let total=0;
let html='<div style="padding:.5rem">';
html+='<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem"><span style="font-size:.72rem;font-weight:600;color:var(--t2)">Recursos existentes no compartment</span><button class="btn bs bsm" onclick="tfLoadResources()" style="font-size:.6rem;padding:1px 6px">Atualizar</button></div>';
for(const cat of cats){
const items=res[cat.key]||[];
if(!items.length)continue;
total+=items.length;
html+=`<div style="margin-bottom:.6rem"><div style="display:flex;align-items:center;gap:.3rem;font-size:.7rem;font-weight:600;color:#7b42bc;margin-bottom:.2rem">${cat.icon} ${cat.label} (${items.length})</div>`;
for(const r of items.slice(0,10)){
html+=`<div style="font-size:.68rem;padding:.15rem .4rem .15rem 1.2rem;color:var(--t2);border-left:2px solid #7b42bc22">${cat.fmt(r)}</div>`;
}
if(items.length>10)html+=`<div style="font-size:.62rem;padding:.1rem .4rem .1rem 1.2rem;color:var(--t3)">... e mais ${items.length-10}</div>`;
html+='</div>';
}
if(!total)html+='<div style="font-size:.72rem;color:var(--t3);text-align:center;padding:1rem">Nenhum recurso encontrado neste compartment</div>';
else html+=`<div style="font-size:.66rem;color:var(--t3);border-top:1px solid var(--bd);padding-top:.3rem;margin-top:.3rem">Total: <strong>${total}</strong> recursos. O agente irá reutilizá-los automaticamente quando possível.</div>`;
html+='</div>';
return html;
}
async function ociInstanceAction(id,action){
const label=action==='START'?'Iniciar':'Parar';
if(!confirm(label+' esta instância?'))return;
try{await $api('/oci/instances/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='START'?'RUNNING':'STOPPED')}catch(e){alert('Erro: '+e.message)}
}
async function ociAdbAction(id,action){
const label=action==='start'?'Iniciar':'Parar';
if(!confirm(label+' este Autonomous Database?'))return;
try{await $api('/oci/autonomous-databases/'+id+'/action',{method:'POST',body:{action,oci_config_id:S.expCfg,region:null}});expPollState(action==='start'?'AVAILABLE':'STOPPED')}catch(e){alert('Erro: '+e.message)}
}
async function ociAdbUpdateNet(id){
try{
const ipResp=await $api('/oci/my-ip');
let ip=ipResp.ip||'';
ip=prompt('Adicionar IP à ACL do Autonomous Database.\nSeu IP detectado:',ip);
if(!ip)return;
await $api('/oci/autonomous-databases/'+id+'/update-network-access',{method:'POST',body:{ip,oci_config_id:S.expCfg}});
alert('IP '+ip+' adicionado à ACL com sucesso.');
expLoadRes();
}catch(e){alert('Erro: '+e.message)}
}
function expPollState(target){
let tries=0;const max=12;
const poll=async()=>{
await expLoadRes();
const items=S.expData||[];
const reached=items.some(i=>i.lifecycle_state===target);
if(reached||++tries>=max)return;
setTimeout(poll,5000);
};
setTimeout(poll,3000);
}
function tfSwitchBtab(tab){
S.tfBtab=tab;S.tfEditIdx=-1;
if(tab==='resources'&&!S.tfResources&&S.tfCompartment)tfLoadResources();
// Update only bottom panel content + tab active states
const left=document.querySelector('.tf-bottom-content');if(left)left.innerHTML=rTfBottomContent();
const tabs=document.querySelectorAll('.tf-btab');tabs.forEach(t=>{const isActive=t.textContent.trim().toLowerCase().startsWith(tab);t.classList.toggle('active',isActive)});
// Update actions
const acts=document.querySelector('.tf-bottom-actions');if(acts){acts.innerHTML=(S.tfFiles.length?'<button class="btn bs bsm" onclick="tfDlAll()" style="font-size:.66rem;padding:2px 8px">Download All</button>':'')+rTfActions()}
}
function tfToggleModelDrop(){const dd=document.getElementById('tfmdd');dd.classList.toggle('open');S.tfMdOpen=dd.classList.contains('open');if(S.tfMdOpen){const inp=document.getElementById('tfmds');if(inp){inp.value='';inp.focus();tfFilterModels('')}}}
function tfFilterModels(q){const list=document.getElementById('tfmdl');if(!list)return;const items=list.querySelectorAll('.itm,.grp');const ql=q.toLowerCase();let lastGrp=null,vis=false;items.forEach(el=>{if(el.classList.contains('grp')){if(lastGrp)lastGrp.style.display=vis?'':'none';lastGrp=el;vis=false}else{const show=!q||el.textContent.toLowerCase().includes(ql);el.style.display=show?'':'none';if(show)vis=true}});if(lastGrp)lastGrp.style.display=vis?'':'none'}
function tfTogglePanel(p){S.tfPanel=S.tfPanel===p?'':p;R()}
async function tfClear(){
if(S.tfWs)try{await $api('/terraform/workspaces/'+S.tfWs,{method:'DELETE'})}catch(e){}
if(S.tfSid)try{await $api('/chat/'+S.tfSid,{method:'DELETE'})}catch(e){}
S.tfMsgs=[];S.tfSid=null;S.tfPlan=[];S.tfCode='';S.tfFiles=[];S.tfWs=null;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfStatus='draft';S.tfRunning=false;S.tfHadApply=false;S.tfConfirmDest=false;S.tfConfirmRollback=false;S.tfComps=[];S.tfCompartment='';S.tfResources=null;S.tfResLoading=false;R()
}
function tfScroll(){setTimeout(()=>{const e=document.getElementById('tfchm');if(e)e.scrollTop=e.scrollHeight},50)}
function copyTfCode(){navigator.clipboard.writeText(S.tfCode).then(()=>alert('Código copiado!'))}
function dlTfCode(){if(!S.tfFiles.length)return;if(S.tfFiles.length===1){const f=S.tfFiles[0];const b=new Blob([f.content],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=f.name;a.click();URL.revokeObjectURL(a.href)}else{tfDlAll()}}
async function tfSend(){
const el=document.getElementById('tfi');const m=el.value.trim();if(!m)return;el.style.height='auto';S._tfiVal='';
if(!S.tfModel){S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione um modelo antes de enviar.',t:tstamp()});R();return}
if(!S.tfCompartment){S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione um compartment antes de enviar.',t:tstamp()});R();return}
el.value='';S.tfMsgs.push({r:'user',c:m,t:tstamp()});R();tfScroll(); // show original msg without injected files
S.tfMsgs.push({r:'assistant',c:IC.wait+' _Gerando Terraform..._',thinking:true});R();tfScroll();
try{
// Inject existing files context when user is correcting/fixing
let finalMsg=m;
if(S.tfFiles.length>0){
const filesCtx=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
finalMsg+='\n\n---\nARQUIVOS TERRAFORM ATUAIS NO WORKSPACE:\n```hcl\n'+filesCtx+'\n```\n\n⚠ REGRA OBRIGATÓRIA: Gere APENAS os arquivos que precisam de correção. NÃO regenere arquivos sem erro. Se o erro está em um arquivo específico, retorne SOMENTE esse arquivo com `// filename:`. Arquivos não mencionados na sua resposta serão MANTIDOS como estão.';
}
if(S.tfPlanOut){finalMsg+='\n\n---\nÚLTIMO OUTPUT DO TERRAFORM PLAN:\n```\n'+S.tfPlanOut.slice(-3000)+'\n```'}
if(S.tfApplyOut){finalMsg+='\n\n---\nÚLTIMO OUTPUT DO TERRAFORM APPLY:\n```\n'+S.tfApplyOut.slice(-3000)+'\n```'}
const body={message:finalMsg,session_id:S.tfSid,use_tools:false};
if(S.tfModel.startsWith('cfg:'))body.genai_config_id=S.tfModel.slice(4);
else{
if(!S.tfOci){S.tfMsgs.pop();S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione uma credencial OCI.',t:tstamp()});R();return}
body.model_id=S.tfModel;body.oci_config_id=S.tfOci;body.genai_region=S.tfRegion;body.compartment_id=S.tfCompartment;
}
const d=await $api('/terraform/chat',{method:'POST',body});
S.tfSid=d.session_id;
if(S.tfHistOpen)loadHistory('terraform');
if(d.status==='processing'&&d.message_id){await tfPollResult(d.message_id)}
else{S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:d.response||d.content||'Sem resposta',t:tstamp()});R();tfScroll()}
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro: '+e.message,t:tstamp()});R()}
}
async function tfPollResult(mid){
for(let i=0;i<600;i++){
await new Promise(r=>setTimeout(r,i<10?1000:3000));
try{const r=await $api('/chat/'+mid+'/status');
if(r.status==='done'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:r.content,t:tstamp()});R();tfScroll();
if(r.content&&r.content.includes('```plan')){S.tfPanel='plan';R()}return}
if(r.status==='failed'){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' '+r.content,t:tstamp()});R();tfScroll();return}
}catch(e){}
}
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.timeout+' Timeout.',t:tstamp()});R()
}
async function tfSaveAndPlan(){
if(!S.tfCode){alert('Gere código Terraform primeiro.');return}
// Resolve OCI config
let ociId=S.tfOci;
if(!ociId&&S.tfModel.startsWith('cfg:')){const g=S.genaiCfg.find(x=>x.id===S.tfModel.slice(4));if(g)ociId=g.oci_config_id}
if(!ociId&&S.ociCfg.length)ociId=S.ociCfg[0].id;
if(!ociId){alert('Selecione uma credencial OCI.');return}
try{
// Create or update workspace
if(!S.tfWs){
const d=await $api('/terraform/workspaces',{method:'POST',body:{session_id:S.tfSid||'none',oci_config_id:ociId,compartment_id:S.tfCompartment||null,name:'workspace',tf_code:S.tfCode}});
S.tfWs=d.id;
}else{
await $api('/terraform/workspaces/'+S.tfWs+'/code',{method:'PUT',body:{tf_code:S.tfCode}});
}
// Run plan
await $api('/terraform/workspaces/'+S.tfWs+'/plan',{method:'POST'});
S.tfStatus='planning';S.tfRunning=true;S.tfPlanOut='';S.tfApplyOut='';S.tfDestroyOut='';S.tfBtab='plan';R();
await tfPollExec();
}catch(e){alert('Erro: '+e.message)}
}
async function tfApply(){
if(!S.tfWs){alert('Execute Plan primeiro.');return}
try{
await $api('/terraform/workspaces/'+S.tfWs+'/apply',{method:'POST'});
S.tfStatus='applying';S.tfRunning=true;S.tfHadApply=true;S.tfApplyOut='';S.tfBtab='plan';R();
await tfPollExec();
}catch(e){alert('Erro: '+e.message)}
}
async function tfConfirmDestroy(){
const inp=document.getElementById('tfDestInput');
if(!inp||inp.value!=='DESTROY'){alert('Digite DESTROY para confirmar.');return}
S.tfConfirmDest=false;R();
if(!S.tfWs)return;
try{
await $api('/terraform/workspaces/'+S.tfWs+'/destroy',{method:'POST',body:{confirm:'DESTROY'}});
S.tfStatus='destroying';S.tfRunning=true;S.tfDestroyOut='';S.tfBtab='plan';R();
await tfPollExec();
}catch(e){alert('Erro: '+e.message)}
}
async function tfCancel(){
if(!S.tfWs)return;
try{await $api('/terraform/workspaces/'+S.tfWs+'/cancel',{method:'POST'})}catch(e){}
S.tfRunning=false;S.tfStatus='failed';R();
}
async function tfAutoFix(){
if(!S.tfPlanOut||!S.tfFiles.length||!S.tfModel)return;
// Extract errors from plan output
const errLines=S.tfPlanOut.split('\n').filter(l=>/^Error:|^\s+on\s+\S+\.tf\s+line|^\s+\d+:|has not been declared|is not expected|Missing required|Unsupported|Invalid|Duplicate|undeclared/i.test(l));
const errCtx=errLines.length?errLines.join('\n'):S.tfPlanOut.slice(-2000);
// Build auto-fix message
const filesCtx=S.tfFiles.map(f=>'// filename: '+f.name+'\n'+f.content).join('\n\n');
const fixMsg='O terraform plan falhou com os seguintes erros. Corrija SOMENTE os arquivos afetados.\n\n---\nERROS DO PLAN:\n```\n'+errCtx+'\n```\n\n---\nARQUIVOS TERRAFORM ATUAIS NO WORKSPACE:\n```hcl\n'+filesCtx+'\n```\n\n⚠ REGRA OBRIGATÓRIA: Gere APENAS os arquivos que precisam de correção com `// filename:`. Arquivos não mencionados serão MANTIDOS como estão.';
// Show in chat
S.tfMsgs.push({r:'user',c:'🔧 Auto-corrigir erros do plan',t:tstamp()});
S.tfMsgs.push({r:'assistant',c:IC.wait+' _Analisando erros e corrigindo..._',thinking:true});R();tfScroll();
try{
const body={message:fixMsg,session_id:S.tfSid,use_tools:false};
if(S.tfModel.startsWith('cfg:'))body.genai_config_id=S.tfModel.slice(4);
else{
if(!S.tfOci){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.warn+' Selecione uma credencial OCI.',t:tstamp()});R();return}
body.model_id=S.tfModel;body.oci_config_id=S.tfOci;body.genai_region=S.tfRegion;body.compartment_id=S.tfCompartment;
}
const d=await $api('/terraform/chat',{method:'POST',body});
S.tfSid=d.session_id;
if(d.status==='processing'&&d.message_id){await tfPollResult(d.message_id)}
else{S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:d.response||'Sem resposta',t:tstamp()});R();tfScroll()}
// Auto re-plan after fix
if(S.tfFiles.length&&S.tfCode){
S.tfMsgs.push({r:'assistant',c:IC.wait+' _Re-executando plan..._',thinking:true});R();
await tfSaveAndPlan();
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);R()
}
}catch(e){S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.err+' Erro auto-fix: '+e.message,t:tstamp()});R()}
}
async function tfPollExec(){
for(let i=0;i<1800;i++){
await new Promise(r=>setTimeout(r,2000));
try{
const r=await $api('/terraform/workspaces/'+S.tfWs+'/status');
S.tfPlanOut=r.plan_output||S.tfPlanOut;
S.tfApplyOut=r.apply_output||S.tfApplyOut;
S.tfDestroyOut=r.destroy_output||S.tfDestroyOut;
S.tfStatus=r.status;
const done=!['planning','applying','destroying'].includes(r.status);
if(done){S.tfRunning=false;if(r.status==='destroyed')S.tfHadApply=false;}
// Update only the terminal body without full re-render
const t=document.getElementById('tfTermOut');
if(t){
const content=S.tfDestroyOut||S.tfApplyOut||S.tfPlanOut||'';
t.innerHTML=content?escHtml(content):'<span style="opacity:.4">Aguardando execução...</span>';
t.scrollTop=t.scrollHeight;
}
if(done){R();return}
}catch(e){}
}
S.tfRunning=false;R();
}
/* ── OCI Explorer ── */
const EXP_GROUPS=[
{name:'Compute',tabs:[
{key:'instances',label:'Instances'},{key:'boot_volumes',label:'Boot Vol'},{key:'instance_pools',label:'Pools'}]},
{name:'Rede',tabs:[
{key:'vcns',label:'VCN'},{key:'subnets',label:'Subnets'},{key:'security_lists',label:'Sec Lists'},
{key:'nsgs',label:'NSGs'},{key:'route_tables',label:'Routes'},{key:'nat_gateways',label:'NAT GW'},
{key:'internet_gateways',label:'IGW'},{key:'service_gateways',label:'SGW'},{key:'public_ips',label:'IPs Públicos'},
{key:'load_balancers',label:'Load Balancer'}]},
{name:'Storage',tabs:[
{key:'buckets',label:'Buckets'},{key:'block_volumes',label:'Block Vol'},
{key:'file_systems',label:'File Storage'},{key:'mount_targets',label:'Mount Targets'}]},
{name:'Database',tabs:[
{key:'databases',label:'ADB'},{key:'db_systems',label:'DB Systems'},{key:'mysql_db_systems',label:'MySQL'}]},
{name:'Containers',tabs:[
{key:'container_instances',label:'Containers'},{key:'oke_clusters',label:'OKE'}]},
{name:'Serverless',tabs:[
{key:'functions',label:'Functions'},{key:'api_gateways',label:'API Gateway'}]},
{name:'Observabilidade',tabs:[
{key:'alarms',label:'Alarmes'},{key:'log_groups',label:'Log Groups'},
{key:'notification_topics',label:'Notificações'},{key:'events_rules',label:'Events'}]},
{name:'Segurança',tabs:[
{key:'vaults',label:'Vaults'},{key:'dns_zones',label:'DNS'},
{key:'network_firewalls',label:'Firewalls'},{key:'network_firewall_policies',label:'FW Policies'}]},
{name:'IAM',tabs:[
{key:'iam_users',label:'Usuários'},{key:'iam_groups',label:'Grupos'},
{key:'iam_policies',label:'Policies'},{key:'iam_dynamic_groups',label:'Dynamic Groups'}]}
];
const EXP_TABS=EXP_GROUPS.flatMap(g=>g.tabs);
function rExplorer(){
if(!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">${IC.cloud}</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>Credenciais OCI</strong>.</p></div></div>`;
if(!S.expCfg&&S.ociCfg.length)S.expCfg=S.ociCfg[0].id;
const cfgOpts=S.ociCfg.map(c=>`<option value="${c.id}"${c.id===S.expCfg?' selected':''}>${c.tenancy_name} (${c.region})</option>`).join('');
const regLabel=S.expSelRegions.length?S.expSelRegions.length+' região(ões)':'Todas as regiões';
const regChecks=S.expRegions.map(r=>`<label style="display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;font-size:.72rem;cursor:pointer;border-radius:4px;transition:background .1s" onmouseover="this.style.background='var(--bg2)'" onmouseout="this.style.background=''"><input type="checkbox" ${S.expSelRegions.includes(r.name)?'checked':''} onchange="expToggleRegion('${r.name}')" style="accent-color:var(--ac)"> ${r.name}${r.is_home?' <span style="font-size:.58rem;color:var(--ac)">(Home)</span>':''}</label>`).join('');
const catIcons={Compute:IC.computer,Rede:IC.globe,Storage:IC.hdd,Database:IC.db,Containers:IC.container,Serverless:IC.zap,Observabilidade:IC.activity,Segurança:IC.shield,IAM:IC.user};
const cats=EXP_GROUPS.map(g=>`<div class="exp-cat${S.expCat===g.name?' active':''}" onclick="expSetCat('${g.name}')">${catIcons[g.name]||''} ${g.name}</div>`).join('');
const activeGrp=EXP_GROUPS.find(g=>g.name===S.expCat)||EXP_GROUPS[0];
const tabs=activeGrp.tabs.map(t=>{const cnt=S.expCounts[t.key];return`<div class="exp-tab${S.expResType===t.key?' active':''}" onclick="expSetTab('${t.key}')">${t.label}${cnt!=null?' <span style="font-size:.58rem;opacity:.7">('+cnt+')</span>':''}</div>`}).join('');
return`<div class="exp-wrap">
<div class="exp-tree" id="exp-tree"><div style="padding:.5rem .65rem .4rem;font-size:.74rem;font-weight:700;color:var(--t2);border-bottom:1px solid var(--bd);margin-bottom:.3rem;display:flex;align-items:center;gap:.35rem">${IC.folder} Compartments</div>${S.expTree.length?rExpTree(null,0):'<div class="ld"><span class="spinner"></span> Carregando...</div>'}</div>
<div class="exp-resize" onmousedown="expStartResize(event)"></div>
<div class="exp-main">
<div class="exp-toolbar"><select onchange="expSetCfg(this.value)">${cfgOpts}</select>
${S.expRegions.length?`<div class="dd" style="position:relative" onclick="event.stopPropagation()"><div onclick="S.expRegDdOpen=!S.expRegDdOpen;R()" style="display:flex;align-items:center;gap:.3rem;cursor:pointer;padding:.3rem .6rem;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;font-size:.72rem;white-space:nowrap;min-width:130px;justify-content:space-between"><span>${IC.globe} ${regLabel}</span><span style="font-size:.55rem;color:var(--t4)">▼</span></div>
${S.expRegDdOpen?`<div style="position:absolute;top:100%;left:0;margin-top:4px;background:var(--bg);border:1px solid var(--bd);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.12);z-index:60;min-width:220px;overflow:hidden">
<div style="padding:.5rem .6rem;border-bottom:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center"><span style="font-size:.7rem;font-weight:600;color:var(--t2)">Regiões</span><div style="display:flex;gap:.3rem"><button class="btn bs" style="font-size:.6rem;padding:.15rem .4rem" onclick="S.expSelRegions=S.expRegions.map(r=>r.name);R()">Todas</button><button class="btn bs" style="font-size:.6rem;padding:.15rem .4rem" onclick="S.expSelRegions=[];R()">Nenhuma</button></div></div>
<div style="max-height:240px;overflow-y:auto;padding:.3rem 0">${regChecks}</div>
<div style="padding:.4rem .6rem;border-top:1px solid var(--bd);text-align:right"><button class="btn bp" style="font-size:.68rem;padding:.25rem .7rem" onclick="S.expRegDdOpen=false;expApplyRegions()">Aplicar</button></div>
</div>`:''}</div>`:''}
${S.expSelRegions.length?`<div style="display:flex;gap:.25rem;flex-wrap:wrap;align-items:center">${S.expSelRegions.map(r=>`<span class="tag" style="font-size:.6rem;display:inline-flex;align-items:center;gap:.2rem;background:var(--acl);color:var(--ac);border:1px solid var(--ac);border-radius:6px;padding:.1rem .35rem">${r}<span onclick="expRemoveRegion('${r}')" style="cursor:pointer;font-weight:700;font-size:.7rem">&times;</span></span>`).join('')}</div>`:''}
<button class="btn" style="margin-left:auto;font-size:.7rem;padding:.25rem .6rem" onclick="expRefresh()">↻ Refresh</button></div>
<div class="exp-nav"><div class="exp-cats">${cats}</div></div>
<div class="exp-tabs">${tabs}</div>
<div class="exp-content">${S.expLoading?'<div class="ld"><span class="spinner"></span> Consultando OCI...</div>':renderExpData(S.expData)}</div>
</div></div>`}
function rExpTree(parentId,depth){
const children=S.expTree.filter(n=>n.parent_id===parentId);
if(!children.length)return'';
return children.map(n=>{
const hasKids=S.expTree.some(c=>c.parent_id===n.id);
const isOpen=S.expTreeOpen[n.id];
const isSel=S.expSelComp===n.id;
return`<div class="exp-node" style="padding-left:${depth*0.6}rem">
<div class="exp-node-row${isSel?' selected':''}" onclick="expSelectComp('${n.id}')">
<span class="exp-node-toggle" onclick="event.stopPropagation();expToggleNode('${n.id}')">${hasKids?(isOpen?'▼':'▶'):' '}</span>
<span>${n.name}</span></div>
${hasKids&&isOpen?`<div class="exp-node-children">${rExpTree(n.id,depth+1)}</div>`:''}</div>`}).join('')}
async function expSetCfg(cid){S.expCfg=cid;S.expTree=[];S.expSelComp='';S.expData=null;S.expSelRegions=[];S.expRegions=[];S.expCounts={};S.expRegDdOpen=false;R();await expLoadTree();await expLoadRegions()}
function expToggleRegion(r){const i=S.expSelRegions.indexOf(r);if(i>=0)S.expSelRegions.splice(i,1);else S.expSelRegions.push(r);R()}
function expRemoveRegion(r){S.expSelRegions=S.expSelRegions.filter(x=>x!==r);if(S.expSelComp)expLoadRes()}
async function expApplyRegions(){if(S.expSelComp)await expLoadRes()}
function expSetCat(c){S.expCat=c;const grp=EXP_GROUPS.find(g=>g.name===c);if(grp&&grp.tabs.length){S.expResType=grp.tabs[0].key;S.expData=null;R();if(S.expSelComp)expLoadRes()}else R()}
function expSetTab(t){S.expResType=t;S.expData=null;R();if(S.expSelComp)expLoadRes()}
function expToggleNode(id){S.expTreeOpen[id]=!S.expTreeOpen[id];R()}
async function expSelectComp(id){S.expSelComp=id;S.expCounts={};await expLoadRes()}
function expStartResize(e){e.preventDefault();const tree=document.getElementById('exp-tree');const handle=e.target;if(!tree)return;handle.classList.add('dragging');const startX=e.clientX;const startW=tree.offsetWidth;
const onMove=ev=>{const w=Math.max(140,Math.min(startW+(ev.clientX-startX),window.innerWidth*0.5));tree.style.width=w+'px';S.expTreeW=w};
const onUp=()=>{handle.classList.remove('dragging');document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);document.body.style.cursor='';document.body.style.userSelect=''};
document.body.style.cursor='col-resize';document.body.style.userSelect='none';document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp)}
async function expRefresh(){if(S.expCfg){S.expTree=[];S.expCounts={};R();await expLoadTree();if(S.expSelComp)await expLoadRes()}}
async function expLoadTree(){
if(!S.expCfg)return;
try{const d=await $api('/oci/explore/'+S.expCfg+'/compartment-tree');
S.expTree=d.error?[]:(d||[]);
if(S.expTree.length&&!S.expSelComp){S.expSelComp=S.expTree[0].id;S.expTreeOpen[S.expTree[0].id]=true}
R();if(S.expSelComp)await expLoadRes()}catch(e){S.expTree=[];R()}}
async function expLoadRegions(){
if(!S.expCfg)return;
try{const d=await $api('/oci/explore/'+S.expCfg+'/regions');S.expRegions=d.error?[]:(d||[]);R()}catch(e){S.expRegions=[];R()}}
async function expLoadRes(){
if(!S.expCfg||!S.expSelComp)return;
S.expLoading=true;S._expExpanded={};R();
const regions=S.expSelRegions.length?S.expSelRegions:[null];
let all=[];let errors=[];
const BATCH=3;const DELAY=800;
for(let i=0;i<regions.length;i+=BATCH){
const batch=regions.slice(i,i+BATCH);
const fetches=batch.map(async reg=>{
const q=new URLSearchParams({compartment_id:S.expSelComp});
if(reg)q.set('region',reg);
try{const d=await $api('/oci/explore/'+S.expCfg+'/'+S.expResType+'?'+q);
if(d.error){errors.push(reg+': '+d.error)}
else if(Array.isArray(d)){d.forEach(item=>{if(reg&&!item.region)item._region=reg});all=all.concat(d)}}
catch(e){errors.push((reg||'default')+': '+e.message)}});
await Promise.all(fetches);
if(i+BATCH<regions.length)await new Promise(r=>setTimeout(r,DELAY))}
if(all.length)S.expData=all;
else if(errors.length)S.expData={error:errors.join('\n')};
else S.expData=[];
S.expCounts[S.expResType]=all.length;
S.expLoading=false;R()}
function renderExpData(d){if(!d)return'<div class="emp"><p>Selecione um compartment na árvore.</p></div>';
if(d.error)return`<div class="al al-e">${d.error}</div>`;if(!Array.isArray(d)||!d.length)return'<div class="emp"><p>Nenhum recurso encontrado neste compartment.</p></div>';
if(S.expResType==='security_lists')return rExpSecLists(d);
if(S.expResType==='nsgs')return rExpNsgs(d);
if(S.expResType==='route_tables')return rExpRouteTables(d);
const isInst=S.expResType==='instances';
const isAdb=S.expResType==='databases';
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);
const name=i.display_name||i.name||i.db_name||i.application||'—';
const skip=['display_name','name','db_name','lifecycle_state','application','rules','routes','whitelisted_ips'];
const aclHtml=isAdb&&i.whitelisted_ips&&i.whitelisted_ips.length?'<br><span style="color:var(--ac);font-size:.66rem;font-weight:600">ACL:</span> <span class="em" style="font-size:.66rem">'+i.whitelisted_ips.join(', ')+'</span>':'';
let actionBtn='';
if(isInst&&i.lifecycle_state==='RUNNING')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:6px" onclick="event.stopPropagation();ociInstanceAction(\''+i.id+'\',\'STOP\')">■ Stop</button>';
else if(isInst&&i.lifecycle_state==='STOPPED')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:6px" onclick="event.stopPropagation();ociInstanceAction(\''+i.id+'\',\'START\')">▶ Start</button>';
else if(isAdb&&i.lifecycle_state==='AVAILABLE')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--rd);color:#fff;border-color:var(--rd);margin-left:6px" onclick="event.stopPropagation();ociAdbAction(\''+i.id+'\',\'stop\')">■ Stop</button> <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--ac);color:#fff;border-color:var(--ac);margin-left:2px" onclick="event.stopPropagation();ociAdbUpdateNet(\''+i.id+'\')">'+IC.shield+' Update Network Access</button>';
else if(isAdb&&i.lifecycle_state==='STOPPED')actionBtn=' <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--gn);color:#fff;border-color:var(--gn);margin-left:6px" onclick="event.stopPropagation();ociAdbAction(\''+i.id+'\',\'start\')">▶ Start</button> <button class="btn bsm" style="font-size:.6rem;padding:1px 8px;background:var(--ac);color:#fff;border-color:var(--ac);margin-left:2px" onclick="event.stopPropagation();ociAdbUpdateNet(\''+i.id+'\')">'+IC.shield+' Update Network Access</button>';
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>`:''}${actionBtn}${aclHtml}<br>
${keys.filter(k=>!skip.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>`}
function rExpSecLists(d){
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} security list(s)</div>${d.map((sl,idx)=>{
const rules=sl.rules||[];
const ingress=rules.filter(r=>r.direction==='ingress');
const egress=rules.filter(r=>r.direction==='egress');
const expanded=S._expExpanded&&S._expExpanded[idx];
const showForm=S._slAddForm===sl.id;
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded[${idx}]=!S._expExpanded[${idx}];R()">
<div><strong style="display:inline">${sl.display_name}</strong> <span class="badge ${sl.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${sl.lifecycle_state}</span></div>
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${ingress.length} ingress · ${egress.length} egress</div></div>
<div class="em" style="margin-top:.3rem">id: ${sl.id}</div>
${expanded?`<div style="margin-top:.5rem">
<div style="display:flex;justify-content:flex-end;margin-bottom:.4rem"><button class="btn bp bsm" onclick="event.stopPropagation();S._slAddForm=S._slAddForm==='${sl.id}'?null:'${sl.id}';R()" style="font-size:.62rem">${showForm?'✕ Cancelar':'+ Adicionar Regra'}</button></div>
${showForm?`<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:.6rem;margin-bottom:.6rem" onclick="event.stopPropagation()">
<div style="font-size:.7rem;font-weight:600;color:var(--t2);margin-bottom:.4rem">${IC.shield} Nova Regra</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.4rem">
<div class="ig"><label style="font-size:.62rem">Direção</label><select id="slDir" style="font-size:.72rem"><option value="ingress">Ingress</option><option value="egress">Egress</option></select></div>
<div class="ig"><label style="font-size:.62rem">Protocolo</label><select id="slProto" style="font-size:.72rem"><option value="6">TCP</option><option value="17">UDP</option><option value="1">ICMP</option><option value="all">ALL</option></select></div>
<div class="ig"><label style="font-size:.62rem">Stateless</label><select id="slStateless" style="font-size:.72rem"><option value="false">Não</option><option value="true">Sim</option></select></div>
<div class="ig"><label style="font-size:.62rem">Origem/Destino (CIDR)</label><input id="slCidr" style="font-size:.72rem" placeholder="0.0.0.0/0" value="0.0.0.0/0"></div>
<div class="ig"><label style="font-size:.62rem">Porta Início</label><input id="slPortMin" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
<div class="ig"><label style="font-size:.62rem">Porta Fim</label><input id="slPortMax" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
</div>
<div class="ig" style="margin-top:.3rem"><label style="font-size:.62rem">Descrição</label><input id="slDesc" style="font-size:.72rem" placeholder="Descrição da regra"></div>
<div style="display:flex;gap:.3rem;margin-top:.5rem;justify-content:flex-end"><button class="btn bp bsm" onclick="slAddRule('${sl.id}')" style="font-size:.64rem">Salvar Regra</button>
<button class="btn bs bsm" onclick="slAddMyIp('${sl.id}')" style="font-size:.64rem" title="Detecta seu IP público e adiciona como regra">${IC.globe} Meu IP</button></div>
<div id="slFormMsg" style="margin-top:.3rem"></div>
</div>`:''}
${ingress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↓ Ingress (${ingress.length})</div>
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Origem</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
${ingress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'&quot;')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();slRemoveRule('${sl.id}','ingress',${r.rule_index})" title="Remover regra">✕</button></td></tr>`).join('')}
</tbody></table>`:''}
${egress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↑ Egress (${egress.length})</div>
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
${egress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'&quot;')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();slRemoveRule('${sl.id}','egress',${r.rule_index})" title="Remover regra">✕</button></td></tr>`).join('')}
</tbody></table>`:''}</div>`:''}</div>`}).join('')}`}
async function slAddRule(slId){
const dir=document.getElementById('slDir').value;
const proto=document.getElementById('slProto').value;
const cidr=document.getElementById('slCidr').value.trim();
const portMin=document.getElementById('slPortMin').value;
const portMax=document.getElementById('slPortMax').value;
const desc=document.getElementById('slDesc').value;
const stateless=document.getElementById('slStateless').value==='true';
if(!cidr){sm('slFormMsg','Informe o CIDR.','e');return}
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
const body={direction:dir,protocol:proto,source_dest:cidr,description:desc,is_stateless:stateless,region:region||undefined};
if(proto!=='all'&&proto!=='1'&&portMin)Object.assign(body,{port_min:parseInt(portMin),port_max:parseInt(portMax||portMin)});
try{const d=await $api('/oci/explore/'+S.expCfg+'/security_lists/'+slId+'/rules',{method:'POST',body});
sm('slFormMsg',IC.ok+' '+d.message,'s');S._slAddForm=null;await expLoadRes()}catch(e){sm('slFormMsg',e.message,'e')}}
async function slRemoveRule(slId,direction,ruleIndex){
if(!confirm('Remover esta regra da Security List?'))return;
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
try{await $api('/oci/explore/'+S.expCfg+'/security_lists/'+slId+'/rules',{method:'DELETE',body:{direction,rule_index:ruleIndex,region:region||undefined}});await expLoadRes()}catch(e){alert('Erro: '+e.message)}}
async function slAddMyIp(slId){
try{const resp=await fetch('https://api.ipify.org?format=json');const data=await resp.json();const myIp=data.ip+'/32';
document.getElementById('slCidr').value=myIp;
document.getElementById('slDesc').value='Acesso temporário - '+myIp;
sm('slFormMsg',IC.ok+' IP detectado: '+myIp+' — ajuste porta e clique Salvar','s')}catch(e){sm('slFormMsg','Não foi possível detectar o IP: '+e.message,'e')}}
function rExpNsgs(d){
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} NSG(s)</div>${d.map((nsg,idx)=>{
const rules=nsg.rules||[];
const ingress=rules.filter(r=>r.direction==='INGRESS');
const egress=rules.filter(r=>r.direction==='EGRESS');
const expanded=S._expExpanded&&S._expExpanded['nsg'+idx];
const showForm=S._nsgAddForm===nsg.id;
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded['nsg'+${idx}]=!S._expExpanded['nsg'+${idx}];R()">
<div><strong style="display:inline">${nsg.display_name}</strong> <span class="badge ${nsg.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${nsg.lifecycle_state}</span></div>
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${ingress.length} ingress · ${egress.length} egress</div></div>
<div class="em" style="margin-top:.3rem">id: ${nsg.id}</div>
${expanded?`<div style="margin-top:.5rem">
<div style="display:flex;justify-content:flex-end;margin-bottom:.4rem"><button class="btn bp bsm" onclick="event.stopPropagation();S._nsgAddForm=S._nsgAddForm==='${nsg.id}'?null:'${nsg.id}';R()" style="font-size:.62rem">${showForm?'✕ Cancelar':'+ Adicionar Regra'}</button></div>
${showForm?`<div style="background:var(--bg3);border:1px solid var(--bd);border-radius:10px;padding:.6rem;margin-bottom:.6rem" onclick="event.stopPropagation()">
<div style="font-size:.7rem;font-weight:600;color:var(--t2);margin-bottom:.4rem">${IC.shield} Nova Regra</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.4rem">
<div class="ig"><label style="font-size:.62rem">Direção</label><select id="nsgDir" style="font-size:.72rem"><option value="INGRESS">Ingress</option><option value="EGRESS">Egress</option></select></div>
<div class="ig"><label style="font-size:.62rem">Protocolo</label><select id="nsgProto" style="font-size:.72rem"><option value="6">TCP</option><option value="17">UDP</option><option value="1">ICMP</option><option value="all">ALL</option></select></div>
<div class="ig"><label style="font-size:.62rem">Stateless</label><select id="nsgStateless" style="font-size:.72rem"><option value="false">Não</option><option value="true">Sim</option></select></div>
<div class="ig"><label style="font-size:.62rem">Origem/Destino (CIDR)</label><input id="nsgCidr" style="font-size:.72rem" placeholder="0.0.0.0/0" value="0.0.0.0/0"></div>
<div class="ig"><label style="font-size:.62rem">Porta Início</label><input id="nsgPortMin" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
<div class="ig"><label style="font-size:.62rem">Porta Fim</label><input id="nsgPortMax" type="number" style="font-size:.72rem" placeholder="ex: 443"></div>
</div>
<div class="ig" style="margin-top:.3rem"><label style="font-size:.62rem">Descrição</label><input id="nsgDesc" style="font-size:.72rem" placeholder="Descrição da regra"></div>
<div style="display:flex;gap:.3rem;margin-top:.5rem;justify-content:flex-end"><button class="btn bp bsm" onclick="nsgAddRule('${nsg.id}')" style="font-size:.64rem">Salvar Regra</button>
<button class="btn bs bsm" onclick="nsgAddMyIp('${nsg.id}')" style="font-size:.64rem" title="Detecta seu IP público e adiciona como regra">${IC.globe} Meu IP</button></div>
<div id="nsgFormMsg" style="margin-top:.3rem"></div>
</div>`:''}
${ingress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↓ Ingress (${ingress.length})</div>
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Origem</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
${ingress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.is_stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'&quot;')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();nsgRemoveRule('${nsg.id}','${r.id}')" title="Remover regra">✕</button></td></tr>`).join('')}
</tbody></table>`:''}
${egress.length?`<div style="font-size:.68rem;font-weight:600;color:var(--t2);margin:.4rem 0 .2rem">↑ Egress (${egress.length})</div>
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Protocolo</th><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Portas</th><th style="padding:.25rem .4rem">Stateless</th><th style="padding:.25rem .4rem">Descrição</th><th style="padding:.25rem .4rem;width:32px"></th></tr></thead><tbody>
${egress.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-weight:600">${r.protocol}</td><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem">${r.source_dest}</td><td style="padding:.25rem .4rem">${r.ports||'ALL'}</td><td style="padding:.25rem .4rem">${r.is_stateless?'Sim':'Não'}</td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'&quot;')}">${r.description||'—'}</td><td style="padding:.25rem .2rem"><button class="btn bd bsm" style="font-size:.55rem;padding:1px 5px" onclick="event.stopPropagation();nsgRemoveRule('${nsg.id}','${r.id}')" title="Remover regra">✕</button></td></tr>`).join('')}
</tbody></table>`:''}</div>`:''}</div>`}).join('')}`}
async function nsgAddRule(nsgId){
const dir=document.getElementById('nsgDir').value;
const proto=document.getElementById('nsgProto').value;
const cidr=document.getElementById('nsgCidr').value.trim();
const portMin=document.getElementById('nsgPortMin').value;
const portMax=document.getElementById('nsgPortMax').value;
const desc=document.getElementById('nsgDesc').value;
const stateless=document.getElementById('nsgStateless').value==='true';
if(!cidr){sm('nsgFormMsg','Informe o CIDR.','e');return}
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
const body={direction:dir,protocol:proto,source_dest:cidr,source_dest_type:'CIDR_BLOCK',description:desc,is_stateless:stateless,region:region||undefined};
if(proto!=='all'&&proto!=='1'&&portMin)Object.assign(body,{port_min:parseInt(portMin),port_max:parseInt(portMax||portMin)});
try{const d=await $api('/oci/explore/'+S.expCfg+'/nsgs/'+nsgId+'/rules',{method:'POST',body});
sm('nsgFormMsg',IC.ok+' '+d.message,'s');S._nsgAddForm=null;await expLoadRes()}catch(e){sm('nsgFormMsg',e.message,'e')}}
async function nsgRemoveRule(nsgId,ruleId){
if(!confirm('Remover esta regra do NSG?'))return;
const region=S.expSelRegions.length===1?S.expSelRegions[0]:null;
const q=region?'?region='+region:'';
try{await $api('/oci/explore/'+S.expCfg+'/nsgs/'+nsgId+'/rules/'+ruleId+q,{method:'DELETE'});await expLoadRes()}catch(e){alert('Erro: '+e.message)}}
async function nsgAddMyIp(nsgId){
try{const resp=await fetch('https://api.ipify.org?format=json');const data=await resp.json();const myIp=data.ip+'/32';
document.getElementById('nsgCidr').value=myIp;
document.getElementById('nsgDesc').value='Acesso temporário - '+myIp;
sm('nsgFormMsg',IC.ok+' IP detectado: '+myIp+' — ajuste porta e clique Salvar','s')}catch(e){sm('nsgFormMsg','Não foi possível detectar o IP: '+e.message,'e')}}
function rExpRouteTables(d){
return`<div style="font-size:.74rem;color:var(--t4);margin-bottom:.5rem">${d.length} route table(s)</div>${d.map((rt,idx)=>{
const routes=rt.routes||[];
const expanded=S._expExpanded&&S._expExpanded['rt'+idx];
return`<div class="exp-c" style="max-width:100%"><div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer" onclick="S._expExpanded=S._expExpanded||{};S._expExpanded['rt'+${idx}]=!S._expExpanded['rt'+${idx}];R()">
<div><strong style="display:inline">${rt.display_name}</strong> <span class="badge ${rt.lifecycle_state==='AVAILABLE'?'b-pass':'b-pend'}">${rt.lifecycle_state}</span></div>
<div style="font-size:.66rem;color:var(--t4)">${expanded?'▼':'▶'} ${routes.length} rota(s)</div></div>
<div class="em" style="margin-top:.3rem">id: ${rt.id}</div>
${expanded&&routes.length?`<div style="margin-top:.5rem">
<table style="width:100%;font-size:.66rem;border-collapse:collapse"><thead><tr style="background:var(--bg3);text-align:left"><th style="padding:.25rem .4rem">Destino</th><th style="padding:.25rem .4rem">Tipo</th><th style="padding:.25rem .4rem">Target</th><th style="padding:.25rem .4rem">Descrição</th></tr></thead><tbody>
${routes.map(r=>`<tr style="border-top:1px solid var(--bd)"><td style="padding:.25rem .4rem;font-family:var(--fm);font-size:.62rem;font-weight:600">${r.destination}</td><td style="padding:.25rem .4rem">${r.destination_type}</td><td style="padding:.25rem .4rem;font-size:.6rem;color:var(--t3)">${r.target_type}<br><span class="em">${r.target_id}</span></td><td style="padding:.25rem .4rem;color:var(--t4);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${(r.description||'').replace(/"/g,'&quot;')}">${r.description||'—'}</td></tr>`).join('')}
</tbody></table></div>`:expanded?'<div style="font-size:.7rem;color:var(--t4);padding:.4rem">Nenhuma rota configurada</div>':''}</div>`}).join('')}`}
/* ── Report ── */
function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
const running=S.reports.filter(r=>r.status==='running');
let progressHtml='';
if(S.trackingReportId||running.length){
if(!S.trackingReportId&&running.length)S.trackingReportId=running[0].id;
const rr=running.find(r=>r.id===S.trackingReportId);
const lines=(rr?.progress||'').split('\n').filter(Boolean);
const last=lines[lines.length-1]||'Iniciando...';
const m=last.match(/\[(\d+)\/(\d+)\]/);const pct=m?Math.round((parseInt(m[1])/parseInt(m[2]))*100):0;
progressHtml=`<div id="rpt-progress">${rProgressCard('running',last,pct,lines,S.trackingReportId)}</div>`;
if(!_rptPollTimer)startRptPoll()}
if(!comp.length)return`${progressHtml}<div class="cd"><div class="ct">${IC.chart} Compliance Reports</div><div class="emp"><div class="eic">${IC.file}</div><p>Nenhum relatório gerado ainda.</p></div></div>${rRunRpt()}${rReportHistory()}`;
const lt=comp[0];
const selRpt=S.rptRselVal?comp.find(r=>r.id===S.rptRselVal):lt;
if(!S.rptRselVal){S.rptRselVal=lt.id;if(!S.rptKpi&&!S.rptKpiLoading)loadRptKpi(lt.id)}
const rselF=S.rptRselFilter.toLowerCase();
const rselLabel=selRpt?`${selRpt.tenancy_name}${selRpt.created_at}`:'Selecione relatório...';
let rselDdItems=comp.filter(r=>!rselF||r.tenancy_name.toLowerCase().includes(rselF)||r.created_at.includes(rselF)).map(r=>`<div class="itm${S.rptRselVal===r.id?' sel':''}" onclick="rptPickRsel('${r.id}')">${r.tenancy_name}${r.created_at}</div>`).join('');
return`${progressHtml}<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem"><span style="font-size:.78rem;color:var(--t3)">Report HTML — ${(selRpt||lt).tenancy_name}</span>
<div class="dd" style="position:relative;max-width:340px;flex:1;margin-left:.5rem"><div onclick="S.rptRselOpen=!S.rptRselOpen;R()" style="display:flex;align-items:center;cursor:pointer;padding:.3rem .65rem;background:var(--bg2);border:1.5px solid var(--bd);border-radius:10px;font-size:.74rem;justify-content:space-between;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
<span style="overflow:hidden;text-overflow:ellipsis">${rselLabel}</span><span style="font-size:.6rem;color:var(--t4);margin-left:.3rem">▼</span></div>
${S.rptRselOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:0;right:0;background:var(--bg);border:1px solid var(--b3);border-radius:6px;max-height:220px;overflow:auto;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.15)">
<div style="padding:.3rem;border-bottom:1px solid var(--b2)"><input type="text" placeholder="Buscar relatório..." value="${S.rptRselFilter}" oninput="S.rptRselFilter=this.value;R()" style="width:100%;border:none;outline:none;font-size:.75rem;padding:.25rem .4rem;background:transparent;color:var(--t1)" onclick="event.stopPropagation()"></div>
${rselDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhum relatório encontrado</div>'}
</div>`:''}
</div></div>
<div id="kpi-wrap">${rKpi()}</div><iframe class="rif" id="rfr" src="${API}/reports/${(selRpt||lt).id}/html"></iframe>${rRunRpt()}${rReportHistory()}`}
function rRunRpt(){if(S.user?.role==='viewer')return'';
const selTags=S.rptSelRegions.map(r=>`<span class="tag" style="background:var(--b2);font-size:.62rem;display:inline-flex;align-items:center;gap:.2rem">${r}<span onclick="rptRemoveRegion('${r}')" style="cursor:pointer;font-weight:700;color:var(--err)">&times;</span></span>`).join(' ');
const f=S.rptRegionFilter.toLowerCase();
let ddItems='';
for(const[grp,regs] of Object.entries(S.ociRegions)){
const filtered=regs.filter(r=>!S.rptSelRegions.includes(r)&&(!f||r.includes(f)));
if(!filtered.length)continue;
ddItems+=`<div class="grp">${grp}</div>`;
filtered.forEach(r=>{ddItems+=`<div class="itm" onclick="rptPickRegion('${r}')">${r}</div>`})}
const ociF=S.rptOciFilter.toLowerCase();
const selOci=S.ociCfg.find(c=>c.id===S.rptOciVal);
const ociLabel=selOci?`${selOci.tenancy_name} (${selOci.region})`:'Selecione config OCI...';
let ociDdItems=S.ociCfg.filter(c=>!ociF||c.tenancy_name.toLowerCase().includes(ociF)||c.region.toLowerCase().includes(ociF)).map(c=>`<div class="itm${S.rptOciVal===c.id?' sel':''}" onclick="rptPickOci('${c.id}')">${c.tenancy_name} (${c.region})</div>`).join('');
return`<div class="cd" style="margin-top:.85rem"><div class="ct">${IC.rocket} Executar Relatório</div>
<div class="g3"><div class="ig"><label>Config OCI</label>
<div class="dd" style="position:relative"><div onclick="S.rptOciOpen=!S.rptOciOpen;R()" style="min-height:34px;display:flex;align-items:center;cursor:pointer;padding:.35rem .65rem;background:var(--bg2);border:1.5px solid var(--bd);border-radius:10px;font-size:.78rem;justify-content:space-between">
<span style="${selOci?'':'color:var(--t4)'}">${ociLabel}</span><span style="font-size:.6rem;color:var(--t4)">▼</span></div>
${S.rptOciOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:0;right:0;background:var(--bg);border:1px solid var(--b3);border-radius:6px;max-height:220px;overflow:auto;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.15)">
<div style="padding:.3rem;border-bottom:1px solid var(--b2)"><input type="text" placeholder="Buscar credencial..." value="${S.rptOciFilter}" oninput="S.rptOciFilter=this.value;R()" style="width:100%;border:none;outline:none;font-size:.75rem;padding:.25rem .4rem;background:transparent;color:var(--t1)" onclick="event.stopPropagation()"></div>
${ociDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma credencial encontrada</div>'}
</div>`:''}
</div></div>
<div class="ig"><label>CIS Level</label><select onchange="S.rptLevel=parseInt(this.value);R()">
<option value="1" ${S.rptLevel===1?'selected':''}>Level 1</option>
<option value="2" ${S.rptLevel===2?'selected':''}>Level 2 (default)</option></select>
<div style="margin-top:.35rem;font-size:.68rem;color:var(--t4);line-height:1.45">
${S.rptLevel===1?'<b>Level 1</b> — Controles essenciais de segurança com impacto mínimo nas operações. Recomendado como baseline para todas as organizações.':'<b>Level 2</b> — Controles avançados que podem restringir funcionalidades ou exigir mais esforço. Recomendado para ambientes de alta segurança.'}
</div></div>
<div class="ig"><label>Regiões (opcional — vazio = todas)</label>
<div class="dd" style="position:relative"><div onclick="S.rptRegionsOpen=!S.rptRegionsOpen;R()" style="min-height:34px;display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;cursor:pointer;padding:.35rem .65rem;background:var(--bg2);border:1.5px solid var(--bd);border-radius:10px;font-size:.78rem">
${S.rptSelRegions.length?selTags:'<span style="color:var(--t4);font-size:.78rem">Todas as regiões</span>'}</div>
${S.rptRegionsOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:0;right:0;background:var(--bg);border:1px solid var(--b3);border-radius:6px;max-height:260px;overflow:auto;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.15)">
<div style="padding:.3rem;border-bottom:1px solid var(--b2)"><input type="text" placeholder="Buscar região..." value="${S.rptRegionFilter}" oninput="S.rptRegionFilter=this.value;R()" style="width:100%;border:none;outline:none;font-size:.75rem;padding:.25rem .4rem;background:transparent;color:var(--t1)" onclick="event.stopPropagation()"></div>
${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma região encontrada</div>'}
</div>`:''}
</div></div></div>
<div style="display:flex;gap:1.2rem;flex-wrap:wrap;margin-top:.5rem">
<label style="display:flex;align-items:center;gap:.35rem;font-size:.76rem;cursor:pointer"><input type="checkbox" ${S.rptObp?'checked':''} onchange="S.rptObp=this.checked"> OCI Best Practices (OBP)</label>
<label style="display:flex;align-items:center;gap:.35rem;font-size:.76rem;cursor:pointer"><input type="checkbox" ${S.rptRaw?'checked':''} onchange="S.rptRaw=this.checked"> Raw Data Output</label>
<label style="display:flex;align-items:center;gap:.35rem;font-size:.76rem;cursor:pointer"><input type="checkbox" ${S.rptRedact?'checked':''} onchange="S.rptRedact=this.checked"> Redact OCIDs</label>
</div>
<button class="btn bp" style="margin-top:.6rem" onclick="runRpt()">▶ Executar CIS Compliance</button></div>${rCisEngine()}`}
function rCisEngine(){
if(S.user?.role!=='admin')return '';
const v=S.cisVer;const cr=S.cisCheckResult;
let status='';
if(S.cisUpdating)status='<span style="color:var(--w);font-size:.74rem">${IC.wait} Atualizando...</span>';
else if(cr){
if(cr.update_available)status=`<span style="color:var(--w);font-size:.74rem">Nova versão: <b>${cr.remote_version}</b> (${cr.remote_date})</span> <button class="btn bp" style="font-size:.7rem;padding:.2rem .6rem;margin-left:.4rem" onclick="cisDoUpdate()">Atualizar</button>`;
else status='<span style="color:var(--ok);font-size:.74rem">✓ Versão mais recente</span>';
}
return`<div class="cd" style="margin-top:.85rem"><div class="ct">${IC.settings} CIS Engine</div>
<div style="display:flex;align-items:center;gap:.8rem;flex-wrap:wrap">
<span style="font-size:.8rem">Versão: <b>${v?v.version:'...'}</b>${v&&v.updated_date!=='unknown'?' ('+v.updated_date+')':''}</span>
<button class="btn bs" style="font-size:.7rem;padding:.2rem .6rem" onclick="cisCheckUpdate()">Verificar Atualização</button>
${status}</div></div>`}
async function cisLoadVersion(){try{S.cisVer=await $api('/cis-engine/version');R()}catch(e){console.error('cisVer',e)}}
async function cisCheckUpdate(){S.cisCheckResult=null;R();try{S.cisCheckResult=await $api('/cis-engine/check-update');R()}catch(e){alert('Erro ao verificar: '+e.message)}}
async function cisDoUpdate(){if(!confirm('Atualizar CIS Engine para a versão mais recente do GitHub?'))return;S.cisUpdating=true;R();try{const d=await $api('/cis-engine/update',{method:'POST'});alert(`Atualizado: ${d.old_version}${d.new_version}\nPatches aplicados: ${d.patches_applied.join(', ')||'nenhum'}`);S.cisCheckResult=null;S.cisUpdating=false;await cisLoadVersion()}catch(e){S.cisUpdating=false;R();alert('Erro: '+e.message)}}
function rptPickRegion(r){S.rptSelRegions.push(r);S.rptRegionFilter='';R()}
function rptRemoveRegion(r){S.rptSelRegions=S.rptSelRegions.filter(x=>x!==r);R()}
function rptPickOci(id){S.rptOciVal=id;S.rptOciOpen=false;S.rptOciFilter='';R()}
function rptPickRsel(id){S.rptRselVal=id;S.rptRselOpen=false;S.rptRselFilter='';const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html';loadRptKpi(id);R()}
async function loadRptKpi(rid){S.rptKpiLoading=true;S.rptKpi=null;const w=document.getElementById('kpi-wrap');if(w)w.innerHTML=rKpi();try{S.rptKpi=await $api('/reports/'+rid+'/summary')}catch(e){S.rptKpi=null}S.rptKpiLoading=false;const w2=document.getElementById('kpi-wrap');if(w2)w2.innerHTML=rKpi()}
let _kpiCharts={};
function _destroyKpiCharts(){Object.values(_kpiCharts).forEach(c=>{try{c.destroy()}catch(e){}});_kpiCharts={}}
function _initKpiCharts(){
const k=S.rptKpi;if(!k||!k.sections)return;
const cs=getComputedStyle(document.documentElement);
const gn=cs.getPropertyValue('--gn').trim();
const rd=cs.getPropertyValue('--rd').trim();
const bl=cs.getPropertyValue('--bl').trim();
const t3=cs.getPropertyValue('--t3').trim();
const t4=cs.getPropertyValue('--t4').trim();
const bg3=cs.getPropertyValue('--bg3').trim();
const fm=cs.getPropertyValue('--fm').trim();
// Donut
const dc=document.getElementById('kpi-donut');
if(dc){_kpiCharts.donut=new Chart(dc,{type:'doughnut',data:{labels:['Compliant','Non-Compliant'],datasets:[{data:[k.passed,k.failed],backgroundColor:[gn,rd],borderWidth:0,hoverOffset:6}]},options:{responsive:true,maintainAspectRatio:false,cutout:'72%',plugins:{legend:{display:false},tooltip:{backgroundColor:'rgba(0,0,0,.8)',titleFont:{family:fm,size:11},bodyFont:{family:fm,size:11},padding:10,cornerRadius:8}}}})}
// Horizontal bar
const secs=Object.entries(k.sections).sort((a,b)=>b[1].failed-a[1].failed);
const bc=document.getElementById('kpi-bar');
if(bc&&secs.length){_kpiCharts.bar=new Chart(bc,{type:'bar',data:{labels:secs.map(([n])=>n.length>22?n.slice(0,20)+'…':n),datasets:[{label:'Compliant',data:secs.map(([,s])=>s.passed),backgroundColor:gn,borderRadius:4,barPercentage:.7},{label:'Non-Compliant',data:secs.map(([,s])=>s.failed),backgroundColor:rd,borderRadius:4,barPercentage:.7}]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,scales:{x:{stacked:true,grid:{color:bg3},ticks:{font:{family:fm,size:10},color:t4}},y:{stacked:true,grid:{display:false},ticks:{font:{family:fm,size:11},color:t3}}},plugins:{legend:{position:'top',labels:{font:{family:fm,size:11},color:t3,boxWidth:12,boxHeight:12,borderRadius:3,useBorderRadius:true,padding:12}},tooltip:{backgroundColor:'rgba(0,0,0,.8)',titleFont:{family:fm,size:11},bodyFont:{family:fm,size:11},padding:10,cornerRadius:8}}}})}
}
function _svgGauge(score){
const r=58,cx=65,cy=65,sw=12;
const circ=Math.PI*r;
const fill=circ*score/100;
const col=score>=80?'var(--gn)':score>=60?'var(--yl)':'var(--rd)';
const ang=-90+(score/100*180);
const rad=ang*Math.PI/180;
const nx=cx+Math.cos(rad)*(r-1);
const ny=cy+Math.sin(rad)*(r-1);
return`<svg viewBox="0 0 130 80" width="180" height="110" style="display:block;margin:0 auto">
<defs><linearGradient id="gg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="var(--rd)"/><stop offset="50%" stop-color="var(--yl)"/><stop offset="100%" stop-color="var(--gn)"/></linearGradient></defs>
<path d="M ${cx-r} ${cy} A ${r} ${r} 0 0 1 ${cx+r} ${cy}" fill="none" stroke="var(--bg3)" stroke-width="${sw}" stroke-linecap="round"/>
<path d="M ${cx-r} ${cy} A ${r} ${r} 0 0 1 ${cx+r} ${cy}" fill="none" stroke="url(#gg)" stroke-width="${sw}" stroke-linecap="round" stroke-dasharray="${circ}" stroke-dashoffset="${circ-fill}" style="transition:stroke-dashoffset .8s ease"/>
<circle cx="${nx}" cy="${ny}" r="4" fill="${col}" stroke="var(--bg1)" stroke-width="2"/>
<text x="${cx}" y="${cy-8}" text-anchor="middle" fill="var(--t1)" font-size="22" font-weight="800" font-family="var(--fs)">${score}%</text>
<text x="${cx}" y="${cy+6}" text-anchor="middle" fill="var(--t4)" font-size="7" font-weight="600" font-family="var(--fm)">COMPLIANCE</text>
</svg>`}
function rKpi(){
if(S.rptKpiLoading)return'<div class="kpi-row"><div class="kpi" style="grid-column:1/-1;text-align:center;padding:1.2rem"><span class="spinner"></span> Carregando métricas...</div></div>';
const k=S.rptKpi;if(!k)return'';
const scoreColor=k.score>=80?'v-ok':k.score>=60?'v-warn':'v-err';
const secs=k.sections?Object.entries(k.sections).sort((a,b)=>b[1].failed-a[1].failed):[];
const barH=Math.max(180,secs.length*38+60);
setTimeout(()=>{_destroyKpiCharts();_initKpiCharts()},60);
return`<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:.75rem;margin-bottom:.85rem">
<div class="kpi kpi-score" style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1rem .5rem .7rem">${_svgGauge(k.score)}<div class="kpi-sub" style="margin-top:.2rem;text-align:center">${k.total} checks · Level ${k.level}</div></div>
<div class="kpi kpi-pass"><div class="kpi-label">${IC.ok} Compliant</div><div class="kpi-val v-ok">${k.passed}</div><div class="kpi-sub">${k.total?Math.round(k.passed/k.total*100):0}% dos checks</div></div>
<div class="kpi kpi-fail"><div class="kpi-label">${IC.err} Non-Compliant</div><div class="kpi-val v-err">${k.failed}</div><div class="kpi-sub">${k.total_findings||0} findings</div></div>
<div class="kpi kpi-info"><div class="kpi-label">${IC.search} Total Avaliado</div><div class="kpi-val">${k.total}</div><div class="kpi-sub">${(k.total_compliant||0)+(k.total_findings||0)} itens analisados</div></div></div>
${secs.length?`<div style="display:grid;grid-template-columns:220px 1fr;gap:.75rem;margin-bottom:.85rem">
<div class="cd" style="margin-bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:1.2rem">
<div class="ct" style="margin-bottom:.65rem;width:100%">${IC.chart} Distribuição</div>
<div style="position:relative;width:160px;height:160px"><canvas id="kpi-donut"></canvas>
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center"><div style="font-size:1.3rem;font-weight:800;color:var(--t1)">${k.score}%</div><div style="font-size:.62rem;color:var(--t4)">score</div></div></div>
<div style="display:flex;gap:1rem;margin-top:.75rem;font-size:.68rem;font-family:var(--fm)">
<span style="display:flex;align-items:center;gap:.3rem"><span style="width:10px;height:10px;border-radius:3px;background:var(--gn);display:inline-block"></span> ${k.passed} ok</span>
<span style="display:flex;align-items:center;gap:.3rem"><span style="width:10px;height:10px;border-radius:3px;background:var(--rd);display:inline-block"></span> ${k.failed} fail</span></div></div>
<div class="cd" style="margin-bottom:0;padding:1.2rem"><div class="ct">${IC.barChart} Findings por Seção</div><div style="height:${barH}px"><canvas id="kpi-bar"></canvas></div></div>
</div>`:''}`}
async function runRpt(){if(S.reports.some(r=>r.status==='running'))return alert('Já existe um relatório em execução. Aguarde ou cancele antes de iniciar outro.');
const c=S.rptOciVal;if(!c)return alert('Selecione config OCI');
const rg=S.rptSelRegions;
S.rptRegionsOpen=false;
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,regions:rg.length?rg:null,level:S.rptLevel,obp:S.rptObp,raw:S.rptRaw,redact_output:S.rptRedact}});
S.trackingReportId=d.report_id;S.rptSelRegions=[];S.rptOciVal='';S.reports=await $api('/reports');R();startRptPoll()}catch(e){alert('Erro: '+e.message)}}
let _rptPollTimer=null;
function startRptPoll(){stopRptPoll();_rptPollTimer=setInterval(pollRptProgress,3000)}
function stopRptPoll(){if(_rptPollTimer){clearInterval(_rptPollTimer);_rptPollTimer=null}}
async function pollRptProgress(){if(!S.trackingReportId)return stopRptPoll();
try{const p=await $api('/reports/'+S.trackingReportId+'/progress');
const el=document.getElementById('rpt-progress');
if(el){const lines=(p.progress||'').split('\n').filter(Boolean);
const last=lines[lines.length-1]||'Iniciando...';
const ft=(p.progress||'').toLowerCase();
let pct=5;
if(ft.includes('identity domains'))pct=10;
if(ft.includes('processing home region'))pct=20;
if(ft.includes('processing regional'))pct=35;
if(ft.includes('cis summary report'))pct=60;
if(ft.includes('writing cis reports'))pct=75;
if(ft.includes('best practices'))pct=85;
if(ft.includes('finished'))pct=100;
const wasOpen=el.querySelector('details[open]')!==null;
el.innerHTML=rProgressCard(p.status,last,pct,lines,S.trackingReportId);
if(wasOpen){const d=el.querySelector('details');if(d)d.open=true}}
if(p.status==='completed'||p.status==='failed'||p.status==='cancelled'){stopRptPoll();S.trackingReportId=null;S.rptKpi=null;S.rptRselVal='';S.reports=await $api('/reports');R()}}catch(e){}}
async function cancelRpt(rid){if(!confirm('Cancelar este relatório?'))return;
try{await $api('/reports/'+rid+'/cancel',{method:'POST'});stopRptPoll();S.trackingReportId=null;S.reports=await $api('/reports');R()}catch(e){alert('Erro: '+e.message)}}
function rProgressCard(status,lastStep,pct,lines,rid){
if(status==='completed')return'<div class="cd" style="margin-bottom:.75rem;border-left:3px solid var(--ok)"><div style="padding:.75rem"><strong style="color:var(--ok)">Relatório concluído!</strong></div></div>';
if(status==='failed')return'<div class="cd" style="margin-bottom:.75rem;border-left:3px solid var(--err)"><div style="padding:.75rem"><strong style="color:var(--err)">Relatório falhou</strong><p style="font-size:.72rem;color:var(--t4);margin-top:.25rem">'+lastStep+'</p></div></div>';
if(status==='cancelled')return'<div class="cd" style="margin-bottom:.75rem;border-left:3px solid var(--t4)"><div style="padding:.75rem"><strong style="color:var(--t4)">Relatório cancelado</strong></div></div>';
const cancelBtn=rid?` <button class="btn bs bsm" style="color:var(--err);border-color:var(--err);font-size:.68rem;padding:.15rem .5rem" onclick="cancelRpt('${rid}')">Cancelar</button>`:'';
return`<div class="cd" style="margin-bottom:.75rem;border-left:3px solid var(--ac)"><div style="padding:.75rem">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem"><strong style="font-size:.82rem">Gerando relatório...${cancelBtn}</strong><span style="font-size:.72rem;color:var(--t4)">${pct}%</span></div>
<div style="background:var(--b2);border-radius:6px;height:8px;overflow:hidden;margin-bottom:.4rem"><div style="background:var(--ac);height:100%;border-radius:6px;width:${pct}%;transition:width .5s ease"></div></div>
<p style="font-size:.72rem;color:var(--t3);margin:0">${lastStep}</p>
${lines.length>1?`<details style="margin-top:.35rem"><summary style="font-size:.68rem;color:var(--t4);cursor:pointer">Ver log completo</summary><pre style="font-size:.62rem;color:var(--t4);margin-top:.25rem;max-height:120px;overflow:auto;white-space:pre-wrap">${lines.join('\n')}</pre></details>`:''}
</div></div>`}
/* ── Report History (inside Reports tab) ── */
function rReportHistory(){
const tenancies=[...new Set(S.reports.map(r=>r.tenancy_name))].sort();
const filtered=S.rptHistFilter?S.reports.filter(r=>r.tenancy_name===S.rptHistFilter):S.reports;
if(!filtered.length&&!S.reports.length)return'';
return`<div class="cd" style="margin-top:.85rem"><div class="ct">${IC.history} Histórico de Relatórios</div>
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.5rem">
<select style="font-size:.76rem;min-width:180px" onchange="S.rptHistFilter=this.value;R()">
<option value="">Todas as Tenancies (${S.reports.length})</option>
${tenancies.map(t=>`<option value="${t}" ${S.rptHistFilter===t?'selected':''}>${t} (${S.reports.filter(r=>r.tenancy_name===t).length})</option>`).join('')}
</select>
<span style="font-size:.72rem;color:var(--t4)">${filtered.length} report(s)</span></div>
<table><thead><tr><th>Tenancy</th><th>Level</th><th>Opções</th><th>Status</th><th>Executado</th><th>Concluído</th><th>Ações</th></tr></thead>
<tbody>${filtered.map(r=>{
const opts=[];if(r.obp_checks)opts.push('OBP');if(r.raw_data)opts.push('Raw');if(r.redact_output)opts.push('Redact');
return`<tr><td><strong>${r.tenancy_name}</strong></td>
<td style="font-size:.72rem">${r.level||2}</td>
<td style="font-size:.68rem">${opts.join(', ')||'—'}</td>
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'||r.status==='cancelled'?'b-fail':'b-pend'}">${r.status==='cancelled'?'cancelado':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}','html')">HTML</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button>`:r.status==='running'?`<button class="btn bs bsm" style="color:var(--err);border-color:var(--err)" onclick="cancelRpt('${r.id}')">Cancelar</button>`:'—'}</td></tr>`}).join('')}</tbody></table>
${!filtered.length?'<div class="emp" style="padding:.5rem"><p style="font-size:.72rem;color:var(--t4)">Nenhum relatório.</p></div>':''}</div>`}
/* ── Downloads (file browser only) ── */
function _dlFmtSize(b){if(b>=1048576)return(b/1048576).toFixed(1)+' MB';if(b>=1024)return(b/1024).toFixed(1)+' KB';return b+' B'}
function _dlFileIcon(name){
const ext=name.split('.').pop().toLowerCase();
const colors={html:'#e44d26',htm:'#e44d26',csv:'#217346',json:'#f5a623',xlsx:'#217346',xls:'#217346',txt:'#6b7280',pdf:'#dc2626',png:'#8b5cf6',svg:'#8b5cf6'};
const c=colors[ext]||'#c74634';
return`<div style="width:28px;height:28px;border-radius:6px;background:${c}14;display:flex;align-items:center;justify-content:center;flex-shrink:0"><span style="font-size:.58rem;font-weight:700;color:${c};text-transform:uppercase;letter-spacing:.3px">${ext}</span></div>`;
}
function rDl(){
const comp=S.reports.filter(r=>r.status==='completed');
const tenancies=[...new Set(comp.map(r=>r.tenancy_name))].sort();
const filtered=S.dlTenancyFilter?comp.filter(r=>r.tenancy_name===S.dlTenancyFilter):comp;
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem;gap:.5rem;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:.5rem">
<select style="font-size:.76rem;min-width:180px" onchange="S.dlTenancyFilter=this.value;S.dlExpandedRid=null;R()">
<option value="">Todas as Tenancies (${comp.length})</option>
${tenancies.map(t=>`<option value="${t}" ${S.dlTenancyFilter===t?'selected':''}>${t} (${comp.filter(r=>r.tenancy_name===t).length})</option>`).join('')}
</select>
<span style="font-size:.72rem;color:var(--t4)">${filtered.length} report(s)</span></div>
<button class="btn bs bsm" onclick="refreshDl()">${IC.refresh} Atualizar</button></div>
${!filtered.length?'<div class="cd"><div class="emp"><div class="eic">${IC.folder}</div><p>Nenhum relatório concluído.</p></div></div>':`
<div style="display:flex;flex-direction:column;gap:.5rem">${filtered.map(r=>{
const isExp=S.dlExpandedRid===r.id;
const fHtml=isExp&&S.reportFiles[r.id]?renderFileList(r.id,S.reportFiles[r.id]):'';
const fCount=S.reportFiles[r.id]?S.reportFiles[r.id].length:null;
return`<div class="cd" style="padding:0;overflow:hidden">
<div style="display:flex;justify-content:space-between;align-items:center;padding:.55rem .8rem;cursor:pointer;transition:background .15s" onmouseover="this.style.background='var(--bg2)'" onmouseout="this.style.background=''" onclick="toggleFiles('${r.id}')">
<div style="display:flex;align-items:center;gap:.5rem">
<svg viewBox="0 0 16 16" width="14" height="14" fill="var(--ac)" style="flex-shrink:0;transition:transform .2s;transform:rotate(${isExp?'90':'0'}deg)"><path d="M6 3l5 5-5 5z"/></svg>
<div><strong style="font-size:.78rem">${r.tenancy_name}</strong>
<div style="font-size:.66rem;color:var(--t4);margin-top:1px">Level ${r.level||2} · ${r.created_at}${fCount!=null?' · '+fCount+' arquivo(s)':''}</div></div></div>
<span style="font-size:.66rem;color:var(--t4)">${isExp?'Ocultar':'Ver Arquivos'}</span></div>
${isExp?fHtml:''}</div>`}).join('')}</div>`}`}
async function toggleFiles(rid){if(S.dlExpandedRid===rid){S.dlExpandedRid=null;R();return}
S.dlExpandedRid=rid;if(!S.reportFiles[rid]){try{S.reportFiles[rid]=await $api('/reports/'+rid+'/files')}catch(e){S.reportFiles[rid]=[]}}R()}
function _dlSection(name){
// Extract CIS section from filename: cis_Networking_2-1.csv → Networking
const m=name.match(/^(?:cis|obp)_([A-Za-z_]+?)_\d/);
if(m)return m[1].replace(/_/g,' ');
if(name.includes('summary'))return 'Summary';
if(name.includes('error'))return 'Error';
return 'Other';
}
const DL_SECTION_TABLE={
'Identity and Access Management':'identityandaccess',
'Networking':'networking',
'Compute':'computeinstances',
'Logging and Monitoring':'loggingandmonitoring',
'Storage Object Storage':'objectstorage',
'Storage Block Volumes':'storageblockvolume',
'Storage File Storage Service':'filestorageservice',
'Asset Management':'assetmanagement',
'Summary':'summaryreportcsvvector'
};
function renderFileList(rid,files){if(!files.length)return'<div style="padding:.8rem;color:var(--t4);font-size:.72rem;text-align:center">Nenhum arquivo encontrado</div>';
const secIcons={'Identity and Access Management':IC.lock,'Networking':IC.globe,'Compute':IC.server,'Logging and Monitoring':IC.log,'Storage Object Storage':IC.db,'Asset Management':IC.pkg,'Summary':IC.chart,'Error':IC.err,'Other':IC.file};
// Group files by section
const bySection={};files.forEach(f=>{const s=_dlSection(f.file_name);if(!bySection[s])bySection[s]=[];bySection[s].push(f)});
const sectionOrder=Object.keys(bySection).sort((a,b)=>{if(a==='Summary')return -1;if(b==='Summary')return 1;if(a==='Other')return 1;if(b==='Other')return -1;return a.localeCompare(b)});
const hasAdb=S.adbCfg.length>0&&(S.genaiCfg.length>0||S.ociCfg.length>0);
let h='<div style="border-top:1px solid var(--bd);padding:.65rem .8rem;background:var(--bg2)">';
// Header with total + embed all button
h+=`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.6rem">`;
h+=`<span style="font-size:.72rem;color:var(--t3)">${files.length} arquivo(s) em ${sectionOrder.length} seções</span>`;
if(hasAdb)h+=`<button class="btn bp bsm" onclick="dlEmbedSection('${rid}',null)" style="font-size:.62rem">${IC.dna} Embed Tudo</button>`;
h+=`</div>`;
sectionOrder.forEach(sec=>{
const sf=bySection[sec];
const icon=secIcons[sec]||IC.file;
// Section header with embed button
h+=`<div style="display:flex;justify-content:space-between;align-items:center;margin-top:.6rem;margin-bottom:.35rem;padding-bottom:.25rem;border-bottom:1px solid var(--bd)">`;
h+=`<div style="display:flex;align-items:center;gap:.4rem"><span style="opacity:.7">${icon}</span><span style="font-size:.72rem;font-weight:700;color:var(--t1)">${sec}</span><span style="font-size:.6rem;color:var(--t4);font-weight:400">(${sf.length})</span></div>`;
if(hasAdb)h+=`<button class="btn bs bsm" onclick="dlEmbedSection('${rid}','${sec.replace(/'/g,"\\'")}')" style="font-size:.58rem;padding:.15rem .45rem" title="Gerar embedding desta seção">${IC.dna} Embed</button>`;
h+=`</div>`;
// File grid
h+=`<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:.4rem;margin-bottom:.3rem">`;
sf.forEach(f=>{
const ext=f.file_name.split('.').pop().toLowerCase();
const colors={html:'#e44d26',csv:'#217346',json:'#f5a623',xlsx:'#217346',txt:'#6b7280',pdf:'#dc2626'};
const ac=colors[ext]||'var(--ac)';
h+=`<a href="${API}/reports/${rid}/files/${f.id}/download?token=${S.token}" target="_blank" class="dl-card" style="display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;text-decoration:none;color:inherit;background:var(--bg);border:1px solid var(--bd);border-radius:8px;transition:all .15s;cursor:pointer" onmouseover="this.style.borderColor='${ac}';this.style.boxShadow='0 2px 8px ${ac}18'" onmouseout="this.style.borderColor='var(--bd)';this.style.boxShadow='none'">
${_dlFileIcon(f.file_name)}
<div style="flex:1;min-width:0;overflow:hidden"><div style="font-size:.7rem;font-weight:500;color:var(--t1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${f.file_name}">${f.file_name}</div>
<div style="font-size:.58rem;color:var(--t4);margin-top:1px">${_dlFmtSize(f.file_size)}</div></div>
<svg viewBox="0 0 16 16" width="12" height="12" style="flex-shrink:0;opacity:.3"><path d="M8 1v9m0 0L5 7m3 3l3-3M2 12v1.5A1.5 1.5 0 003.5 15h9a1.5 1.5 0 001.5-1.5V12" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/></svg></a>`});
h+=`</div>`;
});
h+='</div>';return h}
async function dlEmbedSection(rid,section){
if(!S.adbCfg.length){alert('Nenhuma conexão ADB configurada.');return}
if(!S.genaiCfg.length&&!S.ociCfg.length){alert('Nenhuma credencial OCI configurada.');return}
const vid=S.adbCfg.find(c=>c.is_active)?.id||S.adbCfg[0].id;
const cfgName=S.adbCfg.find(c=>c.id===vid)?.config_name||'ADB';
const report=S.reports.find(r=>r.id===rid);
const reportDate=report?.created_at?new Date(report.created_at).toLocaleDateString('pt-BR'):'';
const tenancy=report?.tenancy_name||'';
const label=section||'todas as seções';
const tableName=section?DL_SECTION_TABLE[section]:null;
const tableInfo=tableName?`\nTabela: ${tableName}`:'(cada seção → tabela correspondente)';
const purge=confirm(`Embed "${label}"\nConexão: ${cfgName}${tableInfo}\nReport: ${tenancy}${reportDate}\n\nDeseja LIMPAR os dados antigos antes de embedar?\n\nOK = Limpar antigos + embedar novos\nCancelar = Apenas adicionar (sem limpar)`);
// Either way, proceed with embedding. purge=true means clean first.
if(section&&tableName){
try{
if(purge)await $api('/embeddings/'+vid+'/purge',{method:'POST',body:{table_name:tableName,tenancy}});
const d=await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,section,table_name:tableName}});
alert(d.message);
}catch(e){alert('Erro: '+e.message)}
}else if(!section){
const files=S.reportFiles[rid]||[];
const sections=[...new Set(files.map(f=>_dlSection(f.file_name)))].filter(s=>DL_SECTION_TABLE[s]);
let ok=0,fail=0;
for(const sec of sections){
const tbl=DL_SECTION_TABLE[sec];
try{
if(purge)await $api('/embeddings/'+vid+'/purge',{method:'POST',body:{table_name:tbl,tenancy}});
await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,section:sec,table_name:tbl}});ok++;
}catch(e){fail++}
}
alert(`Embedding: ${ok} seções enviadas${fail?' ('+fail+' erros)':''}`);
}else{
try{
if(purge)await $api('/embeddings/'+vid+'/purge',{method:'POST',body:{table_name:section,tenancy}});
const d=await $api('/embeddings/report/'+rid,{method:'POST',body:{adb_config_id:vid,section}});
alert(d.message);
}catch(e){alert('Erro: '+e.message)}
}}
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f+'&token='+S.token,'_blank')}
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}
/* ── Edit helpers ── */
function editCfg(type,id){S.editing={type,id};if(type==='oci'){const c=S.ociCfg.find(x=>x.id===id);S.ociFormRegVal=c?c.region:'';S.ociFormRegFilter=''}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;S.ociFormRegVal='';S.ociFormRegFilter='';R()}
/* ── OCI Config ── */
function rOciRegionDd(eo){
if(eo&&!S.ociFormRegVal)S.ociFormRegVal=eo.region||'';
const val=S.ociFormRegVal;const f=S.ociFormRegFilter.toLowerCase();
let ddItems='';
for(const[grp,regs] of Object.entries(S.ociRegions)){
const filtered=regs.filter(r=>!f||r.includes(f));
if(!filtered.length)continue;
ddItems+=`<div class="grp">${grp}</div>`;
filtered.forEach(r=>{ddItems+=`<div class="itm${val===r?' sel':''}" onclick="ociFormPickReg('${r}')">${r}</div>`})}
return`<div class="dd" style="position:relative"><div onclick="S.ociFormRegOpen=!S.ociFormRegOpen;R()" style="min-height:34px;display:flex;align-items:center;cursor:pointer;padding:.35rem .65rem;background:var(--bg2);border:1.5px solid var(--bd);border-radius:10px;font-size:.78rem;justify-content:space-between">
<span style="${val?'':'color:var(--t4)'}">${val||'Selecione a região...'}</span><span style="font-size:.6rem;color:var(--t4)">▼</span></div>
${S.ociFormRegOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:0;right:0;background:var(--bg);border:1px solid var(--b3);border-radius:6px;max-height:260px;overflow:auto;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.15)">
<div style="padding:.3rem;border-bottom:1px solid var(--b2)"><input type="text" placeholder="Buscar região..." value="${S.ociFormRegFilter}" oninput="S.ociFormRegFilter=this.value;R()" style="width:100%;border:none;outline:none;font-size:.75rem;padding:.25rem .4rem;background:transparent;color:var(--t1)" onclick="event.stopPropagation()"></div>
${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma região encontrada</div>'}
</div>`:''}</div>`}
function ociFormPickReg(r){S.ociFormRegVal=r;S.ociFormRegOpen=false;S.ociFormRegFilter='';R()}
function rOci(){const eo=S.editing?.type==='oci'?S.ociCfg.find(c=>c.id===S.editing.id):null;
return`<div class="cd" style="display:flex;align-items:center;justify-content:space-between;padding:.6rem 1rem">
<div style="font-size:.78rem;font-weight:600;color:var(--t2)">${IC.settings} Configuração do Sistema</div>
<div style="display:flex;gap:.4rem;align-items:center">
<button class="btn bp bsm" onclick="cfgExport()">${IC.upload} Exportar</button>
<button class="btn bs bsm" onclick="document.getElementById('cfgImportFile').click()">${IC.download} Importar</button>
<input type="file" id="cfgImportFile" accept=".json" style="display:none" onchange="cfgImport(this)">
</div></div>
<div id="cfgMsg"></div>
<div class="cd"><div class="ct">${IC.cloud} Credenciais Registradas</div><div id="ocm"></div>
<table><thead><tr><th>Tenancy</th><th>Region</th><th>Detalhes</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><br><span style="font-size:.66rem;color:var(--t4)">${c.created_at}</span></td><td><span class="tag">${c.region}</span></td>
<td style="font-family:var(--fm);font-size:.66rem;color:var(--t4);line-height:1.6"><span title="Fingerprint">${IC.key} ••••••••••••••••••••</span><br><span title="Tenancy OCID">${IC.building} ${c.tenancy_ocid}</span><br><span title="User OCID">${IC.user} ${c.user_ocid}</span><br><span title="Compartment">${IC.pkg} ${c.compartment_id||'—'}</span><br><span title="SSH Public Key">${IC.key} SSH: ${c.ssh_public_key_preview||'<em>não configurada</em>'}</span></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?IC.edit+' Editar Credencial':IC.plus+' 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="password" id="ouo" placeholder="ocid1.user.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.user_ocid+'</code></div>':''}</div>
<div class="ig"><label>Fingerprint</label><input type="password" id="ofp" placeholder="aa:bb:cc:dd:..." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.fingerprint+'</code></div>':''}</div>
<div class="ig"><label>OCID Tenancy</label><input type="password" id="oto" placeholder="ocid1.tenancy.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+eo.tenancy_ocid+'</code></div>':''}</div>
<div class="ig"><label>Region</label>${rOciRegionDd(eo)}</div>
<div class="ig"><label>Compartment OCID</label><input type="password" id="ocp" placeholder="ocid1.compartment.oc1.." value="${eo?'':''}" autocomplete="off">${eo?'<div class="ht">Preencha para alterar. Atual: <code>'+(eo.compartment_id||'—')+'</code></div>':''}</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"><label>SSH Public Key</label><div class="ht">Chave SSH para acesso a instâncias compute (ex: ssh-rsa AAAA...)</div><textarea id="osshk" rows="2" placeholder="ssh-rsa AAAAB3NzaC1yc2E... (opcional)" style="font-size:.72rem;font-family:var(--fm);resize:vertical">${eo&&eo.ssh_public_key?eo.ssh_public_key:''}</textarea>${eo&&eo.ssh_public_key_preview?'<div class="ht">Atual: <code>'+eo.ssh_public_key_preview+'</code></div>':''}</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 tn=document.getElementById('otn').value;
if(!tn)return sm('om','Preencha o Tenancy Name','e');
const fd=new FormData();fd.append('tenancy_name',tn);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',S.ociFormRegVal||'');fd.append('compartment_id',document.getElementById('ocp').value);
fd.append('key_passphrase',document.getElementById('okp').value||'');
fd.append('ssh_public_key',document.getElementById('osshk').value||'');
const sk=document.getElementById('osk').files[0];
const isEdit=S.editing?.type==='oci';
if(!isEdit){
if(!document.getElementById('oto').value||!document.getElementById('ouo').value||!document.getElementById('ofp').value)return sm('om','Preencha OCID Tenancy, OCID User e Fingerprint','e');
if(!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.ociFormRegVal='';S.ociFormRegFilter='';S.ociCfg=await $api('/oci/configs');sm('om',IC.ok+' 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 cfgExport(){
try{
const resp=await fetch(API+'/config/export',{headers:S.token?{'Authorization':'Bearer '+S.token}:{}});
if(!resp.ok)throw new Error('Erro ao exportar');
const blob=await resp.blob();
const url=URL.createObjectURL(blob);
const a=document.createElement('a');a.href=url;
a.download=resp.headers.get('content-disposition')?.match(/filename="(.+)"/)?.[1]||'config-export.json';
a.click();URL.revokeObjectURL(url);
sm('cfgMsg',IC.ok+' Configuração exportada com sucesso!','s');
}catch(e){sm('cfgMsg',IC.err+' '+e.message,'e')}
}
async function cfgImport(input){
const file=input.files[0];if(!file)return;
try{
const fd=new FormData();fd.append('file',file);
const resp=await fetch(API+'/config/import',{method:'POST',headers:S.token?{'Authorization':'Bearer '+S.token}:{},body:fd});
if(!resp.ok){const err=await resp.json();throw new Error(err.detail||'Erro ao importar')}
const d=await resp.json();
const parts=[];
if(d.oci_configs)parts.push(d.oci_configs+' OCI configs');
if(d.genai_configs)parts.push(d.genai_configs+' GenAI configs');
if(d.mcp_servers)parts.push(d.mcp_servers+' MCP servers');
if(d.system_prompts)parts.push(d.system_prompts+' prompts');
if(d.app_settings)parts.push(d.app_settings+' settings');
if(d.adb_vector_configs)parts.push(d.adb_vector_configs+' ADB configs');
if(d.skipped)parts.push(d.skipped+' ignorados (já existem)');
sm('cfgMsg',IC.ok+' Importado: '+parts.join(', '),'s');
[S.ociCfg,S.genaiCfg,S.mcpSvr]=await Promise.all([$api('/oci/configs'),$api('/genai/configs'),$api('/mcp/servers')]);
R();
}catch(e){sm('cfgMsg',IC.err+' '+e.message,'e')}
finally{input.value=''}
}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});sm('ocm',d.status==='success'?IC.ok+' Conexão OK!':IC.err+' '+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 defModel='openai.gpt-4.1';
const mOpts='<option value="_custom"'+(eg&&eg.model_ocid?' selected':'')+'>'+IC.link+' Personalizado (usar OCID)</option>'+Object.entries(S.models).map(([k,v])=>`<option value="${k}"${eg?(eg.model_id===k&&!eg.model_ocid?' selected':''):(k===defModel?' 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">${IC.brain} 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">${IC.chat} System Prompts Chat Agent</div>
<div class="cdesc">Prompts de sistema para o <strong>Chat Agent</strong>. O prompt <strong>ativo</strong> é enviado como instrução do sistema em todas as mensagens. Máximo 10 prompts. Outros agents (ex.: provisionamento) terão seus próprios prompts.</div><div id="spm"></div>
<table><thead><tr><th>Nome</th><th>Status</th><th>Ações</th></tr></thead><tbody>
${(S.chatPrompts||[]).map(p=>`<tr${S.editingPrompt===p.id?' style="background:var(--bg3)"':''}><td><strong>${p.name}</strong></td>
<td>${p.is_active?'<span class="tag" style="background:var(--gnl);color:var(--gn)">Ativo</span>':'<span class="tag">Inativo</span>'}</td>
<td><button class="btn bs bsm" onclick="editPrompt('${p.id}')">Editar</button> ${!p.is_active?`<button class="btn bs bsm" onclick="activatePrompt('${p.id}')">Ativar</button>`:''} <button class="btn bd bsm" onclick="delPrompt('${p.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!(S.chatPrompts||[]).length?'<div class="emp" style="padding:.6rem"><p>Nenhum prompt configurado.</p></div>':''}
<div style="margin-top:10px"><div class="g3"><div class="ig" style="grid-column:1/-1"><label>${S.editingPrompt?'Editar Prompt':'Novo Prompt'}</label><input type="text" id="spname" placeholder="Nome do prompt" value="${S.editingPrompt?(S.chatPrompts||[]).find(p=>p.id===S.editingPrompt)?.name||'':''}"></div></div>
<textarea id="gsysp" rows="10" style="width:100%;font-family:var(--fm);font-size:.78rem;resize:vertical;border:1px solid var(--bd);border-radius:6px;padding:10px;background:var(--bg2);color:var(--t1);line-height:1.5" placeholder="Você é um assistente especializado em...">${S.editingPrompt?((S.chatPrompts||[]).find(x=>x.id===S.editingPrompt)||{}).content||'':''}</textarea>
<div style="display:flex;gap:8px;margin-top:8px"><button class="btn bp" onclick="savePrompt()">${S.editingPrompt?'Salvar Alterações':'Criar Prompt'}</button>${S.editingPrompt?'<button class="btn bs" onclick="S.editingPrompt=null;R()">Cancelar</button>':''}</div></div></div>
<div class="cd"><div class="ct">${eg?IC.edit+' Editar Modelo GenAI':IC.plus+' 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',IC.ok+' 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'?IC.ok+' '+d.message+(d.response?' — '+d.response:''):IC.err+' '+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">${IC.plug} 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>
${(()=>{const tools=Array.isArray(m.tools)?m.tools:[];return tools.length?`
<div style="margin-top:.65rem;border-top:1px solid var(--b2);padding-top:.5rem">
<div style="font-size:.74rem;font-weight:600;color:var(--t2);margin-bottom:.35rem">${IC.wrench} Tools (${tools.length})</div>
${tools.map((t,i)=>`<div style="display:flex;align-items:flex-start;gap:.5rem;padding:.3rem .5rem;background:var(--bg);border-radius:6px;margin-bottom:.25rem;font-size:.72rem">
<div style="flex:1"><strong>${typeof t==='string'?t:t.name}</strong>${typeof t==='object'&&t.description?` <span style="color:var(--t4)">— ${t.description}</span>`:''}
${typeof t==='object'&&t.input_schema&&t.input_schema.properties?`<div style="margin-top:.2rem;font-size:.64rem;color:var(--t4)">Params: ${Object.keys(t.input_schema.properties).join(', ')}</div>`:''}</div>
<button class="btn bd bsm" style="font-size:.6rem;padding:.15rem .4rem" onclick="removeMcpTool('${m.id}',${i})">✕</button></div>`).join('')}
</div>`:`<div style="margin-top:.5rem;font-size:.72rem;color:var(--t4)">Nenhuma tool registrada</div>`})()}
<div id="mcpm_${m.id}"></div>
<div style="display:flex;gap:.5rem;margin-top:.65rem;align-items:center;flex-wrap:wrap">
<button class="btn bp bsm" id="mdb_${m.id}" onclick="discoverMcpTools('${m.id}')">${IC.search} Descobrir Tools</button>
<button class="btn bs bsm" onclick="toggleAddTool('${m.id}')">${IC.plus} Adicionar Tool</button>
<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 id="mctf_${m.id}" style="display:none;margin-top:.5rem;padding:.6rem;border:1px solid var(--bd);border-radius:8px;background:var(--bg)">
<div class="g2" style="gap:.5rem"><div class="ig"><label style="font-size:.7rem">Nome da Tool</label><input type="text" id="mctn_${m.id}" placeholder="list_resources" style="font-size:.74rem"></div>
<div class="ig"><label style="font-size:.7rem">Descrição</label><input type="text" id="mctd_${m.id}" placeholder="Lista recursos OCI" style="font-size:.74rem"></div></div>
<div class="ig" style="margin-top:.35rem"><label style="font-size:.7rem">Input Schema (JSON)</label><input type="text" id="mcts_${m.id}" placeholder='{"type":"object","properties":{"region":{"type":"string"}}}' style="font-size:.7rem;font-family:var(--fm)"></div>
<div style="display:flex;gap:.4rem;margin-top:.4rem"><button class="btn bp bsm" onclick="addMcpTool('${m.id}')">Salvar Tool</button><button class="btn bs bsm" onclick="toggleAddTool('${m.id}')">Cancelar</button></div>
</div></div>`}).join('')
:'<div class="emp" style="padding:.85rem"><p>Nenhum MCP server registrado.</p></div>'}</div>
<div class="cd"><div class="ct">${em?IC.edit+' Editar Server':IC.plus+' 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>${IC.terminal} 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>${IC.globe} 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>${IC.python} 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>${IC.db} 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',IC.ok+' '+(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,IC.ok+' 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,IC.ok+' '+(aid?'Vinculado ao ADB!':'ADB desvinculado!'),'s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Vincular ADB'}}
async function discoverMcpTools(mid){const btn=document.getElementById('mdb_'+mid);btn.disabled=true;btn.textContent='Descobrindo...';
sm('mcpm_'+mid,'<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Conectando ao MCP server para descobrir tools...','i');
try{const d=await $api('/mcp/servers/'+mid+'/discover-tools',{method:'POST'});S.mcpSvr=await $api('/mcp/servers');
sm('mcpm_'+mid,IC.ok+` ${d.discovered} tool(s) descoberta(s), ${d.total} total`,'s');R()}catch(e){sm('mcpm_'+mid,IC.err+' '+e.message,'e');btn.disabled=false;btn.textContent=IC.search+' Descobrir Tools'}}
function toggleAddTool(mid){const el=document.getElementById('mctf_'+mid);el.style.display=el.style.display==='none'?'':'none'}
async function addMcpTool(mid){const name=document.getElementById('mctn_'+mid)?.value?.trim();const desc=document.getElementById('mctd_'+mid)?.value?.trim()||'';
const schemaStr=document.getElementById('mcts_'+mid)?.value?.trim();
if(!name)return sm('mcpm_'+mid,'Nome da tool é obrigatório','e');
let schema={};if(schemaStr)try{schema=JSON.parse(schemaStr)}catch(e){return sm('mcpm_'+mid,'Input Schema JSON inválido','e')}
const srv=S.mcpSvr.find(m=>m.id===mid);const existing=Array.isArray(srv?.tools)?srv.tools:[];
existing.push({name,description:desc,input_schema:schema});
try{await $api('/mcp/servers/'+mid+'/tools',{method:'PUT',body:{tools:existing}});S.mcpSvr=await $api('/mcp/servers');
sm('mcpm_'+mid,IC.ok+' Tool adicionada!','s');R()}catch(e){sm('mcpm_'+mid,e.message,'e')}}
async function removeMcpTool(mid,idx){const srv=S.mcpSvr.find(m=>m.id===mid);const tools=Array.isArray(srv?.tools)?[...srv.tools]:[];
if(idx<0||idx>=tools.length)return;const name=typeof tools[idx]==='string'?tools[idx]:tools[idx].name;
if(!confirm(`Remover tool "${name}"?`))return;tools.splice(idx,1);
try{await $api('/mcp/servers/'+mid+'/tools',{method:'PUT',body:{tools}});S.mcpSvr=await $api('/mcp/servers');R()}catch(e){sm('mcpm_'+mid,e.message,'e')}}
/* ── 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">${IC.db} Conexões Registradas</div>
<table><thead><tr><th>Nome</th><th>DSN</th><th>User</th><th>Tabelas</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];const tbls=c.tables||[];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>${tbls.length?tbls.map(t=>'<span class="tag" style="font-size:.62rem">'+t.table_name+'</span>').join(' '):'<span style="color:var(--t4);font-size:.72rem">—</span>'}</td>
<td>${c.use_mtls?IC.ok:'—'}</td><td>${c.wallet_dir?IC.ok:IC.err}</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="checkTables('${c.id}')">${IC.db} Verificar</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>
${ea?`<div class="cd"><div class="ct">${IC.edit} Editar Conexão — ${ea.config_name}</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.config_name}"></div>
<div class="ig"><label>Wallet ZIP</label><div class="ht">Envie novo wallet ou deixe vazio para manter o atual</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.dsn}"></div></div>
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value="${ea.username}"></div>
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder="(manter atual)"></div>
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></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.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.embedding_model_id===k?' selected':'')+'>'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
<div style="display:flex;gap:8px;margin-bottom:1rem"><button class="btn bp" id="abtn" onclick="sAdb()">Salvar Alterações</button><button class="btn bs" onclick="cancelEdit()">Cancelar</button></div>
<div style="border-top:1px solid var(--br);padding-top:.85rem">
<div style="font-weight:600;font-size:.78rem;margin-bottom:.5rem">${IC.log} Tabelas de Vetores</div>
<div class="cdesc" style="margin-bottom:.6rem">Registre aqui as tabelas que já existem no ADB. A busca RAG consultará todas as tabelas ativas.</div><div id="tbm"></div>
${(ea.tables||[]).length?`<table><thead><tr><th>Tabela</th><th>Descrição</th><th>Ativa</th><th>Ações</th></tr></thead><tbody>
${(ea.tables||[]).map(t=>`<tr${!t.is_active?' style="opacity:.55"':''}>
<td><input type="text" value="${t.table_name}" id="tn_${t.id}" style="font-family:var(--fm);font-size:.72rem;width:100%;max-width:180px"></td>
<td><input type="text" value="${t.description||''}" id="td_${t.id}" placeholder="Descrição..." style="font-size:.72rem;width:100%"></td>
<td><button class="btn bs bsm" style="font-size:.62rem" onclick="toggleTable('${ea.id}','${t.id}',${t.is_active?0:1})">${t.is_active?IC.ok+' Sim':IC.err+' Não'}</button></td>
<td style="white-space:nowrap"><button class="btn bs bsm" onclick="saveTable('${ea.id}','${t.id}')">Salvar</button> <button class="btn bd bsm" onclick="removeTable('${ea.id}','${t.id}')">Excluir</button></td>
</tr>`).join('')}</tbody></table>`:'<div class="emp" style="padding:.5rem"><p style="font-size:.74rem">Nenhuma tabela registrada.</p></div>'}
<div style="display:flex;gap:6px;margin-top:.65rem;align-items:end">
<div class="ig" style="flex:1;margin:0"><label>Nome da Tabela Existente</label><input type="text" id="newTblName" placeholder="Ex: networking"></div>
<div class="ig" style="flex:1;margin:0"><label>Descrição</label><input type="text" id="newTblDesc" placeholder="Ex: Dados de networking do CIS"></div>
<button class="btn bs" onclick="addTable('${ea.id}')">+ Registrar</button>
</div></div></div>`
:`<div class="cd"><div class="ct">${IC.plus} 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. 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=""></div>
<div class="ig"><label>Wallet ZIP</label><div class="ht">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=""></div></div>
<div class="ig"><label>Username</label><input type="text" id="auser" placeholder="ADMIN" value=""></div>
<div class="ig"><label>Password</label><input type="password" id="apw" placeholder=""></div>
<div class="ig"><label>Wallet Password</label><input type="password" id="awpw" placeholder="(opcional)"></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>
<div style="display:flex;gap:8px"><button class="btn bp" id="abtn" onclick="sAdb()">Salvar Conexão</button></div></div>`}
${S.adbCfg.length?`<div class="cd"><div class="ct">${IC.upload} 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',IC.ok+' 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('use_mtls','true');
fd.append('genai_config_id',document.getElementById('agcfg').value||'');
fd.append('embedding_model_id',document.getElementById('aemb').value||'cohere.embed-v4.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',IC.ok+' 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'?IC.ok+' '+d.message:IC.err+' '+d.message,d.status==='success'?'s':'e');refreshCLogs('adb')}catch(e){sm('am',e.message,'e')}}
async function checkTables(id){sm('am','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Verificando tabelas no ADB...','i');
try{const d=await $api('/adb/'+id+'/tables/check',{method:'POST'});
if(d.status==='error'){sm('am',IC.err+' '+d.message,'e');return}
let html='<strong>Schema: '+d.schema_user+'</strong> ('+d.adb_tables_total+' tabelas no ADB)<br><br>';
html+='<table style="width:100%;font-size:.74rem"><thead><tr><th>Tabela Registrada</th><th>Status</th><th>Detalhes</th></tr></thead><tbody>';
d.registered.forEach(r=>{const st=r.status==='ok'?'<span style="color:var(--gn)">'+IC.ok+' Existe</span>':r.status==='not_found'?'<span style="color:var(--rd)">'+IC.err+' Não encontrada</span>':'<span style="color:var(--rd)">'+IC.err+' Erro</span>';
html+='<tr><td><strong>'+r.table+'</strong></td><td>'+st+'</td><td>'+(r.rows!==undefined?r.rows+' registros':(r.message||''))+'</td></tr>'});
html+='</tbody></table>';
if(d.unregistered_in_adb&&d.unregistered_in_adb.length){html+='<br><details><summary style="cursor:pointer;font-size:.72rem;color:var(--t3)">Tabelas no ADB não registradas ('+d.unregistered_in_adb.length+')</summary><div style="font-size:.68rem;color:var(--t4);margin-top:.3rem">'+d.unregistered_in_adb.join(', ')+'</div></details>'}
sm('am',html,'i')}catch(e){sm('am',IC.err+' '+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',IC.ok+' Wallet enviada! Arquivos: '+d.files.join(', '),'s');R()}catch(e){sm('wm',e.message,'e');btn.disabled=false;btn.textContent='Upload Wallet'}}
async function addTable(vid){const name=document.getElementById('newTblName').value.trim();if(!name)return sm('tbm','Digite o nome da tabela.','e');
const desc=document.getElementById('newTblDesc').value.trim();
try{await $api('/adb/'+vid+'/tables',{method:'POST',body:{table_name:name,description:desc}});S.adbCfg=await $api('/adb/configs');sm('tbm',IC.ok+' Tabela '+name+' registrada!','s');R()}catch(e){sm('tbm',e.message,'e')}}
async function saveTable(vid,tid){const name=document.getElementById('tn_'+tid)?.value.trim();const desc=document.getElementById('td_'+tid)?.value.trim();
if(!name)return sm('tbm','Nome da tabela é obrigatório.','e');
try{await $api('/adb/'+vid+'/tables/'+tid,{method:'PUT',body:{table_name:name,description:desc}});S.adbCfg=await $api('/adb/configs');sm('tbm',IC.ok+' Tabela atualizada.','s');R()}catch(e){sm('tbm',e.message,'e')}}
async function toggleTable(vid,tid,val){try{await $api('/adb/'+vid+'/tables/'+tid,{method:'PUT',body:{is_active:val}});S.adbCfg=await $api('/adb/configs');R()}catch(e){sm('tbm',e.message,'e')}}
async function removeTable(vid,tid){if(!confirm('Excluir esta tabela do registro?'))return;
try{await $api('/adb/'+vid+'/tables/'+tid,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');sm('tbm',IC.ok+' Tabela removida.','s');R()}catch(e){sm('tbm',e.message,'e')}}
/* ── Embeddings ── */
function tblOpts(vid){const cfg=S.adbCfg.find(c=>c.id===vid);if(!cfg||!(cfg.tables||[]).length)return'<option value="">Padrão</option>';
return cfg.tables.filter(t=>t.is_active).map(t=>'<option value="'+t.table_name+'">'+t.table_name+'</option>').join('')}
function tblOptsAll(vid){const cfg=S.adbCfg.find(c=>c.id===vid);if(!cfg||!(cfg.tables||[]).length)return'<option value="">Todas as tabelas</option>';
return'<option value="">Todas as tabelas</option>'+cfg.tables.filter(t=>t.is_active).map(t=>'<option value="'+t.table_name+'">'+t.table_name+'</option>').join('')}
function rEmbeddings(){
const adbOpts=S.adbCfg.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.length)return`<div class="cd"><div class="emp"><div class="eic">${IC.dna}</div><p>Nenhuma conexão ADB configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>ADB Vector</strong>.</p></div></div>`;
if(!S.genaiCfg.length&&!S.ociCfg.length)return`<div class="cd"><div class="emp"><div class="eic">${IC.dna}</div><p>Nenhuma credencial OCI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>Config → OCI</strong>.</p></div></div>`;
const firstVid=S.adbCfg.find(c=>c.is_active)?.id||S.adbCfg[0]?.id||'';
return`<div class="cd"><div class="ct">${IC.log} Embeddings Existentes</div>
<div class="cdesc">Consulte os documentos armazenados nas tabelas de embeddings do ADB.</div>
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="elsel" onchange="document.getElementById('eltbl').innerHTML=tblOpts(this.value)">${adbOpts}</select></div>
<div class="ig"><label>Tabela</label><select id="eltbl">${tblOpts(firstVid)}</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">${IC.shield} CIS Recommendations</div>
<div class="cdesc">Envie a versão mais recente do PDF para manter as recomendações CIS Oracle Cloud atualizadas na base vetorial.</div><div id="crm"></div>
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="crsel">${adbOpts}</select></div>
<div class="ig"><label>Arquivo PDF</label><input type="file" id="crf" accept=".pdf,.txt"></div>
<div class="ig" style="align-self:end"><button class="btn bp" id="crbtn" onclick="embedCisRecom()">${IC.upload} Upload CIS Recommendations</button></div></div></div>
<div class="cd"><div class="ct">${IC.scroll} Base de Conhecimento</div>
<div class="cdesc">Envie documentos para alimentar a base de conhecimento da equipe. Os arquivos serão vetorizados e disponibilizados para consulta via chat.</div><div id="efm"></div>
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="efsel">${adbOpts}</select></div></div>
<div style="display:flex;gap:.75rem;align-items:start;flex-wrap:wrap;margin-top:.5rem">
<div style="flex:1;min-width:240px">
<label style="font-size:.72rem;font-weight:600;display:block;margin-bottom:.35rem">Arquivo (.txt, .pdf, .csv, .json, .md)</label>
<input type="file" id="eff" accept=".txt,.pdf,.csv,.json,.md">
<button class="btn bp" id="efbtn" onclick="embedFile()" style="margin-top:.5rem;width:100%">Upload Arquivo</button>
</div>
<div style="width:1px;background:var(--brd);align-self:stretch;margin:0 .25rem"></div>
<div style="flex:1;min-width:240px">
<label style="font-size:.72rem;font-weight:600;display:block;margin-bottom:.35rem">URL (página web ou PDF)</label>
<input type="text" id="efurl" placeholder="https://docs.oracle.com/..." style="width:100%;padding:.45rem .6rem;border:1px solid var(--brd);border-radius:var(--r);background:var(--bg2);color:var(--t1);font-size:.78rem">
<button class="btn bp" id="efurlbtn" onclick="embedUrl()" style="margin-top:.5rem;width:100%">Importar URL</button>
</div></div></div>`}
async function loadEmbs(){const vid=document.getElementById('elsel').value;if(!vid)return;
const tbl=document.getElementById('eltbl')?.value||'';
try{const d=await $api('/embeddings/'+vid+'/list'+(tbl?'?table_name='+encodeURIComponent(tbl):''));
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='<div style="overflow-x:auto"><table style="margin-top:.65rem;table-layout:fixed;width:100%"><thead><tr><th style="width:80px">ID</th><th>Metadata</th></tr></thead><tbody>'+
d.documents.map(e=>{let meta=e.metadata||'—';if(typeof meta==='object')meta=JSON.stringify(meta);if(meta.length>200)meta=meta.substring(0,200)+'…';
return'<tr><td style="font-family:var(--fm);font-size:.62rem;word-break:break-all">'+e.id.substring(0,12)+'</td>'+
'<td style="font-size:.68rem;color:var(--t3);word-break:break-word;white-space:pre-wrap;max-width:0;overflow:hidden">'+meta+'</td></tr>'}).join('')+
'</tbody></table></div><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 previewChunks(){const rid=document.getElementById('errpt').value;
if(!rid)return sm('erm','Selecione um relatório.','e');
const btn=document.getElementById('epbtn');btn.disabled=true;btn.textContent='Carregando...';
try{const d=await $api('/embeddings/preview/'+rid);
let html='<div style="margin-top:.75rem;border:1px solid var(--bd);border-radius:var(--r);overflow:hidden">';
html+='<div style="padding:.65rem .85rem;background:var(--bg2);border-bottom:1px solid var(--bd);display:flex;justify-content:space-between;align-items:center">';
html+='<div><strong style="font-size:.82rem">'+d.tenancy+'</strong>';
if(d.regions?.length)html+=' <span style="font-size:.68rem;color:var(--t4)">Regiões: '+d.regions.join(', ')+'</span>';
html+='</div><span class="tag">'+d.total_chunks+' chunks</span></div>';
if(d.compartments?.length)html+='<div style="padding:.4rem .85rem;background:var(--bg2);border-bottom:1px solid var(--bd);font-size:.68rem;color:var(--t3)">Compartments: '+d.compartments.join(', ')+'</div>';
d.chunks.forEach((c,i)=>{
const preview=c.content.length>300?c.content.substring(0,300)+'...':c.content;
html+='<details style="border-bottom:1px solid var(--bd)">';
html+='<summary style="padding:.5rem .85rem;cursor:pointer;font-size:.74rem;display:flex;justify-content:space-between;align-items:center">';
html+='<span><strong>#'+(i+1)+'</strong> '+c.source+'</span>';
html+='<span style="font-size:.64rem;color:var(--t4)">'+c.metadata+'</span></summary>';
html+='<pre style="padding:.5rem .85rem;font-size:.64rem;color:var(--t3);white-space:pre-wrap;max-height:200px;overflow:auto;background:var(--bg2);margin:0">'+preview+'</pre>';
html+='</details>'});
html+='</div>';
document.getElementById('chunk-preview').innerHTML=html;
document.getElementById('erbtn').style.display='';
sm('erm',IC.ok+' '+d.total_chunks+' chunks prontos para embedding.','s');
}catch(e){sm('erm',e.message,'e')}finally{btn.disabled=false;btn.textContent=IC.eye+' Visualizar Chunks'}}
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 tbl=document.getElementById('ertbl')?.value||'';
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,table_name:tbl||undefined}});sm('erm',IC.ok+' '+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 embedCisRecom(){const vid=document.getElementById('crsel').value;const f=document.getElementById('crf').files[0];
if(!vid||!f)return sm('crm','Selecione conexão ADB e arquivo.','e');
const fd=new FormData();fd.append('file',f);fd.append('adb_config_id',vid);fd.append('table_name','cisrecom');
const btn=document.getElementById('crbtn');btn.disabled=true;btn.textContent='Enviando...';sm('crm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Processando PDF e gerando embeddings...','i');
try{const d=await $api('/embeddings/upload',{method:'POST',body:fd,headers:{}});sm('crm',IC.ok+' '+d.message+' → tabela CISRECOM','s');btn.disabled=false;btn.textContent=IC.upload+' Upload CIS Recommendations'}catch(e){sm('crm',e.message,'e');btn.disabled=false;btn.textContent=IC.upload+' Upload CIS Recommendations'}}
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 tbl='engineerknowledgebase';
const fd=new FormData();fd.append('file',f);fd.append('adb_config_id',vid);fd.append('table_name',tbl);
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',IC.ok+' '+d.message,'s');btn.disabled=false;btn.textContent='Upload Arquivo'}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Upload Arquivo'}}
async function embedUrl(){const vid=document.getElementById('efsel').value;const url=document.getElementById('efurl').value.trim();
if(!vid||!url)return sm('efm','Selecione conexão ADB e informe a URL.','e');
const fd=new FormData();fd.append('adb_config_id',vid);fd.append('table_name','engineerknowledgebase');fd.append('url',url);
const btn=document.getElementById('efurlbtn');btn.disabled=true;btn.textContent='Importando...';sm('efm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Buscando conteúdo e gerando embeddings...','i');
try{const d=await $api('/embeddings/upload-url',{method:'POST',body:fd,headers:{}});sm('efm',IC.ok+' '+d.message,'s');btn.disabled=false;btn.textContent='Importar URL';document.getElementById('efurl').value=''}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Importar URL'}}
async function delEmb(vid,docId){if(!confirm('Excluir este embedding?'))return;
const tbl=document.getElementById('eltbl')?.value||'';
try{await $api('/embeddings/'+vid+'/'+docId+(tbl?'?table_name='+encodeURIComponent(tbl):''),{method:'DELETE'});loadEmbs()}catch(e){alert('Erro: '+e.message)}}
/* ── Consult Embeddings ── */
if(!S.consultHistory)S.consultHistory=[];
function rEmbConsultPage(){
if(!S.adbCfg.length)return`<div class="cd"><div class="emp"><div class="eic">${IC.search}</div><p>Nenhuma conexão ADB configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>ADB Vector</strong>.</p></div></div>`;
const firstVid=S.adbCfg.find(c=>c.is_active)?.id||S.adbCfg[0]?.id||'';
return rEmbConsult(firstVid)}
function rEmbConsult(firstVid){
const msgs=S.consultHistory.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='bot'?fm(m.c):m.c}</div></div>`).join('');
return`<div class="cd" style="display:flex;flex-direction:column;height:calc(100vh - 180px)">
<div class="ct">${IC.search} Consultar Embeddings</div>
<div class="cdesc">Faça perguntas sobre os dados armazenados nas bases vetoriais. O modelo de IA consulta os embeddings e retorna uma resposta contextualizada.</div>
<div style="display:flex;gap:.5rem;margin-bottom:.75rem;flex-wrap:wrap">
<div class="ig" style="flex:1;min-width:180px"><label style="font-size:.72rem">Tabela (opcional)</label><select id="ecTbl" style="font-size:.78rem">${tblOptsAll(firstVid)}</select></div>
<div class="ig" style="width:80px"><label style="font-size:.72rem">Top-K</label><input type="number" id="ecTopK" value="10" min="1" max="30" style="font-size:.78rem;text-align:center"></div>
</div>
<div style="flex:1;overflow-y:auto;border:1px solid var(--brd);border-radius:var(--r);padding:.75rem;background:var(--bg2);margin-bottom:.75rem" id="ecMsgs">
${msgs||`<div class="emp"><div class="eic">${IC.search}</div><p>Faça uma consulta para pesquisar nos embeddings.</p><p style="font-size:.74rem;margin-top:.35rem">Ex: "findings para CIS 1.1", "recomendações de networking", "como corrigir MFA"</p></div>`}
</div>
<div style="display:flex;gap:.5rem">
<input type="text" id="ecQuery" placeholder="Digite sua consulta..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();consultEmb()}" style="flex:1;padding:.55rem .75rem;border:1px solid var(--brd);border-radius:var(--r);background:var(--bg1);color:var(--t1);font-size:.82rem">
<button class="btn bp" id="ecBtn" onclick="consultEmb()" style="white-space:nowrap">${IC.search} Consultar</button>
</div></div>`}
async function consultEmb(){
const q=document.getElementById('ecQuery').value.trim();if(!q)return;
const tbl=document.getElementById('ecTbl')?.value||'';
const topK=parseInt(document.getElementById('ecTopK')?.value)||10;
S.consultHistory.push({r:'user',c:q});
document.getElementById('ecQuery').value='';
const msgEl=document.getElementById('ecMsgs');
msgEl.innerHTML=S.consultHistory.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='bot'?fm(m.c):m.c}</div></div>`).join('')+
'<div class="cm cm-bot"><div class="cb"><span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Consultando bases vetoriais...</div></div>';
msgEl.scrollTop=msgEl.scrollHeight;
const btn=document.getElementById('ecBtn');btn.disabled=true;
try{
const d=await $api('/embeddings/consult',{method:'POST',body:{query:q,table_name:tbl,top_k:topK}});
let resp=d.answer||'Sem resposta do modelo.';
if(d.documents?.length){resp+='\n\n---\n**Fontes consultadas ('+d.documents.length+' documentos, '+d.total+' total):**\n';
d.documents.forEach((doc,i)=>{resp+='\n'+(i+1)+'. **'+doc.source+'** (distância: '+doc.distance+')'});
}
S.consultHistory.push({r:'bot',c:resp});
}catch(e){S.consultHistory.push({r:'bot',c:'Erro: '+e.message})}
btn.disabled=false;
msgEl.innerHTML=S.consultHistory.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${m.r==='bot'?fm(m.c):m.c}</div></div>`).join('');
msgEl.scrollTop=msgEl.scrollHeight;
}
/* ── Users ── */
function rUsers(){return`<div class="cd"><div class="ct">${IC.users} 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">${IC.plus} 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', IC.err+' Nome e sobrenome são obrigatórios', 'e');
return;
}
if (!username) {
sm('um2', IC.err+' Usuário (login) é obrigatório', 'e');
return;
}
if (!password) {
sm('um2', IC.err+' 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', IC.ok+' 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">${IC.lock} 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">'+IC.ok+' 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',IC.ok+' MFA ativado com sucesso!','s');R()}catch(e){sm('mm',e.message,'e')}}
/* ── Audit ── */
function rAudit(){return`<div class="cd"><div class="ct">${IC.log} 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>${IC.scroll} 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>`}}
(async()=>{await checkAuth();R()})();
</script>
</body>
</html>