diff --git a/backend/app.py b/backend/app.py index f70c2f2..d908ee2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -231,7 +231,7 @@ def init_db(): password_enc TEXT NOT NULL, wallet_dir TEXT, wallet_password_enc TEXT, - table_name TEXT DEFAULT 'CIS_EMBEDDINGS', + table_name TEXT DEFAULT '', use_mtls INTEGER DEFAULT 1, is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) @@ -2099,8 +2099,7 @@ def _get_adb_connection(cfg: dict): if cfg["use_mtls"] and cfg.get("wallet_dir"): params["config_dir"] = cfg["wallet_dir"] params["wallet_location"] = cfg["wallet_dir"] - if cfg.get("wallet_password_enc"): - params["wallet_password"] = _dec(cfg["wallet_password_enc"]) + params["wallet_password"] = _dec(cfg["wallet_password_enc"]) if cfg.get("wallet_password_enc") else "" return oracledb.connect(**params) def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list: @@ -2129,7 +2128,7 @@ def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list: def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5, table_name: str = None) -> list: """Search ADB vector store using cosine similarity. Returns top-K documents.""" import array - table_name = table_name or cfg.get("table_name", "CIS_EMBEDDINGS") + table_name = table_name or cfg.get("table_name", "") conn = _get_adb_connection(cfg) try: cur = conn.cursor() @@ -2423,7 +2422,7 @@ async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin" async def save_adb( config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...), password: str = Form(...), wallet_password: str = Form(""), - table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"), + table_name: str = Form(""), use_mtls: str = Form("true"), genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-v4.0"), wallet: Optional[UploadFile] = File(None), u=Depends(require("admin","user")) @@ -2513,7 +2512,7 @@ async def update_adb( vid: str, config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...), password: str = Form(""), wallet_password: str = Form(""), - table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"), + table_name: str = Form(""), use_mtls: str = Form("true"), genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-v4.0"), wallet: Optional[UploadFile] = File(None), u=Depends(require("admin","user")) @@ -2625,7 +2624,7 @@ def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id: """Background task: embed and insert documents into ADB via OCI GenAI.""" import array emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0") - table_name = table_name or cfg.get("table_name", "CIS_EMBEDDINGS") + table_name = table_name or cfg.get("table_name", "") conn = _get_adb_connection(cfg) try: cur = conn.cursor() @@ -2789,7 +2788,8 @@ async def list_embeddings(vid: str, table_name: str = Query(""), limit: int = Qu try: conn = _get_adb_connection(dict(cfg)) cur = conn.cursor() - table_name = table_name.strip() or cfg["table_name"] or "CIS_EMBEDDINGS" + table_name = table_name.strip() or cfg["table_name"] + if not table_name: raise HTTPException(400, "Nenhuma tabela selecionada") cur.execute(f"SELECT COUNT(*) FROM {table_name}") total = cur.fetchone()[0] cur.execute(f""" @@ -2814,7 +2814,8 @@ async def delete_embedding(vid: str, doc_id: str, table_name: str = Query(""), u try: conn = _get_adb_connection(dict(cfg)) cur = conn.cursor() - table_name = table_name.strip() or cfg["table_name"] or "CIS_EMBEDDINGS" + table_name = table_name.strip() or cfg["table_name"] + if not table_name: raise HTTPException(400, "Nenhuma tabela selecionada") cur.execute(f"DELETE FROM {table_name} WHERE ID = :1", [doc_id]) conn.commit() cur.close(); conn.close() @@ -2972,6 +2973,55 @@ async def get_report(rid, u=Depends(current_user)): if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403) return dict(r) +@app.get("/api/reports/{rid}/summary") +async def report_summary(rid: str, u=Depends(current_user)): + with db() as c: + r = c.execute("SELECT user_id,json_path,tenancy_name,level,created_at FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone() + if not r: raise HTTPException(404) + if u["role"] != "admin" and r["user_id"] != u["id"]: raise HTTPException(403) + jp = r["json_path"] + if not jp or not Path(jp).exists(): + raise HTTPException(400, "Report JSON not found") + data = json.loads(Path(jp).read_text()) + total = passed = failed = 0 + sections = {} + total_findings = 0 + total_compliant = 0 + # Format: list of checks with Compliant=Yes/No, Section, Findings, Compliant Items, Total + checks = data if isinstance(data, list) else data.get("findings", []) + if isinstance(checks, dict): + checks = list(checks.values()) + for check in checks: + total += 1 + compliant = str(check.get("Compliant", check.get("status", ""))).strip().lower() + is_pass = compliant in ("yes", "pass") + if is_pass: + passed += 1 + else: + failed += 1 + fi = str(check.get("Findings", "0")).strip() + ci = str(check.get("Compliant Items", "0")).strip() + total_findings += int(fi) if fi.isdigit() else 0 + total_compliant += int(ci) if ci.isdigit() else 0 + sec = check.get("Section", check.get("section", "Other")) + s = sections.setdefault(sec, {"passed": 0, "failed": 0, "total": 0}) + s["total"] += 1 + if is_pass: + s["passed"] += 1 + else: + s["failed"] += 1 + score = round((passed / total * 100), 1) if total else 0 + return { + "tenancy": data.get("tenancy", r["tenancy_name"]) if isinstance(data, dict) else r["tenancy_name"], + "level": r["level"] or 2, + "regions": data.get("regions", []) if isinstance(data, dict) else [], + "generated_at": r["created_at"], + "total": total, "passed": passed, "failed": failed, + "total_findings": total_findings, "total_compliant": total_compliant, + "score": score, + "sections": sections + } + @app.get("/api/reports/{rid}/files") async def list_report_files(rid: str, u=Depends(current_user)): with db() as c: @@ -3105,7 +3155,7 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c query_embedding = _embed_text(msg.message, dict(emb_genai), emb_model) tables = _get_tables_for_config(adb_cfg["id"], active_only=True) if not tables: - tables = [{"table_name": adb_cfg.get("table_name", "CIS_EMBEDDINGS")}] + tables = [{"table_name": adb_cfg.get("table_name", "")}] for tbl in tables: try: documents = _vector_search(adb_cfg, query_embedding, top_k=5, table_name=tbl["table_name"]) diff --git a/frontend/index.html b/frontend/index.html index f43cdba..2a2d92c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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} } +
@@ -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)=>`${d}`; +const IC={ + chat:_i(''), + search:_i(''), + chart:_i(''), + folder:_i(''), + cloud:_i(''), + brain:_i(''), + plug:_i(''), + db:_i(''), + dna:_i(''), + users:_i(''), + lock:_i(''), + log:_i(''), + ok:_i(''), + err:_i(''), + warn:_i(''), + wait:_i(''), + timeout:_i(''), + settings:_i(''), + edit:_i(''), + plus:_i(''), + upload:_i(''), + download:_i(''), + bot:_i(''), + wrench:_i(''), + clock:_i(''), + history:_i(''), + file:_i(''), + spark:_i(''), + clip:_i(''), + key:_i(''), + building:_i(''), + user:_i(''), + pkg:_i(''), + globe:_i(''), + python:_i(''), + terminal:_i(''), + rocket:_i(''), + refresh:_i(''), + shield:_i(''), + eye:_i(''), + scroll:_i(''), + link:_i(''), + sun:_i(''), + moon:_i(''), + power:_i(''), + zap:_i(''), + barChart:_i(''), + computer:_i(''), + hdd:_i(''), + container:_i(''), + activity:_i(''), +} const LOGO_W=``; const LOGO_R=``; @@ -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`
${rSb()}
${rTb()}

${LOGO_W} OCI CIS Agent

Infrastructure & Security · v${V}
@@ -470,17 +620,17 @@ ${ctabs.map(t=>`
Administração
${atabs.map(t=>`
${t[1]}${t[2]}
`).join('')}`:''}
${i}
${S.user?.first_name?S.user.first_name+' '+S.user.last_name:S.user?.username}
${S.user?.role}
-
${document.documentElement.classList.contains('dark')?'☀️':'🌙'}
+
${document.documentElement.classList.contains('dark')?IC.sun:IC.moon}
`} -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`
${t[S.tab]||''}
v${V}
`} 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?'
🤖

Inicie uma conversa com o agente.

Selecione um modelo para começar.

' + const ms=S.msgs.length===0?'
${IC.bot}

Inicie uma conversa com o agente.

Selecione um modelo para começar.

' :S.msgs.map(m=>`
${fm(m.c)}
${m.t?`
${m.t}
`:''}
`).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`
-
🕘 Histórico
+ Novo Chat
+
${IC.history} Histórico
+ Novo Chat
${rHistList('chat',S.chatHistory,S.sid)}
${sideContent}
-
${curLabel}
${ddItems}
+
${curLabel}
${ddItems}
${ociSel}${ragBadge}
-
⚙️
-
📋
-
+
${IC.settings}
+
${IC.log}
+
${IC.spark}
${ms}
-${S.chatFiles.length?`
${S.chatFiles.map((f,i)=>f.type==='image'?`
×
`:`
📄 ${f.name}×
`).join('')}
`:''} +${S.chatFiles.length?`
${S.chatFiles.map((f,i)=>f.type==='image'?`
×
`:`
${IC.file} ${f.name}×
`).join('')}
`:''}
- +
`} function rChatConfig(isDirect,toolCount){ const pp=S.chatParams; - return`
⚙️ Configurações
+ return`
${IC.settings} Configurações
@@ -529,7 +679,7 @@ function rChatConfig(isDirect,toolCount){
- + ${toolCount?`${toolCount} tools disponíveis`:''}
${isDirect?`
@@ -537,7 +687,7 @@ ${isDirect?`
x.id===this.value);if(c){S.chatRegion=c.region;S.chatCompartment=c.compartment_id||''}" style="margin-top:3px"> ${S.ociCfg.map(c=>``).join('')}
`:''}`} function rChatLogs(){ - return`
📋 Chat Logs
+ return`
${IC.log} Chat Logs
@@ -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,'$1').replace(/`(.*?)`/g,'$1').replace(/\n/g,'
')} @@ -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='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 `
-
🕘 Histórico
+ Novo Chat
+
${IC.history} Histórico
+ Novo Chat
${rHistList('terraform',S.tfHistory,S.tfSid)}
@@ -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`
☁️

Nenhuma credencial OCI configurada.

Configure em Credenciais OCI.

`; + if(!S.ociCfg.length)return`
${IC.cloud}

Nenhuma credencial OCI configurada.

Configure em Credenciais OCI.

`; if(!S.expCfg&&S.ociCfg.length)S.expCfg=S.ociCfg[0].id; const cfgOpts=S.ociCfg.map(c=>``).join(''); const regLabel=S.expSelRegions.length?S.expSelRegions.length+' região(ões)':'Todas as regiões'; const regChecks=S.expRegions.map(r=>``).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=>`
${catIcons[g.name]||''} ${g.name}
`).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`
${t.label}${cnt!=null?' ('+cnt+')':''}
`}).join(''); return`
-
📁 Compartments
${S.expTree.length?rExpTree(null,0):'
Carregando...
'}
+
${IC.folder} Compartments
${S.expTree.length?rExpTree(null,0):'
Carregando...
'}
-${S.expRegions.length?`
🌍 ${regLabel}
+${S.expRegions.length?`
${IC.globe} ${regLabel}
${S.expRegDdOpen?`
Regiões
${regChecks}
@@ -1579,8 +1729,8 @@ function renderExpData(d){if(!d)return'

Selecione um compartm let actionBtn=''; if(isInst&&i.lifecycle_state==='RUNNING')actionBtn=' '; else if(isInst&&i.lifecycle_state==='STOPPED')actionBtn=' '; - else if(isAdb&&i.lifecycle_state==='AVAILABLE')actionBtn=' '; - else if(isAdb&&i.lifecycle_state==='STOPPED')actionBtn=' '; + else if(isAdb&&i.lifecycle_state==='AVAILABLE')actionBtn=' '; + else if(isAdb&&i.lifecycle_state==='STOPPED')actionBtn=' '; return`

${name}${i.lifecycle_state?` ${i.lifecycle_state}`:''}${actionBtn}${aclHtml}
${keys.filter(k=>!skip.includes(k)).map(k=>`${k}: ${typeof i[k]==='object'?JSON.stringify(i[k]):i[k]}`).join('
')}
`}).join('')}
`} 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=`
${rProgressCard('running',last,pct,lines,S.trackingReportId)}
`; if(!_rptPollTimer)startRptPoll()} - if(!comp.length)return`${progressHtml}
📊 Compliance Reports
📄

Nenhum relatório gerado ainda.

${rRunRpt()}${rReportHistory()}`; + if(!comp.length)return`${progressHtml}
${IC.chart} Compliance Reports
${IC.file}

Nenhum relatório gerado ainda.

${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=>`
${r.tenancy_name} — ${r.created_at}
`).join(''); @@ -1641,7 +1791,7 @@ ${S.rptRselOpen?`
Nenhum relatório encontrado
'}
`:''}
-${rRunRpt()}${rReportHistory()}`} +${rKpi()}${rRunRpt()}${rReportHistory()}`} function rRunRpt(){if(S.user?.role==='viewer')return''; const selTags=S.rptSelRegions.map(r=>`${r}×`).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=>`
${c.tenancy_name} (${c.region})
`).join(''); - return`
🚀 Executar Relatório
+ return`
${IC.rocket} Executar Relatório
${ociLabel}
@@ -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='⏳ Atualizando...'; + if(S.cisUpdating)status='${IC.wait} Atualizando...'; else if(cr){ if(cr.update_available)status=`Nova versão: ${cr.remote_version} (${cr.remote_date}) `; else status='✓ Versão mais recente'; } - return`
⚙️ CIS Engine
+ return`
${IC.settings} CIS Engine
Versão: ${v?v.version:'...'}${v&&v.updated_date!=='unknown'?' ('+v.updated_date+')':''} @@ -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` + + + + +${score}% +COMPLIANCE +`} +function rKpi(){ + if(S.rptKpiLoading)return'
Carregando métricas...
'; + 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`
+
${_svgGauge(k.score)}
${k.total} checks · Level ${k.level}
+
${IC.ok} Compliant
${k.passed}
${k.total?Math.round(k.passed/k.total*100):0}% dos checks
+
${IC.err} Non-Compliant
${k.failed}
${k.total_findings||0} findings
+
${IC.search} Total Avaliado
${k.total}
${(k.total_compliant||0)+(k.total_findings||0)} itens analisados
+${secs.length?`
+
+
${IC.chart} Distribuição
+
+
${k.score}%
score
+
+ ${k.passed} ok + ${k.failed} fail
+
${IC.barChart} Findings por Seção
+
`:''}`} 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`
📋 Histórico de Relatórios
+ return`
${IC.history} Histórico de Relatórios
${filtered.length} report(s)
-
-${!filtered.length?'
📂

Nenhum relatório concluído.

':` +
+${!filtered.length?'
${IC.folder}

Nenhum relatório concluído.

':`
${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'
Nenhum arquivo encontrado
'; 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='
'; for(const[cat,cf] of Object.entries(groups)){ - h+=`
${icons[cat]||'📄'} ${labels[cat]||cat} (${cf.length})
`; + h+=`
${icons[cat]||IC.file} ${labels[cat]||cat} (${cf.length})
`; cf.forEach(f=>{ h+=` ${_dlFileIcon(f.file_name)} @@ -1847,20 +2057,20 @@ ${ddItems||'
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`
-
⚙️ Configuração do Sistema
+
${IC.settings} Configuração do Sistema
- - + +
-
☁️ Credenciais Registradas
+
${IC.cloud} Credenciais Registradas
${S.ociCfg.map(c=>` - +`).join('')}
TenancyRegionDetalhesAções
${c.tenancy_name}
${c.created_at}
${c.region}🔑 ••••••••••••••••••••
🏢 ${c.tenancy_ocid}
👤 ${c.user_ocid}
📦 ${c.compartment_id||'—'}
${IC.key} ••••••••••••••••••••
${IC.building} ${c.tenancy_ocid}
${IC.user} ${c.user_ocid}
${IC.pkg} ${c.compartment_id||'—'}
${!S.ociCfg.length?'

Nenhuma credencial registrada.

':''}
-
${eo?'✏️ Editar Credencial':'➕ Nova Credencial'}
+
${eo?IC.edit+' Editar Credencial':IC.plus+' Nova Credencial'}
${eo?'Editando: '+eo.tenancy_name+'':'Adicione as credenciais da sua conta OCI para executar reports e acessar serviços.'}
@@ -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',' 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=''+Object.entries(S.models).map(([k,v])=>``).join(''); + const mOpts=''+Object.entries(S.models).map(([k,v])=>``).join(''); const rOpts=S.regions.map(r=>``).join(''); - return`
🧠 Modelos Configurados
+ return`
${IC.brain} Modelos Configurados
${S.genaiCfg.map(g=>{const mi=S.models[g.model_id]||{};return``}).join('')}
ConfigModeloRegiãoAções
${g.name||'—'}${g.is_default?' ⭐':''} ${mi.name||(g.model_ocid?'OCID Personalizado':g.model_id)}
${mi.provider||(g.model_ocid?'custom':'?')}
${g.genai_region}
${!S.genaiCfg.length?'

Nenhum modelo configurado.

':''}
-
💬 System Prompts — Chat Agent
+
${IC.chat} System Prompts — Chat Agent
Prompts de sistema para o Chat Agent. O prompt ativo é enviado como instrução do sistema em todas as mensagens. Máximo 10 prompts. Outros agents (ex.: provisionamento) terão seus próprios prompts.
${(S.chatPrompts||[]).map(p=>` @@ -1950,7 +2160,7 @@ ${!(S.chatPrompts||[]).length?'

Nenhum

${S.editingPrompt?'':''}
-
${eg?'✏️ Editar Modelo GenAI':'➕ Novo Modelo GenAI'}
+
${eg?IC.edit+' Editar Modelo GenAI':IC.plus+' Novo Modelo GenAI'}
${eg?'Editando: '+eg.name+'':'Adicione modelos personalizados que não estão no catálogo do Chat Agent, ou crie presets com credenciais específicas.'}
+ ${IC.terminal} stdio + ${IC.globe} SSE (HTTP)
+ ${IC.python} Python Module
Comando para iniciar o servidor
Array de argumentos passados ao comando
-
Opcional — o MCP server poderá usar este banco como ferramenta.
+
Opcional — o MCP server poderá usar este banco como ferramenta.
${em?'':''}
`+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,' 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,' 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`
🗄️ Conexões Registradas
+ return`
${IC.db} Conexões Registradas
NomeStatusAções
${p.name}
${S.adbCfg.map(c=>{const em=S.embModels[c.embedding_model_id];const tbls=c.tables||[];return` - +`}).join('')}
NomeDSNUserTabelasmTLSWalletEmbedAções
${c.config_name}${c.dsn} ${c.username}${tbls.length?tbls.map(t=>''+t.table_name+'').join(' '):''}${c.use_mtls?'✅':'—'}${c.wallet_dir?'✅':'❌'}${c.use_mtls?IC.ok:'—'}${c.wallet_dir?IC.ok:IC.err} ${c.genai_config_id?''+(em?em.name:c.embedding_model_id)+'':''}
${!S.adbCfg.length?'

Nenhuma conexão ADB registrada.

':''}
-
${ea?'✏️ Editar Conexão':'➕ Nova Conexão'}
+
${ea?IC.edit+' Editar Conexão':IC.plus+' Nova Conexão'}
${ea?'Editando: '+ea.config_name+'':'Conexão via python-oracledb Thin mode com Wallet (mTLS) para Oracle Autonomous Database. Envie o Wallet ZIP para preencher o DSN automaticamente.'}
${ea?'Envie novo wallet ou deixe vazio para manter o atual':'Envie o wallet para extrair os DSNs do tnsnames.ora'}
@@ -2111,13 +2321,13 @@ ${!S.adbCfg.length?'

Nenhuma conexão

Modelo OCI GenAI para gerar vetores.
${ea?'':''}
-${ea?`
📋 Tabelas de Vetores — ${ea.config_name}
+${ea?`
${IC.log} Tabelas de Vetores — ${ea.config_name}
Tabelas no ADB com dados vetorizados. A busca RAG consultará todas as tabelas ativas.
${(ea.tables||[]).length?` ${(ea.tables||[]).map(t=>` - +`).join('')}
TabelaDescriçãoAtivaAções
`:'

Nenhuma tabela registrada.

'}
@@ -2125,7 +2335,7 @@ ${(ea.tables||[]).map(t=>`
`:''} -${S.adbCfg.length?`
📤 Atualizar Wallet
+${S.adbCfg.length?`
${IC.upload} Atualizar Wallet
`:''} @@ -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=''; - sm('am','✅ Wallet analisado! '+d.dsn_names.length+' DSN(s) encontrado(s): '+d.dsn_names.join(', ')+'. Arquivos: '+d.files.join(', '),'s'); + sm('am',IC.ok+' Wallet analisado! '+d.dsn_names.length+' DSN(s) encontrado(s): '+d.dsn_names.join(', ')+'. 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',' 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',' 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''; @@ -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=>'').join(''); const rptOpts=S.reports.filter(r=>r.status==='completed').map(r=>'').join(''); - if(!S.adbCfg.filter(c=>c.genai_config_id).length)return`
🧬

Nenhuma conexão ADB com GenAI configurada.

Configure em ADB Vector vinculando uma Config GenAI.

`; + if(!S.adbCfg.filter(c=>c.genai_config_id).length)return`
${IC.dna}

Nenhuma conexão ADB com GenAI configurada.

Configure em ADB Vector vinculando uma Config GenAI.

`; const firstVid=S.adbCfg.filter(c=>c.genai_config_id)[0]?.id||''; - return`
📋 Embeddings Existentes
+ return`
${IC.log} Embeddings Existentes
Consulte os documentos armazenados nas tabelas de embeddings do ADB.
-
📊 Embed Relatório CIS
+
${IC.chart} Embed Relatório CIS
Visualize os chunks gerados antes de criar os embeddings. Cada seção do relatório gera um chunk com tenancy, regiões e compartments.
-
+
-
📄 Upload de Arquivo
+
${IC.file} Upload de Arquivo
Faça upload de um arquivo .txt para gerar embeddings automaticamente (chunking por parágrafos).
@@ -2226,27 +2436,27 @@ async function previewChunks(){const rid=document.getElementById('errpt').value; html+='
'; 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',' 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',' 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`
👥 Gerenciamento de Usuários
+function rUsers(){return`
${IC.users} Gerenciamento de Usuários
${S.users.map(u=>` @@ -2262,7 +2472,7 @@ ${S.users.map(u=>` ${u.username!=='admin'&&u.is_active?` `:''}`).join('')}
NomeUsuárioEmailRoleMFAStatusÚltimo LoginAções
${u.first_name||''} ${u.last_name||''}
-
➕ Criar Novo Usuário
+
${IC.plus} Criar Novo Usuário
@@ -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`
🔐 Autenticação MFA (TOTP)
+function rMfa(){return`
${IC.lock} Autenticação MFA (TOTP)
Proteja sua conta com autenticação em dois fatores via Google Authenticator ou Authy.
-${S.user?.mfa_enabled?'
✅ MFA está ativado na sua conta.
' +${S.user?.mfa_enabled?'
'+IC.ok+' MFA está ativado na sua conta.
' :`
`}
`} async function sMfa(){try{const d=await $api('/mfa/setup',{method:'POST'}); document.getElementById('msa').innerHTML=`
Secret: ${d.secret}
`}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`
📋 Log de Auditoria
Carregando logs...
`} +function rAudit(){return`
${IC.log} Log de Auditoria
Carregando logs...
`} async function loadAudit(){try{const logs=await $api('/audit-log?limit=50'); document.getElementById('ac').innerHTML=` ${logs.map(l=>` @@ -2342,7 +2552,7 @@ ${logs.map(l=>`
DataUsuárioAçãoRecursoIP
${l.created_at}${l.username||'—'}${l.action}
${l.created_at}< /* ── Config Logs ── */ function rConfigLogs(type){return`
-📜 Log de Atividades +${IC.scroll} Log de Atividades