feat: UI modernization phases 1B-5, KPI dashboard, charts, and ADB fix

- Phase 1B: replace all emojis with inline SVG icons (Lucide-based, 45+ icons)
- Phase 2: KPI cards for compliance reports (score, passed, failed, total)
- Phase 3: Chart.js integration (donut distribution + horizontal bar by section)
- Phase 4: SVG gauge/speedometer for compliance score with gradient arc
- Phase 5: polish animations (fade-in stagger, smooth theme transition, hover effects, skeleton loading, alert slide-in, button press, dropdown animation)
- New endpoint GET /api/reports/{rid}/summary for KPI data extraction
- Fix ADB wallet connection (DPY-6005) by always passing wallet_password
- Remove CIS_EMBEDDINGS default from ADB config
- Custom brain icon (Lucide) for GenAI section
This commit is contained in:
nogueiraguh
2026-03-10 00:08:28 -03:00
parent f205f819ab
commit 4fc458cdcb
2 changed files with 396 additions and 136 deletions

View File

@@ -83,9 +83,7 @@ body{font-family:var(--fs);background:var(--bg);color:var(--t1);height:100vh;ove
.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);transition:box-shadow .3s var(--trans)}
.cd:hover{box-shadow:var(--sh2)}
.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}
@@ -120,6 +118,25 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%
.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);
@@ -212,7 +229,6 @@ tbody tr:last-child td{border-bottom:none}
.lc .st{color:var(--t4);font-size:.76rem;margin-top:.15rem;margin-bottom:1.35rem;font-weight:400}
/* ── Alerts ── */
.al{padding:.65rem .85rem;border-radius:10px;font-size:.78rem;margin-bottom:.75rem;font-weight:500}
.al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(220,38,38,.1)}
.al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(22,163,74,.1)}
.al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(37,99,235,.1)}
@@ -258,13 +274,91 @@ tbody tr:last-child td{border-bottom:none}
.exp-node-children{padding-left:.85rem;border-left:1px solid var(--bg4);margin-left:.65rem}
/* ── Animations ── */
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.fi{animation:fi .35s var(--trans)}
@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}}
.ld{animation:pu 1.2s ease infinite;color:var(--t4);font-size:.82rem;text-align:center;padding:1.5rem}
@keyframes spin{to{transform:rotate(360deg)}}
@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 */
.pc>.cd,.pc>.kpi-row,.pc>.exp-wrap,.pc>.ch-c,.pc>div{animation:fi .4s var(--trans) both}
.pc>.cd:nth-child(2),.pc>div:nth-child(2){animation-delay:.05s}
.pc>.cd:nth-child(3),.pc>div:nth-child(3){animation-delay:.1s}
.pc>.cd:nth-child(4),.pc>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 */
.kpi{animation:fiScale .4s var(--trans) both}
.kpi:nth-child(2){animation-delay:.06s}
.kpi:nth-child(3){animation-delay:.12s}
.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 */
tbody tr{animation:fi .3s var(--trans) both}
tbody tr:nth-child(2){animation-delay:.03s}
tbody tr:nth-child(3){animation-delay:.06s}
tbody tr:nth-child(4){animation-delay:.09s}
tbody tr:nth-child(5){animation-delay:.12s}
/* Explorer card stagger */
.exp-c{animation:fiScale .3s var(--trans) both}
.exp-c:nth-child(2){animation-delay:.04s}
.exp-c:nth-child(3){animation-delay:.08s}
.exp-c:nth-child(4){animation-delay:.12s}
.exp-c:nth-child(5){animation-delay:.16s}
.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}
@@ -359,6 +453,7 @@ tbody tr:last-child td{border-bottom:none}
.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>
@@ -367,6 +462,61 @@ 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"/>'),
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>`;
@@ -376,7 +526,7 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
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:'',
cisVer:null,cisCheckResult:null,cisUpdating:false,rptHistFilter:'',chatFiles:[],
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,tfConfirmDest:false,tfMdOpen:false,
tfComps:[],tfCompLoading:false,tfFiles:[],tfBtab:'files',tfEditIdx:-1,tfResources:null,tfResLoading:false,tfRefStatus:'',tfBottomH:null};
@@ -457,9 +607,9 @@ function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div cla
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','💬','Chat Agent'],['terraform',TF_IC,'Terraform'],['explorer','🔍','OCI Explorer'],['report','📊','Reports'],['downloads','📁','Downloads']];
const ctabs=[['oci-config','☁️','Credenciais OCI'],['genai','🧠','GenAI Config'],['mcp','🔌','MCP Servers'],['adb','🗄️','ADB Vector'],['embeddings','🧬','Embeddings']];
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
const tabs=[['chat',IC.chat,'Chat Agent'],['terraform',TF_IC,'Terraform'],['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']];
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>
@@ -470,17 +620,17 @@ ${ctabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[
${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')?'☀️':'🌙'}</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':'💬 AI Agent Chat','terraform':TF_IC+' Terraform Agent','explorer':'🔍 OCI Account Explorer','report':'📊 Compliance Reports','downloads':'📁 Downloads','oci-config':'☁️ Credenciais OCI','genai':'🧠 OCI Generative AI','mcp':'🔌 MCP Servers','adb':'🗄️ Autonomous DB Vector','embeddings':'🧬 Embeddings','users':'👥 Gerenciar Usuários','mfa':'🔐 Autenticação MFA','audit':'📋 Audit Log'};
function rTb(){const t={'chat':IC.chat+' AI Agent Chat','terraform':TF_IC+' Terraform Agent','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','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'explorer':return rExplorer();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'genai':return rGenAI();case'mcp':return rMCP();case'adb':return rADB();case'embeddings':return rEmbeddings();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
/* ── Chat ── */
function rChat(){
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Inicie uma conversa com o agente.</p><p style="font-size:.74rem;margin-top:.35rem">Selecione um modelo para começar.</p></div>'
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'];
@@ -499,27 +649,27 @@ function rChat(){
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>🕘 Histórico</span><div class="ch-hp-new" onclick="newChat()">+ Novo Chat</div></div>
<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>🧠 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>
<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">⚙️</div>
<div class="ch-icon-btn${S.chatPanel==='logs'?' active':''}" onclick="togglePanel('logs')" title="Chat Logs">📋</div>
<div class="ch-icon-btn" onclick="newChat()" title="Nova conversa"></div></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>📄 ${f.name}</span><span onclick="rmChatFile(${i})" style="color:var(--err);cursor:pointer;font-weight:700">&times;</span></div>`).join('')}</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">📎</button>
<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 rChatConfig(isDirect,toolCount){
const pp=S.chatParams;
return`<div class="ch-side-title">⚙️ Configurações</div>
return`<div class="ch-side-title">${IC.settings} Configurações</div>
<div class="ch-side-section"><div class="g2">
<div><label>Temperature</label><input type="number" value="${pp.temperature}" step="0.1" min="0" max="2" onchange="S.chatParams.temperature=+this.value"></div>
<div><label>Max Tokens</label><input type="number" value="${pp.max_tokens}" min="1" max="128000" onchange="S.chatParams.max_tokens=+this.value"></div>
@@ -529,7 +679,7 @@ function rChatConfig(isDirect,toolCount){
<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"> 🔌 MCP Tools</label>
<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">
@@ -537,7 +687,7 @@ ${isDirect?`<div class="ch-side-section" style="border-top:1px solid var(--bd);p
<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">📋 Chat Logs</div>
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>
@@ -561,7 +711,7 @@ async function savePrompt(){const name=document.getElementById('spname').value;c
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',' Prompt salvo!','s');R()}catch(e){sm('spm',e.message,'e')}}
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>')}
@@ -574,17 +724,17 @@ 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:'⚠️ Selecione um modelo antes de enviar uma mensagem.'});R();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':'')+'📎 '+fileNames.join(', ');
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:' _Pensando..._',thinking:true});R();scCh();
S.msgs.push({r:'assistant',c:IC.wait+' _Pensando..._',thinking:true});R();scCh();
try{let d;
if(hasFiles){
const fd=new FormData();
@@ -593,7 +743,7 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
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:'⚠️ Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;return}
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);
@@ -607,7 +757,7 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
else if(S.chatModel){
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI.'});R();el.disabled=false;btns.forEach(b=>{b.disabled=false});if(sendBtn)sendBtn.textContent=sendBtn.dataset.origText;return}
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}
@@ -619,18 +769,18 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
}else{
S.msgs=S.msgs.filter(x=>!x.thinking);
let resp=d.response;
if(d.tools_used&&d.tools_used.length){resp+='\n\n🔧 **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
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:' Erro: '+e.message,t:tstamp()});R()}
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:' '+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:' Timeout: a resposta está demorando muito. Tente novamente.',t:tstamp()});R()}
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()}
@@ -1030,7 +1180,7 @@ function rTerraform(){
return `<div class="tf-wrap">
<div style="display:flex;flex:1;min-height:0;overflow:hidden">
<div class="ch-hp${S.tfHistOpen?' open':''}"><div class="ch-hp-inner">
<div class="ch-hp-hdr"><span>🕘 Histórico</span><div class="ch-hp-new" onclick="newTfChat()">+ Novo Chat</div></div>
<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">
@@ -1346,10 +1496,10 @@ function dlTfCode(){if(!S.tfFiles.length)return;if(S.tfFiles.length===1){const f
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:'⚠️ Selecione um modelo antes de enviar.',t:tstamp()});R();return}
if(!S.tfCompartment){S.tfMsgs.push({r:'assistant',c:'⚠️ Selecione um compartment antes de enviar.',t:tstamp()});R();return}
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:' _Gerando Terraform..._',thinking:true});R();tfScroll();
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;
@@ -1362,7 +1512,7 @@ async function tfSend(){
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:'⚠️ Selecione uma credencial OCI.',t:tstamp()});R();return}
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});
@@ -1370,7 +1520,7 @@ async function tfSend(){
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:' Erro: '+e.message,t:tstamp()});R()}
}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){
@@ -1379,10 +1529,10 @@ async function tfPollResult(mid){
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:' '+r.content,t:tstamp()});R();tfScroll();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:' Timeout.',t:tstamp()});R()
S.tfMsgs=S.tfMsgs.filter(x=>!x.thinking);S.tfMsgs.push({r:'assistant',c:IC.timeout+' Timeout.',t:tstamp()});R()
}
async function tfSaveAndPlan(){
@@ -1483,21 +1633,21 @@ const EXP_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">☁️</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.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:'💻',Rede:'🌐',Storage:'💾',Database:'🗄️',Containers:'📦',Serverless:'⚡',Observabilidade:'📊',Segurança:'🔒',IAM:'👤'};
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">📁 Compartments</div>${S.expTree.length?rExpTree(null,0):'<div class="ld"><span class="spinner"></span> Carregando...</div>'}</div>
<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>🌍 ${regLabel}</span><span style="font-size:.55rem;color:var(--t4)">▼</span></div>
${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>
@@ -1579,8 +1729,8 @@ function renderExpData(d){if(!d)return'<div class="emp"><p>Selecione um compartm
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+'\')">🔒 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+'\')">🔒 Update Network Access</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){
@@ -1626,10 +1776,10 @@ function rReport(){const comp=S.reports.filter(r=>r.status==='completed');
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">📊 Compliance Reports</div><div class="emp"><div class="eic">📄</div><p>Nenhum relatório gerado ainda.</p></div></div>${rRunRpt()}${rReportHistory()}`;
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.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('');
@@ -1641,7 +1791,7 @@ ${S.rptRselOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:
${rselDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhum relatório encontrado</div>'}
</div>`:''}
</div></div>
<iframe class="rif" id="rfr" src="${API}/reports/${(selRpt||lt).id}/html"></iframe>${rRunRpt()}${rReportHistory()}`}
${rKpi()}<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();
@@ -1655,7 +1805,7 @@ function rRunRpt(){if(S.user?.role==='viewer')return'';
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">🚀 Executar Relatório</div>
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>
@@ -1688,12 +1838,12 @@ 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"> Atualizando...</span>';
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">⚙️ CIS Engine</div>
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>
@@ -1704,7 +1854,67 @@ async function cisDoUpdate(){if(!confirm('Atualizar CIS Engine para a versão ma
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';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;R();try{S.rptKpi=await $api('/reports/'+rid+'/summary')}catch(e){S.rptKpi=null}S.rptKpiLoading=false;R()}
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;
@@ -1731,7 +1941,7 @@ async function pollRptProgress(){if(!S.trackingReportId)return stopRptPoll();
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.reports=await $api('/reports');R()}}catch(e){}}
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){
@@ -1751,7 +1961,7 @@ 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">📋 Histórico de Relatórios</div>
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>
@@ -1789,8 +1999,8 @@ function rDl(){
${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()">🔄 Atualizar</button></div>
${!filtered.length?'<div class="cd"><div class="emp"><div class="eic">📂</div><p>Nenhum relatório concluído.</p></div></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]):'';
@@ -1808,10 +2018,10 @@ async function toggleFiles(rid){if(S.dlExpandedRid===rid){S.dlExpandedRid=null;R
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 groups={};files.forEach(f=>{if(!groups[f.file_category])groups[f.file_category]=[];groups[f.file_category].push(f)});
const labels={summary:'Summary Reports',cis_finding:'CIS Findings',obp_finding:'OBP Findings',obp_best_practice:'OBP Best Practices',raw_data:'Raw Data',error:'Error Reports',diagram:'Diagrams',consolidated:'Consolidated',other:'Other'};
const icons={summary:'📊',cis_finding:'🛡️',obp_finding:'⚠️',obp_best_practice:'✅',raw_data:'📋',error:'❌',diagram:'📈',consolidated:'📑',other:'📄'};
const icons={summary:IC.chart,cis_finding:IC.shield,obp_finding:IC.warn,obp_best_practice:IC.ok,raw_data:IC.log,error:IC.err,diagram:IC.barChart,consolidated:IC.file,other:IC.file};
let h='<div style="border-top:1px solid var(--bd);background:var(--bg2)">';
for(const[cat,cf] of Object.entries(groups)){
h+=`<div style="padding:.4rem .8rem .1rem"><div style="font-size:.64rem;font-weight:600;color:var(--t4);text-transform:uppercase;letter-spacing:.4px;margin-bottom:.25rem">${icons[cat]||'📄'} ${labels[cat]||cat} (${cf.length})</div></div>`;
h+=`<div style="padding:.4rem .8rem .1rem"><div style="font-size:.64rem;font-weight:600;color:var(--t4);text-transform:uppercase;letter-spacing:.4px;margin-bottom:.25rem">${icons[cat]||IC.file} ${labels[cat]||cat} (${cf.length})</div></div>`;
cf.forEach(f=>{
h+=`<a href="${API}/reports/${rid}/files/${f.id}/download" target="_blank" style="display:flex;align-items:center;gap:.5rem;padding:.35rem .8rem;text-decoration:none;color:inherit;transition:background .12s;cursor:pointer" onmouseover="this.style.background='var(--bg3)'" onmouseout="this.style.background=''">
${_dlFileIcon(f.file_name)}
@@ -1847,20 +2057,20 @@ ${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma
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)">⚙️ Configuração do Sistema</div>
<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()">📤 Exportar</button>
<button class="btn bs bsm" onclick="document.getElementById('cfgImportFile').click()">📥 Importar</button>
<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">☁️ Credenciais Registradas</div><div id="ocm"></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">🔑 ••••••••••••••••••••</span><br><span title="Tenancy OCID">🏢 ${c.tenancy_ocid}</span><br><span title="User OCID">👤 ${c.user_ocid}</span><br><span title="Compartment">📦 ${c.compartment_id||'—'}</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></td>
<td><button class="btn bs bsm" onclick="editCfg('oci','${c.id}')">Editar</button> <button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button> <button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.ociCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma credencial registrada.</p></div>':''}</div>
<div class="cd"><div class="ct">${eo?'✏️ Editar Credencial':' Nova Credencial'}</div>
<div class="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>
@@ -1891,7 +2101,7 @@ async function sOci(){const tn=document.getElementById('otn').value;
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',' 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'}}
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}:{}});
@@ -1901,8 +2111,8 @@ async function cfgExport(){
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',' Configuração exportada com sucesso!','s');
}catch(e){sm('cfgMsg',' '+e.message,'e')}
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;
@@ -1919,28 +2129,28 @@ async function cfgImport(input){
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',' Importado: '+parts.join(', '),'s');
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',' '+e.message,'e')}
}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'?' Conexão OK!':' '+d.message,d.status==='success'?'s':'e');refreshCLogs('oci')}catch(e){sm('ocm',e.message,'e')}}
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':'')+'>🔗 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 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">🧠 Modelos Configurados</div>
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">💬 System Prompts Chat Agent</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>
@@ -1950,7 +2160,7 @@ ${!(S.chatPrompts||[]).length?'<div class="emp" style="padding:.6rem"><p>Nenhum
<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?'✏️ Editar Modelo GenAI':' Novo Modelo GenAI'}</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>
@@ -1973,12 +2183,12 @@ async function sGenai(){const btn=document.getElementById('gbtn');btn.disabled=t
endpoint:null,serving_type:'ON_DEMAND',dedicated_endpoint_id:null,
is_default:isEdit?(S.genaiCfg.find(g=>g.id===S.editing.id)?.is_default||false):S.genaiCfg.length===0};
try{const url=isEdit?'/genai/configs/'+S.editing.id:'/genai/config';const method=isEdit?'PUT':'POST';
await $api(url,{method,body});S.editing=null;S.genaiCfg=await $api('/genai/configs');sm('gm',' Modelo '+(isEdit?'atualizado':'salvo')+' com sucesso!','s');R()}catch(e){sm('gm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Modelo'}}
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'?' '+d.message+(d.response?' — '+d.response:''):' '+d.message,d.status==='success'?'s':'e');refreshCLogs('genai')}catch(e){sm('gm',e.message,'e')}}
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 ── */
@@ -1989,7 +2199,7 @@ function mcpTypeFields(){const t=document.querySelector('input[name="mt"]:checke
if(t==='module'){document.getElementById('mf-cmd').style.display='';document.getElementById('mf-args').style.display=''}}
function rMCP(){const em=S.editing?.type==='mcp'?S.mcpSvr.find(m=>m.id===S.editing.id):null;
const adbOpts=S.adbCfg.map(a=>`<option value="${a.id}">${a.config_name}</option>`).join('');
return`<div class="cd"><div class="ct">🔌 Servidores Registrados</div>
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)">
@@ -2006,7 +2216,7 @@ ${m.module_path?`<div><span style="color:var(--t3);font-weight:600">Módulo:</sp
<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">🔧 Tools (${tools.length})</div>
<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>
@@ -2014,8 +2224,8 @@ ${typeof t==='object'&&t.input_schema&&t.input_schema.properties?`<div style="ma
</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}')">🔍 Descobrir Tools</button>
<button class="btn bs bsm" onclick="toggleAddTool('${m.id}')"> Adicionar Tool</button>
<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)">
@@ -2028,25 +2238,25 @@ ${typeof t==='object'&&t.input_schema&&t.input_schema.properties?`<div style="ma
<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?'✏️ Editar Server':' Registrar Novo Server'}</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>⌨️ stdio</span></label>
<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>🌐 SSE (HTTP)</span></label>
<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>🐍 Python Module</span></label></div></div>
<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>🗄️ Vincular ADB Vector</label><div class="ht">Opcional — o MCP server poderá usar este banco como ferramenta.</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')}
@@ -2059,19 +2269,19 @@ async function sMcp(){const btn=document.getElementById('mcbtn');const isEdit=S.
linked_adb_id:document.getElementById('madb').value||null};
const url=isEdit?'/mcp/servers/'+S.editing.id:'/mcp/servers';const method=isEdit?'PUT':'POST';
await $api(url,{method,body});S.editing=null;
S.mcpSvr=await $api('/mcp/servers');sm('mcm',' '+(isEdit?'Atualizado':'Registrado')+'!','s');R()}catch(e){sm('mcm',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Registrar Server'}}
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,' Upload realizado!','s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Upload .py'}}
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,' '+(aid?'Vinculado ao ADB!':'ADB desvinculado!'),'s');R()}catch(e){sm('mcpm_'+id,e.message,'e');btn.disabled=false;btn.textContent='Vincular ADB'}}
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,` ${d.discovered} tool(s) descoberta(s), ${d.total} total`,'s');R()}catch(e){sm('mcpm_'+mid,' '+e.message,'e');btn.disabled=false;btn.textContent='🔍 Descobrir Tools'}}
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();
@@ -2080,7 +2290,7 @@ async function addMcpTool(mid){const name=document.getElementById('mctn_'+mid)?.
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,' Tool adicionada!','s');R()}catch(e){sm('mcpm_'+mid,e.message,'e')}}
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);
@@ -2088,15 +2298,15 @@ async function removeMcpTool(mid,idx){const srv=S.mcpSvr.find(m=>m.id===mid);con
/* ── ADB Vector ── */
function rADB(){const ea=S.editing?.type==='adb'?S.adbCfg.find(c=>c.id===S.editing.id):null;
return`<div class="cd"><div class="ct">🗄️ Conexões Registradas</div>
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?'✅':'—'}</td><td>${c.wallet_dir?'✅':'❌'}</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 bd bsm" onclick="dAdb('${c.id}')">Excluir</button></td></tr>`}).join('')}</tbody></table>
${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão ADB registrada.</p></div>':''}</div>
<div class="cd"><div class="ct">${ea?'✏️ Editar Conexão':' Nova Conexão'}</div>
<div class="cd"><div class="ct">${ea?IC.edit+' Editar Conexão':IC.plus+' Nova Conexão'}</div>
<div class="cdesc">${ea?'Editando: <strong>'+ea.config_name+'</strong>':'Conexão via <code style="font-size:.72rem">python-oracledb</code> Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.'}</div><div id="am"></div>
<div class="g2"><div class="ig"><label>Nome da Conexão</label><input type="text" id="an" placeholder="Produção ADB" value="${ea?ea.config_name:''}"></div>
<div class="ig"><label>Wallet ZIP</label><div class="ht">${ea?'Envie novo wallet ou deixe vazio para manter o atual':'Envie o wallet para extrair os DSNs do tnsnames.ora'}</div>
@@ -2111,13 +2321,13 @@ ${!S.adbCfg.length?'<div class="emp" style="padding:.85rem"><p>Nenhuma conexão
<div class="ig"><label>Modelo de Embedding</label><div class="ht">Modelo OCI GenAI para gerar vetores.</div>
<select id="aemb">${Object.entries(S.embModels).map(([k,v])=>'<option value="'+k+'"'+(ea&&ea.embedding_model_id===k?' selected':'')+'>'+v.name+' ('+v.dims+'d)</option>').join('')}</select></div></div>
<div style="display:flex;gap:8px"><button class="btn bp" id="abtn" onclick="sAdb()">${ea?'Salvar Alterações':'Salvar Conexão'}</button>${ea?'<button class="btn bs" onclick="cancelEdit()">Cancelar</button>':''}</div></div>
${ea?`<div class="cd"><div class="ct">📋 Tabelas de Vetores — ${ea.config_name}</div>
${ea?`<div class="cd"><div class="ct">${IC.log} Tabelas de Vetores — ${ea.config_name}</div>
<div class="cdesc">Tabelas no ADB com dados vetorizados. 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;text-transform:uppercase;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?' Sim':' Não'}</button></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">
@@ -2125,7 +2335,7 @@ ${(ea.tables||[]).map(t=>`<tr${!t.is_active?' style="opacity:.55"':''}>
<div class="ig" style="flex:1;margin:0"><label>Descrição</label><input type="text" id="newTblDesc" placeholder="Ex: Dados de compliance CIS"></div>
<button class="btn bs" onclick="addTable('${ea.id}')">+ Adicionar</button>
</div></div>`:''}
${S.adbCfg.length?`<div class="cd"><div class="ct">📤 Atualizar Wallet</div><div id="wm"></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>`:''}
@@ -2135,7 +2345,7 @@ async function parseWallet(){const f=document.getElementById('awf').files[0];if(
try{const fd=new FormData();fd.append('wallet',f);const d=await $api('/adb/parse-wallet',{method:'POST',body:fd,headers:{}});
const wrap=document.getElementById('adsn-wrap');
wrap.innerHTML='<select id="adsn">'+d.dsn_names.map(n=>'<option value="'+n+'">'+n+'</option>').join('')+'</select>';
sm('am',' Wallet analisado! '+d.dsn_names.length+' DSN(s) encontrado(s): <strong>'+d.dsn_names.join(', ')+'</strong>. Arquivos: '+d.files.join(', '),'s');
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';
@@ -2148,22 +2358,22 @@ async function sAdb(){const btn=document.getElementById('abtn');btn.disabled=tru
const wf=document.getElementById('awf').files[0];if(wf)fd.append('wallet',wf);
const url=isEdit?'/adb/configs/'+S.editing.id:'/adb/config';const method=isEdit?'PUT':'POST';
await $api(url,{method,body:fd,headers:{}});S.editing=null;
S.adbCfg=await $api('/adb/configs');sm('am',' Conexão '+(isEdit?'atualizada':'salva')+'!'+(wf?' Wallet incluído.':''),'s');R()}catch(e){sm('am',e.message,'e');btn.disabled=false;btn.textContent=isEdit?'Salvar Alterações':'Salvar Conexão'}}
async function tAdb(id){try{const d=await $api('/adb/test/'+id,{method:'POST'});sm('am',d.status==='success'?' '+d.message:' '+d.message,d.status==='success'?'s':'e');refreshCLogs('adb')}catch(e){sm('am',e.message,'e')}}
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 dAdb(id){if(!confirm('Excluir conexão?'))return;await $api('/adb/configs/'+id,{method:'DELETE'});S.adbCfg=await $api('/adb/configs');R()}
async function uWallet(){const fd=new FormData();const f=document.getElementById('awf2').files[0];if(!f)return sm('wm','Selecione um arquivo wallet ZIP.','e');
fd.append('wallet',f);const id=document.getElementById('awsel').value;if(!id)return sm('wm','Selecione uma conexão ADB.','e');
const btn=document.getElementById('wbtn');btn.disabled=true;btn.textContent='Enviando...';sm('wm','<span class="spinner" style="display:inline-block;width:14px;height:14px;vertical-align:middle"></span> Enviando wallet...','i');
try{const d=await $api('/adb/'+id+'/upload-wallet',{method:'POST',body:fd,headers:{}});S.adbCfg=await $api('/adb/configs');sm('wm',' Wallet enviada! Arquivos: '+d.files.join(', '),'s');R()}catch(e){sm('wm',e.message,'e');btn.disabled=false;btn.textContent='Upload Wallet'}}
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',' Tabela '+name+' registrada!','s');R()}catch(e){sm('tbm',e.message,'e')}}
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.toUpperCase(),description:desc}});S.adbCfg=await $api('/adb/configs');sm('tbm',' Tabela atualizada.','s');R()}catch(e){sm('tbm',e.message,'e')}}
try{await $api('/adb/'+vid+'/tables/'+tid,{method:'PUT',body:{table_name:name.toUpperCase(),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',' Tabela removida.','s');R()}catch(e){sm('tbm',e.message,'e')}}
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>';
@@ -2171,23 +2381,23 @@ function tblOpts(vid){const cfg=S.adbCfg.find(c=>c.id===vid);if(!cfg||!(cfg.tabl
function rEmbeddings(){
const adbOpts=S.adbCfg.filter(c=>c.genai_config_id).map(c=>'<option value="'+c.id+'">'+c.config_name+'</option>').join('');
const rptOpts=S.reports.filter(r=>r.status==='completed').map(r=>'<option value="'+r.id+'">'+r.tenancy_name+' ('+r.created_at?.substring(0,10)+')</option>').join('');
if(!S.adbCfg.filter(c=>c.genai_config_id).length)return`<div class="cd"><div class="emp"><div class="eic">🧬</div><p>Nenhuma conexão ADB com GenAI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>ADB Vector</strong> vinculando uma Config GenAI.</p></div></div>`;
if(!S.adbCfg.filter(c=>c.genai_config_id).length)return`<div class="cd"><div class="emp"><div class="eic">${IC.dna}</div><p>Nenhuma conexão ADB com GenAI configurada.</p><p style="font-size:.74rem;margin-top:.35rem">Configure em <strong>ADB Vector</strong> vinculando uma Config GenAI.</p></div></div>`;
const firstVid=S.adbCfg.filter(c=>c.genai_config_id)[0]?.id||'';
return`<div class="cd"><div class="ct">📋 Embeddings Existentes</div>
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">📊 Embed Relatório CIS</div>
<div class="cd"><div class="ct">${IC.chart} Embed Relatório CIS</div>
<div class="cdesc">Visualize os chunks gerados antes de criar os embeddings. Cada seção do relatório gera um chunk com tenancy, regiões e compartments.</div><div id="erm"></div>
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="ersel" onchange="document.getElementById('ertbl').innerHTML=tblOpts(this.value)">${adbOpts}</select></div>
<div class="ig"><label>Tabela</label><select id="ertbl">${tblOpts(firstVid)}</select></div>
<div class="ig"><label>Relatório</label><select id="errpt">${rptOpts||'<option value="">Nenhum relatório disponível</option>'}</select></div></div>
<div style="display:flex;gap:8px;margin-top:.35rem"><button class="btn bs" id="epbtn" onclick="previewChunks()">👁 Visualizar Chunks</button>
<div style="display:flex;gap:8px;margin-top:.35rem"><button class="btn bs" id="epbtn" onclick="previewChunks()">${IC.eye} Visualizar Chunks</button>
<button class="btn bp" id="erbtn" onclick="embedReport()" style="display:none">Gerar Embeddings</button></div>
<div id="chunk-preview"></div></div>
<div class="cd"><div class="ct">📄 Upload de Arquivo</div>
<div class="cd"><div class="ct">${IC.file} Upload de Arquivo</div>
<div class="cdesc">Faça upload de um arquivo .txt para gerar embeddings automaticamente (chunking por parágrafos).</div><div id="efm"></div>
<div class="g3"><div class="ig"><label>Conexão ADB</label><select id="efsel" onchange="document.getElementById('eftbl').innerHTML=tblOpts(this.value)">${adbOpts}</select></div>
<div class="ig"><label>Tabela</label><select id="eftbl">${tblOpts(firstVid)}</select></div>
@@ -2226,27 +2436,27 @@ async function previewChunks(){const rid=document.getElementById('errpt').value;
html+='</div>';
document.getElementById('chunk-preview').innerHTML=html;
document.getElementById('erbtn').style.display='';
sm('erm',' '+d.total_chunks+' chunks prontos para embedding.','s');
}catch(e){sm('erm',e.message,'e')}finally{btn.disabled=false;btn.textContent='👁 Visualizar Chunks'}}
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',' '+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'}}
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 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=document.getElementById('eftbl')?.value||'';
const fd=new FormData();fd.append('file',f);fd.append('adb_config_id',vid);if(tbl)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',' '+d.message,'s');btn.disabled=false;btn.textContent='Upload e Gerar Embeddings'}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Upload e Gerar Embeddings'}}
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 e Gerar Embeddings'}catch(e){sm('efm',e.message,'e');btn.disabled=false;btn.textContent='Upload e Gerar Embeddings'}}
async function delEmb(vid,docId){if(!confirm('Excluir este embedding?'))return;
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)}}
/* ── Users ── */
function rUsers(){return`<div class="cd"><div class="ct">👥 Gerenciamento de Usuários</div><div id="um"></div>
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>
@@ -2262,7 +2472,7 @@ ${S.users.map(u=>`<tr>
<option value="viewer"${u.role==='viewer'?' selected':''}>Viewer</option></select>
${u.username!=='admin'&&u.is_active?` <button class="btn bd bsm" onclick="deact('${u.id}')">Desativar</button>`:''}</td></tr>`).join('')}
</tbody></table></div>
<div class="cd"><div class="ct"> Criar Novo Usuário</div><div id="um2"></div>
<div class="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>
@@ -2281,15 +2491,15 @@ async function cUser(){
const role = document.getElementById('nr').value;
if (!firstName || !lastName) {
sm('um2', ' Nome e sobrenome são obrigatórios', 'e');
sm('um2', IC.err+' Nome e sobrenome são obrigatórios', 'e');
return;
}
if (!username) {
sm('um2', ' Usuário (login) é obrigatório', 'e');
sm('um2', IC.err+' Usuário (login) é obrigatório', 'e');
return;
}
if (!password) {
sm('um2', ' Senha é obrigatória', 'e');
sm('um2', IC.err+' Senha é obrigatória', 'e');
return;
}
@@ -2306,7 +2516,7 @@ async function cUser(){
}
});
S.users = await $api('/users');
sm('um2', ' Usuário criado com sucesso!', 's');
sm('um2', IC.ok+' Usuário criado com sucesso!', 's');
document.getElementById('nfn').value = '';
document.getElementById('nln').value = '';
document.getElementById('nu').value = '';
@@ -2322,19 +2532,19 @@ async function uRole(id,r){try{await $api('/users/'+id,{method:'PUT',body:{role:
async function deact(id){if(!confirm('Desativar usuário?'))return;await $api('/users/'+id,{method:'DELETE'});S.users=await $api('/users');R()}
/* ── MFA ── */
function rMfa(){return`<div class="cd"><div class="ct">🔐 Autenticação MFA (TOTP)</div>
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"> MFA está ativado na sua conta.</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',' MFA ativado com sucesso!','s');R()}catch(e){sm('mm',e.message,'e')}}
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">📋 Log de Auditoria</div><div id="ac"><div class="ld"><span class="spinner"></span> Carregando logs...</div></div></div>`}
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>
@@ -2342,7 +2552,7 @@ ${logs.map(l=>`<tr><td style="font-size:.68rem;color:var(--t4)">${l.created_at}<
/* ── Config Logs ── */
function rConfigLogs(type){return`<div class="cd"><div class="ct" style="display:flex;justify-content:space-between;align-items:center">
<span>📜 Log de Atividades</span>
<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>